diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py index 2b72794b323..d4c0b1a45ca 100644 --- a/homeassistant/components/homematicip_cloud/const.py +++ b/homeassistant/components/homematicip_cloud/const.py @@ -19,6 +19,7 @@ PLATFORMS = [ Platform.LOCK, Platform.SENSOR, Platform.SWITCH, + Platform.VALVE, Platform.WEATHER, ] diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 95de7f15af0..1ed483b86ad 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -33,6 +33,7 @@ from homematicip.device import ( TemperatureHumiditySensorOutdoor, TemperatureHumiditySensorWithoutDisplay, TiltVibrationSensor, + WateringActuator, WeatherSensor, WeatherSensorPlus, WeatherSensorPro, @@ -167,6 +168,29 @@ def get_device_handlers(hap: HomematicipHAP) -> dict[type, Callable]: HomematicipTiltStateSensor(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: [ HomematicipTemperatureSensor(hap, device), HomematicipHumiditySensor(hap, device), @@ -267,6 +291,65 @@ async def async_setup_entry( 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): """Representation of the HomematicIP tilt angle sensor.""" diff --git a/homeassistant/components/homematicip_cloud/valve.py b/homeassistant/components/homematicip_cloud/valve.py new file mode 100644 index 00000000000..aaeaa3c565c --- /dev/null +++ b/homeassistant/components/homematicip_cloud/valve.py @@ -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 diff --git a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json index c9eab0cf4f5..44d8cc33c80 100644 --- a/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json +++ b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json @@ -8936,6 +8936,161 @@ "serializedGlobalTradeItemNumber": "3014F71100000000000RGBW2", "type": "RGBW_DIMMER", "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": { diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 4fb9f9eede8..8bff1798255 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices( 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( diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index a107214b373..77e90ccaff6 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -35,6 +35,8 @@ from homeassistant.const import ( UnitOfPower, UnitOfSpeed, UnitOfTemperature, + UnitOfVolume, + UnitOfVolumeFlowRate, ) 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) 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 diff --git a/tests/components/homematicip_cloud/test_valve.py b/tests/components/homematicip_cloud/test_valve.py new file mode 100644 index 00000000000..5c2840dc28f --- /dev/null +++ b/tests/components/homematicip_cloud/test_valve.py @@ -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