mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 07:07:28 +00:00
Create zwave-js select platform and discover additional siren values (#53018)
* Create zwave-js select platform and add siren values to number and select platforms * use constants while we wait for lib release * comments * rename stuff in tests to prepare for protection CC PR * Switch to 0-1 range for number entity * Update homeassistant/components/zwave_js/number.py Co-authored-by: kpine <keith.pine@gmail.com> * Change step * Switch to ToneID * Better error handling * Add test for coerage Co-authored-by: kpine <keith.pine@gmail.com>
This commit is contained in:
parent
441552e04c
commit
a41ee9e870
@ -642,6 +642,30 @@ DISCOVERY_SCHEMAS = [
|
|||||||
platform="siren",
|
platform="siren",
|
||||||
primary_value=SIREN_TONE_SCHEMA,
|
primary_value=SIREN_TONE_SCHEMA,
|
||||||
),
|
),
|
||||||
|
# select
|
||||||
|
# siren default tone
|
||||||
|
ZWaveDiscoverySchema(
|
||||||
|
platform="select",
|
||||||
|
hint="Default tone",
|
||||||
|
primary_value=ZWaveValueDiscoverySchema(
|
||||||
|
command_class={CommandClass.SOUND_SWITCH},
|
||||||
|
property={"defaultToneId"},
|
||||||
|
type={"number"},
|
||||||
|
),
|
||||||
|
required_values=[SIREN_TONE_SCHEMA],
|
||||||
|
),
|
||||||
|
# number
|
||||||
|
# siren default volume
|
||||||
|
ZWaveDiscoverySchema(
|
||||||
|
platform="number",
|
||||||
|
hint="volume",
|
||||||
|
primary_value=ZWaveValueDiscoverySchema(
|
||||||
|
command_class={CommandClass.SOUND_SWITCH},
|
||||||
|
property={"defaultVolume"},
|
||||||
|
type={"number"},
|
||||||
|
),
|
||||||
|
required_values=[SIREN_TONE_SCHEMA],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,7 +26,10 @@ async def async_setup_entry(
|
|||||||
def async_add_number(info: ZwaveDiscoveryInfo) -> None:
|
def async_add_number(info: ZwaveDiscoveryInfo) -> None:
|
||||||
"""Add Z-Wave number entity."""
|
"""Add Z-Wave number entity."""
|
||||||
entities: list[ZWaveBaseEntity] = []
|
entities: list[ZWaveBaseEntity] = []
|
||||||
entities.append(ZwaveNumberEntity(config_entry, client, info))
|
if info.platform_hint == "volume":
|
||||||
|
entities.append(ZwaveVolumeNumberEntity(config_entry, client, info))
|
||||||
|
else:
|
||||||
|
entities.append(ZwaveNumberEntity(config_entry, client, info))
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
config_entry.async_on_unload(
|
config_entry.async_on_unload(
|
||||||
@ -87,3 +90,38 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity):
|
|||||||
async def async_set_value(self, value: float) -> None:
|
async def async_set_value(self, value: float) -> None:
|
||||||
"""Set new value."""
|
"""Set new value."""
|
||||||
await self.info.node.async_set_value(self._target_value, value)
|
await self.info.node.async_set_value(self._target_value, value)
|
||||||
|
|
||||||
|
|
||||||
|
class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity):
|
||||||
|
"""Representation of a volume number entity."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a ZwaveVolumeNumberEntity entity."""
|
||||||
|
super().__init__(config_entry, client, info)
|
||||||
|
self.correction_factor = int(
|
||||||
|
self.info.primary_value.metadata.max - self.info.primary_value.metadata.min
|
||||||
|
)
|
||||||
|
# Fallback in case we can't properly calculate correction factor
|
||||||
|
if self.correction_factor == 0:
|
||||||
|
self.correction_factor = 1
|
||||||
|
|
||||||
|
# Entity class attributes
|
||||||
|
self._attr_min_value = 0
|
||||||
|
self._attr_max_value = 1
|
||||||
|
self._attr_step = 0.01
|
||||||
|
self._attr_name = self.generate_name(include_value_name=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> float | None:
|
||||||
|
"""Return the entity value."""
|
||||||
|
if self.info.primary_value.value is None:
|
||||||
|
return None
|
||||||
|
return float(self.info.primary_value.value) / self.correction_factor
|
||||||
|
|
||||||
|
async def async_set_value(self, value: float) -> None:
|
||||||
|
"""Set new value."""
|
||||||
|
await self.info.node.async_set_value(
|
||||||
|
self.info.primary_value, round(value * self.correction_factor)
|
||||||
|
)
|
||||||
|
91
homeassistant/components/zwave_js/select.py
Normal file
91
homeassistant/components/zwave_js/select.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
"""Support for Z-Wave controls using the select platform."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from zwave_js_server.client import Client as ZwaveClient
|
||||||
|
from zwave_js_server.const import CommandClass, ToneID
|
||||||
|
|
||||||
|
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN, SelectEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import DATA_CLIENT, DOMAIN
|
||||||
|
from .discovery import ZwaveDiscoveryInfo
|
||||||
|
from .entity import ZWaveBaseEntity
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Z-Wave Select entity from Config Entry."""
|
||||||
|
client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_select(info: ZwaveDiscoveryInfo) -> None:
|
||||||
|
"""Add Z-Wave select entity."""
|
||||||
|
entities: list[ZWaveBaseEntity] = []
|
||||||
|
if info.platform_hint == "Default tone":
|
||||||
|
entities.append(ZwaveDefaultToneSelectEntity(config_entry, client, info))
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
config_entry.async_on_unload(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
hass,
|
||||||
|
f"{DOMAIN}_{config_entry.entry_id}_add_{SELECT_DOMAIN}",
|
||||||
|
async_add_select,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity):
|
||||||
|
"""Representation of a Z-Wave default tone select entity."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a ZwaveDefaultToneSelectEntity entity."""
|
||||||
|
super().__init__(config_entry, client, info)
|
||||||
|
self._tones_value = self.get_zwave_value(
|
||||||
|
"toneId", command_class=CommandClass.SOUND_SWITCH
|
||||||
|
)
|
||||||
|
|
||||||
|
# Entity class attributes
|
||||||
|
self._attr_name = self.generate_name(
|
||||||
|
include_value_name=True, alternate_value_name=info.platform_hint
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> list[str]:
|
||||||
|
"""Return a set of selectable options."""
|
||||||
|
# We know we can assert because this value is part of the discovery schema
|
||||||
|
assert self._tones_value
|
||||||
|
return [
|
||||||
|
val
|
||||||
|
for key, val in self._tones_value.metadata.states.items()
|
||||||
|
if int(key) not in (ToneID.DEFAULT, ToneID.OFF)
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str | None:
|
||||||
|
"""Return the selected entity option to represent the entity state."""
|
||||||
|
# We know we can assert because this value is part of the discovery schema
|
||||||
|
assert self._tones_value
|
||||||
|
return str(
|
||||||
|
self._tones_value.metadata.states.get(
|
||||||
|
str(self.info.primary_value.value), self.info.primary_value.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str | int) -> None:
|
||||||
|
"""Change the selected option."""
|
||||||
|
# We know we can assert because this value is part of the discovery schema
|
||||||
|
assert self._tones_value
|
||||||
|
key = next(
|
||||||
|
key
|
||||||
|
for key, val in self._tones_value.metadata.states.items()
|
||||||
|
if val == option
|
||||||
|
)
|
||||||
|
await self.info.node.async_set_value(self.info.primary_value, int(key))
|
@ -1,11 +1,13 @@
|
|||||||
"""Test the Z-Wave JS number platform."""
|
"""Test the Z-Wave JS number platform."""
|
||||||
from zwave_js_server.event import Event
|
from zwave_js_server.event import Event
|
||||||
|
|
||||||
|
from homeassistant.const import STATE_UNKNOWN
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
from .common import BASIC_NUMBER_ENTITY
|
from .common import BASIC_NUMBER_ENTITY
|
||||||
|
|
||||||
NUMBER_ENTITY = "number.thermostat_hvac_valve_control"
|
NUMBER_ENTITY = "number.thermostat_hvac_valve_control"
|
||||||
|
VOLUME_NUMBER_ENTITY = "number.indoor_siren_6_default_volume_2"
|
||||||
|
|
||||||
|
|
||||||
async def test_number(hass, client, aeotec_radiator_thermostat, integration):
|
async def test_number(hass, client, aeotec_radiator_thermostat, integration):
|
||||||
@ -73,6 +75,98 @@ async def test_number(hass, client, aeotec_radiator_thermostat, integration):
|
|||||||
assert state.state == "99.0"
|
assert state.state == "99.0"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_volume_number(hass, client, aeotec_zw164_siren, integration):
|
||||||
|
"""Test the volume number entity."""
|
||||||
|
node = aeotec_zw164_siren
|
||||||
|
state = hass.states.get(VOLUME_NUMBER_ENTITY)
|
||||||
|
|
||||||
|
assert state
|
||||||
|
assert state.state == "1.0"
|
||||||
|
assert state.attributes["step"] == 0.01
|
||||||
|
assert state.attributes["max"] == 1.0
|
||||||
|
assert state.attributes["min"] == 0
|
||||||
|
|
||||||
|
# Test turn on setting value
|
||||||
|
await hass.services.async_call(
|
||||||
|
"number",
|
||||||
|
"set_value",
|
||||||
|
{"entity_id": VOLUME_NUMBER_ENTITY, "value": 0.3},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(client.async_send_command.call_args_list) == 1
|
||||||
|
args = client.async_send_command.call_args[0][0]
|
||||||
|
assert args["command"] == "node.set_value"
|
||||||
|
assert args["nodeId"] == node.node_id
|
||||||
|
assert args["valueId"] == {
|
||||||
|
"endpoint": 2,
|
||||||
|
"commandClass": 121,
|
||||||
|
"commandClassName": "Sound Switch",
|
||||||
|
"property": "defaultVolume",
|
||||||
|
"propertyName": "defaultVolume",
|
||||||
|
"ccVersion": 1,
|
||||||
|
"metadata": {
|
||||||
|
"type": "number",
|
||||||
|
"readable": True,
|
||||||
|
"writeable": True,
|
||||||
|
"label": "Default volume",
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"unit": "%",
|
||||||
|
},
|
||||||
|
"value": 100,
|
||||||
|
}
|
||||||
|
assert args["value"] == 30
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test value update from value updated event
|
||||||
|
event = Event(
|
||||||
|
type="value updated",
|
||||||
|
data={
|
||||||
|
"source": "node",
|
||||||
|
"event": "value updated",
|
||||||
|
"nodeId": 4,
|
||||||
|
"args": {
|
||||||
|
"commandClassName": "Sound Switch",
|
||||||
|
"commandClass": 121,
|
||||||
|
"endpoint": 2,
|
||||||
|
"property": "defaultVolume",
|
||||||
|
"newValue": 30,
|
||||||
|
"prevValue": 100,
|
||||||
|
"propertyName": "defaultVolume",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
node.receive_event(event)
|
||||||
|
|
||||||
|
state = hass.states.get(VOLUME_NUMBER_ENTITY)
|
||||||
|
assert state.state == "0.3"
|
||||||
|
|
||||||
|
# Test null value
|
||||||
|
event = Event(
|
||||||
|
type="value updated",
|
||||||
|
data={
|
||||||
|
"source": "node",
|
||||||
|
"event": "value updated",
|
||||||
|
"nodeId": 4,
|
||||||
|
"args": {
|
||||||
|
"commandClassName": "Sound Switch",
|
||||||
|
"commandClass": 121,
|
||||||
|
"endpoint": 2,
|
||||||
|
"property": "defaultVolume",
|
||||||
|
"newValue": None,
|
||||||
|
"prevValue": 30,
|
||||||
|
"propertyName": "defaultVolume",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
node.receive_event(event)
|
||||||
|
|
||||||
|
state = hass.states.get(VOLUME_NUMBER_ENTITY)
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
async def test_disabled_basic_number(hass, ge_in_wall_dimmer_switch, integration):
|
async def test_disabled_basic_number(hass, ge_in_wall_dimmer_switch, integration):
|
||||||
"""Test number is created from Basic CC and is disabled."""
|
"""Test number is created from Basic CC and is disabled."""
|
||||||
ent_reg = er.async_get(hass)
|
ent_reg = er.async_get(hass)
|
||||||
|
101
tests/components/zwave_js/test_select.py
Normal file
101
tests/components/zwave_js/test_select.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
"""Test the Z-Wave JS number platform."""
|
||||||
|
from zwave_js_server.event import Event
|
||||||
|
|
||||||
|
DEFAULT_TONE_SELECT_ENTITY = "select.indoor_siren_6_default_tone_2"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_default_tone_select(hass, client, aeotec_zw164_siren, integration):
|
||||||
|
"""Test the default tone select entity."""
|
||||||
|
node = aeotec_zw164_siren
|
||||||
|
state = hass.states.get(DEFAULT_TONE_SELECT_ENTITY)
|
||||||
|
|
||||||
|
assert state
|
||||||
|
assert state.state == "17ALAR~1 (35 sec)"
|
||||||
|
attr = state.attributes
|
||||||
|
assert attr["options"] == [
|
||||||
|
"01DING~1 (5 sec)",
|
||||||
|
"02DING~1 (9 sec)",
|
||||||
|
"03TRAD~1 (11 sec)",
|
||||||
|
"04ELEC~1 (2 sec)",
|
||||||
|
"05WEST~1 (13 sec)",
|
||||||
|
"06CHIM~1 (7 sec)",
|
||||||
|
"07CUCK~1 (31 sec)",
|
||||||
|
"08TRAD~1 (6 sec)",
|
||||||
|
"09SMOK~1 (11 sec)",
|
||||||
|
"10SMOK~1 (6 sec)",
|
||||||
|
"11FIRE~1 (35 sec)",
|
||||||
|
"12COSE~1 (5 sec)",
|
||||||
|
"13KLAX~1 (38 sec)",
|
||||||
|
"14DEEP~1 (41 sec)",
|
||||||
|
"15WARN~1 (37 sec)",
|
||||||
|
"16TORN~1 (46 sec)",
|
||||||
|
"17ALAR~1 (35 sec)",
|
||||||
|
"18DEEP~1 (62 sec)",
|
||||||
|
"19ALAR~1 (15 sec)",
|
||||||
|
"20ALAR~1 (7 sec)",
|
||||||
|
"21DIGI~1 (8 sec)",
|
||||||
|
"22ALER~1 (64 sec)",
|
||||||
|
"23SHIP~1 (4 sec)",
|
||||||
|
"25CHRI~1 (4 sec)",
|
||||||
|
"26GONG~1 (12 sec)",
|
||||||
|
"27SING~1 (1 sec)",
|
||||||
|
"28TONA~1 (5 sec)",
|
||||||
|
"29UPWA~1 (2 sec)",
|
||||||
|
"30DOOR~1 (27 sec)",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Test select option with string value
|
||||||
|
await hass.services.async_call(
|
||||||
|
"select",
|
||||||
|
"select_option",
|
||||||
|
{"entity_id": DEFAULT_TONE_SELECT_ENTITY, "option": "30DOOR~1 (27 sec)"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(client.async_send_command.call_args_list) == 1
|
||||||
|
args = client.async_send_command.call_args[0][0]
|
||||||
|
assert args["command"] == "node.set_value"
|
||||||
|
assert args["nodeId"] == node.node_id
|
||||||
|
assert args["valueId"] == {
|
||||||
|
"endpoint": 2,
|
||||||
|
"commandClass": 121,
|
||||||
|
"commandClassName": "Sound Switch",
|
||||||
|
"property": "defaultToneId",
|
||||||
|
"propertyName": "defaultToneId",
|
||||||
|
"ccVersion": 1,
|
||||||
|
"metadata": {
|
||||||
|
"type": "number",
|
||||||
|
"readable": True,
|
||||||
|
"writeable": True,
|
||||||
|
"label": "Default tone ID",
|
||||||
|
"min": 0,
|
||||||
|
"max": 254,
|
||||||
|
},
|
||||||
|
"value": 17,
|
||||||
|
}
|
||||||
|
assert args["value"] == 30
|
||||||
|
|
||||||
|
client.async_send_command.reset_mock()
|
||||||
|
|
||||||
|
# Test value update from value updated event
|
||||||
|
event = Event(
|
||||||
|
type="value updated",
|
||||||
|
data={
|
||||||
|
"source": "node",
|
||||||
|
"event": "value updated",
|
||||||
|
"nodeId": node.node_id,
|
||||||
|
"args": {
|
||||||
|
"commandClassName": "Sound Switch",
|
||||||
|
"commandClass": 121,
|
||||||
|
"endpoint": 2,
|
||||||
|
"property": "defaultToneId",
|
||||||
|
"newValue": 30,
|
||||||
|
"prevValue": 17,
|
||||||
|
"propertyName": "defaultToneId",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
node.receive_event(event)
|
||||||
|
|
||||||
|
state = hass.states.get(DEFAULT_TONE_SELECT_ENTITY)
|
||||||
|
assert state.state == "30DOOR~1 (27 sec)"
|
Loading…
x
Reference in New Issue
Block a user