Skip to content
Merged
Show file tree
Hide file tree
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 Apr 27, 2020
08348af
Delint
MartinHjelmare Apr 27, 2020
afc69ec
Exclude csv files from pre-commit codespell
MartinHjelmare Apr 27, 2020
61e022d
Copy tests
MartinHjelmare Apr 27, 2020
7d94a3c
Sleep 0 seconds in tests
MartinHjelmare Apr 27, 2020
d286ff7
Fix set config parameter return value
MartinHjelmare Apr 28, 2020
8a0e383
Remove not used service descriptions
MartinHjelmare Apr 28, 2020
1884d33
Improve config flow and add test
MartinHjelmare Apr 28, 2020
83082be
Remove platforms for now except switch
MartinHjelmare Apr 28, 2020
fa35c06
Remove platforms from const
MartinHjelmare Apr 28, 2020
728cd34
Clean up const
MartinHjelmare Apr 28, 2020
b368f14
Add Marcel as code owner
MartinHjelmare Apr 28, 2020
5f2dfff
Clean up discovery
MartinHjelmare Apr 28, 2020
1f5ca08
Use new switch base class
MartinHjelmare Apr 28, 2020
ad5a29b
Clean up switch
MartinHjelmare Apr 28, 2020
5e985c4
Update to latest entity base methods
MartinHjelmare Apr 28, 2020
82d29eb
Polish config flow test name
MartinHjelmare Apr 28, 2020
500e573
Move generic fixture
MartinHjelmare Apr 28, 2020
16f8db1
Test switch receive
MartinHjelmare Apr 28, 2020
e065476
Allow passing in entry to setup helper
MartinHjelmare Apr 28, 2020
a1d0592
Clean up
MartinHjelmare Apr 28, 2020
0219fdf
Add unload entry test
MartinHjelmare Apr 28, 2020
23db18d
Extract load fixture data to pytest fixture
MartinHjelmare Apr 28, 2020
919e612
Bump python-openzwave-mqtt to 0.0.9
MartinHjelmare Apr 28, 2020
ab62c12
Handle node update
MartinHjelmare Apr 29, 2020
5f0830c
Sort listens
MartinHjelmare Apr 30, 2020
4650fc5
Exclude from coverage for now
MartinHjelmare Apr 30, 2020
99044f7
Enhance config flow
MartinHjelmare Apr 30, 2020
c08897e
Add config flow tests
MartinHjelmare Apr 30, 2020
ccaf4db
Fix permissions
MartinHjelmare Apr 30, 2020
b400ba9
Add guard clause to handle remove node
MartinHjelmare Apr 30, 2020
37cc103
Fix dispatch signal domain prefix
MartinHjelmare Apr 30, 2020
5e1a1ae
Fix switch is on
MartinHjelmare Apr 30, 2020
327aca7
Apply suggestions from code review
MartinHjelmare May 1, 2020
a51d983
Add OZW instance to unique id for value
MartinHjelmare May 1, 2020
18bfb57
Remove non essential services for now
MartinHjelmare May 1, 2020
c053778
Rename and prefix with async
MartinHjelmare May 1, 2020
56fb5f7
Removea clear
MartinHjelmare May 1, 2020
9d07552
Use tuples instead of lists in discovery
MartinHjelmare May 1, 2020
f68a90e
Waste less during discovery
MartinHjelmare May 1, 2020
45c0e39
Bump python-openzwave-mqtt to 1.0.1
MartinHjelmare May 1, 2020
1cb669e
Merge branch 'add-zwave_mqtt' of github.com:home-assistant/core into …
MartinHjelmare May 1, 2020
f8adb59
Fix node update
MartinHjelmare May 1, 2020
a7af47a
Use ozw mqtt lib for discovery constants
MartinHjelmare May 1, 2020
45832a0
Clean and fix tests
MartinHjelmare May 1, 2020
94f6488
Scope generic data fixture per session
MartinHjelmare May 1, 2020
d01df22
Clean up mock mqtt message
MartinHjelmare May 1, 2020
28614e6
Sort imports
MartinHjelmare May 1, 2020
d6b564a
Use block till done
MartinHjelmare May 1, 2020
4023d74
Clean wait for platforms
MartinHjelmare May 1, 2020
2f4a69c
Use ozw mqtt ready states constant
MartinHjelmare May 1, 2020
5db5be0
Clean up services
MartinHjelmare May 1, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,10 @@ omit =
homeassistant/components/zoneminder/*
homeassistant/components/supla/*
homeassistant/components/zwave/util.py
homeassistant/components/zwave_mqtt/__init__.py
homeassistant/components/zwave_mqtt/discovery.py
homeassistant/components/zwave_mqtt/entity.py
homeassistant/components/zwave_mqtt/services.py

[report]
# Regexes for lines to exclude from consideration
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ repos:
- id: codespell
args:
- --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing
- --skip="./.*,*.json"
- --skip="./.*,*.csv,*.json"
- --quiet-level=2
exclude_types: [json]
exclude_types: [csv, json]
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
hooks:
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ homeassistant/components/zha/* @dmulcahey @adminiuga
homeassistant/components/zone/* @home-assistant/core
homeassistant/components/zoneminder/* @rohankapoorcom
homeassistant/components/zwave/* @home-assistant/z-wave
homeassistant/components/zwave_mqtt/* @cgarwood @marcelveldt @MartinHjelmare

# Individual files
homeassistant/components/demo/weather @fabaff
333 changes: 333 additions & 0 deletions homeassistant/components/zwave_mqtt/__init__.py
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
Comment thread
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,
Comment thread
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)
Comment thread
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()
Comment thread
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()
Comment thread
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()
Comment thread
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)], [])
Comment thread
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,
},
)
24 changes: 24 additions & 0 deletions homeassistant/components/zwave_mqtt/config_flow.py
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):
Comment thread
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")
Loading