From d68fdbc283ed257684477975cea94d7a14031bbb Mon Sep 17 00:00:00 2001 From: Charles Garwood <cgarwood@gmail.com> Date: Sun, 10 Jan 2021 18:08:25 -0500 Subject: [PATCH] Add zwave_js integration (#45020) * Run zwave_js scaffold (#44891) * Add zwave_js basic connection to zwave server (#44904) * add the basic connection to zwave server * fix name * Fix requirements * Fix things * Version bump dep to 0.1.2 * fix pylint Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * Bump zwave-js-server-python to 0.2.0 * Use zwave js server version check instead of fetching full state (#44943) * Use version check instead of fetching full state * Fix tests * Use 0.3.0 * Also catch aiohttp client errors * Update docstring * Lint * Unignore zwave_js * Add zwave_js entity discovery basics and sensor platform (#44927) Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Paulus Schoutsen <balloob@gmail.com> * Complete zwave_js typing (#44960) * Type discovery * Type init * Type entity * Type config flow * Type sensor * Require typing of zwave_js * Complete zwave_js config flow test coverage (#44955) * Correct zwave_js sensor device class (#44968) * Fix zwave_js KeyError on entry setup timeout (#44966) * Bump zwave-js-server-python to 0.5.0 (#44975) * Remove stale callback signal from zwave_js (#44994) * Add light platform to zwave_js integration (#44974) * add light platform * styling fix * fix type hint * Fix typing * Update homeassistant/components/zwave_js/const.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zwave_js/entity.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zwave_js/entity.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zwave_js/entity.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zwave_js/entity.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zwave_js/entity.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * color temp should be integer * guard Nonetype error * Update homeassistant/components/zwave_js/light.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zwave_js/light.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * some fixes after merging * add additional guards for None values * adjustments for rgb lights * Fix typing * Fix black * Bump zwave-js-server-python to 0.6.0 * guard value updated log * remove value_id lookup as its no longer needed * fiz sending white value * Update homeassistant/components/zwave_js/light.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Add zwave_js test foundation (#44983) * Exclude text files from codespell * Add basic dump fixture * Add test foundation * Fix test after rebase * Exclude jsonl files from codespell * Rename fixture file type to jsonl * Update fixture path * Fix stale docstring * Add controller state json fixture * Add multisensor 6 state json fixture * Update fixtures * Remove basic dump fixture * Fix fixtures after library bump * Update codeowner * Minor cleanup Z-Wave JS (#45021) * Update zwave_js device_info (#45023) Co-authored-by: Martin Hjelmare <marhje52@gmail.com> Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com> Co-authored-by: Paulus Schoutsen <balloob@gmail.com> --- .coveragerc | 5 + .pre-commit-config.yaml | 1 + CODEOWNERS | 1 + homeassistant/components/zwave_js/__init__.py | 179 ++ .../components/zwave_js/config_flow.py | 78 + homeassistant/components/zwave_js/const.py | 9 + .../components/zwave_js/discovery.py | 160 ++ homeassistant/components/zwave_js/entity.py | 151 ++ homeassistant/components/zwave_js/light.py | 322 +++ .../components/zwave_js/manifest.json | 8 + homeassistant/components/zwave_js/sensor.py | 149 ++ .../components/zwave_js/strings.json | 20 + .../components/zwave_js/translations/en.json | 20 + homeassistant/generated/config_flows.py | 3 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + setup.cfg | 2 +- tests/components/zwave_js/__init__.py | 1 + tests/components/zwave_js/conftest.py | 58 + tests/components/zwave_js/test_config_flow.py | 99 + tests/components/zwave_js/test_sensor.py | 14 + tests/fixtures/zwave_js/controller_state.json | 98 + .../zwave_js/multisensor_6_state.json | 1830 +++++++++++++++++ 23 files changed, 3212 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/zwave_js/__init__.py create mode 100644 homeassistant/components/zwave_js/config_flow.py create mode 100644 homeassistant/components/zwave_js/const.py create mode 100644 homeassistant/components/zwave_js/discovery.py create mode 100644 homeassistant/components/zwave_js/entity.py create mode 100644 homeassistant/components/zwave_js/light.py create mode 100644 homeassistant/components/zwave_js/manifest.json create mode 100644 homeassistant/components/zwave_js/sensor.py create mode 100644 homeassistant/components/zwave_js/strings.json create mode 100644 homeassistant/components/zwave_js/translations/en.json create mode 100644 tests/components/zwave_js/__init__.py create mode 100644 tests/components/zwave_js/conftest.py create mode 100644 tests/components/zwave_js/test_config_flow.py create mode 100644 tests/components/zwave_js/test_sensor.py create mode 100644 tests/fixtures/zwave_js/controller_state.json create mode 100644 tests/fixtures/zwave_js/multisensor_6_state.json diff --git a/.coveragerc b/.coveragerc index 2b08fc50336..9a8b062468f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1092,6 +1092,11 @@ omit = homeassistant/components/zoneminder/* homeassistant/components/supla/* homeassistant/components/zwave/util.py + homeassistant/components/zwave_js/__init__.py + homeassistant/components/zwave_js/discovery.py + homeassistant/components/zwave_js/entity.py + homeassistant/components/zwave_js/light.py + homeassistant/components/zwave_js/sensor.py [report] # Regexes for lines to exclude from consideration diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 087dc914035..00f2373f2db 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,7 @@ repos: - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] + exclude: ^tests/fixtures/ - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.4 hooks: diff --git a/CODEOWNERS b/CODEOWNERS index 9f6e08216b5..d81247ce629 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -537,6 +537,7 @@ homeassistant/components/zodiac/* @JulienTant homeassistant/components/zone/* @home-assistant/core homeassistant/components/zoneminder/* @rohankapoorcom homeassistant/components/zwave/* @home-assistant/z-wave +homeassistant/components/zwave_js/* @home-assistant/z-wave # Individual files homeassistant/components/demo/weather @fabaff diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py new file mode 100644 index 00000000000..605235f61ba --- /dev/null +++ b/homeassistant/components/zwave_js/__init__.py @@ -0,0 +1,179 @@ +"""The Z-Wave JS integration.""" +import asyncio +import logging + +from async_timeout import timeout +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.model.node import Node as ZwaveNode + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN, PLATFORMS +from .discovery import async_discover_values + +LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the Z-Wave JS component.""" + hass.data[DOMAIN] = {} + return True + + +@callback +def register_node_in_dev_reg( + entry: ConfigEntry, + dev_reg: device_registry.DeviceRegistry, + client: ZwaveClient, + node: ZwaveNode, +) -> None: + """Register node in dev reg.""" + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")}, + sw_version=node.firmware_version, + name=node.name or node.device_config.description, + model=node.device_config.label or str(node.product_type), + manufacturer=node.device_config.manufacturer or str(node.manufacturer_id), + ) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Z-Wave JS from a config entry.""" + client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) + initialized = asyncio.Event() + dev_reg = await device_registry.async_get_registry(hass) + + async def async_on_connect() -> None: + """Handle websocket is (re)connected.""" + LOGGER.info("Connected to Zwave JS Server") + if initialized.is_set(): + # update entity availability + async_dispatcher_send(hass, f"{DOMAIN}_connection_state") + + async def async_on_disconnect() -> None: + """Handle websocket is disconnected.""" + LOGGER.info("Disconnected from Zwave JS Server") + async_dispatcher_send(hass, f"{DOMAIN}_connection_state") + + async def async_on_initialized() -> None: + """Handle initial full state received.""" + LOGGER.info("Connection to Zwave JS Server initialized.") + initialized.set() + + @callback + def async_on_node_ready(node: ZwaveNode) -> None: + """Handle node ready event.""" + LOGGER.debug("Processing node %s", node) + + # register (or update) node in device registry + register_node_in_dev_reg(entry, dev_reg, client, node) + + # run discovery on all node values and create/update entities + for disc_info in async_discover_values(node): + LOGGER.debug("Discovered entity: %s", disc_info) + async_dispatcher_send(hass, f"{DOMAIN}_add_{disc_info.platform}", disc_info) + + @callback + def async_on_node_added(node: ZwaveNode) -> None: + """Handle node added event.""" + LOGGER.debug("Node added: %s - waiting for it to become ready.", node.node_id) + # we only want to run discovery when the node has reached ready state, + # otherwise we'll have all kinds of missing info issues. + if node.ready: + async_on_node_ready(node) + return + # if node is not yet ready, register one-time callback for ready state + node.once( + "ready", + lambda event: async_on_node_ready(event["node"]), + ) + # we do submit the node to device registry so user has + # some visual feedback that something is (in the process of) being added + register_node_in_dev_reg(entry, dev_reg, client, node) + + async def handle_ha_shutdown(event: Event) -> None: + """Handle HA shutdown.""" + await client.disconnect() + + # register main event callbacks. + unsubs = [ + client.register_on_initialized(async_on_initialized), + client.register_on_disconnect(async_on_disconnect), + client.register_on_connect(async_on_connect), + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown), + ] + + # connect and throw error if connection failed + asyncio.create_task(client.connect()) + try: + async with timeout(10): + await initialized.wait() + except asyncio.TimeoutError as err: + for unsub in unsubs: + unsub() + await client.disconnect() + raise ConfigEntryNotReady from err + + hass.data[DOMAIN][entry.entry_id] = { + DATA_CLIENT: client, + DATA_UNSUBSCRIBE: unsubs, + } + + async def start_platforms() -> None: + """Start platforms and perform discovery.""" + # wait until all required platforms are ready + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, component) + for component in PLATFORMS + ] + ) + + # run discovery on all ready nodes + for node in client.driver.controller.nodes.values(): + if node.ready: + async_on_node_ready(node) + continue + # if node is not yet ready, register one-time callback for ready state + node.once( + "ready", + lambda event: async_on_node_ready(event["node"]), + ) + # listen for new nodes being added to the mesh + client.driver.controller.on( + "node added", lambda event: async_on_node_added(event["node"]) + ) + + hass.async_create_task(start_platforms()) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + 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 + + info = hass.data[DOMAIN].pop(entry.entry_id) + + for unsub in info[DATA_UNSUBSCRIBE]: + unsub() + + await info[DATA_CLIENT].disconnect() + + return True diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py new file mode 100644 index 00000000000..2edb7012878 --- /dev/null +++ b/homeassistant/components/zwave_js/config_flow.py @@ -0,0 +1,78 @@ +"""Config flow for Z-Wave JS integration.""" +import asyncio +import logging +from typing import Any, Dict, Optional + +import aiohttp +from async_timeout import timeout +import voluptuous as vol +from zwave_js_server.version import VersionInfo, get_server_version + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_URL +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN, NAME # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema({CONF_URL: str}) + + +async def validate_input(hass: core.HomeAssistant, user_input: dict) -> VersionInfo: + """Validate if the user input allows us to connect.""" + ws_address = user_input[CONF_URL] + + if not ws_address.startswith(("ws://", "wss://")): + raise InvalidInput("invalid_ws_url") + + async with timeout(10): + try: + return await get_server_version(ws_address, async_get_clientsession(hass)) + except (asyncio.TimeoutError, aiohttp.ClientError) as err: + raise InvalidInput("cannot_connect") from err + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Z-Wave JS.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_user( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + assert self.hass # typing + + try: + version_info = await validate_input(self.hass, user_input) + except InvalidInput as err: + errors["base"] = err.error + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(version_info.home_id) + self._abort_if_unique_id_configured(user_input) + return self.async_create_entry(title=NAME, data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class InvalidInput(exceptions.HomeAssistantError): + """Error to indicate input data is invalid.""" + + def __init__(self, error: str) -> None: + """Initialize error.""" + super().__init__() + self.error = error diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py new file mode 100644 index 00000000000..c2a9ac7b3cf --- /dev/null +++ b/homeassistant/components/zwave_js/const.py @@ -0,0 +1,9 @@ +"""Constants for the Z-Wave JS integration.""" + + +DOMAIN = "zwave_js" +NAME = "Z-Wave JS" +PLATFORMS = ["light", "sensor"] + +DATA_CLIENT = "client" +DATA_UNSUBSCRIBE = "unsubs" diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py new file mode 100644 index 00000000000..34f715a2d40 --- /dev/null +++ b/homeassistant/components/zwave_js/discovery.py @@ -0,0 +1,160 @@ +"""Map Z-Wave nodes and values to Home Assistant entities.""" + +from dataclasses import dataclass +from typing import Generator, Optional, Set, Union + +from zwave_js_server.const import CommandClass +from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.model.value import Value as ZwaveValue + +from homeassistant.core import callback + + +@dataclass +class ZwaveDiscoveryInfo: + """Info discovered from (primary) ZWave Value to create entity.""" + + node: ZwaveNode # node to which the value(s) belongs + primary_value: ZwaveValue # the value object itself for primary value + platform: str # the home assistant platform for which an entity should be created + platform_hint: Optional[ + str + ] = "" # hint for the platform about this discovered entity + + @property + def value_id(self) -> str: + """Return the unique value_id belonging to primary value.""" + return f"{self.node.node_id}.{self.primary_value.value_id}" + + +@dataclass +class ZWaveDiscoverySchema: + """Z-Wave discovery schema. + + The (primary) value for an entity must match these conditions. + Use the Z-Wave specifications to find out the values for these parameters: + https://github.com/zwave-js/node-zwave-js/tree/master/specs + """ + + # specify the hass platform for which this scheme applies (e.g. light, sensor) + platform: str + # [optional] hint for platform + hint: Optional[str] = None + # [optional] the node's basic device class must match ANY of these values + device_class_basic: Optional[Set[str]] = None + # [optional] the node's generic device class must match ANY of these values + device_class_generic: Optional[Set[str]] = None + # [optional] the node's specific device class must match ANY of these values + device_class_specific: Optional[Set[str]] = None + # [optional] the value's command class must match ANY of these values + command_class: Optional[Set[int]] = None + # [optional] the value's endpoint must match ANY of these values + endpoint: Optional[Set[int]] = None + # [optional] the value's property must match ANY of these values + property: Optional[Set[Union[str, int]]] = None + # [optional] the value's metadata_type must match ANY of these values + type: Optional[Set[str]] = None + + +DISCOVERY_SCHEMAS = [ + # light + # primary value is the currentValue (brightness) + ZWaveDiscoverySchema( + platform="light", + device_class_generic={"Multilevel Switch", "Remote Switch"}, + device_class_specific={ + "Multilevel Tunable Color Light", + "Binary Tunable Color Light", + "Multilevel Remote Switch", + "Multilevel Power Switch", + }, + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + # generic text sensors + ZWaveDiscoverySchema( + platform="sensor", + hint="string_sensor", + command_class={ + CommandClass.ALARM, + CommandClass.SENSOR_ALARM, + CommandClass.INDICATOR, + CommandClass.NOTIFICATION, + }, + type={"string"}, + ), + # generic numeric sensors + ZWaveDiscoverySchema( + platform="sensor", + hint="numeric_sensor", + command_class={ + CommandClass.SENSOR_MULTILEVEL, + CommandClass.METER, + CommandClass.ALARM, + CommandClass.SENSOR_ALARM, + CommandClass.INDICATOR, + CommandClass.BATTERY, + CommandClass.NOTIFICATION, + CommandClass.BASIC, + }, + type={"number"}, + ), +] + + +@callback +def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None, None]: + """Run discovery on ZWave node and return matching (primary) values.""" + for value in node.values.values(): + disc_val = async_discover_value(value) + if disc_val: + yield disc_val + + +@callback +def async_discover_value(value: ZwaveValue) -> Optional[ZwaveDiscoveryInfo]: + """Run discovery on Z-Wave value and return ZwaveDiscoveryInfo if match found.""" + for schema in DISCOVERY_SCHEMAS: + # check device_class_basic + if ( + schema.device_class_basic is not None + and value.node.device_class.basic not in schema.device_class_basic + ): + continue + # check device_class_generic + if ( + schema.device_class_generic is not None + and value.node.device_class.generic not in schema.device_class_generic + ): + continue + # check device_class_specific + if ( + schema.device_class_specific is not None + and value.node.device_class.specific not in schema.device_class_specific + ): + continue + # check command_class + if ( + schema.command_class is not None + and value.command_class not in schema.command_class + ): + continue + # check endpoint + if schema.endpoint is not None and value.endpoint not in schema.endpoint: + continue + # check property + if schema.property is not None and value.property_ not in schema.property: + continue + # check metadata_type + if schema.type is not None and value.metadata.type not in schema.type: + continue + # all checks passed, this value belongs to an entity + return ZwaveDiscoveryInfo( + node=value.node, + primary_value=value, + platform=schema.platform, + platform_hint=schema.hint, + ) + + return None diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py new file mode 100644 index 00000000000..70630cbd89c --- /dev/null +++ b/homeassistant/components/zwave_js/entity.py @@ -0,0 +1,151 @@ +"""Generic Z-Wave Entity Class.""" + +import logging +from typing import Optional, Union + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.model.value import Value as ZwaveValue, get_value_id + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN +from .discovery import ZwaveDiscoveryInfo + +LOGGER = logging.getLogger(__name__) + +EVENT_VALUE_UPDATED = "value updated" + + +class ZWaveBaseEntity(Entity): + """Generic Entity Class for a Z-Wave Device.""" + + def __init__(self, client: ZwaveClient, info: ZwaveDiscoveryInfo) -> None: + """Initialize a generic Z-Wave device entity.""" + self.client = client + self.info = info + # entities requiring additional values, can add extra ids to this list + self.watched_value_ids = {self.info.primary_value.value_id} + + @callback + def on_value_update(self) -> None: + """Call when one of the watched values change. + + To be overridden by platforms needing this event. + """ + + async def async_added_to_hass(self) -> None: + """Call when entity is added.""" + assert self.hass # typing + # Add value_changed callbacks. + self.async_on_remove( + self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, f"{DOMAIN}_connection_state", self.async_write_ha_state + ) + ) + + @property + def device_info(self) -> dict: + """Return device information for the device registry.""" + # device is precreated in main handler + return { + "identifiers": { + ( + DOMAIN, + f"{self.client.driver.controller.home_id}-{self.info.node.node_id}", + ) + }, + } + + @property + def name(self) -> str: + """Return default name from device name and value name combination.""" + node_name = self.info.node.name or self.info.node.device_config.description + value_name = ( + self.info.primary_value.metadata.label + or self.info.primary_value.property_key_name + or self.info.primary_value.property_name + ) + return f"{node_name}: {value_name}" + + @property + def unique_id(self) -> str: + """Return the unique_id of the entity.""" + return f"{self.client.driver.controller.home_id}.{self.info.value_id}" + + @property + def available(self) -> bool: + """Return entity availability.""" + return self.client.connected and bool(self.info.node.ready) + + @callback + def _value_changed(self, event_data: Union[dict, ZwaveValue]) -> None: + """Call when (one of) our watched values changes. + + Should not be overridden by subclasses. + """ + if isinstance(event_data, ZwaveValue): + value_id = event_data.value_id + else: + value_id = event_data["value"].value_id + + if value_id not in self.watched_value_ids: + return + + value = self.info.node.values[value_id] + + LOGGER.debug( + "[%s] Value %s/%s changed to: %s", + self.entity_id, + value.property_, + value.property_key_name, + value.value, + ) + + self.on_value_update() + self.async_write_ha_state() + + @callback + def get_zwave_value( + self, + value_property: Union[str, int], + command_class: Optional[int] = None, + endpoint: Optional[int] = None, + value_property_key_name: Optional[str] = None, + add_to_watched_value_ids: bool = True, + ) -> Optional[ZwaveValue]: + """Return specific ZwaveValue on this ZwaveNode.""" + # use commandclass and endpoint from primary value if omitted + return_value = None + if command_class is None: + command_class = self.info.primary_value.command_class + if endpoint is None: + endpoint = self.info.primary_value.endpoint + # lookup value by value_id + value_id = get_value_id( + self.info.node, + { + "commandClass": command_class, + "endpoint": endpoint, + "property": value_property, + "propertyKeyName": value_property_key_name, + }, + ) + return_value = self.info.node.values.get(value_id) + # add to watched_ids list so we will be triggered when the value updates + if ( + return_value + and return_value.value_id not in self.watched_value_ids + and add_to_watched_value_ids + ): + self.watched_value_ids.add(return_value.value_id) + return return_value + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py new file mode 100644 index 00000000000..45bd68aef81 --- /dev/null +++ b/homeassistant/components/zwave_js/light.py @@ -0,0 +1,322 @@ +"""Support for Z-Wave lights.""" +import logging +from typing import Any, Callable, List, Optional + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_TRANSITION, + ATTR_WHITE_VALUE, + DOMAIN as LIGHT_DOMAIN, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_TRANSITION, + SUPPORT_WHITE_VALUE, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.util.color as color_util + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave Light from Config Entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_light(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave Light.""" + + light = ZwaveLight(client, info) + async_add_entities([light]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect(hass, f"{DOMAIN}_add_{LIGHT_DOMAIN}", async_add_light) + ) + + +def byte_to_zwave_brightness(value: int) -> int: + """Convert brightness in 0-255 scale to 0-99 scale. + + `value` -- (int) Brightness byte value from 0-255. + """ + if value > 0: + return max(1, round((value / 255) * 99)) + return 0 + + +class ZwaveLight(ZWaveBaseEntity, LightEntity): + """Representation of a Z-Wave light.""" + + def __init__(self, client: ZwaveClient, info: ZwaveDiscoveryInfo) -> None: + """Initialize the light.""" + super().__init__(client, info) + self._supports_color = False + self._supports_white_value = False + self._supports_color_temp = False + self._hs_color: Optional[List[float]] = None + self._white_value: Optional[int] = None + self._color_temp: Optional[int] = None + self._min_mireds = 153 # 6500K as a safe default + self._max_mireds = 370 # 2700K as a safe default + self._supported_features = SUPPORT_BRIGHTNESS + + # get additional (optional) values and set features + self._target_value = self.get_zwave_value("targetValue") + self._dimming_duration = self.get_zwave_value("duration") + if self._dimming_duration is not None: + self._supported_features |= SUPPORT_TRANSITION + self._calculate_color_values() + if self._supports_color: + self._supported_features |= SUPPORT_COLOR + if self._supports_color_temp: + self._supported_features |= SUPPORT_COLOR_TEMP + if self._supports_white_value: + self._supported_features |= SUPPORT_WHITE_VALUE + + @callback + def on_value_update(self) -> None: + """Call when a watched value is added or updated.""" + self._calculate_color_values() + + @property + def brightness(self) -> int: + """Return the brightness of this light between 0..255. + + Z-Wave multilevel switches use a range of [0, 99] to control brightness. + """ + if self._target_value is not None and self._target_value.value is not None: + return round((self._target_value.value / 99) * 255) + if self.info.primary_value.value is not None: + return round((self.info.primary_value.value / 99) * 255) + return 0 + + @property + def is_on(self) -> bool: + """Return true if device is on (brightness above 0).""" + return self.brightness > 0 + + @property + def hs_color(self) -> Optional[List[float]]: + """Return the hs color.""" + return self._hs_color + + @property + def white_value(self) -> Optional[int]: + """Return the white value of this light between 0..255.""" + return self._white_value + + @property + def color_temp(self) -> Optional[int]: + """Return the color temperature.""" + return self._color_temp + + @property + def min_mireds(self) -> int: + """Return the coldest color_temp that this light supports.""" + return self._min_mireds + + @property + def max_mireds(self) -> int: + """Return the warmest color_temp that this light supports.""" + return self._max_mireds + + @property + def supported_features(self) -> Optional[int]: + """Flag supported features.""" + return self._supported_features + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + # RGB/HS color + hs_color = kwargs.get(ATTR_HS_COLOR) + if hs_color is not None and self._supports_color: + # set white levels to 0 when setting rgb + await self._async_set_color("Warm White", 0) + await self._async_set_color("Cold White", 0) + red, green, blue = color_util.color_hs_to_RGB(*hs_color) + await self._async_set_color("Red", red) + await self._async_set_color("Green", green) + await self._async_set_color("Blue", blue) + else: + # turn off rgb when setting white values + await self._async_set_color("Red", 0) + await self._async_set_color("Green", 0) + await self._async_set_color("Blue", 0) + + # Color temperature + color_temp = kwargs.get(ATTR_COLOR_TEMP) + if color_temp is not None and self._supports_color_temp: + # Limit color temp to min/max values + cold = max( + 0, + min( + 255, + round( + (self._max_mireds - color_temp) + / (self._max_mireds - self._min_mireds) + * 255 + ), + ), + ) + warm = 255 - cold + await self._async_set_color("Warm White", warm) + await self._async_set_color("Cold White", cold) + + # White value + white_value = kwargs.get(ATTR_WHITE_VALUE) + if white_value is not None and self._supports_white_value: + await self._async_set_color("Warm White", white_value) + + # set brightness + await self._async_set_brightness( + kwargs.get(ATTR_BRIGHTNESS), kwargs.get(ATTR_TRANSITION) + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION)) + + async def _async_set_color(self, color_name: str, new_value: int) -> None: + """Set defined color to given value.""" + cur_zwave_value = self.get_zwave_value( + "currentColor", + CommandClass.SWITCH_COLOR, + value_property_key_name=color_name, + ) + # guard for unsupported command + if cur_zwave_value is None: + return + # no need to send same value + if cur_zwave_value.value == new_value: + return + # actually set the new color value + target_zwave_value = self.get_zwave_value( + "targetColor", + CommandClass.SWITCH_COLOR, + value_property_key_name=color_name, + ) + if target_zwave_value is None: + return + await self.info.node.async_set_value(target_zwave_value, new_value) + + async def _async_set_brightness( + self, brightness: Optional[int], transition: Optional[int] = None + ) -> None: + """Set new brightness to light.""" + if self.info.primary_value.value == brightness: + # no point in setting same brightness + return + if brightness is None and self.info.primary_value.value: + # there is no point in setting default brightness when light is already on + return + if brightness is None: + # Level 255 means to set it to previous value. + brightness = 255 + else: + # Zwave multilevel switches use a range of [0, 99] to control brightness. + brightness = byte_to_zwave_brightness(brightness) + # set transition value before seinding new brightness + await self._async_set_transition_duration(transition) + # setting a value requires setting targetValue + await self.info.node.async_set_value(self._target_value, brightness) + + async def _async_set_transition_duration( + self, duration: Optional[int] = None + ) -> None: + """Set the transition time for the brightness value.""" + if self._dimming_duration is None: + return + # pylint: disable=fixme,unreachable + # TODO: setting duration needs to be fixed upstream + # https://github.com/zwave-js/node-zwave-js/issues/1321 + return + + if duration is None: # type: ignore + # no transition specified by user, use defaults + duration = 7621 # anything over 7620 uses the factory default + else: + # transition specified by user + transition = duration + if transition <= 127: + duration = transition + else: + minutes = round(transition / 60) + LOGGER.debug( + "Transition rounded to %d minutes for %s", + minutes, + self.entity_id, + ) + duration = minutes + 128 + + # only send value if it differs from current + # this prevents sending a command for nothing + if self._dimming_duration.value != duration: + await self.info.node.async_set_value(self._dimming_duration, duration) + + @callback + def _calculate_color_values(self) -> None: + """Calculate light colors.""" + + # RGB support + red_val = self.get_zwave_value( + "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Red" + ) + green_val = self.get_zwave_value( + "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Green" + ) + blue_val = self.get_zwave_value( + "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Blue" + ) + if red_val and green_val and blue_val: + self._supports_color = True + # convert to HS + if ( + red_val.value is not None + and green_val.value is not None + and blue_val.value is not None + ): + self._hs = color_util.color_RGB_to_hs( + red_val.value, green_val.value, blue_val.value + ) + + # White colors + ww_val = self.get_zwave_value( + "currentColor", + CommandClass.SWITCH_COLOR, + value_property_key_name="Warm White", + ) + cw_val = self.get_zwave_value( + "currentColor", + CommandClass.SWITCH_COLOR, + value_property_key_name="Cold White", + ) + if ww_val and cw_val: + # Color temperature (CW + WW) Support + self._supports_color_temp = True + # Calculate color temps based on whites + cold_level = cw_val.value or 0 + if cold_level or ww_val.value is not None: + self._color_temp = round( + self._max_mireds + - ((cold_level / 255) * (self._max_mireds - self._min_mireds)) + ) + else: + self._color_temp = None + elif ww_val or cw_val: + # only one white channel + self._supports_white_value = True diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json new file mode 100644 index 00000000000..718a4da2b85 --- /dev/null +++ b/homeassistant/components/zwave_js/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "zwave_js", + "name": "Z-Wave JS", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/zwave_js", + "requirements": ["zwave-js-server-python==0.6.0"], + "codeowners": ["@home-assistant/z-wave"] +} diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py new file mode 100644 index 00000000000..ef1a68bb7a7 --- /dev/null +++ b/homeassistant/components/zwave_js/sensor.py @@ -0,0 +1,149 @@ +"""Representation of Z-Wave sensors.""" + +import logging +from typing import Callable, Dict, List, Optional + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass + +from homeassistant.components.sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DOMAIN as SENSOR_DOMAIN, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave sensor from config entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_sensor(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave Sensor.""" + entities: List[ZWaveBaseEntity] = [] + + if info.platform_hint == "string_sensor": + entities.append(ZWaveStringSensor(client, info)) + elif info.platform_hint == "numeric_sensor": + entities.append(ZWaveNumericSensor(client, info)) + else: + LOGGER.warning( + "Sensor not implemented for %s/%s", + info.platform_hint, + info.primary_value.propertyname, + ) + return + + async_add_entities(entities) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, f"{DOMAIN}_add_{SENSOR_DOMAIN}", async_add_sensor + ) + ) + + +class ZwaveSensorBase(ZWaveBaseEntity): + """Basic Representation of a Z-Wave sensor.""" + + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + if self.info.primary_value.command_class == CommandClass.BATTERY: + return DEVICE_CLASS_BATTERY + if self.info.primary_value.command_class == CommandClass.METER: + return DEVICE_CLASS_POWER + if self.info.primary_value.property_key_name == "W_Consumed": + return DEVICE_CLASS_POWER + if self.info.primary_value.property_key_name == "kWh_Consumed": + return DEVICE_CLASS_ENERGY + if self.info.primary_value.property_ == "Air temperature": + return DEVICE_CLASS_TEMPERATURE + return None + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + # We hide some of the more advanced sensors by default to not overwhelm users + if self.info.primary_value.command_class in [ + CommandClass.BASIC, + CommandClass.INDICATOR, + CommandClass.NOTIFICATION, + ]: + return False + return True + + @property + def force_update(self) -> bool: + """Force updates.""" + return True + + +class ZWaveStringSensor(ZwaveSensorBase): + """Representation of a Z-Wave String sensor.""" + + @property + def state(self) -> Optional[str]: + """Return state of the sensor.""" + if self.info.primary_value.value is None: + return None + return str(self.info.primary_value.value) + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return unit of measurement the value is expressed in.""" + if self.info.primary_value.metadata.unit is None: + return None + return str(self.info.primary_value.metadata.unit) + + +class ZWaveNumericSensor(ZwaveSensorBase): + """Representation of a Z-Wave Numeric sensor.""" + + @property + def state(self) -> float: + """Return state of the sensor.""" + if self.info.primary_value.value is None: + return 0 + return round(float(self.info.primary_value.value), 2) + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return unit of measurement the value is expressed in.""" + if self.info.primary_value.metadata.unit is None: + return None + if self.info.primary_value.metadata.unit == "C": + return TEMP_CELSIUS + if self.info.primary_value.metadata.unit == "F": + return TEMP_FAHRENHEIT + + return str(self.info.primary_value.metadata.unit) + + @property + def device_state_attributes(self) -> Optional[Dict[str, str]]: + """Return the device specific state attributes.""" + if ( + self.info.primary_value.value is None + or not self.info.primary_value.metadata.states + ): + return None + # add the value's label as property for multi-value (list) items + label = self.info.primary_value.metadata.states.get( + self.info.primary_value.value + ) or self.info.primary_value.metadata.states.get( + str(self.info.primary_value.value) + ) + return {"label": label} diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json new file mode 100644 index 00000000000..29136a03f48 --- /dev/null +++ b/homeassistant/components/zwave_js/strings.json @@ -0,0 +1,20 @@ +{ + "title": "Z-Wave JS", + "config": { + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]" + } + } + }, + "error": { + "invalid_ws_url": "Invalid websocket URL", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json new file mode 100644 index 00000000000..13b2e736bae --- /dev/null +++ b/homeassistant/components/zwave_js/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_ws_url": "Invalid websocket URL", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b696e29964a..3218ab4baa5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -240,5 +240,6 @@ FLOWS = [ "yeelight", "zerproc", "zha", - "zwave" + "zwave", + "zwave_js" ] diff --git a/requirements_all.txt b/requirements_all.txt index a6d094d1592..40b29e579d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2370,3 +2370,6 @@ zigpy==0.29.0 # homeassistant.components.zoneminder zm-py==0.5.2 + +# homeassistant.components.zwave_js +zwave-js-server-python==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6dc4f724a7b..b6a004aed2c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1169,3 +1169,6 @@ zigpy-znp==0.3.0 # homeassistant.components.zha zigpy==0.29.0 + +# homeassistant.components.zwave_js +zwave-js-server-python==0.6.0 diff --git a/setup.cfg b/setup.cfg index 1fc973ef21c..4137554257f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,7 @@ warn_redundant_casts = true warn_unused_configs = true -[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*] +[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.components.zwave_js.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*] strict = true ignore_errors = false warn_unreachable = true diff --git a/tests/components/zwave_js/__init__.py b/tests/components/zwave_js/__init__.py new file mode 100644 index 00000000000..bd4b740c856 --- /dev/null +++ b/tests/components/zwave_js/__init__.py @@ -0,0 +1 @@ +"""Tests for the Z-Wave JS integration.""" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py new file mode 100644 index 00000000000..61e50d86a2d --- /dev/null +++ b/tests/components/zwave_js/conftest.py @@ -0,0 +1,58 @@ +"""Provide common Z-Wave JS fixtures.""" +import json +from unittest.mock import patch + +import pytest +from zwave_js_server.model.driver import Driver +from zwave_js_server.model.node import Node + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture(name="controller_state", scope="session") +def controller_state_fixture(): + """Load the controller state fixture data.""" + return json.loads(load_fixture("zwave_js/controller_state.json")) + + +@pytest.fixture(name="multisensor_6_state", scope="session") +def multisensor_6_state_fixture(): + """Load the multisensor 6 node state fixture data.""" + return json.loads(load_fixture("zwave_js/multisensor_6_state.json")) + + +@pytest.fixture(name="client") +def mock_client_fixture(controller_state): + """Mock a client.""" + with patch( + "homeassistant.components.zwave_js.ZwaveClient", autospec=True + ) as client_class: + driver = Driver(client_class.return_value, controller_state) + client_class.return_value.driver = driver + yield client_class.return_value + + +@pytest.fixture(name="multisensor_6") +def multisensor_6_fixture(client, multisensor_6_state): + """Mock a multisensor 6 node.""" + node = Node(client, multisensor_6_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="integration") +async def integration_fixture(hass, client): + """Set up the zwave_js integration.""" + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + + def initialize_client(async_on_initialized): + """Init the client.""" + hass.async_create_task(async_on_initialized()) + + client.register_on_initialized.side_effect = initialize_client + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py new file mode 100644 index 00000000000..c6885377490 --- /dev/null +++ b/tests/components/zwave_js/test_config_flow.py @@ -0,0 +1,99 @@ +"""Test the Z-Wave JS config flow.""" +import asyncio +from unittest.mock import patch + +from zwave_js_server.version import VersionInfo + +from homeassistant import config_entries, setup +from homeassistant.components.zwave_js.const import DOMAIN + + +async def test_user_step_full(hass): + """Test we create an entry with user step.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + + with patch( + "homeassistant.components.zwave_js.config_flow.get_server_version", + return_value=VersionInfo( + driver_version="mock-driver-version", + server_version="mock-server-version", + home_id=1234, + ), + ), patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:3000", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Z-Wave JS" + assert result2["data"] == { + "url": "ws://localhost:3000", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert result2["result"].unique_id == 1234 + + +async def test_user_step_invalid_input(hass): + """Test we handle invalid auth in the user step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.zwave_js.config_flow.get_server_version", + side_effect=asyncio.TimeoutError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:3000", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "not-ws-url", + }, + ) + + assert result3["type"] == "form" + assert result3["errors"] == {"base": "invalid_ws_url"} + + +async def test_user_step_unexpected_exception(hass): + """Test we handle unexpected exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.zwave_js.config_flow.get_server_version", + side_effect=Exception("Boom"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:3000", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py new file mode 100644 index 00000000000..79876a5b453 --- /dev/null +++ b/tests/components/zwave_js/test_sensor.py @@ -0,0 +1,14 @@ +"""Test the Z-Wave JS sensor platform.""" +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS + +AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" + + +async def test_numeric_sensor(hass, multisensor_6, integration): + """Test the numeric sensor.""" + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state + assert state.state == "9.0" + assert state.attributes["unit_of_measurement"] == TEMP_CELSIUS + assert state.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE diff --git a/tests/fixtures/zwave_js/controller_state.json b/tests/fixtures/zwave_js/controller_state.json new file mode 100644 index 00000000000..df026e8fd2c --- /dev/null +++ b/tests/fixtures/zwave_js/controller_state.json @@ -0,0 +1,98 @@ +{ + "controller": { + "libraryVersion": "Z-Wave 3.95", + "type": 1, + "homeId": 3245146787, + "ownNodeId": 1, + "isSecondary": false, + "isUsingHomeIdFromOtherNetwork": false, + "isSISPresent": true, + "wasRealPrimary": true, + "isStaticUpdateController": true, + "isSlave": false, + "serialApiVersion": "1.0", + "manufacturerId": 134, + "productType": 257, + "productId": 90, + "supportedFunctionTypes": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 28, + 32, + 33, + 34, + 35, + 36, + 39, + 41, + 42, + 43, + 44, + 45, + 65, + 66, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 80, + 81, + 83, + 84, + 85, + 86, + 87, + 94, + 96, + 97, + 98, + 99, + 102, + 103, + 128, + 144, + 146, + 147, + 152, + 180, + 182, + 183, + 184, + 185, + 186, + 189, + 190, + 191, + 210, + 211, + 212, + 238, + 239 + ], + "sucNodeId": 1, + "supportsTimers": false + }, + "nodes": [ + ] +} diff --git a/tests/fixtures/zwave_js/multisensor_6_state.json b/tests/fixtures/zwave_js/multisensor_6_state.json new file mode 100644 index 00000000000..3c508ffd3ff --- /dev/null +++ b/tests/fixtures/zwave_js/multisensor_6_state.json @@ -0,0 +1,1830 @@ +{ + "nodeId": 52, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079, + "status": 1, + "ready": true, + "deviceClass": { + "basic": "Static Controller", + "generic": "Multilevel Sensor", + "specific": "Routing Multilevel Sensor", + "mandatorySupportedCCs": [ + "Basic", + "Multilevel Sensor" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 134, + "productId": 100, + "productType": 258, + "firmwareVersion": "1.12", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 134, + "manufacturer": "AEON Labs", + "label": "ZW100", + "description": "Multisensor 6", + "devices": [ + { + "productType": "0x0002", + "productId": "0x0064" + }, + { + "productType": "0x0102", + "productId": "0x0064" + }, + { + "productType": "0x0202", + "productId": "0x0064" + } + ], + "firmwareVersion": { + "min": "1.10", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "ZW100", + "neighbors": [ + 1, + 32 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 52, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079 + } + ], + "values": [ + { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 255 + }, + { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "commandClassName": "Binary Sensor", + "commandClass": 48, + "endpoint": 0, + "property": "Any", + "propertyName": "Any", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Any", + "ccSpecific": { + "sensorType": 255 + } + }, + "value": false + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Air temperature", + "propertyName": "Air temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "°C", + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + } + }, + "value": 9 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Illuminance", + "propertyName": "Illuminance", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "Lux", + "label": "Illuminance", + "ccSpecific": { + "sensorType": 3, + "scale": 1 + } + }, + "value": 0 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Humidity", + "propertyName": "Humidity", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "%", + "label": "Humidity", + "ccSpecific": { + "sensorType": 5, + "scale": 0 + } + }, + "value": 65 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Ultraviolet", + "propertyName": "Ultraviolet", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Ultraviolet", + "ccSpecific": { + "sensorType": 27, + "scale": 0 + } + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "Stay Awake in Battery Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Stay Awake in Battery Mode", + "description": "Stay awake for 10 minutes at power on", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 3, + "propertyName": "Motion Sensor reset timeout", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 10, + "max": 3600, + "default": 240, + "format": 0, + "allowManualEntry": true, + "label": "Motion Sensor reset timeout", + "description": "Motion Sensor reset timeout", + "isFromConfig": true + }, + "value": 240 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 4, + "propertyName": "Motion sensor sensitivity", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 5, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable, sensitivity level 1 (minimum)", + "2": "Enable, sensitivity level 2", + "3": "Enable, sensitivity level 3", + "4": "Enable, sensitivity level 4", + "5": "Enable, sensitivity level 5 (maximum)" + }, + "label": "Motion sensor sensitivity", + "description": "Sensitivity level of PIR sensor (1=minimum, 5=maximum)", + "isFromConfig": true + }, + "value": 5 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 5, + "propertyName": "Motion Sensor Triggered Command", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 1, + "format": 1, + "allowManualEntry": false, + "states": { + "1": "Send Basic Set CC", + "2": "Send Sensor Binary Report CC" + }, + "label": "Motion Sensor Triggered Command", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 8, + "propertyName": "Timeout after wake up", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 8, + "max": 255, + "default": 30, + "format": 1, + "allowManualEntry": true, + "label": "Timeout after wake up", + "description": "Set the timeout of awake after the Wake Up CC is sent out...", + "isFromConfig": true + }, + "value": 15 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 39, + "propertyName": "Low Battery Report", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 10, + "max": 50, + "default": 20, + "format": 0, + "allowManualEntry": true, + "label": "Low Battery Report", + "description": "Report Low Battery if below this value", + "isFromConfig": true + }, + "value": 20 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 40, + "propertyName": "Selective Reporting", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Selective Reporting", + "description": "Select to report on thresholds", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 42, + "propertyName": "Humidity Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 10, + "format": 0, + "allowManualEntry": true, + "label": "Humidity Threshold", + "description": "Humidity percent change threshold", + "isFromConfig": true + }, + "value": 10 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 43, + "propertyName": "Luminance Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 1000, + "default": 100, + "format": 0, + "allowManualEntry": true, + "label": "Luminance Threshold", + "description": "Luminance change threshold", + "isFromConfig": true + }, + "value": 100 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 44, + "propertyName": "Battery Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 10, + "format": 0, + "allowManualEntry": true, + "label": "Battery Threshold", + "description": "Battery level threshold", + "isFromConfig": true + }, + "value": 10 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 45, + "propertyName": "Ultraviolet Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 2, + "format": 0, + "allowManualEntry": true, + "label": "Ultraviolet Threshold", + "description": "Ultraviolet change threshold", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 46, + "propertyName": "Send Alarm Report if low temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Send Alarm Report if low temperature", + "description": "Send an alarm report if temperature is less than -15 °C", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 48, + "propertyName": "Send a report if the measurement is out of limits", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Send a report if the measurement is out of limits", + "description": "Send report when measurement is at upper/lower limit", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 51, + "propertyName": "Upper limit value of humidity sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 60, + "format": 0, + "allowManualEntry": true, + "label": "Upper limit value of humidity sensor", + "description": "Upper limit value of humidity sensor", + "isFromConfig": true + }, + "value": 60 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 52, + "propertyName": "Lower limit value of humidity sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Lower limit value of humidity sensor", + "description": "Lower limit value of humidity sensor", + "isFromConfig": true + }, + "value": 50 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 53, + "propertyName": "Upper limit value of Lighting sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 30000, + "default": 1000, + "format": 0, + "allowManualEntry": true, + "label": "Upper limit value of Lighting sensor", + "description": "Upper limit value of Lighting sensor", + "isFromConfig": true + }, + "value": 1000 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 54, + "propertyName": "Lower limit value of Lighting sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 30000, + "default": 100, + "format": 0, + "allowManualEntry": true, + "label": "Lower limit value of Lighting sensor", + "description": "Lower limit value of Lighting sensor", + "isFromConfig": true + }, + "value": 100 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 55, + "propertyName": "Upper limit value of ultraviolet sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 11, + "default": 8, + "format": 0, + "allowManualEntry": true, + "label": "Upper limit value of ultraviolet sensor", + "description": "Upper limit value of ultraviolet sensor", + "isFromConfig": true + }, + "value": 8 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 56, + "propertyName": "Lower limit value of ultraviolet sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 11, + "default": 4, + "format": 0, + "allowManualEntry": true, + "label": "Lower limit value of ultraviolet sensor", + "description": "Lower limit value of ultraviolet sensor", + "isFromConfig": true + }, + "value": 4 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 57, + "propertyName": "Recover limit value of temperature sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 65535, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Recover limit value of temperature sensor", + "description": "Recover limit value of temperature sensor", + "isFromConfig": true + }, + "value": 5122 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 58, + "propertyName": "Recover limit value of humidity sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 50, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Recover limit value of humidity sensor", + "description": "Recover limit value of humidity sensor", + "isFromConfig": true + }, + "value": 5 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 59, + "propertyName": "Recover limit value of Lighting sensor.", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 255, + "default": 10, + "format": 1, + "allowManualEntry": true, + "label": "Recover limit value of Lighting sensor.", + "description": "Recover limit value of Lighting sensor.", + "isFromConfig": true + }, + "value": 10 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 60, + "propertyName": "Recover limit value of Ultraviolet sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 5, + "default": 2, + "format": 0, + "allowManualEntry": true, + "label": "Recover limit value of Ultraviolet sensor", + "description": "Recover limit value of Ultraviolet sensor", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 61, + "propertyName": "Out-of-limit state of the Sensors", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Out-of-limit state of the Sensors", + "description": "Out-of-limit state of the Sensors", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 64, + "propertyName": "Default unit of the automatic temperature report", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Default unit of the automatic temperature report", + "description": "Default unit of the automatic temperature report", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 81, + "propertyName": "LED function", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Enable LED blinking", + "1": "Disable PIR LED", + "2": "Disable ALL" + }, + "label": "LED function", + "description": "Disable/Enable LED function", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 111, + "propertyName": "Group 1 Report Interval", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 3600, + "format": 0, + "allowManualEntry": true, + "label": "Group 1 Report Interval", + "description": "How often to update Group 1", + "isFromConfig": true + }, + "value": 3600 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 112, + "propertyName": "Group 2 Report Interval", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 3600, + "format": 0, + "allowManualEntry": true, + "label": "Group 2 Report Interval", + "description": "Group 2 Report Interval", + "isFromConfig": true + }, + "value": 3600 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 113, + "propertyName": "Group 3 Report Interval", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 3600, + "format": 0, + "allowManualEntry": true, + "label": "Group 3 Report Interval", + "description": "Group 3 Report Interval", + "isFromConfig": true + }, + "value": 3600 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 202, + "propertyName": "Humidity Sensor Calibration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -50, + "max": 50, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Humidity Sensor Calibration", + "description": "Humidity Sensor Calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 203, + "propertyName": "Luminance Sensor Calibration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": -1000, + "max": 1000, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Luminance Sensor Calibration", + "description": "Luminance Sensor Calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 204, + "propertyName": "Ultraviolet Sensor Calibration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -10, + "max": 10, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Ultraviolet Sensor Calibration", + "description": "Ultraviolet Sensor Calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 252, + "propertyName": "Disable/Enable Configuration Lock", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Disable/Enable Configuration Lock", + "description": "Disable/Enable Configuration Lock (0=Disable, 1=Enable)", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 1, + "propertyName": "Group 1: Send battery reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send battery reports", + "description": "Include battery information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 16, + "propertyName": "Group 1: Send ultraviolet reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send ultraviolet reports", + "description": "Include ultraviolet information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 32, + "propertyName": "Group 1: Send temperature reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send temperature reports", + "description": "Include temperature information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 64, + "propertyName": "Group 1: Send humidity reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send humidity reports", + "description": "Include humidity information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 128, + "propertyName": "Group 1: Send luminance reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send luminance reports", + "description": "Include luminance information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 1, + "propertyName": "Group 2: Send battery reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 16, + "propertyName": "Group 2: Send ultraviolet reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send ultraviolet reports", + "description": "Include ultraviolet information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 32, + "propertyName": "Group 2: Send temperature reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send temperature reports", + "description": "Include temperature information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 64, + "propertyName": "Group 2: Send humidity reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send humidity reports", + "description": "Include humidity information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 128, + "propertyName": "Group 2: Send luminance reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send luminance reports", + "description": "Include luminance information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 1, + "propertyName": "Group 3: Send battery reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send battery reports", + "description": "Include battery information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 16, + "propertyName": "Group 3: Send ultraviolet reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send ultraviolet reports", + "description": "Include ultraviolet information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 32, + "propertyName": "Group 3: Send temperature reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send temperature reports", + "description": "Include temperature information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 64, + "propertyName": "Group 3: Send humidity reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send humidity reports", + "description": "Include humidity information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 128, + "propertyName": "Group 3: Send luminance reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send luminance reports", + "description": "Include luminance information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 9, + "propertyKey": 1, + "propertyName": "Sleep State", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 2, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Asleep", + "1": "Awake" + }, + "label": "Sleep State", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 9, + "propertyKey": 256, + "propertyName": "Power Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 2, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "USB", + "1": "Battery" + }, + "label": "Power Mode", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 41, + "propertyKey": 15, + "propertyName": "Temperature Threshold (Unit)", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 3, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Celsius", + "2": "Fahrenheit" + }, + "label": "Temperature Threshold (Unit)", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 41, + "propertyKey": 16776960, + "propertyName": "Temperature Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 3, + "min": 0, + "max": 100, + "default": 20, + "format": 0, + "allowManualEntry": true, + "label": "Temperature Threshold", + "description": "Threshold change in temperature to induce an automatic report.", + "isFromConfig": true + }, + "value": 5122 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 49, + "propertyKey": 65280, + "propertyName": "Upper temperature limit (Unit)", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Celsius", + "2": "Fahrenheit" + }, + "label": "Upper temperature limit (Unit)", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 49, + "propertyKey": 4294901760, + "propertyName": "Upper temperature limit", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": -400, + "max": 2120, + "default": 280, + "format": 0, + "allowManualEntry": true, + "label": "Upper temperature limit", + "isFromConfig": true + }, + "value": 824 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 50, + "propertyKey": 65280, + "propertyName": "Lower temperature limit (Unit)", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Celsius", + "2": "Fahrenheit" + }, + "label": "Lower temperature limit (Unit)", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 50, + "propertyKey": 4294901760, + "propertyName": "Lower temperature limit", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": -400, + "max": 2120, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Lower temperature limit", + "isFromConfig": true + }, + "value": 320 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 201, + "propertyKey": 255, + "propertyName": "Temperature Calibration (Unit)", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Celsius", + "2": "Fahrenheit" + }, + "label": "Temperature Calibration (Unit)", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 201, + "propertyKey": 65280, + "propertyName": "Temperature Calibration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": -127, + "max": 127, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Temperature Calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 100, + "propertyName": "Set parameters 101-103 to default.", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Set parameters 101-103 to default.", + "description": "Reset 101-103 to defaults", + "isFromConfig": true + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 110, + "propertyName": "Set parameters 111-113 to default.", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Set parameters 111-113 to default.", + "description": "Set parameters 111-113 to default.", + "isFromConfig": true + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 255, + "propertyName": "Reset to default factory settings", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1431655765, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Resets all configuration parameters to defaults", + "1431655765": "Reset to default factory settings and be excluded" + }, + "label": "Reset to default factory settings", + "isFromConfig": true + } + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Home Security", + "propertyKey": "Cover status", + "propertyName": "Home Security", + "propertyKeyName": "Cover status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Cover status", + "states": { + "0": "idle", + "3": "Tampering, product cover removed" + }, + "ccSpecific": { + "notificationType": 7 + } + }, + "value": 0 + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Home Security", + "propertyKey": "Motion sensor status", + "propertyName": "Home Security", + "propertyKeyName": "Motion sensor status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Motion sensor status", + "states": { + "0": "idle", + "8": "Motion detection" + }, + "ccSpecific": { + "notificationType": 7 + } + }, + "value": 8 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 134 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 258 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 100 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "commandClassName": "Wake Up", + "commandClass": 132, + "endpoint": 0, + "property": "wakeUpInterval", + "propertyName": "wakeUpInterval", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "min": 240, + "max": 3600, + "label": "Wake Up interval", + "steps": 60, + "default": 3600 + }, + "value": 3600 + }, + { + "commandClassName": "Wake Up", + "commandClass": 132, + "endpoint": 0, + "property": "controllerNodeId", + "propertyName": "controllerNodeId", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Node ID of the controller" + }, + "value": 1 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Libary type" + }, + "value": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.54" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.12" + ] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} -- GitLab