diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py new file mode 100644 index 00000000000..c478525753a --- /dev/null +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -0,0 +1,60 @@ +"""Support for Netatmo binary sensors.""" + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +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 NETATMO_CREATE_WEATHER_SENSOR +from .data_handler import NetatmoDevice +from .entity import NetatmoWeatherModuleEntity + +BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="reachable", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Netatmo binary sensors based on a config entry.""" + + @callback + def _create_weather_binary_sensor_entity(netatmo_device: NetatmoDevice) -> None: + async_add_entities( + NetatmoWeatherBinarySensor(netatmo_device, description) + for description in BINARY_SENSOR_TYPES + if description.key in netatmo_device.device.features + ) + + entry.async_on_unload( + async_dispatcher_connect( + hass, NETATMO_CREATE_WEATHER_SENSOR, _create_weather_binary_sensor_entity + ) + ) + + +class NetatmoWeatherBinarySensor(NetatmoWeatherModuleEntity, BinarySensorEntity): + """Implementation of a Netatmo binary sensor.""" + + def __init__( + self, device: NetatmoDevice, description: BinarySensorEntityDescription + ) -> None: + """Initialize a Netatmo binary sensor.""" + super().__init__(device) + self.entity_description = description + self._attr_unique_id = f"{self.device.entity_id}-{description.key}" + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + self._attr_is_on = self.device.reachable + self.async_write_ha_state() diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 8109b418066..74f2ebc84b2 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -9,6 +9,7 @@ MANUFACTURER = "Netatmo" DEFAULT_ATTRIBUTION = f"Data provided by {MANUFACTURER}" PLATFORMS = [ + Platform.BINARY_SENSOR, Platform.CAMERA, Platform.CLIMATE, Platform.COVER, diff --git a/homeassistant/components/netatmo/entity.py b/homeassistant/components/netatmo/entity.py index 5f08cb941d6..6fdebcf0c3f 100644 --- a/homeassistant/components/netatmo/entity.py +++ b/homeassistant/components/netatmo/entity.py @@ -3,12 +3,13 @@ from __future__ import annotations from abc import abstractmethod -from typing import Any +from typing import Any, cast from pyatmo import DeviceType, Home, Module, Room -from pyatmo.modules.base_class import NetatmoBase +from pyatmo.modules.base_class import NetatmoBase, Place from pyatmo.modules.device_types import DEVICE_DESCRIPTION_MAP +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo @@ -16,6 +17,7 @@ from homeassistant.helpers.entity import Entity from .const import ( CONF_URL_ENERGY, + CONF_URL_WEATHER, DATA_DEVICE_IDS, DEFAULT_ATTRIBUTION, DOMAIN, @@ -166,3 +168,39 @@ class NetatmoModuleEntity(NetatmoDeviceEntity): def device_type(self) -> DeviceType: """Return the device type.""" return self.device.device_type + + +class NetatmoWeatherModuleEntity(NetatmoModuleEntity): + """Netatmo weather module entity base class.""" + + _attr_configuration_url = CONF_URL_WEATHER + + def __init__(self, device: NetatmoDevice) -> None: + """Set up a Netatmo weather module entity.""" + super().__init__(device) + category = getattr(self.device.device_category, "name") + self._publishers.extend( + [ + { + "name": category, + SIGNAL_NAME: category, + }, + ] + ) + + if hasattr(self.device, "place"): + place = cast(Place, getattr(self.device, "place")) + if hasattr(place, "location") and place.location is not None: + self._attr_extra_state_attributes.update( + { + ATTR_LATITUDE: place.location.latitude, + ATTR_LONGITUDE: place.location.longitude, + } + ) + + @property + def device_type(self) -> DeviceType: + """Return the Netatmo device type.""" + if "." not in self.device.device_type: + return super().device_type + return DeviceType(self.device.device_type.partition(".")[2]) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 7e7b6029572..4e470437f7a 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -8,7 +8,6 @@ import logging from typing import Any, cast import pyatmo -from pyatmo import DeviceType from pyatmo.modules import PublicWeatherArea from homeassistant.components.sensor import ( @@ -48,7 +47,6 @@ from homeassistant.helpers.typing import StateType from .const import ( CONF_URL_ENERGY, CONF_URL_PUBLIC_WEATHER, - CONF_URL_WEATHER, CONF_WEATHER_AREAS, DATA_HANDLER, DOMAIN, @@ -59,7 +57,12 @@ from .const import ( SIGNAL_NAME, ) from .data_handler import HOME, PUBLIC, NetatmoDataHandler, NetatmoDevice, NetatmoRoom -from .entity import NetatmoBaseEntity, NetatmoModuleEntity, NetatmoRoomEntity +from .entity import ( + NetatmoBaseEntity, + NetatmoModuleEntity, + NetatmoRoomEntity, + NetatmoWeatherModuleEntity, +) from .helper import NetatmoArea _LOGGER = logging.getLogger(__name__) @@ -491,11 +494,10 @@ async def async_setup_entry( await add_public_entities(False) -class NetatmoWeatherSensor(NetatmoModuleEntity, SensorEntity): +class NetatmoWeatherSensor(NetatmoWeatherModuleEntity, SensorEntity): """Implementation of a Netatmo weather/home coach sensor.""" entity_description: NetatmoSensorEntityDescription - _attr_configuration_url = CONF_URL_WEATHER def __init__( self, @@ -506,34 +508,8 @@ class NetatmoWeatherSensor(NetatmoModuleEntity, SensorEntity): super().__init__(netatmo_device) self.entity_description = description self._attr_translation_key = description.netatmo_name - category = getattr(self.device.device_category, "name") - self._publishers.extend( - [ - { - "name": category, - SIGNAL_NAME: category, - }, - ] - ) self._attr_unique_id = f"{self.device.entity_id}-{description.key}" - if hasattr(self.device, "place"): - place = cast(pyatmo.modules.base_class.Place, getattr(self.device, "place")) - if hasattr(place, "location") and place.location is not None: - self._attr_extra_state_attributes.update( - { - ATTR_LATITUDE: place.location.latitude, - ATTR_LONGITUDE: place.location.longitude, - } - ) - - @property - def device_type(self) -> DeviceType: - """Return the Netatmo device type.""" - if "." not in self.device.device_type: - return super().device_type - return DeviceType(self.device.device_type.partition(".")[2]) - @property def available(self) -> bool: """Return True if entity is available.""" diff --git a/tests/components/netatmo/snapshots/test_binary_sensor.ambr b/tests/components/netatmo/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..6a90b4dd77a --- /dev/null +++ b/tests/components/netatmo/snapshots/test_binary_sensor.ambr @@ -0,0 +1,541 @@ +# serializer version: 1 +# name: test_entity[binary_sensor.baby_bedroom_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.baby_bedroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:26:68:92-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.baby_bedroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Baby Bedroom Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.baby_bedroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.bedroom_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.bedroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:26:69:0c-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.bedroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Bedroom Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.bedroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity[binary_sensor.kitchen_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.kitchen_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:25:cf:a8-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.kitchen_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Kitchen Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.kitchen_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.livingroom_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.livingroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:26:65:14-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.livingroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Livingroom Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.livingroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.parents_bedroom_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.parents_bedroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:3e:c5:46-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.parents_bedroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Parents Bedroom Connectivity', + 'latitude': 13.377726, + 'longitude': 52.516263, + }), + 'context': , + 'entity_id': 'binary_sensor.parents_bedroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_bathroom_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.villa_bathroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:7e:18-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_bathroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Bathroom Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_bathroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_bedroom_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.villa_bedroom_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:44:92-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_bedroom_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Bedroom Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_bedroom_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.villa_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:bb:26-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Connectivity', + 'latitude': 46.123456, + 'longitude': 6.1234567, + }), + 'context': , + 'entity_id': 'binary_sensor.villa_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_garden_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.villa_garden_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:03:1b:e4-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_garden_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Garden Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_garden_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity[binary_sensor.villa_outdoor_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.villa_outdoor_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:1c:42-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_outdoor_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Outdoor Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_outdoor_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entity[binary_sensor.villa_rain_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.villa_rain_connectivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Connectivity', + 'platform': 'netatmo', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12:34:56:80:c1:ea-reachable', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity[binary_sensor.villa_rain_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by Netatmo', + 'device_class': 'connectivity', + 'friendly_name': 'Villa Rain Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.villa_rain_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/netatmo/test_binary_sensor.py b/tests/components/netatmo/test_binary_sensor.py new file mode 100644 index 00000000000..53aea461fde --- /dev/null +++ b/tests/components/netatmo/test_binary_sensor.py @@ -0,0 +1,31 @@ +"""Support for Netatmo binary sensors.""" + +from unittest.mock import AsyncMock + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry +from tests.components.netatmo.common import snapshot_platform_entities + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_entity( + hass: HomeAssistant, + config_entry: MockConfigEntry, + netatmo_auth: AsyncMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test entities.""" + await snapshot_platform_entities( + hass, + config_entry, + Platform.BINARY_SENSOR, + entity_registry, + snapshot, + )