Add Humidifier support to zwave_js (#65847)
parent
cfd763db40
commit
87593fa3ec
|
@ -16,6 +16,9 @@ from zwave_js_server.const import (
|
|||
from zwave_js_server.const.command_class.barrier_operator import (
|
||||
SIGNALING_STATE_PROPERTY,
|
||||
)
|
||||
from zwave_js_server.const.command_class.humidity_control import (
|
||||
HUMIDITY_CONTROL_MODE_PROPERTY,
|
||||
)
|
||||
from zwave_js_server.const.command_class.lock import (
|
||||
CURRENT_MODE_PROPERTY,
|
||||
DOOR_STATUS_PROPERTY,
|
||||
|
@ -492,6 +495,16 @@ DISCOVERY_SCHEMAS = [
|
|||
type={"any"},
|
||||
),
|
||||
),
|
||||
# humidifier
|
||||
# hygrostats supporting mode (and optional setpoint)
|
||||
ZWaveDiscoverySchema(
|
||||
platform="humidifier",
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.HUMIDITY_CONTROL_MODE},
|
||||
property={HUMIDITY_CONTROL_MODE_PROPERTY},
|
||||
type={"number"},
|
||||
),
|
||||
),
|
||||
# climate
|
||||
# thermostats supporting mode (and optional setpoint)
|
||||
ZWaveDiscoverySchema(
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
"""Representation of Z-Wave humidifiers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.const import CommandClass
|
||||
from zwave_js_server.const.command_class.humidity_control import (
|
||||
HUMIDITY_CONTROL_SETPOINT_PROPERTY,
|
||||
HumidityControlMode,
|
||||
HumidityControlSetpointType,
|
||||
)
|
||||
from zwave_js_server.model.value import Value as ZwaveValue
|
||||
|
||||
from homeassistant.components.humidifier import (
|
||||
HumidifierDeviceClass,
|
||||
HumidifierEntity,
|
||||
HumidifierEntityDescription,
|
||||
)
|
||||
from homeassistant.components.humidifier.const import (
|
||||
DEFAULT_MAX_HUMIDITY,
|
||||
DEFAULT_MIN_HUMIDITY,
|
||||
DOMAIN as HUMIDIFIER_DOMAIN,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@dataclass
|
||||
class ZwaveHumidifierEntityDescriptionRequiredKeys:
|
||||
"""A class for humidifier entity description required keys."""
|
||||
|
||||
# The "on" control mode for this entity, e.g. HUMIDIFY for humidifier
|
||||
on_mode: HumidityControlMode
|
||||
|
||||
# The "on" control mode for the inverse entity, e.g. DEHUMIDIFY for humidifier
|
||||
inverse_mode: HumidityControlMode
|
||||
|
||||
# The setpoint type controlled by this entity
|
||||
setpoint_type: HumidityControlSetpointType
|
||||
|
||||
|
||||
@dataclass
|
||||
class ZwaveHumidifierEntityDescription(
|
||||
HumidifierEntityDescription, ZwaveHumidifierEntityDescriptionRequiredKeys
|
||||
):
|
||||
"""A class that describes the humidifier or dehumidifier entity."""
|
||||
|
||||
|
||||
HUMIDIFIER_ENTITY_DESCRIPTION = ZwaveHumidifierEntityDescription(
|
||||
key="humidifier",
|
||||
device_class=HumidifierDeviceClass.HUMIDIFIER,
|
||||
on_mode=HumidityControlMode.HUMIDIFY,
|
||||
inverse_mode=HumidityControlMode.DEHUMIDIFY,
|
||||
setpoint_type=HumidityControlSetpointType.HUMIDIFIER,
|
||||
)
|
||||
|
||||
|
||||
DEHUMIDIFIER_ENTITY_DESCRIPTION = ZwaveHumidifierEntityDescription(
|
||||
key="dehumidifier",
|
||||
device_class=HumidifierDeviceClass.DEHUMIDIFIER,
|
||||
on_mode=HumidityControlMode.DEHUMIDIFY,
|
||||
inverse_mode=HumidityControlMode.HUMIDIFY,
|
||||
setpoint_type=HumidityControlSetpointType.DEHUMIDIFIER,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Z-Wave humidifier from config entry."""
|
||||
client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
|
||||
|
||||
@callback
|
||||
def async_add_humidifier(info: ZwaveDiscoveryInfo) -> None:
|
||||
"""Add Z-Wave Humidifier."""
|
||||
entities: list[ZWaveBaseEntity] = []
|
||||
|
||||
if (
|
||||
str(HumidityControlMode.HUMIDIFY.value)
|
||||
in info.primary_value.metadata.states
|
||||
):
|
||||
entities.append(
|
||||
ZWaveHumidifier(
|
||||
config_entry, client, info, HUMIDIFIER_ENTITY_DESCRIPTION
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
str(HumidityControlMode.DEHUMIDIFY.value)
|
||||
in info.primary_value.metadata.states
|
||||
):
|
||||
entities.append(
|
||||
ZWaveHumidifier(
|
||||
config_entry, client, info, DEHUMIDIFIER_ENTITY_DESCRIPTION
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
f"{DOMAIN}_{config_entry.entry_id}_add_{HUMIDIFIER_DOMAIN}",
|
||||
async_add_humidifier,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity):
|
||||
"""Representation of a Z-Wave Humidifier or Dehumidifier."""
|
||||
|
||||
entity_description: ZwaveHumidifierEntityDescription
|
||||
_current_mode: ZwaveValue
|
||||
_setpoint: ZwaveValue | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
client: ZwaveClient,
|
||||
info: ZwaveDiscoveryInfo,
|
||||
description: ZwaveHumidifierEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize humidifier."""
|
||||
super().__init__(config_entry, client, info)
|
||||
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_name = f"{self._attr_name} {description.key}"
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{description.key}"
|
||||
|
||||
self._current_mode = self.info.primary_value
|
||||
|
||||
self._setpoint = self.get_zwave_value(
|
||||
HUMIDITY_CONTROL_SETPOINT_PROPERTY,
|
||||
command_class=CommandClass.HUMIDITY_CONTROL_SETPOINT,
|
||||
value_property_key=description.setpoint_type,
|
||||
add_to_watched_value_ids=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return True if entity is on."""
|
||||
return int(self._current_mode.value) in [
|
||||
self.entity_description.on_mode,
|
||||
HumidityControlMode.AUTO,
|
||||
]
|
||||
|
||||
def _supports_inverse_mode(self) -> bool:
|
||||
return (
|
||||
str(self.entity_description.inverse_mode.value)
|
||||
in self._current_mode.metadata.states
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on device."""
|
||||
mode = int(self._current_mode.value)
|
||||
if mode == HumidityControlMode.OFF:
|
||||
new_mode = self.entity_description.on_mode
|
||||
elif mode == self.entity_description.inverse_mode:
|
||||
new_mode = HumidityControlMode.AUTO
|
||||
else:
|
||||
return
|
||||
|
||||
await self.info.node.async_set_value(self._current_mode, new_mode)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off device."""
|
||||
mode = int(self._current_mode.value)
|
||||
if mode == HumidityControlMode.AUTO:
|
||||
if self._supports_inverse_mode():
|
||||
new_mode = self.entity_description.inverse_mode
|
||||
else:
|
||||
new_mode = HumidityControlMode.OFF
|
||||
elif mode == self.entity_description.on_mode:
|
||||
new_mode = HumidityControlMode.OFF
|
||||
else:
|
||||
return
|
||||
|
||||
await self.info.node.async_set_value(self._current_mode, new_mode)
|
||||
|
||||
@property
|
||||
def target_humidity(self) -> int | None:
|
||||
"""Return the humidity we try to reach."""
|
||||
if not self._setpoint:
|
||||
return None
|
||||
return int(self._setpoint.value)
|
||||
|
||||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set new target humidity."""
|
||||
if self._setpoint:
|
||||
await self.info.node.async_set_value(self._setpoint, humidity)
|
||||
|
||||
@property
|
||||
def min_humidity(self) -> int:
|
||||
"""Return the minimum humidity."""
|
||||
min_value = DEFAULT_MIN_HUMIDITY
|
||||
if self._setpoint and self._setpoint.metadata.min:
|
||||
min_value = self._setpoint.metadata.min
|
||||
return min_value
|
||||
|
||||
@property
|
||||
def max_humidity(self) -> int:
|
||||
"""Return the maximum humidity."""
|
||||
max_value = DEFAULT_MAX_HUMIDITY
|
||||
if self._setpoint and self._setpoint.metadata.max:
|
||||
max_value = self._setpoint.metadata.max
|
||||
return max_value
|
|
@ -37,5 +37,7 @@ ID_LOCK_CONFIG_PARAMETER_SENSOR = (
|
|||
ZEN_31_ENTITY = "light.kitchen_under_cabinet_lights"
|
||||
METER_ENERGY_SENSOR = "sensor.smart_switch_6_electric_consumed_kwh"
|
||||
METER_VOLTAGE_SENSOR = "sensor.smart_switch_6_electric_consumed_v"
|
||||
HUMIDIFIER_ADC_T3000_ENTITY = "humidifier.adc_t3000_humidifier"
|
||||
DEHUMIDIFIER_ADC_T3000_ENTITY = "humidifier.adc_t3000_dehumidifier"
|
||||
|
||||
PROPERTY_ULTRAVIOLET = "Ultraviolet"
|
||||
|
|
|
@ -276,6 +276,12 @@ def climate_radio_thermostat_ct100_plus_different_endpoints_state_fixture():
|
|||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_adc_t3000_state", scope="session")
|
||||
def climate_adc_t3000_state_fixture():
|
||||
"""Load the climate ADC-T3000 node state fixture data."""
|
||||
return json.loads(load_fixture("zwave_js/climate_adc_t3000_state.json"))
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_danfoss_lc_13_state", scope="session")
|
||||
def climate_danfoss_lc_13_state_fixture():
|
||||
"""Load the climate Danfoss (LC-13) electronic radiator thermostat node state fixture data."""
|
||||
|
@ -610,6 +616,46 @@ def climate_radio_thermostat_ct100_plus_different_endpoints_fixture(
|
|||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_adc_t3000")
|
||||
def climate_adc_t3000_fixture(client, climate_adc_t3000_state):
|
||||
"""Mock a climate ADC-T3000 node."""
|
||||
node = Node(client, copy.deepcopy(climate_adc_t3000_state))
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_adc_t3000_missing_setpoint")
|
||||
def climate_adc_t3000_missing_setpoint_fixture(client, climate_adc_t3000_state):
|
||||
"""Mock a climate ADC-T3000 node with missing de-humidify setpoint."""
|
||||
data = copy.deepcopy(climate_adc_t3000_state)
|
||||
data["name"] = f"{data['name']} missing setpoint"
|
||||
for value in data["values"][:]:
|
||||
if (
|
||||
value["commandClassName"] == "Humidity Control Setpoint"
|
||||
and value["propertyKeyName"] == "De-humidifier"
|
||||
):
|
||||
data["values"].remove(value)
|
||||
node = Node(client, data)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_adc_t3000_missing_mode")
|
||||
def climate_adc_t3000_missing_mode_fixture(client, climate_adc_t3000_state):
|
||||
"""Mock a climate ADC-T3000 node with missing mode setpoint."""
|
||||
data = copy.deepcopy(climate_adc_t3000_state)
|
||||
data["name"] = f"{data['name']} missing mode"
|
||||
for value in data["values"]:
|
||||
if value["commandClassName"] == "Humidity Control Mode":
|
||||
states = value["metadata"]["states"]
|
||||
for key in list(states.keys()):
|
||||
if states[key] == "De-humidify":
|
||||
del states[key]
|
||||
node = Node(client, data)
|
||||
client.driver.controller.nodes[node.node_id] = node
|
||||
return node
|
||||
|
||||
|
||||
@pytest.fixture(name="climate_danfoss_lc_13")
|
||||
def climate_danfoss_lc_13_fixture(client, climate_danfoss_lc_13_state):
|
||||
"""Mock a climate radio danfoss LC-13 node."""
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue