diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 726c0ed43c8..49e02f566a0 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -31,7 +31,10 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -47,6 +50,7 @@ from .const import ( EVENT_TYPE_SCHEDULE, EVENT_TYPE_SET_POINT, EVENT_TYPE_THERM_MODE, + NETATMO_CREATE_BATTERY, SERVICE_SET_SCHEDULE, SIGNAL_NAME, TYPE_ENERGY, @@ -55,6 +59,7 @@ from .data_handler import ( CLIMATE_STATE_CLASS_NAME, CLIMATE_TOPOLOGY_CLASS_NAME, NetatmoDataHandler, + NetatmoDevice, ) from .netatmo_entity_base import NetatmoBase @@ -241,6 +246,21 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ) ) + for module in self._room.modules.values(): + if getattr(module.device_type, "value") not in [NA_THERM, NA_VALVE]: + continue + + async_dispatcher_send( + self.hass, + NETATMO_CREATE_BATTERY, + NetatmoDevice( + self.data_handler, + module, + self._id, + self._climate_state_class, + ), + ) + @callback def handle_event(self, event: dict) -> None: """Handle webhook events.""" diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 14e165b5cb4..a642d59ff1e 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -69,6 +69,7 @@ CAMERA_DATA = "netatmo_camera" HOME_DATA = "netatmo_home_data" DATA_HANDLER = "netatmo_data_handler" SIGNAL_NAME = "signal_name" +NETATMO_CREATE_BATTERY = "netatmo_create_battery" CONF_CLOUDHOOK_URL = "cloudhook_url" CONF_WEATHER_AREAS = "weather_areas" diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 97321b0da53..c62522a931a 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -57,6 +57,16 @@ DEFAULT_INTERVALS = { SCAN_INTERVAL = 60 +@dataclass +class NetatmoDevice: + """Netatmo device class.""" + + data_handler: NetatmoDataHandler + device: pyatmo.climate.NetatmoModule + parent_id: str + state_class_name: str + + @dataclass class NetatmoDataClass: """Class for keeping track of Netatmo data class metadata.""" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index d06c96b3c6a..5b3416e3b09 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -42,6 +42,7 @@ from .const import ( DATA_HANDLER, DOMAIN, MANUFACTURER, + NETATMO_CREATE_BATTERY, SIGNAL_NAME, TYPE_WEATHER, ) @@ -50,6 +51,7 @@ from .data_handler import ( PUBLICDATA_DATA_CLASS_NAME, WEATHERSTATION_DATA_CLASS_NAME, NetatmoDataHandler, + NetatmoDevice, ) from .helper import NetatmoArea from .netatmo_entity_base import NetatmoBase @@ -454,6 +456,16 @@ async def async_setup_entry( hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}", add_public_entities ) + @callback + def _create_entity(netatmo_device: NetatmoDevice) -> None: + entity = NetatmoClimateBatterySensor(netatmo_device) + _LOGGER.debug("Adding climate battery sensor %s", entity) + async_add_entities([entity]) + + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_BATTERY, _create_entity) + ) + await add_public_entities(False) if platform_not_ready: @@ -561,6 +573,73 @@ class NetatmoSensor(NetatmoBase, SensorEntity): self.async_write_ha_state() +class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity): + """Implementation of a Netatmo sensor.""" + + entity_description: NetatmoSensorEntityDescription + + def __init__( + self, + netatmo_device: NetatmoDevice, + ) -> None: + """Initialize the sensor.""" + super().__init__(netatmo_device.data_handler) + self.entity_description = NetatmoSensorEntityDescription( + key="battery_percent", + name="Battery Percent", + netatmo_name="battery_percent", + entity_registry_enabled_default=True, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, + ) + + self._module = netatmo_device.device + self._id = netatmo_device.parent_id + self._attr_name = f"{self._module.name} {self.entity_description.name}" + + self._state_class_name = netatmo_device.state_class_name + self._room_id = self._module.room_id + self._model = getattr(self._module.device_type, "value") + + self._attr_unique_id = ( + f"{self._id}-{self._module.entity_id}-{self.entity_description.key}" + ) + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + if not self._module.reachable: + if self.available: + self._attr_available = False + self._attr_native_value = None + return + + self._attr_available = True + self._attr_native_value = self._process_battery_state() + + def _process_battery_state(self) -> int | None: + """Construct room status.""" + if battery_state := self._module.battery_state: + return process_battery_percentage(battery_state) + + return None + + +def process_battery_percentage(data: str) -> int: + """Process battery data and return percent (int) for display.""" + mapping = { + "max": 100, + "full": 90, + "high": 75, + "medium": 50, + "low": 25, + "very low": 10, + } + return mapping[data] + + def fix_angle(angle: int) -> int: """Fix angle when value is negative.""" if angle < 0: diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index bebd8e0191c..b1b5b11265a 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -233,3 +233,17 @@ async def test_weather_sensor_enabling( assert len(hass.states.async_all()) > states_before assert hass.states.get(f"sensor.{name}").state == expected + + +async def test_climate_battery_sensor(hass, config_entry, netatmo_auth): + """Test climate device battery sensor.""" + with patch("time.time", return_value=TEST_TIME), selected_platforms( + ["sensor", "climate"] + ): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + prefix = "sensor.livingroom_" + + assert hass.states.get(f"{prefix}battery_percent").state == "75"