Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create zwave-js select platform and discover additional siren values #53018

Merged
merged 11 commits into from Aug 16, 2021
24 changes: 24 additions & 0 deletions homeassistant/components/zwave_js/discovery.py
Expand Up @@ -642,6 +642,30 @@ def get_config_parameter_discovery_schema(
platform="siren",
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],
),
]


Expand Down
40 changes: 39 additions & 1 deletion homeassistant/components/zwave_js/number.py
Expand Up @@ -26,7 +26,10 @@ async def async_setup_entry(
def async_add_number(info: ZwaveDiscoveryInfo) -> None:
"""Add Z-Wave number entity."""
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)

config_entry.async_on_unload(
Expand Down Expand Up @@ -87,3 +90,38 @@ def unit_of_measurement(self) -> str | None:
async def async_set_value(self, value: float) -> None:
"""Set new 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
raman325 marked this conversation as resolved.
Show resolved Hide resolved

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 changes: 91 additions & 0 deletions homeassistant/components/zwave_js/select.py
@@ -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(
raman325 marked this conversation as resolved.
Show resolved Hide resolved
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))
94 changes: 94 additions & 0 deletions tests/components/zwave_js/test_number.py
@@ -1,11 +1,13 @@
"""Test the Z-Wave JS number platform."""
from zwave_js_server.event import Event

from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers import entity_registry as er

from .common import BASIC_NUMBER_ENTITY

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):
Expand Down Expand Up @@ -73,6 +75,98 @@ async def test_number(hass, client, aeotec_radiator_thermostat, integration):
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):
"""Test number is created from Basic CC and is disabled."""
ent_reg = er.async_get(hass)
Expand Down
101 changes: 101 additions & 0 deletions tests/components/zwave_js/test_select.py
@@ -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)"