mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
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>
This commit is contained in:
parent
1402e7ae56
commit
d68fdbc283
@ -1092,6 +1092,11 @@ omit =
|
|||||||
homeassistant/components/zoneminder/*
|
homeassistant/components/zoneminder/*
|
||||||
homeassistant/components/supla/*
|
homeassistant/components/supla/*
|
||||||
homeassistant/components/zwave/util.py
|
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]
|
[report]
|
||||||
# Regexes for lines to exclude from consideration
|
# Regexes for lines to exclude from consideration
|
||||||
|
@ -21,6 +21,7 @@ repos:
|
|||||||
- --skip="./.*,*.csv,*.json"
|
- --skip="./.*,*.csv,*.json"
|
||||||
- --quiet-level=2
|
- --quiet-level=2
|
||||||
exclude_types: [csv, json]
|
exclude_types: [csv, json]
|
||||||
|
exclude: ^tests/fixtures/
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
rev: 3.8.4
|
rev: 3.8.4
|
||||||
hooks:
|
hooks:
|
||||||
|
@ -537,6 +537,7 @@ homeassistant/components/zodiac/* @JulienTant
|
|||||||
homeassistant/components/zone/* @home-assistant/core
|
homeassistant/components/zone/* @home-assistant/core
|
||||||
homeassistant/components/zoneminder/* @rohankapoorcom
|
homeassistant/components/zoneminder/* @rohankapoorcom
|
||||||
homeassistant/components/zwave/* @home-assistant/z-wave
|
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||||
|
homeassistant/components/zwave_js/* @home-assistant/z-wave
|
||||||
|
|
||||||
# Individual files
|
# Individual files
|
||||||
homeassistant/components/demo/weather @fabaff
|
homeassistant/components/demo/weather @fabaff
|
||||||
|
179
homeassistant/components/zwave_js/__init__.py
Normal file
179
homeassistant/components/zwave_js/__init__.py
Normal file
@ -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
|
78
homeassistant/components/zwave_js/config_flow.py
Normal file
78
homeassistant/components/zwave_js/config_flow.py
Normal file
@ -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
|
9
homeassistant/components/zwave_js/const.py
Normal file
9
homeassistant/components/zwave_js/const.py
Normal file
@ -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"
|
160
homeassistant/components/zwave_js/discovery.py
Normal file
160
homeassistant/components/zwave_js/discovery.py
Normal file
@ -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
|
151
homeassistant/components/zwave_js/entity.py
Normal file
151
homeassistant/components/zwave_js/entity.py
Normal file
@ -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
|
322
homeassistant/components/zwave_js/light.py
Normal file
322
homeassistant/components/zwave_js/light.py
Normal file
@ -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
|
8
homeassistant/components/zwave_js/manifest.json
Normal file
8
homeassistant/components/zwave_js/manifest.json
Normal file
@ -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"]
|
||||||
|
}
|
149
homeassistant/components/zwave_js/sensor.py
Normal file
149
homeassistant/components/zwave_js/sensor.py
Normal file
@ -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}
|
20
homeassistant/components/zwave_js/strings.json
Normal file
20
homeassistant/components/zwave_js/strings.json
Normal file
@ -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%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
homeassistant/components/zwave_js/translations/en.json
Normal file
20
homeassistant/components/zwave_js/translations/en.json
Normal file
@ -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"
|
||||||
|
}
|
@ -240,5 +240,6 @@ FLOWS = [
|
|||||||
"yeelight",
|
"yeelight",
|
||||||
"zerproc",
|
"zerproc",
|
||||||
"zha",
|
"zha",
|
||||||
"zwave"
|
"zwave",
|
||||||
|
"zwave_js"
|
||||||
]
|
]
|
||||||
|
@ -2370,3 +2370,6 @@ zigpy==0.29.0
|
|||||||
|
|
||||||
# homeassistant.components.zoneminder
|
# homeassistant.components.zoneminder
|
||||||
zm-py==0.5.2
|
zm-py==0.5.2
|
||||||
|
|
||||||
|
# homeassistant.components.zwave_js
|
||||||
|
zwave-js-server-python==0.6.0
|
||||||
|
@ -1169,3 +1169,6 @@ zigpy-znp==0.3.0
|
|||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
zigpy==0.29.0
|
zigpy==0.29.0
|
||||||
|
|
||||||
|
# homeassistant.components.zwave_js
|
||||||
|
zwave-js-server-python==0.6.0
|
||||||
|
@ -42,7 +42,7 @@ warn_redundant_casts = true
|
|||||||
warn_unused_configs = 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
|
strict = true
|
||||||
ignore_errors = false
|
ignore_errors = false
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
1
tests/components/zwave_js/__init__.py
Normal file
1
tests/components/zwave_js/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the Z-Wave JS integration."""
|
58
tests/components/zwave_js/conftest.py
Normal file
58
tests/components/zwave_js/conftest.py
Normal file
@ -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
|
99
tests/components/zwave_js/test_config_flow.py
Normal file
99
tests/components/zwave_js/test_config_flow.py
Normal file
@ -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"}
|
14
tests/components/zwave_js/test_sensor.py
Normal file
14
tests/components/zwave_js/test_sensor.py
Normal file
@ -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
|
98
tests/fixtures/zwave_js/controller_state.json
vendored
Normal file
98
tests/fixtures/zwave_js/controller_state.json
vendored
Normal file
@ -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": [
|
||||||
|
]
|
||||||
|
}
|
1830
tests/fixtures/zwave_js/multisensor_6_state.json
vendored
Normal file
1830
tests/fixtures/zwave_js/multisensor_6_state.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user