* Allow string values on zwave.set_node_value This allows for: * Accessing longer value_ids. In some cases, value ids in z-wave nodes are very large (17 digits in my case). Passing them as int does not seem to work well (python probably truncates the number), but passing them as string works fine * Changing color values, which are represented as hex string reformat test * update services.yaml with string set_node_value
1331 lines
46 KiB
Python
1331 lines
46 KiB
Python
"""Support for Z-Wave."""
|
|
import asyncio
|
|
import copy
|
|
from importlib import import_module
|
|
import logging
|
|
from pprint import pprint
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant import config_entries
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_ID,
|
|
EVENT_HOMEASSISTANT_START,
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
)
|
|
from homeassistant.core import CoreState, callback
|
|
from homeassistant.helpers import discovery
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.device_registry import (
|
|
async_get_registry as async_get_device_registry,
|
|
)
|
|
from homeassistant.helpers.dispatcher import (
|
|
async_dispatcher_connect,
|
|
async_dispatcher_send,
|
|
)
|
|
from homeassistant.helpers.entity import generate_entity_id
|
|
from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL
|
|
from homeassistant.helpers.entity_platform import EntityPlatform
|
|
from homeassistant.helpers.entity_registry import (
|
|
async_get_registry as async_get_entity_registry,
|
|
)
|
|
from homeassistant.helpers.entity_values import EntityValues
|
|
from homeassistant.helpers.event import async_track_time_change
|
|
from homeassistant.util import convert
|
|
import homeassistant.util.dt as dt_util
|
|
|
|
from . import config_flow # noqa: F401 pylint: disable=unused-import
|
|
from . import const, websocket_api as wsapi, workaround
|
|
from .const import (
|
|
CONF_AUTOHEAL,
|
|
CONF_CONFIG_PATH,
|
|
CONF_DEBUG,
|
|
CONF_NETWORK_KEY,
|
|
CONF_POLLING_INTERVAL,
|
|
CONF_USB_STICK_PATH,
|
|
DATA_DEVICES,
|
|
DATA_ENTITY_VALUES,
|
|
DATA_NETWORK,
|
|
DATA_ZWAVE_CONFIG,
|
|
DEFAULT_CONF_AUTOHEAL,
|
|
DEFAULT_CONF_USB_STICK_PATH,
|
|
DEFAULT_DEBUG,
|
|
DEFAULT_POLLING_INTERVAL,
|
|
DOMAIN,
|
|
)
|
|
from .discovery_schemas import DISCOVERY_SCHEMAS
|
|
from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity
|
|
from .util import (
|
|
check_has_unique_id,
|
|
check_node_schema,
|
|
check_value_schema,
|
|
is_node_parsed,
|
|
node_device_id_and_name,
|
|
node_name,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CLASS_ID = "class_id"
|
|
|
|
ATTR_POWER = "power_consumption"
|
|
|
|
CONF_POLLING_INTENSITY = "polling_intensity"
|
|
CONF_IGNORED = "ignored"
|
|
CONF_INVERT_OPENCLOSE_BUTTONS = "invert_openclose_buttons"
|
|
CONF_INVERT_PERCENT = "invert_percent"
|
|
CONF_REFRESH_VALUE = "refresh_value"
|
|
CONF_REFRESH_DELAY = "delay"
|
|
CONF_DEVICE_CONFIG = "device_config"
|
|
CONF_DEVICE_CONFIG_GLOB = "device_config_glob"
|
|
CONF_DEVICE_CONFIG_DOMAIN = "device_config_domain"
|
|
|
|
DEFAULT_CONF_IGNORED = False
|
|
DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False
|
|
DEFAULT_CONF_INVERT_PERCENT = False
|
|
DEFAULT_CONF_REFRESH_VALUE = False
|
|
DEFAULT_CONF_REFRESH_DELAY = 5
|
|
|
|
SUPPORTED_PLATFORMS = [
|
|
"binary_sensor",
|
|
"climate",
|
|
"cover",
|
|
"fan",
|
|
"lock",
|
|
"light",
|
|
"sensor",
|
|
"switch",
|
|
]
|
|
|
|
RENAME_NODE_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
|
|
vol.Required(const.ATTR_NAME): cv.string,
|
|
vol.Optional(const.ATTR_UPDATE_IDS, default=False): cv.boolean,
|
|
}
|
|
)
|
|
|
|
RENAME_VALUE_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
|
|
vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int),
|
|
vol.Required(const.ATTR_NAME): cv.string,
|
|
vol.Optional(const.ATTR_UPDATE_IDS, default=False): cv.boolean,
|
|
}
|
|
)
|
|
|
|
SET_CONFIG_PARAMETER_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
|
|
vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int),
|
|
vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(vol.Coerce(int), cv.string),
|
|
vol.Optional(const.ATTR_CONFIG_SIZE, default=2): vol.Coerce(int),
|
|
}
|
|
)
|
|
|
|
SET_NODE_VALUE_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
|
|
vol.Required(const.ATTR_VALUE_ID): vol.Any(vol.Coerce(int), cv.string),
|
|
vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(vol.Coerce(int), cv.string),
|
|
}
|
|
)
|
|
|
|
REFRESH_NODE_VALUE_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
|
|
vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int),
|
|
}
|
|
)
|
|
|
|
SET_POLL_INTENSITY_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
|
|
vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int),
|
|
vol.Required(const.ATTR_POLL_INTENSITY): vol.Coerce(int),
|
|
}
|
|
)
|
|
|
|
PRINT_CONFIG_PARAMETER_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
|
|
vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int),
|
|
}
|
|
)
|
|
|
|
NODE_SERVICE_SCHEMA = vol.Schema({vol.Required(const.ATTR_NODE_ID): vol.Coerce(int)})
|
|
|
|
REFRESH_ENTITY_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_id})
|
|
|
|
RESET_NODE_METERS_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
|
|
vol.Optional(const.ATTR_INSTANCE, default=1): vol.Coerce(int),
|
|
}
|
|
)
|
|
|
|
CHANGE_ASSOCIATION_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(const.ATTR_ASSOCIATION): cv.string,
|
|
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
|
|
vol.Required(const.ATTR_TARGET_NODE_ID): vol.Coerce(int),
|
|
vol.Required(const.ATTR_GROUP): vol.Coerce(int),
|
|
vol.Optional(const.ATTR_INSTANCE, default=0x00): vol.Coerce(int),
|
|
}
|
|
)
|
|
|
|
SET_WAKEUP_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
|
|
vol.Required(const.ATTR_CONFIG_VALUE): vol.All(
|
|
vol.Coerce(int), cv.positive_int
|
|
),
|
|
}
|
|
)
|
|
|
|
HEAL_NODE_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
|
|
vol.Optional(const.ATTR_RETURN_ROUTES, default=False): cv.boolean,
|
|
}
|
|
)
|
|
|
|
TEST_NODE_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
|
|
vol.Optional(const.ATTR_MESSAGES, default=1): cv.positive_int,
|
|
}
|
|
)
|
|
|
|
|
|
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema(
|
|
{
|
|
vol.Optional(CONF_POLLING_INTENSITY): cv.positive_int,
|
|
vol.Optional(CONF_IGNORED, default=DEFAULT_CONF_IGNORED): cv.boolean,
|
|
vol.Optional(
|
|
CONF_INVERT_OPENCLOSE_BUTTONS, default=DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS
|
|
): cv.boolean,
|
|
vol.Optional(
|
|
CONF_INVERT_PERCENT, default=DEFAULT_CONF_INVERT_PERCENT
|
|
): cv.boolean,
|
|
vol.Optional(
|
|
CONF_REFRESH_VALUE, default=DEFAULT_CONF_REFRESH_VALUE
|
|
): cv.boolean,
|
|
vol.Optional(
|
|
CONF_REFRESH_DELAY, default=DEFAULT_CONF_REFRESH_DELAY
|
|
): cv.positive_int,
|
|
}
|
|
)
|
|
|
|
SIGNAL_REFRESH_ENTITY_FORMAT = "zwave_refresh_entity_{}"
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
DOMAIN: vol.Schema(
|
|
{
|
|
vol.Optional(CONF_AUTOHEAL, default=DEFAULT_CONF_AUTOHEAL): cv.boolean,
|
|
vol.Optional(CONF_CONFIG_PATH): cv.string,
|
|
vol.Optional(CONF_NETWORK_KEY): vol.All(
|
|
cv.string, vol.Match(r"(0x\w\w,\s?){15}0x\w\w")
|
|
),
|
|
vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema(
|
|
{cv.entity_id: DEVICE_CONFIG_SCHEMA_ENTRY}
|
|
),
|
|
vol.Optional(CONF_DEVICE_CONFIG_GLOB, default={}): vol.Schema(
|
|
{cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}
|
|
),
|
|
vol.Optional(CONF_DEVICE_CONFIG_DOMAIN, default={}): vol.Schema(
|
|
{cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}
|
|
),
|
|
vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean,
|
|
vol.Optional(
|
|
CONF_POLLING_INTERVAL, default=DEFAULT_POLLING_INTERVAL
|
|
): cv.positive_int,
|
|
vol.Optional(CONF_USB_STICK_PATH): cv.string,
|
|
}
|
|
)
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
|
|
def _obj_to_dict(obj):
|
|
"""Convert an object into a hash for debug."""
|
|
return {
|
|
key: getattr(obj, key)
|
|
for key in dir(obj)
|
|
if key[0] != "_" and not callable(getattr(obj, key))
|
|
}
|
|
|
|
|
|
def _value_name(value):
|
|
"""Return the name of the value."""
|
|
return "{} {}".format(node_name(value.node), value.label).strip()
|
|
|
|
|
|
def nice_print_node(node):
|
|
"""Print a nice formatted node to the output (debug method)."""
|
|
node_dict = _obj_to_dict(node)
|
|
node_dict["values"] = {
|
|
value_id: _obj_to_dict(value) for value_id, value in node.values.items()
|
|
}
|
|
|
|
_LOGGER.info("FOUND NODE %s \n%s", node.product_name, node_dict)
|
|
|
|
|
|
def get_config_value(node, value_index, tries=5):
|
|
"""Return the current configuration value for a specific index."""
|
|
try:
|
|
for value in node.values.values():
|
|
if (
|
|
value.command_class == const.COMMAND_CLASS_CONFIGURATION
|
|
and value.index == value_index
|
|
):
|
|
return value.data
|
|
except RuntimeError:
|
|
# If we get a runtime error the dict has changed while
|
|
# we was looking for a value, just do it again
|
|
return (
|
|
None if tries <= 0 else get_config_value(node, value_index, tries=tries - 1)
|
|
)
|
|
return None
|
|
|
|
|
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
|
"""Set up the Z-Wave platform (generic part)."""
|
|
if discovery_info is None or DATA_NETWORK not in hass.data:
|
|
return False
|
|
|
|
device = hass.data[DATA_DEVICES].get(discovery_info[const.DISCOVERY_DEVICE], None)
|
|
if device is None:
|
|
return False
|
|
|
|
async_add_entities([device])
|
|
return True
|
|
|
|
|
|
async def async_setup(hass, config):
|
|
"""Set up Z-Wave components."""
|
|
if DOMAIN not in config:
|
|
return True
|
|
|
|
conf = config[DOMAIN]
|
|
hass.data[DATA_ZWAVE_CONFIG] = conf
|
|
|
|
if not hass.config_entries.async_entries(DOMAIN):
|
|
hass.async_create_task(
|
|
hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_IMPORT},
|
|
data={
|
|
CONF_USB_STICK_PATH: conf.get(
|
|
CONF_USB_STICK_PATH, DEFAULT_CONF_USB_STICK_PATH
|
|
),
|
|
CONF_NETWORK_KEY: conf.get(CONF_NETWORK_KEY),
|
|
},
|
|
)
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
async def async_setup_entry(hass, config_entry):
|
|
"""Set up Z-Wave from a config entry.
|
|
|
|
Will automatically load components to support devices found on the network.
|
|
"""
|
|
from pydispatch import dispatcher
|
|
|
|
# pylint: disable=import-error
|
|
from openzwave.option import ZWaveOption
|
|
from openzwave.network import ZWaveNetwork
|
|
from openzwave.group import ZWaveGroup
|
|
|
|
# Merge config entry and yaml config
|
|
config = config_entry.data
|
|
if DATA_ZWAVE_CONFIG in hass.data:
|
|
config = {**config, **hass.data[DATA_ZWAVE_CONFIG]}
|
|
|
|
# Update hass.data with merged config so we can access it elsewhere
|
|
hass.data[DATA_ZWAVE_CONFIG] = config
|
|
|
|
# Load configuration
|
|
use_debug = config.get(CONF_DEBUG, DEFAULT_DEBUG)
|
|
autoheal = config.get(CONF_AUTOHEAL, DEFAULT_CONF_AUTOHEAL)
|
|
device_config = EntityValues(
|
|
config.get(CONF_DEVICE_CONFIG),
|
|
config.get(CONF_DEVICE_CONFIG_DOMAIN),
|
|
config.get(CONF_DEVICE_CONFIG_GLOB),
|
|
)
|
|
|
|
usb_path = config[CONF_USB_STICK_PATH]
|
|
|
|
_LOGGER.info("Z-Wave USB path is %s", usb_path)
|
|
|
|
# Setup options
|
|
options = ZWaveOption(
|
|
usb_path,
|
|
user_path=hass.config.config_dir,
|
|
config_path=config.get(CONF_CONFIG_PATH),
|
|
)
|
|
|
|
options.set_console_output(use_debug)
|
|
|
|
if config.get(CONF_NETWORK_KEY):
|
|
options.addOption("NetworkKey", config[CONF_NETWORK_KEY])
|
|
|
|
await hass.async_add_executor_job(options.lock)
|
|
network = hass.data[DATA_NETWORK] = ZWaveNetwork(options, autostart=False)
|
|
hass.data[DATA_DEVICES] = {}
|
|
hass.data[DATA_ENTITY_VALUES] = []
|
|
|
|
registry = await async_get_entity_registry(hass)
|
|
|
|
wsapi.async_load_websocket_api(hass)
|
|
|
|
if use_debug: # pragma: no cover
|
|
|
|
def log_all(signal, value=None):
|
|
"""Log all the signals."""
|
|
print("")
|
|
print("SIGNAL *****", signal)
|
|
if value and signal in (
|
|
ZWaveNetwork.SIGNAL_VALUE_CHANGED,
|
|
ZWaveNetwork.SIGNAL_VALUE_ADDED,
|
|
ZWaveNetwork.SIGNAL_SCENE_EVENT,
|
|
ZWaveNetwork.SIGNAL_NODE_EVENT,
|
|
ZWaveNetwork.SIGNAL_AWAKE_NODES_QUERIED,
|
|
ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED,
|
|
ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED_SOME_DEAD,
|
|
):
|
|
pprint(_obj_to_dict(value))
|
|
|
|
print("")
|
|
|
|
dispatcher.connect(log_all, weak=False)
|
|
|
|
def value_added(node, value):
|
|
"""Handle new added value to a node on the network."""
|
|
# Check if this value should be tracked by an existing entity
|
|
for values in hass.data[DATA_ENTITY_VALUES]:
|
|
values.check_value(value)
|
|
|
|
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, schema, value, config, device_config, registry
|
|
)
|
|
|
|
# We create a new list and update the reference here so that
|
|
# the list can be safely iterated over in the main thread
|
|
new_values = hass.data[DATA_ENTITY_VALUES] + [values]
|
|
hass.data[DATA_ENTITY_VALUES] = new_values
|
|
|
|
platform = EntityPlatform(
|
|
hass=hass,
|
|
logger=_LOGGER,
|
|
domain=DOMAIN,
|
|
platform_name=DOMAIN,
|
|
platform=None,
|
|
scan_interval=DEFAULT_SCAN_INTERVAL,
|
|
entity_namespace=None,
|
|
)
|
|
platform.config_entry = config_entry
|
|
|
|
def node_added(node):
|
|
"""Handle a new node on the network."""
|
|
entity = ZWaveNodeEntity(node, network)
|
|
|
|
async def _add_node_to_component():
|
|
if hass.data[DATA_DEVICES].get(entity.unique_id):
|
|
return
|
|
|
|
name = node_name(node)
|
|
generated_id = generate_entity_id(DOMAIN + ".{}", name, [])
|
|
node_config = device_config.get(generated_id)
|
|
if node_config.get(CONF_IGNORED):
|
|
_LOGGER.info(
|
|
"Ignoring node entity %s due to device settings", generated_id
|
|
)
|
|
return
|
|
|
|
hass.data[DATA_DEVICES][entity.unique_id] = entity
|
|
await platform.async_add_entities([entity])
|
|
|
|
if entity.unique_id:
|
|
hass.async_add_job(_add_node_to_component())
|
|
return
|
|
|
|
@callback
|
|
def _on_ready(sec):
|
|
_LOGGER.info("Z-Wave node %d ready after %d seconds", entity.node_id, sec)
|
|
hass.async_add_job(_add_node_to_component)
|
|
|
|
@callback
|
|
def _on_timeout(sec):
|
|
_LOGGER.warning(
|
|
"Z-Wave node %d not ready after %d seconds, continuing anyway",
|
|
entity.node_id,
|
|
sec,
|
|
)
|
|
hass.async_add_job(_add_node_to_component)
|
|
|
|
hass.add_job(check_has_unique_id, entity, _on_ready, _on_timeout)
|
|
|
|
def node_removed(node):
|
|
node_id = node.node_id
|
|
node_key = f"node-{node_id}"
|
|
for key in list(hass.data[DATA_DEVICES]):
|
|
if key is None:
|
|
continue
|
|
if not key.startswith(f"{node_id}-"):
|
|
continue
|
|
|
|
entity = hass.data[DATA_DEVICES][key]
|
|
_LOGGER.debug(
|
|
"Removing Entity - value: %s - entity_id: %s", key, entity.entity_id
|
|
)
|
|
hass.add_job(entity.node_removed())
|
|
del hass.data[DATA_DEVICES][key]
|
|
|
|
entity = hass.data[DATA_DEVICES][node_key]
|
|
hass.add_job(entity.node_removed())
|
|
del hass.data[DATA_DEVICES][node_key]
|
|
|
|
hass.add_job(_remove_device(node))
|
|
|
|
async def _remove_device(node):
|
|
dev_reg = await async_get_device_registry(hass)
|
|
identifier, name = node_device_id_and_name(node)
|
|
device = dev_reg.async_get_device(identifiers={identifier}, connections=set())
|
|
if device is not None:
|
|
_LOGGER.debug("Removing Device - %s - %s", device.id, name)
|
|
dev_reg.async_remove_device(device.id)
|
|
|
|
def network_ready():
|
|
"""Handle the query of all awake nodes."""
|
|
_LOGGER.info(
|
|
"Z-Wave network is ready for use. All awake nodes "
|
|
"have been queried. Sleeping nodes will be "
|
|
"queried when they awake."
|
|
)
|
|
hass.bus.fire(const.EVENT_NETWORK_READY)
|
|
|
|
def network_complete():
|
|
"""Handle the querying of all nodes on network."""
|
|
_LOGGER.info(
|
|
"Z-Wave network is complete. All nodes on the network have been queried"
|
|
)
|
|
hass.bus.fire(const.EVENT_NETWORK_COMPLETE)
|
|
|
|
def network_complete_some_dead():
|
|
"""Handle the querying of all nodes on network."""
|
|
_LOGGER.info(
|
|
"Z-Wave network is complete. All nodes on the network "
|
|
"have been queried, but some nodes are marked dead"
|
|
)
|
|
hass.bus.fire(const.EVENT_NETWORK_COMPLETE_SOME_DEAD)
|
|
|
|
dispatcher.connect(value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False)
|
|
dispatcher.connect(node_added, ZWaveNetwork.SIGNAL_NODE_ADDED, weak=False)
|
|
dispatcher.connect(node_removed, ZWaveNetwork.SIGNAL_NODE_REMOVED, weak=False)
|
|
dispatcher.connect(
|
|
network_ready, ZWaveNetwork.SIGNAL_AWAKE_NODES_QUERIED, weak=False
|
|
)
|
|
dispatcher.connect(
|
|
network_complete, ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED, weak=False
|
|
)
|
|
dispatcher.connect(
|
|
network_complete_some_dead,
|
|
ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED_SOME_DEAD,
|
|
weak=False,
|
|
)
|
|
|
|
def add_node(service):
|
|
"""Switch into inclusion mode."""
|
|
_LOGGER.info("Z-Wave add_node have been initialized")
|
|
network.controller.add_node()
|
|
|
|
def add_node_secure(service):
|
|
"""Switch into secure inclusion mode."""
|
|
_LOGGER.info("Z-Wave add_node_secure have been initialized")
|
|
network.controller.add_node(True)
|
|
|
|
def remove_node(service):
|
|
"""Switch into exclusion mode."""
|
|
_LOGGER.info("Z-Wave remove_node have been initialized")
|
|
network.controller.remove_node()
|
|
|
|
def cancel_command(service):
|
|
"""Cancel a running controller command."""
|
|
_LOGGER.info("Cancel running Z-Wave command")
|
|
network.controller.cancel_command()
|
|
|
|
def heal_network(service):
|
|
"""Heal the network."""
|
|
_LOGGER.info("Z-Wave heal running")
|
|
network.heal()
|
|
|
|
def soft_reset(service):
|
|
"""Soft reset the controller."""
|
|
_LOGGER.info("Z-Wave soft_reset have been initialized")
|
|
network.controller.soft_reset()
|
|
|
|
def test_network(service):
|
|
"""Test the network by sending commands to all the nodes."""
|
|
_LOGGER.info("Z-Wave test_network have been initialized")
|
|
network.test()
|
|
|
|
def stop_network(_service_or_event):
|
|
"""Stop Z-Wave network."""
|
|
_LOGGER.info("Stopping Z-Wave network")
|
|
network.stop()
|
|
if hass.state == CoreState.running:
|
|
hass.bus.fire(const.EVENT_NETWORK_STOP)
|
|
|
|
async def rename_node(service):
|
|
"""Rename a node."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
node = network.nodes[node_id]
|
|
name = service.data.get(const.ATTR_NAME)
|
|
node.name = name
|
|
_LOGGER.info("Renamed Z-Wave node %d to %s", node_id, name)
|
|
update_ids = service.data.get(const.ATTR_UPDATE_IDS)
|
|
# We want to rename the device, the node entity,
|
|
# and all the contained entities
|
|
node_key = f"node-{node_id}"
|
|
entity = hass.data[DATA_DEVICES][node_key]
|
|
await entity.node_renamed(update_ids)
|
|
for key in list(hass.data[DATA_DEVICES]):
|
|
if not key.startswith(f"{node_id}-"):
|
|
continue
|
|
entity = hass.data[DATA_DEVICES][key]
|
|
await entity.value_renamed(update_ids)
|
|
|
|
async def rename_value(service):
|
|
"""Rename a node value."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
value_id = service.data.get(const.ATTR_VALUE_ID)
|
|
node = network.nodes[node_id]
|
|
value = node.values[value_id]
|
|
name = service.data.get(const.ATTR_NAME)
|
|
value.label = name
|
|
_LOGGER.info(
|
|
"Renamed Z-Wave value (Node %d Value %d) to %s", node_id, value_id, name
|
|
)
|
|
update_ids = service.data.get(const.ATTR_UPDATE_IDS)
|
|
value_key = f"{node_id}-{value_id}"
|
|
entity = hass.data[DATA_DEVICES][value_key]
|
|
await entity.value_renamed(update_ids)
|
|
|
|
def set_poll_intensity(service):
|
|
"""Set the polling intensity of a node value."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
value_id = service.data.get(const.ATTR_VALUE_ID)
|
|
node = network.nodes[node_id]
|
|
value = node.values[value_id]
|
|
intensity = service.data.get(const.ATTR_POLL_INTENSITY)
|
|
if intensity == 0:
|
|
if value.disable_poll():
|
|
_LOGGER.info("Polling disabled (Node %d Value %d)", node_id, value_id)
|
|
return
|
|
_LOGGER.info(
|
|
"Polling disabled failed (Node %d Value %d)", node_id, value_id
|
|
)
|
|
else:
|
|
if value.enable_poll(intensity):
|
|
_LOGGER.info(
|
|
"Set polling intensity (Node %d Value %d) to %s",
|
|
node_id,
|
|
value_id,
|
|
intensity,
|
|
)
|
|
return
|
|
_LOGGER.info(
|
|
"Set polling intensity failed (Node %d Value %d)", node_id, value_id
|
|
)
|
|
|
|
def remove_failed_node(service):
|
|
"""Remove failed node."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
_LOGGER.info("Trying to remove zwave node %d", node_id)
|
|
network.controller.remove_failed_node(node_id)
|
|
|
|
def replace_failed_node(service):
|
|
"""Replace failed node."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
_LOGGER.info("Trying to replace zwave node %d", node_id)
|
|
network.controller.replace_failed_node(node_id)
|
|
|
|
def set_config_parameter(service):
|
|
"""Set a config parameter to a node."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
node = network.nodes[node_id]
|
|
param = service.data.get(const.ATTR_CONFIG_PARAMETER)
|
|
selection = service.data.get(const.ATTR_CONFIG_VALUE)
|
|
size = service.data.get(const.ATTR_CONFIG_SIZE)
|
|
for value in node.get_values(
|
|
class_id=const.COMMAND_CLASS_CONFIGURATION
|
|
).values():
|
|
if value.index != param:
|
|
continue
|
|
if value.type == const.TYPE_BOOL:
|
|
value.data = int(selection == "True")
|
|
_LOGGER.info(
|
|
"Setting configuration parameter %s on Node %s with bool selection %s",
|
|
param,
|
|
node_id,
|
|
str(selection),
|
|
)
|
|
return
|
|
if value.type == const.TYPE_LIST:
|
|
value.data = str(selection)
|
|
_LOGGER.info(
|
|
"Setting configuration parameter %s on Node %s with list selection %s",
|
|
param,
|
|
node_id,
|
|
str(selection),
|
|
)
|
|
return
|
|
if value.type == const.TYPE_BUTTON:
|
|
network.manager.pressButton(value.value_id)
|
|
network.manager.releaseButton(value.value_id)
|
|
_LOGGER.info(
|
|
"Setting configuration parameter %s on Node %s "
|
|
"with button selection %s",
|
|
param,
|
|
node_id,
|
|
selection,
|
|
)
|
|
return
|
|
value.data = int(selection)
|
|
_LOGGER.info(
|
|
"Setting configuration parameter %s on Node %s with selection %s",
|
|
param,
|
|
node_id,
|
|
selection,
|
|
)
|
|
return
|
|
node.set_config_param(param, selection, size)
|
|
_LOGGER.info(
|
|
"Setting unknown configuration parameter %s on Node %s with selection %s",
|
|
param,
|
|
node_id,
|
|
selection,
|
|
)
|
|
|
|
def refresh_node_value(service):
|
|
"""Refresh the specified value from a node."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
value_id = service.data.get(const.ATTR_VALUE_ID)
|
|
node = network.nodes[node_id]
|
|
node.values[value_id].refresh()
|
|
_LOGGER.info("Node %s value %s refreshed", node_id, value_id)
|
|
|
|
def set_node_value(service):
|
|
"""Set the specified value on a node."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
value_id = service.data.get(const.ATTR_VALUE_ID)
|
|
value = service.data.get(const.ATTR_CONFIG_VALUE)
|
|
node = network.nodes[node_id]
|
|
node.values[value_id].data = value
|
|
_LOGGER.info("Node %s value %s set to %s", node_id, value_id, value)
|
|
|
|
def print_config_parameter(service):
|
|
"""Print a config parameter from a node."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
node = network.nodes[node_id]
|
|
param = service.data.get(const.ATTR_CONFIG_PARAMETER)
|
|
_LOGGER.info(
|
|
"Config parameter %s on Node %s: %s",
|
|
param,
|
|
node_id,
|
|
get_config_value(node, param),
|
|
)
|
|
|
|
def print_node(service):
|
|
"""Print all information about z-wave node."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
node = network.nodes[node_id]
|
|
nice_print_node(node)
|
|
|
|
def set_wakeup(service):
|
|
"""Set wake-up interval of a node."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
node = network.nodes[node_id]
|
|
value = service.data.get(const.ATTR_CONFIG_VALUE)
|
|
if node.can_wake_up():
|
|
for value_id in node.get_values(class_id=const.COMMAND_CLASS_WAKE_UP):
|
|
node.values[value_id].data = value
|
|
_LOGGER.info("Node %s wake-up set to %d", node_id, value)
|
|
else:
|
|
_LOGGER.info("Node %s is not wakeable", node_id)
|
|
|
|
def change_association(service):
|
|
"""Change an association in the zwave network."""
|
|
association_type = service.data.get(const.ATTR_ASSOCIATION)
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
target_node_id = service.data.get(const.ATTR_TARGET_NODE_ID)
|
|
group = service.data.get(const.ATTR_GROUP)
|
|
instance = service.data.get(const.ATTR_INSTANCE)
|
|
|
|
node = ZWaveGroup(group, network, node_id)
|
|
if association_type == "add":
|
|
node.add_association(target_node_id, instance)
|
|
_LOGGER.info(
|
|
"Adding association for node:%s in group:%s "
|
|
"target node:%s, instance=%s",
|
|
node_id,
|
|
group,
|
|
target_node_id,
|
|
instance,
|
|
)
|
|
if association_type == "remove":
|
|
node.remove_association(target_node_id, instance)
|
|
_LOGGER.info(
|
|
"Removing association for node:%s in group:%s "
|
|
"target node:%s, instance=%s",
|
|
node_id,
|
|
group,
|
|
target_node_id,
|
|
instance,
|
|
)
|
|
|
|
async def async_refresh_entity(service):
|
|
"""Refresh values that specific entity depends on."""
|
|
entity_id = service.data.get(ATTR_ENTITY_ID)
|
|
async_dispatcher_send(hass, SIGNAL_REFRESH_ENTITY_FORMAT.format(entity_id))
|
|
|
|
def refresh_node(service):
|
|
"""Refresh all node info."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
node = network.nodes[node_id]
|
|
node.refresh_info()
|
|
|
|
def reset_node_meters(service):
|
|
"""Reset meter counters of a node."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
instance = service.data.get(const.ATTR_INSTANCE)
|
|
node = network.nodes[node_id]
|
|
for value in node.get_values(class_id=const.COMMAND_CLASS_METER).values():
|
|
if value.index != const.INDEX_METER_RESET:
|
|
continue
|
|
if value.instance != instance:
|
|
continue
|
|
network.manager.pressButton(value.value_id)
|
|
network.manager.releaseButton(value.value_id)
|
|
_LOGGER.info(
|
|
"Resetting meters on node %s instance %s....", node_id, instance
|
|
)
|
|
return
|
|
_LOGGER.info(
|
|
"Node %s on instance %s does not have resettable meters.",
|
|
node_id,
|
|
instance,
|
|
)
|
|
|
|
def heal_node(service):
|
|
"""Heal a node on the network."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
update_return_routes = service.data.get(const.ATTR_RETURN_ROUTES)
|
|
node = network.nodes[node_id]
|
|
_LOGGER.info("Z-Wave node heal running for node %s", node_id)
|
|
node.heal(update_return_routes)
|
|
|
|
def test_node(service):
|
|
"""Send test messages to a node on the network."""
|
|
node_id = service.data.get(const.ATTR_NODE_ID)
|
|
messages = service.data.get(const.ATTR_MESSAGES)
|
|
node = network.nodes[node_id]
|
|
_LOGGER.info("Sending %s test-messages to node %s.", messages, node_id)
|
|
node.test(messages)
|
|
|
|
def start_zwave(_service_or_event):
|
|
"""Startup Z-Wave network."""
|
|
_LOGGER.info("Starting Z-Wave network...")
|
|
network.start()
|
|
hass.bus.fire(const.EVENT_NETWORK_START)
|
|
|
|
async def _check_awaked():
|
|
"""Wait for Z-wave awaked state (or timeout) and finalize start."""
|
|
_LOGGER.debug("network state: %d %s", network.state, network.state_str)
|
|
|
|
start_time = dt_util.utcnow()
|
|
while True:
|
|
waited = int((dt_util.utcnow() - start_time).total_seconds())
|
|
|
|
if network.state >= network.STATE_AWAKED:
|
|
# Need to be in STATE_AWAKED before talking to nodes.
|
|
_LOGGER.info("Z-Wave ready after %d seconds", waited)
|
|
break
|
|
|
|
if waited >= const.NETWORK_READY_WAIT_SECS:
|
|
# Wait up to NETWORK_READY_WAIT_SECS seconds for the Z-Wave
|
|
# network to be ready.
|
|
_LOGGER.warning(
|
|
"Z-Wave not ready after %d seconds, continuing anyway", waited
|
|
)
|
|
_LOGGER.info(
|
|
"final network state: %d %s", network.state, network.state_str
|
|
)
|
|
break
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
hass.async_add_job(_finalize_start)
|
|
|
|
hass.add_job(_check_awaked)
|
|
|
|
def _finalize_start():
|
|
"""Perform final initializations after Z-Wave network is awaked."""
|
|
polling_interval = convert(config.get(CONF_POLLING_INTERVAL), int)
|
|
if polling_interval is not None:
|
|
network.set_poll_interval(polling_interval, False)
|
|
|
|
poll_interval = network.get_poll_interval()
|
|
_LOGGER.info("Z-Wave polling interval set to %d ms", poll_interval)
|
|
|
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_network)
|
|
|
|
# Register node services for Z-Wave network
|
|
hass.services.register(DOMAIN, const.SERVICE_ADD_NODE, add_node)
|
|
hass.services.register(DOMAIN, const.SERVICE_ADD_NODE_SECURE, add_node_secure)
|
|
hass.services.register(DOMAIN, const.SERVICE_REMOVE_NODE, remove_node)
|
|
hass.services.register(DOMAIN, const.SERVICE_CANCEL_COMMAND, cancel_command)
|
|
hass.services.register(DOMAIN, const.SERVICE_HEAL_NETWORK, heal_network)
|
|
hass.services.register(DOMAIN, const.SERVICE_SOFT_RESET, soft_reset)
|
|
hass.services.register(DOMAIN, const.SERVICE_TEST_NETWORK, test_network)
|
|
hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK, stop_network)
|
|
hass.services.register(
|
|
DOMAIN, const.SERVICE_RENAME_NODE, rename_node, schema=RENAME_NODE_SCHEMA
|
|
)
|
|
hass.services.register(
|
|
DOMAIN, const.SERVICE_RENAME_VALUE, rename_value, schema=RENAME_VALUE_SCHEMA
|
|
)
|
|
hass.services.register(
|
|
DOMAIN,
|
|
const.SERVICE_SET_CONFIG_PARAMETER,
|
|
set_config_parameter,
|
|
schema=SET_CONFIG_PARAMETER_SCHEMA,
|
|
)
|
|
hass.services.register(
|
|
DOMAIN,
|
|
const.SERVICE_SET_NODE_VALUE,
|
|
set_node_value,
|
|
schema=SET_NODE_VALUE_SCHEMA,
|
|
)
|
|
hass.services.register(
|
|
DOMAIN,
|
|
const.SERVICE_REFRESH_NODE_VALUE,
|
|
refresh_node_value,
|
|
schema=REFRESH_NODE_VALUE_SCHEMA,
|
|
)
|
|
hass.services.register(
|
|
DOMAIN,
|
|
const.SERVICE_PRINT_CONFIG_PARAMETER,
|
|
print_config_parameter,
|
|
schema=PRINT_CONFIG_PARAMETER_SCHEMA,
|
|
)
|
|
hass.services.register(
|
|
DOMAIN,
|
|
const.SERVICE_REMOVE_FAILED_NODE,
|
|
remove_failed_node,
|
|
schema=NODE_SERVICE_SCHEMA,
|
|
)
|
|
hass.services.register(
|
|
DOMAIN,
|
|
const.SERVICE_REPLACE_FAILED_NODE,
|
|
replace_failed_node,
|
|
schema=NODE_SERVICE_SCHEMA,
|
|
)
|
|
|
|
hass.services.register(
|
|
DOMAIN,
|
|
const.SERVICE_CHANGE_ASSOCIATION,
|
|
change_association,
|
|
schema=CHANGE_ASSOCIATION_SCHEMA,
|
|
)
|
|
hass.services.register(
|
|
DOMAIN, const.SERVICE_SET_WAKEUP, set_wakeup, schema=SET_WAKEUP_SCHEMA
|
|
)
|
|
hass.services.register(
|
|
DOMAIN, const.SERVICE_PRINT_NODE, print_node, schema=NODE_SERVICE_SCHEMA
|
|
)
|
|
hass.services.register(
|
|
DOMAIN,
|
|
const.SERVICE_REFRESH_ENTITY,
|
|
async_refresh_entity,
|
|
schema=REFRESH_ENTITY_SCHEMA,
|
|
)
|
|
hass.services.register(
|
|
DOMAIN, const.SERVICE_REFRESH_NODE, refresh_node, schema=NODE_SERVICE_SCHEMA
|
|
)
|
|
hass.services.register(
|
|
DOMAIN,
|
|
const.SERVICE_RESET_NODE_METERS,
|
|
reset_node_meters,
|
|
schema=RESET_NODE_METERS_SCHEMA,
|
|
)
|
|
hass.services.register(
|
|
DOMAIN,
|
|
const.SERVICE_SET_POLL_INTENSITY,
|
|
set_poll_intensity,
|
|
schema=SET_POLL_INTENSITY_SCHEMA,
|
|
)
|
|
hass.services.register(
|
|
DOMAIN, const.SERVICE_HEAL_NODE, heal_node, schema=HEAL_NODE_SCHEMA
|
|
)
|
|
hass.services.register(
|
|
DOMAIN, const.SERVICE_TEST_NODE, test_node, schema=TEST_NODE_SCHEMA
|
|
)
|
|
|
|
# Setup autoheal
|
|
if autoheal:
|
|
_LOGGER.info("Z-Wave network autoheal is enabled")
|
|
async_track_time_change(hass, heal_network, hour=0, minute=0, second=0)
|
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_zwave)
|
|
|
|
hass.services.async_register(DOMAIN, const.SERVICE_START_NETWORK, start_zwave)
|
|
|
|
for entry_component in SUPPORTED_PLATFORMS:
|
|
hass.async_create_task(
|
|
hass.config_entries.async_forward_entry_setup(config_entry, entry_component)
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
class ZWaveDeviceEntityValues:
|
|
"""Manages entity access to the underlying zwave value objects."""
|
|
|
|
def __init__(
|
|
self, hass, schema, primary_value, zwave_config, device_config, registry
|
|
):
|
|
"""Initialize the values object with the passed entity schema."""
|
|
self._hass = hass
|
|
self._zwave_config = zwave_config
|
|
self._device_config = device_config
|
|
self._schema = copy.deepcopy(schema)
|
|
self._values = {}
|
|
self._entity = None
|
|
self._workaround_ignore = False
|
|
self._registry = registry
|
|
|
|
for name in self._schema[const.DISC_VALUES].keys():
|
|
self._values[name] = None
|
|
self._schema[const.DISC_VALUES][name][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]
|
|
|
|
# Check values that have already been discovered for node
|
|
for value in self._node.values.values():
|
|
self.check_value(value)
|
|
|
|
self._check_entity_ready()
|
|
|
|
def __getattr__(self, name):
|
|
"""Get the specified value for this entity."""
|
|
return self._values[name]
|
|
|
|
def __iter__(self):
|
|
"""Allow iteration over all values."""
|
|
return iter(self._values.values())
|
|
|
|
def 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.
|
|
"""
|
|
if not check_node_schema(value.node, self._schema):
|
|
return
|
|
for name in self._values:
|
|
if self._values[name] is not None:
|
|
continue
|
|
if not check_value_schema(value, self._schema[const.DISC_VALUES][name]):
|
|
continue
|
|
self._values[name] = value
|
|
if self._entity:
|
|
self._entity.value_added()
|
|
self._entity.value_changed()
|
|
|
|
self._check_entity_ready()
|
|
|
|
def _check_entity_ready(self):
|
|
"""Check if all required values are discovered and create entity."""
|
|
if self._workaround_ignore:
|
|
return
|
|
if self._entity is not None:
|
|
return
|
|
|
|
for name in self._schema[const.DISC_VALUES]:
|
|
if self._values[name] is None and not self._schema[const.DISC_VALUES][
|
|
name
|
|
].get(const.DISC_OPTIONAL):
|
|
return
|
|
|
|
component = self._schema[const.DISC_COMPONENT]
|
|
|
|
workaround_component = workaround.get_device_component_mapping(self.primary)
|
|
if workaround_component and workaround_component != component:
|
|
if workaround_component == workaround.WORKAROUND_IGNORE:
|
|
_LOGGER.info(
|
|
"Ignoring Node %d Value %d due to workaround.",
|
|
self.primary.node.node_id,
|
|
self.primary.value_id,
|
|
)
|
|
# No entity will be created for this value
|
|
self._workaround_ignore = True
|
|
return
|
|
_LOGGER.debug("Using %s instead of %s", workaround_component, component)
|
|
component = workaround_component
|
|
|
|
entity_id = self._registry.async_get_entity_id(
|
|
component, DOMAIN, compute_value_unique_id(self._node, self.primary)
|
|
)
|
|
if entity_id is None:
|
|
value_name = _value_name(self.primary)
|
|
entity_id = generate_entity_id(component + ".{}", value_name, [])
|
|
node_config = self._device_config.get(entity_id)
|
|
|
|
# Configure node
|
|
_LOGGER.debug(
|
|
"Adding Node_id=%s Generic_command_class=%s, "
|
|
"Specific_command_class=%s, "
|
|
"Command_class=%s, Value type=%s, "
|
|
"Genre=%s as %s",
|
|
self._node.node_id,
|
|
self._node.generic,
|
|
self._node.specific,
|
|
self.primary.command_class,
|
|
self.primary.type,
|
|
self.primary.genre,
|
|
component,
|
|
)
|
|
|
|
if node_config.get(CONF_IGNORED):
|
|
_LOGGER.info("Ignoring entity %s due to device settings", entity_id)
|
|
# No entity will be created for this value
|
|
self._workaround_ignore = True
|
|
return
|
|
|
|
polling_intensity = convert(node_config.get(CONF_POLLING_INTENSITY), int)
|
|
if polling_intensity:
|
|
self.primary.enable_poll(polling_intensity)
|
|
|
|
platform = import_module(f".{component}", __name__)
|
|
|
|
device = platform.get_device(
|
|
node=self._node, values=self, node_config=node_config, hass=self._hass
|
|
)
|
|
if device is None:
|
|
# No entity will be created for this value
|
|
self._workaround_ignore = True
|
|
return
|
|
|
|
self._entity = device
|
|
|
|
@callback
|
|
def _on_ready(sec):
|
|
_LOGGER.info(
|
|
"Z-Wave entity %s (node_id: %d) ready after %d seconds",
|
|
device.name,
|
|
self._node.node_id,
|
|
sec,
|
|
)
|
|
self._hass.async_add_job(discover_device, component, device)
|
|
|
|
@callback
|
|
def _on_timeout(sec):
|
|
_LOGGER.warning(
|
|
"Z-Wave entity %s (node_id: %d) not ready after %d seconds, "
|
|
"continuing anyway",
|
|
device.name,
|
|
self._node.node_id,
|
|
sec,
|
|
)
|
|
self._hass.async_add_job(discover_device, component, device)
|
|
|
|
async def discover_device(component, device):
|
|
"""Put device in a dictionary and call discovery on it."""
|
|
if self._hass.data[DATA_DEVICES].get(device.unique_id):
|
|
return
|
|
|
|
self._hass.data[DATA_DEVICES][device.unique_id] = device
|
|
if component in SUPPORTED_PLATFORMS:
|
|
async_dispatcher_send(self._hass, f"zwave_new_{component}", device)
|
|
else:
|
|
await discovery.async_load_platform(
|
|
self._hass,
|
|
component,
|
|
DOMAIN,
|
|
{const.DISCOVERY_DEVICE: device.unique_id},
|
|
self._zwave_config,
|
|
)
|
|
|
|
if device.unique_id:
|
|
self._hass.add_job(discover_device, component, device)
|
|
else:
|
|
self._hass.add_job(check_has_unique_id, device, _on_ready, _on_timeout)
|
|
|
|
|
|
class ZWaveDeviceEntity(ZWaveBaseEntity):
|
|
"""Representation of a Z-Wave node entity."""
|
|
|
|
def __init__(self, values, domain):
|
|
"""Initialize the z-Wave device."""
|
|
# pylint: disable=import-error
|
|
super().__init__()
|
|
from openzwave.network import ZWaveNetwork
|
|
from pydispatch import dispatcher
|
|
|
|
self.values = values
|
|
self.node = values.primary.node
|
|
self.values.primary.set_change_verified(False)
|
|
|
|
self._name = _value_name(self.values.primary)
|
|
self._unique_id = self._compute_unique_id()
|
|
self._update_attributes()
|
|
|
|
dispatcher.connect(
|
|
self.network_value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED
|
|
)
|
|
|
|
def network_value_changed(self, value):
|
|
"""Handle a value change on the network."""
|
|
if value.value_id in [v.value_id for v in self.values if v]:
|
|
return self.value_changed()
|
|
|
|
def value_added(self):
|
|
"""Handle a new value of this entity."""
|
|
pass
|
|
|
|
def value_changed(self):
|
|
"""Handle a changed value for this entity's node."""
|
|
self._update_attributes()
|
|
self.update_properties()
|
|
self.maybe_schedule_update()
|
|
|
|
async def value_renamed(self, update_ids=False):
|
|
"""Rename the node and update any IDs."""
|
|
self._name = _value_name(self.values.primary)
|
|
if update_ids:
|
|
# Update entity ID.
|
|
ent_reg = await async_get_entity_registry(self.hass)
|
|
new_entity_id = ent_reg.async_generate_entity_id(
|
|
self.platform.domain,
|
|
self._name,
|
|
self.platform.entities.keys() - {self.entity_id},
|
|
)
|
|
if new_entity_id != self.entity_id:
|
|
# Don't change the name attribute, it will be None unless
|
|
# customised and if it's been customised, keep the
|
|
# customisation.
|
|
ent_reg.async_update_entity(self.entity_id, new_entity_id=new_entity_id)
|
|
return
|
|
# else for the above two ifs, update if not using update_entity
|
|
self.async_schedule_update_ha_state()
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Add device to dict."""
|
|
async_dispatcher_connect(
|
|
self.hass,
|
|
SIGNAL_REFRESH_ENTITY_FORMAT.format(self.entity_id),
|
|
self.refresh_from_network,
|
|
)
|
|
|
|
def _update_attributes(self):
|
|
"""Update the node attributes. May only be used inside callback."""
|
|
self.node_id = self.node.node_id
|
|
self._name = _value_name(self.values.primary)
|
|
if not self._unique_id:
|
|
self._unique_id = self._compute_unique_id()
|
|
if self._unique_id:
|
|
self.try_remove_and_add()
|
|
|
|
if self.values.power:
|
|
self.power_consumption = round(
|
|
self.values.power.data, self.values.power.precision
|
|
)
|
|
else:
|
|
self.power_consumption = None
|
|
|
|
def update_properties(self):
|
|
"""Update on data changes for node values."""
|
|
pass
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""No polling needed."""
|
|
return False
|
|
|
|
@property
|
|
def unique_id(self):
|
|
"""Return a unique ID."""
|
|
return self._unique_id
|
|
|
|
@property
|
|
def device_info(self):
|
|
"""Return device information."""
|
|
identifier, name = node_device_id_and_name(
|
|
self.node, self.values.primary.instance
|
|
)
|
|
info = {
|
|
"name": name,
|
|
"identifiers": {identifier},
|
|
"manufacturer": self.node.manufacturer_name,
|
|
"model": self.node.product_name,
|
|
}
|
|
if self.values.primary.instance > 1:
|
|
info["via_device"] = (DOMAIN, self.node_id)
|
|
elif self.node_id > 1:
|
|
info["via_device"] = (DOMAIN, 1)
|
|
return info
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the device."""
|
|
return self._name
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the device specific state attributes."""
|
|
attrs = {
|
|
const.ATTR_NODE_ID: self.node_id,
|
|
const.ATTR_VALUE_INDEX: self.values.primary.index,
|
|
const.ATTR_VALUE_INSTANCE: self.values.primary.instance,
|
|
const.ATTR_VALUE_ID: str(self.values.primary.value_id),
|
|
}
|
|
|
|
if self.power_consumption is not None:
|
|
attrs[ATTR_POWER] = self.power_consumption
|
|
|
|
return attrs
|
|
|
|
def refresh_from_network(self):
|
|
"""Refresh all dependent values from zwave network."""
|
|
for value in self.values:
|
|
if value is not None:
|
|
self.node.refresh_value(value.value_id)
|
|
|
|
def _compute_unique_id(self):
|
|
if (
|
|
is_node_parsed(self.node) and self.values.primary.label != "Unknown"
|
|
) or self.node.is_ready:
|
|
return compute_value_unique_id(self.node, self.values.primary)
|
|
return None
|
|
|
|
|
|
def compute_value_unique_id(node, value):
|
|
"""Compute unique_id a value would get if it were to get one."""
|
|
return f"{node.node_id}-{value.object_id}"
|