Add battery sensor for Netatmo climate devices (#60911)
This commit is contained in:
parent
a80447f096
commit
cf7a614309
5 changed files with 125 additions and 1 deletions
|
@ -31,7 +31,10 @@ from homeassistant.const import (
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import PlatformNotReady
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
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 import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
@ -47,6 +50,7 @@ from .const import (
|
||||||
EVENT_TYPE_SCHEDULE,
|
EVENT_TYPE_SCHEDULE,
|
||||||
EVENT_TYPE_SET_POINT,
|
EVENT_TYPE_SET_POINT,
|
||||||
EVENT_TYPE_THERM_MODE,
|
EVENT_TYPE_THERM_MODE,
|
||||||
|
NETATMO_CREATE_BATTERY,
|
||||||
SERVICE_SET_SCHEDULE,
|
SERVICE_SET_SCHEDULE,
|
||||||
SIGNAL_NAME,
|
SIGNAL_NAME,
|
||||||
TYPE_ENERGY,
|
TYPE_ENERGY,
|
||||||
|
@ -55,6 +59,7 @@ from .data_handler import (
|
||||||
CLIMATE_STATE_CLASS_NAME,
|
CLIMATE_STATE_CLASS_NAME,
|
||||||
CLIMATE_TOPOLOGY_CLASS_NAME,
|
CLIMATE_TOPOLOGY_CLASS_NAME,
|
||||||
NetatmoDataHandler,
|
NetatmoDataHandler,
|
||||||
|
NetatmoDevice,
|
||||||
)
|
)
|
||||||
from .netatmo_entity_base import NetatmoBase
|
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
|
@callback
|
||||||
def handle_event(self, event: dict) -> None:
|
def handle_event(self, event: dict) -> None:
|
||||||
"""Handle webhook events."""
|
"""Handle webhook events."""
|
||||||
|
|
|
@ -69,6 +69,7 @@ CAMERA_DATA = "netatmo_camera"
|
||||||
HOME_DATA = "netatmo_home_data"
|
HOME_DATA = "netatmo_home_data"
|
||||||
DATA_HANDLER = "netatmo_data_handler"
|
DATA_HANDLER = "netatmo_data_handler"
|
||||||
SIGNAL_NAME = "signal_name"
|
SIGNAL_NAME = "signal_name"
|
||||||
|
NETATMO_CREATE_BATTERY = "netatmo_create_battery"
|
||||||
|
|
||||||
CONF_CLOUDHOOK_URL = "cloudhook_url"
|
CONF_CLOUDHOOK_URL = "cloudhook_url"
|
||||||
CONF_WEATHER_AREAS = "weather_areas"
|
CONF_WEATHER_AREAS = "weather_areas"
|
||||||
|
|
|
@ -57,6 +57,16 @@ DEFAULT_INTERVALS = {
|
||||||
SCAN_INTERVAL = 60
|
SCAN_INTERVAL = 60
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NetatmoDevice:
|
||||||
|
"""Netatmo device class."""
|
||||||
|
|
||||||
|
data_handler: NetatmoDataHandler
|
||||||
|
device: pyatmo.climate.NetatmoModule
|
||||||
|
parent_id: str
|
||||||
|
state_class_name: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NetatmoDataClass:
|
class NetatmoDataClass:
|
||||||
"""Class for keeping track of Netatmo data class metadata."""
|
"""Class for keeping track of Netatmo data class metadata."""
|
||||||
|
|
|
@ -42,6 +42,7 @@ from .const import (
|
||||||
DATA_HANDLER,
|
DATA_HANDLER,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
MANUFACTURER,
|
MANUFACTURER,
|
||||||
|
NETATMO_CREATE_BATTERY,
|
||||||
SIGNAL_NAME,
|
SIGNAL_NAME,
|
||||||
TYPE_WEATHER,
|
TYPE_WEATHER,
|
||||||
)
|
)
|
||||||
|
@ -50,6 +51,7 @@ from .data_handler import (
|
||||||
PUBLICDATA_DATA_CLASS_NAME,
|
PUBLICDATA_DATA_CLASS_NAME,
|
||||||
WEATHERSTATION_DATA_CLASS_NAME,
|
WEATHERSTATION_DATA_CLASS_NAME,
|
||||||
NetatmoDataHandler,
|
NetatmoDataHandler,
|
||||||
|
NetatmoDevice,
|
||||||
)
|
)
|
||||||
from .helper import NetatmoArea
|
from .helper import NetatmoArea
|
||||||
from .netatmo_entity_base import NetatmoBase
|
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
|
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)
|
await add_public_entities(False)
|
||||||
|
|
||||||
if platform_not_ready:
|
if platform_not_ready:
|
||||||
|
@ -561,6 +573,73 @@ class NetatmoSensor(NetatmoBase, SensorEntity):
|
||||||
self.async_write_ha_state()
|
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:
|
def fix_angle(angle: int) -> int:
|
||||||
"""Fix angle when value is negative."""
|
"""Fix angle when value is negative."""
|
||||||
if angle < 0:
|
if angle < 0:
|
||||||
|
|
|
@ -233,3 +233,17 @@ async def test_weather_sensor_enabling(
|
||||||
|
|
||||||
assert len(hass.states.async_all()) > states_before
|
assert len(hass.states.async_all()) > states_before
|
||||||
assert hass.states.get(f"sensor.{name}").state == expected
|
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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue