Add zwave mqtt (#34987)

This commit is contained in:
Martin Hjelmare 2020-05-03 02:54:16 +02:00 committed by GitHub
parent 984a2769db
commit aeb891649e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1578 additions and 3 deletions

View file

@ -0,0 +1,328 @@
"""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,
create_device_name,
create_value_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
hass.data[DOMAIN] = {}
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up zwave_mqtt from a config entry."""
zwave_mqtt_data = hass.data[DOMAIN][entry.entry_id] = {}
zwave_mqtt_data[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)
@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(async_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(async_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 = create_value_id(value)
for values in node_data_values:
values.async_check_value(value)
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.async_setup()
# 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,
]:
async_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 = create_value_id(value)
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)
services.async_register()
@callback
def async_receive_message(msg):
manager.receive_message(msg.topic, msg.payload)
async def start_platforms():
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_setup(entry, component)
for component in PLATFORMS
]
)
zwave_mqtt_data[DATA_UNSUBSCRIBE].append(
await mqtt.async_subscribe(
hass, f"{TOPIC_OPENZWAVE}/#", async_receive_message
)
)
hass.async_create_task(start_platforms())
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].pop(entry.entry_id)
return True
async def async_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 async_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
dev_id = create_device_id(node)
device = dev_registry.async_get_device({(DOMAIN, dev_id)}, set())
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
dev_name = create_device_name(node)
dev_registry.async_update_device(
item.id,
manufacturer=node.node_manufacturer_name,
model=node.node_product_name,
name=dev_name,
)
@callback
def async_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,
},
)

View file

@ -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):
"""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")

View file

@ -0,0 +1,43 @@
"""Constants for the zwave_mqtt integration."""
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
DOMAIN = "zwave_mqtt"
DATA_UNSUBSCRIBE = "unsubscribe"
PLATFORMS = [SWITCH_DOMAIN]
# MQTT Topics
TOPIC_OPENZWAVE = "OpenZWave"
# Common Attributes
ATTR_INSTANCE_ID = "instance_id"
ATTR_SECURE = "secure"
ATTR_NODE_ID = "node_id"
ATTR_SCENE_ID = "scene_id"
ATTR_SCENE_LABEL = "scene_label"
ATTR_SCENE_VALUE_ID = "scene_value_id"
ATTR_SCENE_VALUE_LABEL = "scene_value_label"
# Service specific
SERVICE_ADD_NODE = "add_node"
SERVICE_REMOVE_NODE = "remove_node"
# Home Assistant Events
EVENT_SCENE_ACTIVATED = f"{DOMAIN}.scene_activated"
# Signals
SIGNAL_DELETE_ENTITY = f"{DOMAIN}_delete_entity"
# Discovery Information
DISC_COMMAND_CLASS = "command_class"
DISC_COMPONENT = "component"
DISC_GENERIC_DEVICE_CLASS = "generic_device_class"
DISC_GENRE = "genre"
DISC_INDEX = "index"
DISC_INSTANCE = "instance"
DISC_NODE_ID = "node_id"
DISC_OPTIONAL = "optional"
DISC_PRIMARY = "primary"
DISC_SCHEMAS = "schemas"
DISC_SPECIFIC_DEVICE_CLASS = "specific_device_class"
DISC_TYPE = "type"
DISC_VALUES = "values"

View file

@ -0,0 +1,83 @@
"""Map Z-Wave nodes and values to Home Assistant entities."""
import openzwavemqtt.const as const_ozw
from openzwavemqtt.const import CommandClass, ValueGenre, ValueType
from . import const
DISCOVERY_SCHEMAS = (
{ # Switch platform
const.DISC_COMPONENT: "switch",
const.DISC_GENERIC_DEVICE_CLASS: (
const_ozw.GENERIC_TYPE_METER,
const_ozw.GENERIC_TYPE_SENSOR_ALARM,
const_ozw.GENERIC_TYPE_SENSOR_BINARY,
const_ozw.GENERIC_TYPE_SWITCH_BINARY,
const_ozw.GENERIC_TYPE_ENTRY_CONTROL,
const_ozw.GENERIC_TYPE_SENSOR_MULTILEVEL,
const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL,
const_ozw.GENERIC_TYPE_GENERIC_CONTROLLER,
const_ozw.GENERIC_TYPE_SWITCH_REMOTE,
const_ozw.GENERIC_TYPE_REPEATER_SLAVE,
const_ozw.GENERIC_TYPE_THERMOSTAT,
const_ozw.GENERIC_TYPE_WALL_CONTROLLER,
),
const.DISC_VALUES: {
const.DISC_PRIMARY: {
const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_BINARY,),
const.DISC_TYPE: ValueType.BOOL,
const.DISC_GENRE: ValueGenre.USER,
}
},
},
)
def check_node_schema(node, schema):
"""Check if node matches the passed node schema."""
if const.DISC_NODE_ID in schema and node.node_id not in schema[const.DISC_NODE_ID]:
return False
if const.DISC_GENERIC_DEVICE_CLASS in schema and not eq_or_in(
node.node_generic, schema[const.DISC_GENERIC_DEVICE_CLASS]
):
return False
if const.DISC_SPECIFIC_DEVICE_CLASS in schema and not eq_or_in(
node.node_specific, schema[const.DISC_SPECIFIC_DEVICE_CLASS]
):
return False
return True
def check_value_schema(value, schema):
"""Check if the value matches the passed value schema."""
if (
const.DISC_COMMAND_CLASS in schema
and value.parent.command_class_id not in schema[const.DISC_COMMAND_CLASS]
):
return False
if const.DISC_TYPE in schema and not eq_or_in(value.type, schema[const.DISC_TYPE]):
return False
if const.DISC_GENRE in schema and not eq_or_in(
value.genre, schema[const.DISC_GENRE]
):
return False
if const.DISC_INDEX in schema and not eq_or_in(
value.index, schema[const.DISC_INDEX]
):
return False
if const.DISC_INSTANCE in schema and not eq_or_in(
value.instance, schema[const.DISC_INSTANCE]
):
return False
if const.DISC_SCHEMAS in schema:
found = False
for schema_item in schema[const.DISC_SCHEMAS]:
found = found or check_value_schema(value, schema_item)
if not found:
return False
return True
def eq_or_in(val, options):
"""Return True if options contains value or if value is equal to options."""
return val in options if isinstance(options, tuple) else val == options

View file

@ -0,0 +1,289 @@
"""Generic Z-Wave Entity Classes."""
import copy
import logging
from openzwavemqtt.const import (
EVENT_INSTANCE_STATUS_CHANGED,
EVENT_VALUE_CHANGED,
OZW_READY_STATES,
)
from openzwavemqtt.models.node import OZWNode
from openzwavemqtt.models.value import OZWValue
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from . import const
from .const import DOMAIN, PLATFORMS
from .discovery import check_node_schema, check_value_schema
_LOGGER = logging.getLogger(__name__)
class ZWaveDeviceEntityValues:
"""Manages entity access to the underlying Z-Wave value objects."""
def __init__(self, hass, options, schema, primary_value):
"""Initialize the values object with the passed entity schema."""
self._hass = hass
self._entity_created = False
self._schema = copy.deepcopy(schema)
self._values = {}
self.options = options
# Go through values listed in the discovery schema, initialize them,
# and add a check to the schema to make sure the Instance matches.
for name, disc_settings in self._schema[const.DISC_VALUES].items():
self._values[name] = None
disc_settings[const.DISC_INSTANCE] = [primary_value.instance]
self._values[const.DISC_PRIMARY] = primary_value
self._node = primary_value.node
self._schema[const.DISC_NODE_ID] = [self._node.node_id]
def async_setup(self):
"""Set up values instance."""
# Check values that have already been discovered for node
# and see if they match the schema and need added to the entity.
for value in self._node.values():
self.async_check_value(value)
# Check if all the _required_ values in the schema are present and
# create the entity.
self._async_check_entity_ready()
def __getattr__(self, name):
"""Get the specified value for this entity."""
return self._values.get(name, None)
def __iter__(self):
"""Allow iteration over all values."""
return iter(self._values.values())
def __contains__(self, name):
"""Check if the specified name/key exists in the values."""
return name in self._values
@callback
def async_check_value(self, value):
"""Check if the new value matches a missing value for this entity.
If a match is found, it is added to the values mapping.
"""
# Make sure the node matches the schema for this entity.
if not check_node_schema(value.node, self._schema):
return
# Go through the possible values for this entity defined by the schema.
for name in self._values:
# Skip if it's already been added.
if self._values[name] is not None:
continue
# Skip if the value doesn't match the schema.
if not check_value_schema(value, self._schema[const.DISC_VALUES][name]):
continue
# Add value to mapping.
self._values[name] = value
# If the entity has already been created, notify it of the new value.
if self._entity_created:
async_dispatcher_send(
self._hass, f"{DOMAIN}_{self.values_id}_value_added"
)
# Check if entity has all required values and create the entity if needed.
self._async_check_entity_ready()
@callback
def _async_check_entity_ready(self):
"""Check if all required values are discovered and create entity."""
# Abort if the entity has already been created
if self._entity_created:
return
# Go through values defined in the schema and abort if a required value is missing.
for name, disc_settings in self._schema[const.DISC_VALUES].items():
if self._values[name] is None and not disc_settings.get(
const.DISC_OPTIONAL
):
return
# We have all the required values, so create the entity.
component = self._schema[const.DISC_COMPONENT]
_LOGGER.debug(
"Adding Node_id=%s Generic_command_class=%s, "
"Specific_command_class=%s, "
"Command_class=%s, Index=%s, Value type=%s, "
"Genre=%s as %s",
self._node.node_id,
self._node.node_generic,
self._node.node_specific,
self.primary.command_class,
self.primary.index,
self.primary.type,
self.primary.genre,
component,
)
self._entity_created = True
if component in PLATFORMS:
async_dispatcher_send(self._hass, f"{DOMAIN}_new_{component}", self)
@property
def values_id(self):
"""Identification for this values collection."""
return create_value_id(self.primary)
class ZWaveDeviceEntity(Entity):
"""Generic Entity Class for a Z-Wave Device."""
def __init__(self, values):
"""Initialize a generic Z-Wave device entity."""
self.values = values
self.options = values.options
@callback
def on_value_update(self):
"""Call when a value is added/updated in the entity EntityValues Collection.
To be overridden by platforms needing this event.
"""
async def async_added_to_hass(self):
"""Call when entity is added."""
# add dispatcher and OZW listeners callbacks,
self.options.listen(EVENT_VALUE_CHANGED, self._value_changed)
self.options.listen(EVENT_INSTANCE_STATUS_CHANGED, self._instance_updated)
# add to on_remove so they will be cleaned up on entity removal
self.async_on_remove(
async_dispatcher_connect(
self.hass, const.SIGNAL_DELETE_ENTITY, self._delete_callback
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self.values.values_id}_value_added",
self._value_added,
)
)
@property
def device_info(self):
"""Return device information for the device registry."""
node = self.values.primary.node
node_instance = self.values.primary.instance
dev_id = create_device_id(node, self.values.primary.instance)
device_info = {
"identifiers": {(DOMAIN, dev_id)},
"name": create_device_name(node),
"manufacturer": node.node_manufacturer_name,
"model": node.node_product_name,
}
# device with multiple instances is split up into virtual devices for each instance
if node_instance > 1:
parent_dev_id = create_device_id(node)
device_info["name"] += f" - Instance {node_instance}"
device_info["via_device"] = (DOMAIN, parent_dev_id)
return device_info
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {const.ATTR_NODE_ID: self.values.primary.node.node_id}
@property
def name(self):
"""Return the name of the entity."""
node = self.values.primary.node
return f"{create_device_name(node)}: {self.values.primary.label}"
@property
def unique_id(self):
"""Return the unique_id of the entity."""
return self.values.values_id
@property
def available(self) -> bool:
"""Return entity availability."""
# Use OZW Daemon status for availability.
instance_status = self.values.primary.ozw_instance.get_status()
return instance_status and instance_status.status in (
state.value for state in OZW_READY_STATES
)
@callback
def _value_changed(self, value):
"""Call when a value from ZWaveDeviceEntityValues is changed.
Should not be overridden by subclasses.
"""
if value.value_id_key in (v.value_id_key for v in self.values if v):
self.on_value_update()
self.async_write_ha_state()
@callback
def _value_added(self):
"""Call when a value from ZWaveDeviceEntityValues is added.
Should not be overridden by subclasses.
"""
self.on_value_update()
@callback
def _instance_updated(self, new_status):
"""Call when the instance status changes.
Should not be overridden by subclasses.
"""
self.on_value_update()
self.async_write_ha_state()
async def _delete_callback(self, values_id):
"""Remove this entity."""
if not self.values:
return # race condition: delete already requested
if values_id == self.values.values_id:
await self.async_remove()
async def async_will_remove_from_hass(self) -> None:
"""Call when entity will be removed from hass."""
# cleanup OZW listeners
self.options.listeners[EVENT_VALUE_CHANGED].remove(self._value_changed)
self.options.listeners[EVENT_INSTANCE_STATUS_CHANGED].remove(
self._instance_updated
)
def create_device_name(node: OZWNode):
"""Generate sensible (short) default device name from a OZWNode."""
if node.meta_data["Name"]:
dev_name = node.meta_data["Name"]
elif node.node_product_name:
dev_name = node.node_product_name
elif node.node_device_type_string:
dev_name = node.node_device_type_string
else:
dev_name = node.specific_string
return dev_name
def create_device_id(node: OZWNode, node_instance: int = 1):
"""Generate unique device_id from a OZWNode."""
ozw_instance = node.parent.id
dev_id = f"{ozw_instance}.{node.node_id}.{node_instance}"
return dev_id
def create_value_id(value: OZWValue):
"""Generate unique value_id from an OZWValue."""
# [OZW_INSTANCE_ID]-[NODE_ID]-[VALUE_ID_KEY]
return f"{value.node.parent.id}-{value.node.id}-{value.value_id_key}"

View file

@ -0,0 +1,17 @@
{
"domain": "zwave_mqtt",
"name": "Z-Wave over MQTT",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_mqtt",
"requirements": [
"python-openzwave-mqtt==1.0.1"
],
"after_dependencies": [
"mqtt"
],
"codeowners": [
"@cgarwood",
"@marcelveldt",
"@MartinHjelmare"
]
}

View file

@ -0,0 +1,53 @@
"""Methods and classes related to executing Z-Wave commands and publishing these to hass."""
import voluptuous as vol
from homeassistant.core import callback
from . import const
class ZWaveServices:
"""Class that holds our services ( Zwave Commands) that should be published to hass."""
def __init__(self, hass, manager):
"""Initialize with both hass and ozwmanager objects."""
self._hass = hass
self._manager = manager
@callback
def async_register(self):
"""Register all our services."""
self._hass.services.async_register(
const.DOMAIN,
const.SERVICE_ADD_NODE,
self.async_add_node,
schema=vol.Schema(
{
vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int),
vol.Optional(const.ATTR_SECURE, default=False): vol.Coerce(bool),
}
),
)
self._hass.services.async_register(
const.DOMAIN,
const.SERVICE_REMOVE_NODE,
self.async_remove_node,
schema=vol.Schema(
{vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int)}
),
)
@callback
def async_add_node(self, service):
"""Enter inclusion mode on the controller."""
instance_id = service.data[const.ATTR_INSTANCE_ID]
secure = service.data[const.ATTR_SECURE]
instance = self._manager.get_instance(instance_id)
instance.add_node(secure)
@callback
def async_remove_node(self, service):
"""Enter exclusion mode on the controller."""
instance_id = service.data[const.ATTR_INSTANCE_ID]
instance = self._manager.get_instance(instance_id)
instance.remove_node()

View file

@ -0,0 +1,14 @@
# Describes the format for available Z-Wave services
add_node:
description: Add a new node to the Z-Wave network.
fields:
secure:
description: Add the new node with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices.
instance_id:
description: (Optional) The OZW Instance/Controller to use, defaults to 1.
remove_node:
description: Remove a node from the Z-Wave network. Will set the controller into exclusion mode.
fields:
instance_id:
description: (Optional) The OZW Instance/Controller to use, defaults to 1.

View file

@ -0,0 +1,14 @@
{
"title": "Z-Wave over MQTT",
"config": {
"step": {
"user": {
"title": "Confirm set up"
}
},
"abort": {
"one_instance_allowed": "The integration only supports one Z-Wave instance",
"mqtt_required": "The MQTT integration is not set up"
}
}
}

View file

@ -0,0 +1,41 @@
"""Representation of Z-Wave switches."""
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import DATA_UNSUBSCRIBE, DOMAIN
from .entity import ZWaveDeviceEntity
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Z-Wave switch from config entry."""
@callback
def async_add_switch(value):
"""Add Z-Wave Switch."""
switch = ZWaveSwitch(value)
async_add_entities([switch])
hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
async_dispatcher_connect(
hass, f"{DOMAIN}_new_{SWITCH_DOMAIN}", async_add_switch
)
)
class ZWaveSwitch(ZWaveDeviceEntity, SwitchEntity):
"""Representation of a Z-Wave switch."""
@property
def is_on(self):
"""Return a boolean for the state of the switch."""
return bool(self.values.primary.value)
async def async_turn_on(self, **kwargs):
"""Turn the switch on."""
self.values.primary.send_value(True)
async def async_turn_off(self, **kwargs):
"""Turn the switch off."""
self.values.primary.send_value(False)

View file

@ -0,0 +1,14 @@
{
"config": {
"abort": {
"mqtt_required": "The MQTT integration is not set up",
"one_instance_allowed": "The integration only supports one Z-Wave instance"
},
"step": {
"user": {
"title": "Confirm set up"
}
}
},
"title": "Z-Wave over MQTT"
}