-
-
Notifications
You must be signed in to change notification settings - Fork 37.8k
Add zwave mqtt #34987
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add zwave mqtt #34987
Changes from 33 commits
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
28a98e0
Copy from custom component
MartinHjelmare 08348af
Delint
MartinHjelmare afc69ec
Exclude csv files from pre-commit codespell
MartinHjelmare 61e022d
Copy tests
MartinHjelmare 7d94a3c
Sleep 0 seconds in tests
MartinHjelmare d286ff7
Fix set config parameter return value
MartinHjelmare 8a0e383
Remove not used service descriptions
MartinHjelmare 1884d33
Improve config flow and add test
MartinHjelmare 83082be
Remove platforms for now except switch
MartinHjelmare fa35c06
Remove platforms from const
MartinHjelmare 728cd34
Clean up const
MartinHjelmare b368f14
Add Marcel as code owner
MartinHjelmare 5f2dfff
Clean up discovery
MartinHjelmare 1f5ca08
Use new switch base class
MartinHjelmare ad5a29b
Clean up switch
MartinHjelmare 5e985c4
Update to latest entity base methods
MartinHjelmare 82d29eb
Polish config flow test name
MartinHjelmare 500e573
Move generic fixture
MartinHjelmare 16f8db1
Test switch receive
MartinHjelmare e065476
Allow passing in entry to setup helper
MartinHjelmare a1d0592
Clean up
MartinHjelmare 0219fdf
Add unload entry test
MartinHjelmare 23db18d
Extract load fixture data to pytest fixture
MartinHjelmare 919e612
Bump python-openzwave-mqtt to 0.0.9
MartinHjelmare ab62c12
Handle node update
MartinHjelmare 5f0830c
Sort listens
MartinHjelmare 4650fc5
Exclude from coverage for now
MartinHjelmare 99044f7
Enhance config flow
MartinHjelmare c08897e
Add config flow tests
MartinHjelmare ccaf4db
Fix permissions
MartinHjelmare b400ba9
Add guard clause to handle remove node
MartinHjelmare 37cc103
Fix dispatch signal domain prefix
MartinHjelmare 5e1a1ae
Fix switch is on
MartinHjelmare 327aca7
Apply suggestions from code review
MartinHjelmare a51d983
Add OZW instance to unique id for value
MartinHjelmare 18bfb57
Remove non essential services for now
MartinHjelmare c053778
Rename and prefix with async
MartinHjelmare 56fb5f7
Removea clear
MartinHjelmare 9d07552
Use tuples instead of lists in discovery
MartinHjelmare f68a90e
Waste less during discovery
MartinHjelmare 45c0e39
Bump python-openzwave-mqtt to 1.0.1
MartinHjelmare 1cb669e
Merge branch 'add-zwave_mqtt' of github.com:home-assistant/core into …
MartinHjelmare f8adb59
Fix node update
MartinHjelmare a7af47a
Use ozw mqtt lib for discovery constants
MartinHjelmare 45832a0
Clean and fix tests
MartinHjelmare 94f6488
Scope generic data fixture per session
MartinHjelmare d01df22
Clean up mock mqtt message
MartinHjelmare 28614e6
Sort imports
MartinHjelmare d6b564a
Use block till done
MartinHjelmare 4023d74
Clean wait for platforms
MartinHjelmare 2f4a69c
Use ozw mqtt ready states constant
MartinHjelmare 5db5be0
Clean up services
MartinHjelmare File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,333 @@ | ||
| """The zwave_mqtt integration.""" | ||
| import asyncio | ||
| import json | ||
| import logging | ||
|
|
||
| from openzwavemqtt import OZWManager, OZWOptions | ||
| from openzwavemqtt.const import ( | ||
| EVENT_INSTANCE_EVENT, | ||
| EVENT_NODE_ADDED, | ||
| EVENT_NODE_CHANGED, | ||
| EVENT_NODE_REMOVED, | ||
| EVENT_VALUE_ADDED, | ||
| EVENT_VALUE_CHANGED, | ||
| EVENT_VALUE_REMOVED, | ||
| CommandClass, | ||
| ValueType, | ||
| ) | ||
| from openzwavemqtt.models.node import OZWNode | ||
| from openzwavemqtt.models.value import OZWValue | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.components import mqtt | ||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.core import HomeAssistant, callback | ||
| from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg | ||
| from homeassistant.helpers.dispatcher import async_dispatcher_send | ||
|
|
||
| from . import const | ||
| from .const import DATA_UNSUBSCRIBE, DOMAIN, PLATFORMS, TOPIC_OPENZWAVE | ||
| from .discovery import DISCOVERY_SCHEMAS, check_node_schema, check_value_schema | ||
| from .entity import ZWaveDeviceEntityValues, create_device_id | ||
| from .services import ZWaveServices | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) | ||
| DATA_DEVICES = "zwave-mqtt-devices" | ||
|
|
||
|
|
||
| async def async_setup(hass: HomeAssistant, config: dict): | ||
| """Initialize basic config of zwave_mqtt component.""" | ||
| if "mqtt" not in hass.config.components: | ||
| _LOGGER.error("MQTT integration is not set up") | ||
| return False | ||
|
MartinHjelmare marked this conversation as resolved.
|
||
| hass.data[DOMAIN] = {} | ||
| return True | ||
|
|
||
|
|
||
| async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): | ||
| """Set up zwave_mqtt from a config entry.""" | ||
|
|
||
| @callback | ||
| def async_receive_message(msg): | ||
| manager.receive_message(msg.topic, msg.payload) | ||
|
|
||
| platforms_loaded = [] | ||
|
|
||
| async def mark_platform_loaded(platform): | ||
| platforms_loaded.append(platform) | ||
|
|
||
| if len(platforms_loaded) != len(PLATFORMS): | ||
| return | ||
|
|
||
| hass.data[DOMAIN][entry.entry_id][DATA_UNSUBSCRIBE].append( | ||
| await mqtt.async_subscribe( | ||
| hass, f"{TOPIC_OPENZWAVE}/#", async_receive_message | ||
| ) | ||
| ) | ||
|
|
||
| hass.data[DOMAIN][entry.entry_id] = { | ||
| "mark_platform_loaded": mark_platform_loaded, | ||
|
MartinHjelmare marked this conversation as resolved.
Outdated
|
||
| DATA_UNSUBSCRIBE: [], | ||
| } | ||
|
|
||
| data_nodes = {} | ||
| data_values = {} | ||
| removed_nodes = [] | ||
|
|
||
| @callback | ||
| def send_message(topic, payload): | ||
| mqtt.async_publish(hass, topic, json.dumps(payload)) | ||
|
|
||
| options = OZWOptions(send_message=send_message, topic_prefix=f"{TOPIC_OPENZWAVE}/") | ||
| manager = OZWManager(options) | ||
|
|
||
| for component in PLATFORMS: | ||
| hass.async_create_task( | ||
| hass.config_entries.async_forward_entry_setup(entry, component) | ||
| ) | ||
|
|
||
| @callback | ||
| def async_node_added(node): | ||
| # Caution: This is also called on (re)start. | ||
| _LOGGER.debug("[NODE ADDED] node_id: %s", node.id) | ||
| data_nodes[node.id] = node | ||
| if node.id not in data_values: | ||
| data_values[node.id] = [] | ||
|
|
||
| @callback | ||
| def async_node_changed(node): | ||
| _LOGGER.debug("[NODE CHANGED] node_id: %s", node.id) | ||
| data_nodes[node.id] = node | ||
| # notify devices about the node change | ||
| if node.id not in removed_nodes: | ||
| hass.async_create_task(handle_node_update(hass, node)) | ||
|
|
||
| @callback | ||
| def async_node_removed(node): | ||
| _LOGGER.debug("[NODE REMOVED] node_id: %s", node.id) | ||
| data_nodes.pop(node.id) | ||
| # node added/removed events also happen on (re)starts of hass/mqtt/ozw | ||
| # cleanup device/entity registry if we know this node is permanently deleted | ||
| # entities itself are removed by the values logic | ||
| if node.id in removed_nodes: | ||
| hass.async_create_task(handle_remove_node(hass, node)) | ||
| removed_nodes.remove(node.id) | ||
|
|
||
| @callback | ||
| def async_instance_event(message): | ||
| event = message["event"] | ||
| event_data = message["data"] | ||
| _LOGGER.debug("[INSTANCE EVENT]: %s - data: %s", event, event_data) | ||
| # The actual removal action of a Z-Wave node is reported as instance event | ||
| # Only when this event is detected we cleanup the device and entities from hass | ||
| if event == "removenode" and "Node" in event_data: | ||
| removed_nodes.append(event_data["Node"]) | ||
|
|
||
| @callback | ||
| def async_value_added(value): | ||
| node = value.node | ||
| node_id = value.node.node_id | ||
|
|
||
| # Filter out CommandClasses we're definitely not interested in. | ||
| if value.command_class in [ | ||
| CommandClass.CONFIGURATION, | ||
| CommandClass.VERSION, | ||
| CommandClass.MANUFACTURER_SPECIFIC, | ||
| ]: | ||
| return | ||
|
|
||
| _LOGGER.debug( | ||
| "[VALUE ADDED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", | ||
| value.node.id, | ||
| value.label, | ||
| value.value, | ||
| value.value_id_key, | ||
| value.command_class, | ||
| ) | ||
|
|
||
| node_data_values = data_values[node_id] | ||
|
|
||
| # Check if this value should be tracked by an existing entity | ||
| value_unique_id = f"{value.node.id}-{value.value_id_key}" | ||
| for values in node_data_values: | ||
| values.check_value(value) | ||
|
MartinHjelmare marked this conversation as resolved.
Outdated
|
||
| if values.values_id == value_unique_id: | ||
| return # this value already has an entity | ||
|
|
||
| # Run discovery on it and see if any entities need created | ||
| for schema in DISCOVERY_SCHEMAS: | ||
| if not check_node_schema(node, schema): | ||
| continue | ||
| if not check_value_schema( | ||
| value, schema[const.DISC_VALUES][const.DISC_PRIMARY] | ||
| ): | ||
| continue | ||
|
|
||
| values = ZWaveDeviceEntityValues(hass, options, schema, value) | ||
| values.setup() | ||
|
MartinHjelmare marked this conversation as resolved.
Outdated
|
||
|
|
||
| # We create a new list and update the reference here so that | ||
| # the list can be safely iterated over in the main thread | ||
| data_values[node_id] = node_data_values + [values] | ||
|
|
||
| @callback | ||
| def async_value_changed(value): | ||
| # if an entity belonging to this value needs updating, | ||
| # it's handled within the entity logic | ||
| _LOGGER.debug( | ||
| "[VALUE CHANGED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", | ||
| value.node.id, | ||
| value.label, | ||
| value.value, | ||
| value.value_id_key, | ||
| value.command_class, | ||
| ) | ||
| # Handle a scene activation message | ||
| if value.command_class in [ | ||
| CommandClass.SCENE_ACTIVATION, | ||
| CommandClass.CENTRAL_SCENE, | ||
| ]: | ||
| handle_scene_activated(hass, value) | ||
| return | ||
|
|
||
| @callback | ||
| def async_value_removed(value): | ||
| _LOGGER.debug( | ||
| "[VALUE REMOVED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", | ||
| value.node.id, | ||
| value.label, | ||
| value.value, | ||
| value.value_id_key, | ||
| value.command_class, | ||
| ) | ||
| # signal all entities using this value for removal | ||
| value_unique_id = f"{value.node.id}-{value.value_id_key}" | ||
| async_dispatcher_send(hass, const.SIGNAL_DELETE_ENTITY, value_unique_id) | ||
| # remove value from our local list | ||
| node_data_values = data_values[value.node.id] | ||
| node_data_values[:] = [ | ||
| item for item in node_data_values if item.values_id != value_unique_id | ||
| ] | ||
|
|
||
| # Listen to events for node and value changes | ||
| options.listen(EVENT_NODE_ADDED, async_node_added) | ||
| options.listen(EVENT_NODE_CHANGED, async_node_changed) | ||
| options.listen(EVENT_NODE_REMOVED, async_node_removed) | ||
| options.listen(EVENT_VALUE_ADDED, async_value_added) | ||
| options.listen(EVENT_VALUE_CHANGED, async_value_changed) | ||
| options.listen(EVENT_VALUE_REMOVED, async_value_removed) | ||
| options.listen(EVENT_INSTANCE_EVENT, async_instance_event) | ||
|
|
||
| # Register Services | ||
| services = ZWaveServices(hass, manager, data_nodes) | ||
| services.register() | ||
|
MartinHjelmare marked this conversation as resolved.
Outdated
|
||
|
|
||
| return True | ||
|
|
||
|
|
||
| async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): | ||
| """Unload a config entry.""" | ||
| # cleanup platforms | ||
| unload_ok = all( | ||
| await asyncio.gather( | ||
| *[ | ||
| hass.config_entries.async_forward_entry_unload(entry, component) | ||
| for component in PLATFORMS | ||
| ] | ||
| ) | ||
| ) | ||
| if not unload_ok: | ||
| return False | ||
|
|
||
| # unsubscribe all listeners | ||
| for unsubscribe_listener in hass.data[DOMAIN][entry.entry_id][DATA_UNSUBSCRIBE]: | ||
| unsubscribe_listener() | ||
| hass.data[DOMAIN][entry.entry_id][DATA_UNSUBSCRIBE].clear() | ||
|
MartinHjelmare marked this conversation as resolved.
Outdated
|
||
| hass.data[DOMAIN].pop(entry.entry_id) | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| async def handle_remove_node(hass: HomeAssistant, node: OZWNode): | ||
| """Handle the removal of a Z-Wave node, removing all traces in device/entity registry.""" | ||
| dev_registry = await get_dev_reg(hass) | ||
| # grab device in device registry attached to this node | ||
| dev_id = create_device_id(node) | ||
| device = dev_registry.async_get_device({(DOMAIN, dev_id)}, set()) | ||
| if not device: | ||
| return | ||
| devices_to_remove = [device.id] | ||
| # also grab slave devices (node instances) | ||
| for item in dev_registry.devices.values(): | ||
| if item.via_device_id == device.id: | ||
| devices_to_remove.append(item.id) | ||
| # remove all devices in registry related to this node | ||
| # note: removal of entity registry is handled by core | ||
| for dev_id in devices_to_remove: | ||
| dev_registry.async_remove_device(dev_id) | ||
|
|
||
|
|
||
| async def handle_node_update(hass: HomeAssistant, node: OZWNode): | ||
| """ | ||
| Handle a node updated event from OZW. | ||
|
|
||
| Meaning some of the basic info like name/model is updated. | ||
| We want these changes to be pushed to the device registry. | ||
| """ | ||
| dev_registry = await get_dev_reg(hass) | ||
| # grab device in device registry attached to this node | ||
| device = dev_registry.async_get_device([(DOMAIN, node.id)], []) | ||
|
MartinHjelmare marked this conversation as resolved.
Outdated
|
||
| if not device: | ||
| return | ||
| # update device in device registry with (updated) info | ||
| for item in dev_registry.devices.values(): | ||
| if item.id != device.id and item.via_device_id != device.id: | ||
| continue | ||
| if node.meta_data.get("Name"): | ||
| dev_name = node.meta_data["Name"] | ||
| else: | ||
| dev_name = node.node_product_name | ||
| dev_registry.async_update_device( | ||
| item.id, | ||
| manufacturer=node.node_manufacturer_name, | ||
| model=node.node_product_name, | ||
| name=dev_name, | ||
| ) | ||
|
|
||
|
|
||
| @callback | ||
| def handle_scene_activated(hass: HomeAssistant, scene_value: OZWValue): | ||
| """Handle a (central) scene activation message.""" | ||
| node_id = scene_value.node.id | ||
| scene_id = scene_value.index | ||
| scene_label = scene_value.label | ||
| if scene_value.command_class == CommandClass.SCENE_ACTIVATION: | ||
| # legacy/network scene | ||
| scene_value_id = scene_value.value | ||
| scene_value_label = scene_value.label | ||
| else: | ||
| # central scene command | ||
| if scene_value.type != ValueType.LIST: | ||
| return | ||
| scene_value_label = scene_value.value["Selected"] | ||
| scene_value_id = scene_value.value["Selected_id"] | ||
|
|
||
| _LOGGER.debug( | ||
| "[SCENE_ACTIVATED] node_id: %s - scene_id: %s - scene_value_id: %s", | ||
| node_id, | ||
| scene_id, | ||
| scene_value_id, | ||
| ) | ||
| # Simply forward it to the hass event bus | ||
| hass.bus.async_fire( | ||
| const.EVENT_SCENE_ACTIVATED, | ||
| { | ||
| const.ATTR_NODE_ID: node_id, | ||
| const.ATTR_SCENE_ID: scene_id, | ||
| const.ATTR_SCENE_LABEL: scene_label, | ||
| const.ATTR_SCENE_VALUE_ID: scene_value_id, | ||
| const.ATTR_SCENE_VALUE_LABEL: scene_value_label, | ||
| }, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| """Config flow for zwave_mqtt integration.""" | ||
| from homeassistant import config_entries | ||
|
|
||
| from .const import DOMAIN # pylint:disable=unused-import | ||
|
|
||
| TITLE = "Z-Wave MQTT" | ||
|
|
||
|
|
||
| class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||
| """Handle a config flow for zwave_mqtt.""" | ||
|
|
||
| VERSION = 1 | ||
| CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH | ||
|
|
||
| async def async_step_user(self, user_input=None): | ||
|
MartinHjelmare marked this conversation as resolved.
|
||
| """Handle the initial step.""" | ||
| if self._async_current_entries(): | ||
| return self.async_abort(reason="one_instance_allowed") | ||
| if "mqtt" not in self.hass.config.components: | ||
| return self.async_abort(reason="mqtt_required") | ||
| if user_input is not None: | ||
| return self.async_create_entry(title=TITLE, data={}) | ||
|
|
||
| return self.async_show_form(step_id="user") | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.