From ac97cebe117b6804c77528afae2977d4cfff2961 Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Tue, 12 Mar 2019 14:52:13 +0100 Subject: [PATCH] Add Weather Sensors to Homematic IP (#21887) * Add HmIP Weather Sensor Devices * Fix test and icons * fix test * Fix comments --- .../homematicip_cloud/binary_sensor.py | 85 ++++++++++++- .../components/homematicip_cloud/const.py | 1 + .../components/homematicip_cloud/sensor.py | 120 +++++++++++++++++- .../components/homematicip_cloud/weather.py | 93 ++++++++++++++ .../components/homematicip_cloud/test_hap.py | 6 +- 5 files changed, 290 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/homematicip_cloud/weather.py diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index d6ce4152001..9445d6521cc 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -18,6 +18,7 @@ ATTR_WINDOWSTATE = 'window state' ATTR_MOISTUREDETECTED = 'moisture detected' ATTR_WATERLEVELDETECTED = 'water level detected' ATTR_SMOKEDETECTORALARM = 'smoke detector alarm' +ATTR_TODAY_SUNSHINE_DURATION = 'today_sunshine_duration_in_minutes' async def async_setup_platform( @@ -31,7 +32,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): from homematicip.aio.device import ( AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector, AsyncWaterSensor, AsyncRotaryHandleSensor, - AsyncMotionDetectorPushButton) + AsyncMotionDetectorPushButton, AsyncWeatherSensor, + AsyncWeatherSensorPlus, AsyncWeatherSensorPro) from homematicip.aio.group import ( AsyncSecurityGroup, AsyncSecurityZoneGroup) @@ -41,13 +43,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for device in home.devices: if isinstance(device, (AsyncShutterContact, AsyncRotaryHandleSensor)): devices.append(HomematicipShutterContact(home, device)) - elif isinstance(device, (AsyncMotionDetectorIndoor, - AsyncMotionDetectorPushButton)): + if isinstance(device, (AsyncMotionDetectorIndoor, + AsyncMotionDetectorPushButton)): devices.append(HomematicipMotionDetector(home, device)) - elif isinstance(device, AsyncSmokeDetector): + if isinstance(device, AsyncSmokeDetector): devices.append(HomematicipSmokeDetector(home, device)) - elif isinstance(device, AsyncWaterSensor): + if isinstance(device, AsyncWaterSensor): devices.append(HomematicipWaterDetector(home, device)) + if isinstance(device, (AsyncWeatherSensorPlus, + AsyncWeatherSensorPro)): + devices.append(HomematicipRainSensor(home, device)) + if isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, + AsyncWeatherSensorPro)): + devices.append(HomematicipStormSensor(home, device)) + devices.append(HomematicipSunshineSensor(home, device)) for group in home.groups: if isinstance(group, AsyncSecurityGroup): @@ -121,10 +130,74 @@ class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self): - """Return true if moisture or waterlevel is detected.""" + """Return true, if moisture or waterlevel is detected.""" return self._device.moistureDetected or self._device.waterlevelDetected +class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorDevice): + """Representation of a HomematicIP Cloud storm sensor.""" + + def __init__(self, home, device): + """Initialize storm sensor.""" + super().__init__(home, device, "Storm") + + @property + def icon(self): + """Return the icon.""" + return 'mdi:weather-windy' if self.is_on else 'mdi:pinwheel-outline' + + @property + def is_on(self): + """Return true, if storm is detected.""" + return self._device.storm + + +class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorDevice): + """Representation of a HomematicIP Cloud rain sensor.""" + + def __init__(self, home, device): + """Initialize rain sensor.""" + super().__init__(home, device, "Raining") + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'moisture' + + @property + def is_on(self): + """Return true, if it is raining.""" + return self._device.raining + + +class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): + """Representation of a HomematicIP Cloud sunshine sensor.""" + + def __init__(self, home, device): + """Initialize sunshine sensor.""" + super().__init__(home, device, 'Sunshine') + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'light' + + @property + def is_on(self): + """Return true if sun is shining.""" + return self._device.sunshine + + @property + def device_state_attributes(self): + """Return the state attributes of the illuminance sensor.""" + attr = super().device_state_attributes + if hasattr(self._device, 'todaySunshineDuration') and \ + self._device.todaySunshineDuration: + attr[ATTR_TODAY_SUNSHINE_DURATION] = \ + self._device.todaySunshineDuration + return attr + + class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud security zone group.""" diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py index 06864d50ad1..fbda56f2805 100644 --- a/homeassistant/components/homematicip_cloud/const.py +++ b/homeassistant/components/homematicip_cloud/const.py @@ -13,6 +13,7 @@ COMPONENTS = [ 'light', 'sensor', 'switch', + 'weather', ] CONF_ACCESSPOINT = 'accesspoint' diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 9ded8fe65d2..d6155998332 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -11,11 +11,11 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['homematicip_cloud'] +ATTR_TEMPERATURE_OFFSET = 'temperature_offset' ATTR_VALVE_STATE = 'valve_state' ATTR_VALVE_POSITION = 'valve_position' -ATTR_TEMPERATURE = 'temperature' -ATTR_TEMPERATURE_OFFSET = 'temperature_offset' -ATTR_HUMIDITY = 'humidity' +ATTR_WIND_DIRECTION = 'wind_direction' +ATTR_WIND_DIRECTION_VARIATION = 'wind_direction_variation_in_degree' async def async_setup_platform( @@ -33,7 +33,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): AsyncTemperatureHumiditySensorOutdoor, AsyncMotionDetectorPushButton, AsyncLightSensor, AsyncPlugableSwitchMeasuring, AsyncBrandSwitchMeasuring, - AsyncFullFlushSwitchMeasuring) + AsyncFullFlushSwitchMeasuring, AsyncWeatherSensor, + AsyncWeatherSensorPlus, AsyncWeatherSensorPro) home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [HomematicipAccesspointStatus(home)] @@ -43,11 +44,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices.append(HomematicipHeatingThermostat(home, device)) if isinstance(device, (AsyncTemperatureHumiditySensorDisplay, AsyncTemperatureHumiditySensorWithoutDisplay, - AsyncTemperatureHumiditySensorOutdoor)): + AsyncTemperatureHumiditySensorOutdoor, + AsyncWeatherSensor, + AsyncWeatherSensorPlus, + AsyncWeatherSensorPro)): devices.append(HomematicipTemperatureSensor(home, device)) devices.append(HomematicipHumiditySensor(home, device)) if isinstance(device, (AsyncMotionDetectorIndoor, - AsyncMotionDetectorPushButton)): + AsyncMotionDetectorPushButton, + AsyncWeatherSensor, + AsyncWeatherSensorPlus, + AsyncWeatherSensorPro)): devices.append(HomematicipIlluminanceSensor(home, device)) if isinstance(device, AsyncLightSensor): devices.append(HomematicipLightSensor(home, device)) @@ -55,6 +62,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): AsyncBrandSwitchMeasuring, AsyncFullFlushSwitchMeasuring)): devices.append(HomematicipPowerSensor(home, device)) + if isinstance(device, (AsyncWeatherSensor, + AsyncWeatherSensorPlus, + AsyncWeatherSensorPro)): + devices.append(HomematicipWindspeedSensor(home, device)) + if isinstance(device, (AsyncWeatherSensorPlus, + AsyncWeatherSensorPro)): + devices.append(HomematicipTodayRainSensor(home, device)) if devices: async_add_entities(devices) @@ -177,6 +191,15 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): """Return the unit this state is expressed in.""" return TEMP_CELSIUS + @property + def device_state_attributes(self): + """Return the state attributes of the windspeed sensor.""" + attr = super().device_state_attributes + if hasattr(self._device, 'temperatureOffset') and \ + self._device.temperatureOffset: + attr[ATTR_TEMPERATURE_OFFSET] = self._device.temperatureOffset + return attr + class HomematicipIlluminanceSensor(HomematicipGenericDevice): """Represenation of a HomematicIP Illuminance device.""" @@ -226,3 +249,88 @@ class HomematicipPowerSensor(HomematicipGenericDevice): def unit_of_measurement(self): """Return the unit this state is expressed in.""" return POWER_WATT + + +class HomematicipWindspeedSensor(HomematicipGenericDevice): + """Represenation of a HomematicIP wind speed sensor.""" + + def __init__(self, home, device): + """Initialize the device.""" + super().__init__(home, device, 'Windspeed') + + @property + def state(self): + """Represenation of the HomematicIP wind speed value.""" + return self._device.windSpeed + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return 'km/h' + + @property + def device_state_attributes(self): + """Return the state attributes of the wind speed sensor.""" + attr = super().device_state_attributes + if hasattr(self._device, 'windDirection') and \ + self._device.windDirection: + attr[ATTR_WIND_DIRECTION] = \ + _get_wind_direction(self._device.windDirection) + if hasattr(self._device, 'windDirectionVariation') and \ + self._device.windDirectionVariation: + attr[ATTR_WIND_DIRECTION_VARIATION] = \ + self._device.windDirectionVariation + return attr + + +class HomematicipTodayRainSensor(HomematicipGenericDevice): + """Represenation of a HomematicIP rain counter of a day sensor.""" + + def __init__(self, home, device): + """Initialize the device.""" + super().__init__(home, device, 'Today Rain') + + @property + def state(self): + """Represenation of the HomematicIP todays rain value.""" + return round(self._device.todayRainCounter, 2) + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return 'mm' + + +def _get_wind_direction(wind_direction_degree): + """Convert wind direction degree to named direction.""" + if 11.25 <= wind_direction_degree < 33.75: + return 'NNE' + if 33.75 <= wind_direction_degree < 56.25: + return 'NE' + if 56.25 <= wind_direction_degree < 78.75: + return 'ENE' + if 78.75 <= wind_direction_degree < 101.25: + return 'E' + if 101.25 <= wind_direction_degree < 123.75: + return 'ESE' + if 123.75 <= wind_direction_degree < 146.25: + return 'SE' + if 146.25 <= wind_direction_degree < 168.75: + return 'SSE' + if 168.75 <= wind_direction_degree < 191.25: + return 'S' + if 191.25 <= wind_direction_degree < 213.75: + return 'SSW' + if 213.75 <= wind_direction_degree < 236.25: + return 'SW' + if 236.25 <= wind_direction_degree < 258.75: + return 'WSW' + if 258.75 <= wind_direction_degree < 281.25: + return 'W' + if 281.25 <= wind_direction_degree < 303.75: + return 'WNW' + if 303.75 <= wind_direction_degree < 326.25: + return 'NW' + if 326.25 <= wind_direction_degree < 348.75: + return 'NNW' + return 'N' diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py new file mode 100644 index 00000000000..5a6261195da --- /dev/null +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -0,0 +1,93 @@ + +"""Support for HomematicIP Cloud weather devices.""" +import logging + +from homeassistant.components.homematicip_cloud import ( + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) +from homeassistant.components.weather import WeatherEntity + +DEPENDENCIES = ['homematicip_cloud'] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the HomematicIP Cloud weather sensor.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the HomematicIP weather sensor from a config entry.""" + from homematicip.aio.device import ( + AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro, + ) + + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + devices = [] + for device in home.devices: + if isinstance(device, AsyncWeatherSensorPro): + devices.append(HomematicipWeatherSensorPro(home, device)) + elif isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus)): + devices.append(HomematicipWeatherSensor(home, device)) + + if devices: + async_add_entities(devices) + + +class HomematicipWeatherSensor(HomematicipGenericDevice, WeatherEntity): + """representation of a HomematicIP Cloud weather sensor plus & basic.""" + + def __init__(self, home, device): + """Initialize the weather sensor.""" + super().__init__(home, device) + + @property + def name(self): + """Return the name of the sensor.""" + return self._device.label + + @property + def temperature(self): + """Return the platform temperature.""" + return self._device.actualTemperature + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self.hass.config.units.temperature_unit + + @property + def humidity(self): + """Return the humidity.""" + return self._device.humidity + + @property + def wind_speed(self): + """Return the wind speed.""" + return self._device.windSpeed + + @property + def attribution(self): + """Return the attribution.""" + return "Powered by Homematic IP" + + @property + def condition(self): + """Return the current condition.""" + if hasattr(self._device, "raining") and self._device.raining: + return 'rainy' + if self._device.storm: + return 'windy' + if self._device.sunshine: + return 'sunny' + return '' + + +class HomematicipWeatherSensorPro(HomematicipWeatherSensor): + """representation of a HomematicIP weather sensor pro.""" + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self._device.windDirection diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 61ca3300d60..fd20360b8da 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -68,7 +68,7 @@ async def test_hap_setup_works(aioclient_mock): assert await hap.async_setup() is True assert hap.home is home - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 7 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 8 assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \ (entry, 'alarm_control_panel') assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == \ @@ -111,10 +111,10 @@ async def test_hap_reset_unloads_entry_if_setup(): assert hap.home is home assert len(hass.services.async_register.mock_calls) == 0 - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 7 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 8 hass.config_entries.async_forward_entry_unload.return_value = \ mock_coro(True) await hap.async_reset() - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 7 + assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 8