Add support for ELV-SH-WSM to homematicip (#149098)

This commit is contained in:
hahn-th 2025-07-22 14:06:03 +02:00 committed by GitHub
parent c075134845
commit 3f67ba4c02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 399 additions and 1 deletions

View File

@ -19,6 +19,7 @@ PLATFORMS = [
Platform.LOCK, Platform.LOCK,
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH, Platform.SWITCH,
Platform.VALVE,
Platform.WEATHER, Platform.WEATHER,
] ]

View File

@ -33,6 +33,7 @@ from homematicip.device import (
TemperatureHumiditySensorOutdoor, TemperatureHumiditySensorOutdoor,
TemperatureHumiditySensorWithoutDisplay, TemperatureHumiditySensorWithoutDisplay,
TiltVibrationSensor, TiltVibrationSensor,
WateringActuator,
WeatherSensor, WeatherSensor,
WeatherSensorPlus, WeatherSensorPlus,
WeatherSensorPro, WeatherSensorPro,
@ -167,6 +168,29 @@ def get_device_handlers(hap: HomematicipHAP) -> dict[type, Callable]:
HomematicipTiltStateSensor(hap, device), HomematicipTiltStateSensor(hap, device),
HomematicipTiltAngleSensor(hap, device), HomematicipTiltAngleSensor(hap, device),
], ],
WateringActuator: lambda device: [
entity
for ch in device.functionalChannels
if ch.functionalChannelType
== FunctionalChannelType.WATERING_ACTUATOR_CHANNEL
for entity in (
HomematicipWaterFlowSensor(
hap, device, channel=ch.index, post="currentWaterFlow"
),
HomematicipWaterVolumeSensor(
hap,
device,
channel=ch.index,
post="waterVolume",
attribute="waterVolume",
),
HomematicipWaterVolumeSinceOpenSensor(
hap,
device,
channel=ch.index,
),
)
],
WeatherSensor: lambda device: [ WeatherSensor: lambda device: [
HomematicipTemperatureSensor(hap, device), HomematicipTemperatureSensor(hap, device),
HomematicipHumiditySensor(hap, device), HomematicipHumiditySensor(hap, device),
@ -267,6 +291,65 @@ async def async_setup_entry(
async_add_entities(entities) async_add_entities(entities)
class HomematicipWaterFlowSensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP watering flow sensor."""
_attr_native_unit_of_measurement = UnitOfVolumeFlowRate.LITERS_PER_MINUTE
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(
self, hap: HomematicipHAP, device: Device, channel: int, post: str
) -> None:
"""Initialize the watering flow sensor device."""
super().__init__(hap, device, post=post, channel=channel, is_multi_channel=True)
@property
def native_value(self) -> float | None:
"""Return the state."""
return self.functional_channel.waterFlow
class HomematicipWaterVolumeSensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP watering volume sensor."""
_attr_native_unit_of_measurement = UnitOfVolume.LITERS
_attr_state_class = SensorStateClass.TOTAL_INCREASING
def __init__(
self,
hap: HomematicipHAP,
device: Device,
channel: int,
post: str,
attribute: str,
) -> None:
"""Initialize the watering volume sensor device."""
super().__init__(hap, device, post=post, channel=channel, is_multi_channel=True)
self._attribute_name = attribute
@property
def native_value(self) -> float | None:
"""Return the state."""
return getattr(self.functional_channel, self._attribute_name, None)
class HomematicipWaterVolumeSinceOpenSensor(HomematicipWaterVolumeSensor):
"""Representation of the HomematicIP watering volume since open sensor."""
_attr_native_unit_of_measurement = UnitOfVolume.LITERS
_attr_state_class = SensorStateClass.TOTAL_INCREASING
def __init__(self, hap: HomematicipHAP, device: Device, channel: int) -> None:
"""Initialize the watering flow volume since open device."""
super().__init__(
hap,
device,
channel=channel,
post="waterVolumeSinceOpen",
attribute="waterVolumeSinceOpen",
)
class HomematicipTiltAngleSensor(HomematicipGenericEntity, SensorEntity): class HomematicipTiltAngleSensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP tilt angle sensor.""" """Representation of the HomematicIP tilt angle sensor."""

View File

@ -0,0 +1,59 @@
"""Support for HomematicIP Cloud valve devices."""
from homematicip.base.functionalChannels import FunctionalChannelType
from homematicip.device import Device
from homeassistant.components.valve import (
ValveDeviceClass,
ValveEntity,
ValveEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP valves from a config entry."""
hap = config_entry.runtime_data
entities = [
HomematicipWateringValve(hap, device, ch.index)
for device in hap.home.devices
for ch in device.functionalChannels
if ch.functionalChannelType == FunctionalChannelType.WATERING_ACTUATOR_CHANNEL
]
async_add_entities(entities)
class HomematicipWateringValve(HomematicipGenericEntity, ValveEntity):
"""Representation of a HomematicIP valve."""
_attr_reports_position = False
_attr_supported_features = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
_attr_device_class = ValveDeviceClass.WATER
def __init__(self, hap: HomematicipHAP, device: Device, channel: int) -> None:
"""Initialize the valve."""
super().__init__(
hap, device=device, channel=channel, post="watering", is_multi_channel=True
)
async def async_open_valve(self) -> None:
"""Open the valve."""
await self.functional_channel.set_watering_switch_state_async(True)
async def async_close_valve(self) -> None:
"""Close valve."""
await self.functional_channel.set_watering_switch_state_async(False)
@property
def is_closed(self) -> bool:
"""Return if the valve is closed."""
return self.functional_channel.wateringActive is False

View File

@ -8936,6 +8936,161 @@
"serializedGlobalTradeItemNumber": "3014F71100000000000RGBW2", "serializedGlobalTradeItemNumber": "3014F71100000000000RGBW2",
"type": "RGBW_DIMMER", "type": "RGBW_DIMMER",
"updateState": "UP_TO_DATE" "updateState": "UP_TO_DATE"
},
"3014F71100000000000SHWSM": {
"availableFirmwareVersion": "0.0.0",
"connectionType": "HMIP_RF",
"deviceArchetype": "HMIP",
"firmwareVersion": "1.0.10",
"firmwareVersionInteger": 65546,
"functionalChannels": {
"0": {
"altitude": null,
"busConfigMismatch": null,
"coProFaulty": false,
"coProRestartNeeded": false,
"coProUpdateFailure": false,
"configPending": false,
"controlsMountingOrientation": null,
"daliBusState": null,
"dataDecodingFailedError": null,
"defaultLinkedGroup": [],
"deviceAliveSignalEnabled": null,
"deviceCommunicationError": null,
"deviceDriveError": null,
"deviceDriveModeError": null,
"deviceId": "3014F71100000000000SHWSM",
"deviceOperationMode": null,
"deviceOverheated": false,
"deviceOverloaded": false,
"devicePowerFailureDetected": false,
"deviceUndervoltage": false,
"displayContrast": null,
"displayMode": null,
"displayMountingOrientation": null,
"dutyCycle": false,
"frostProtectionError": false,
"frostProtectionErrorAcknowledged": null,
"functionalChannelType": "DEVICE_BASE",
"groupIndex": 0,
"groups": ["00000000-0000-0000-0000-000000000022"],
"index": 0,
"inputLayoutMode": null,
"invertedDisplayColors": null,
"label": "",
"lockJammed": null,
"lowBat": false,
"mountingModuleError": null,
"mountingOrientation": null,
"multicastRoutingEnabled": false,
"noDataFromLinkyError": null,
"operationDays": null,
"particulateMatterSensorCommunicationError": null,
"particulateMatterSensorError": null,
"powerShortCircuit": null,
"profilePeriodLimitReached": null,
"routerModuleEnabled": false,
"routerModuleSupported": false,
"rssiDeviceValue": -46,
"rssiPeerValue": -43,
"sensorCommunicationError": null,
"sensorError": null,
"shortCircuitDataLine": null,
"supportedOptionalFeatures": {
"IFeatureBusConfigMismatch": false,
"IFeatureDataDecodingFailedError": false,
"IFeatureDeviceCoProError": false,
"IFeatureDeviceCoProRestart": false,
"IFeatureDeviceCoProUpdate": false,
"IFeatureDeviceCommunicationError": false,
"IFeatureDeviceDaliBusError": false,
"IFeatureDeviceDriveError": false,
"IFeatureDeviceDriveModeError": false,
"IFeatureDeviceIdentify": false,
"IFeatureDeviceMountingModuleError": false,
"IFeatureDeviceOverheated": true,
"IFeatureDeviceOverloaded": false,
"IFeatureDeviceParticulateMatterSensorCommunicationError": false,
"IFeatureDeviceParticulateMatterSensorError": false,
"IFeatureDevicePowerFailure": false,
"IFeatureDeviceSensorCommunicationError": false,
"IFeatureDeviceSensorError": false,
"IFeatureDeviceTempSensorError": false,
"IFeatureDeviceTemperatureHumiditySensorCommunicationError": false,
"IFeatureDeviceTemperatureHumiditySensorError": false,
"IFeatureDeviceTemperatureOutOfRange": false,
"IFeatureDeviceUndervoltage": true,
"IFeatureMulticastRouter": false,
"IFeatureNoDataFromLinkyError": false,
"IFeaturePowerShortCircuit": false,
"IFeatureProfilePeriodLimit": false,
"IFeatureRssiValue": true,
"IFeatureShortCircuitDataLine": false,
"IFeatureTicVersionError": false,
"IOptionalFeatureAltitude": false,
"IOptionalFeatureDefaultLinkedGroup": false,
"IOptionalFeatureDeviceAliveSignalEnabled": false,
"IOptionalFeatureDeviceErrorLockJammed": false,
"IOptionalFeatureDeviceFrostProtectionError": true,
"IOptionalFeatureDeviceInputLayoutMode": false,
"IOptionalFeatureDeviceOperationMode": false,
"IOptionalFeatureDeviceSwitchChannelMode": false,
"IOptionalFeatureDeviceValveError": true,
"IOptionalFeatureDeviceWaterError": true,
"IOptionalFeatureDisplayContrast": false,
"IOptionalFeatureDisplayMode": false,
"IOptionalFeatureDutyCycle": true,
"IOptionalFeatureInvertedDisplayColors": false,
"IOptionalFeatureLowBat": true,
"IOptionalFeatureMountingOrientation": false,
"IOptionalFeatureOperationDays": false
},
"switchChannelMode": null,
"temperatureHumiditySensorCommunicationError": null,
"temperatureHumiditySensorError": null,
"temperatureOutOfRange": false,
"temperatureSensorError": null,
"ticVersionError": null,
"unreach": false,
"valveFlowError": false,
"valveWaterError": false
},
"1": {
"channelRole": "WATERING_ACTUATOR",
"deviceId": "3014F71100000000000SHWSM",
"functionalChannelType": "WATERING_ACTUATOR_CHANNEL",
"groupIndex": 1,
"groups": ["00000000-0000-0000-0000-000000000023"],
"index": 1,
"label": "",
"profileMode": "AUTOMATIC",
"supportedOptionalFeatures": {
"IFeatureWateringGroupActuatorChannel": true,
"IFeatureWateringProfileActuatorChannel": true
},
"userDesiredProfileMode": "AUTOMATIC",
"waterFlow": 12.0,
"waterVolume": 455.0,
"waterVolumeSinceOpen": 67.0,
"wateringActive": false,
"wateringOnTime": 3600.0
}
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F71100000000000SHWSM",
"label": "Bewaesserungsaktor",
"lastStatusUpdate": 1749501203047,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manuallyUpdateForced": false,
"manufacturerCode": 9,
"measuredAttributes": {},
"modelId": 586,
"modelType": "ELV-SH-WSM",
"oem": "eQ-3",
"permanentlyReachable": true,
"serializedGlobalTradeItemNumber": "3014F71100000000000SHWSM",
"type": "WATERING_ACTUATOR",
"updateState": "UP_TO_DATE"
} }
}, },
"groups": { "groups": {

View File

@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(
test_devices=None, test_groups=None test_devices=None, test_groups=None
) )
assert len(mock_hap.hmip_device_by_entity_id) == 335 assert len(mock_hap.hmip_device_by_entity_id) == 340
async def test_hmip_remove_device( async def test_hmip_remove_device(

View File

@ -35,6 +35,8 @@ from homeassistant.const import (
UnitOfPower, UnitOfPower,
UnitOfSpeed, UnitOfSpeed,
UnitOfTemperature, UnitOfTemperature,
UnitOfVolume,
UnitOfVolumeFlowRate,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -796,3 +798,66 @@ async def test_hmip_absolute_humidity_sensor_invalid_value(
ha_state = hass.states.get(entity_id) ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_UNKNOWN assert ha_state.state == STATE_UNKNOWN
async def test_hmip_water_valve_current_water_flow(
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
) -> None:
"""Test HomematicipCurrentWaterFlow."""
entity_id = "sensor.bewaesserungsaktor_currentwaterflow"
entity_name = "Bewaesserungsaktor currentWaterFlow"
device_model = "ELV-SH-WSM"
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=["Bewaesserungsaktor"]
)
ha_state, hmip_device = get_and_check_entity_basics(
hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "12.0"
assert (
ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT]
== UnitOfVolumeFlowRate.LITERS_PER_MINUTE
)
assert ha_state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
async def test_hmip_water_valve_water_volume(
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
) -> None:
"""Test HomematicipWaterVolume."""
entity_id = "sensor.bewaesserungsaktor_watervolume"
entity_name = "Bewaesserungsaktor waterVolume"
device_model = "ELV-SH-WSM"
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=["Bewaesserungsaktor"]
)
ha_state, hmip_device = get_and_check_entity_basics(
hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "455.0"
assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfVolume.LITERS
assert ha_state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING
async def test_hmip_water_valve_water_volume_since_open(
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
) -> None:
"""Test HomematicipWaterVolumeSinceOpen."""
entity_id = "sensor.bewaesserungsaktor_watervolumesinceopen"
entity_name = "Bewaesserungsaktor waterVolumeSinceOpen"
device_model = "ELV-SH-WSM"
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=["Bewaesserungsaktor"]
)
ha_state, hmip_device = get_and_check_entity_basics(
hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "67.0"
assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfVolume.LITERS
assert ha_state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING

View File

@ -0,0 +1,35 @@
"""Test HomematicIP Cloud valve entities."""
from homeassistant.components.valve import SERVICE_OPEN_VALVE, ValveState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .helper import HomeFactory, async_manipulate_test_data, get_and_check_entity_basics
async def test_watering_valve(
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
) -> None:
"""Test HomematicIP watering valve."""
entity_id = "valve.bewaesserungsaktor_watering"
entity_name = "Bewaesserungsaktor watering"
device_model = "ELV-SH-WSM"
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=["Bewaesserungsaktor"]
)
ha_state, hmip_device = get_and_check_entity_basics(
hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == ValveState.CLOSED
await hass.services.async_call(
Platform.VALVE, SERVICE_OPEN_VALVE, {"entity_id": entity_id}, blocking=True
)
await async_manipulate_test_data(
hass, hmip_device, "wateringActive", True, channel=1
)
ha_state = hass.states.get(entity_id)
assert ha_state.state == ValveState.OPEN