1 """Support for the Netatmo sensors."""
3 from __future__
import annotations
5 from collections.abc
import Callable
6 from dataclasses
import dataclass
8 from typing
import Any, cast
11 from pyatmo.modules
import PublicWeatherArea
16 SensorEntityDescription,
23 CONCENTRATION_PARTS_PER_MILLION,
28 UnitOfPrecipitationDepth,
38 async_dispatcher_connect,
39 async_dispatcher_send,
46 CONF_URL_PUBLIC_WEATHER,
50 NETATMO_CREATE_BATTERY,
51 NETATMO_CREATE_ROOM_SENSOR,
52 NETATMO_CREATE_SENSOR,
53 NETATMO_CREATE_WEATHER_SENSOR,
56 from .data_handler
import HOME, PUBLIC, NetatmoDataHandler, NetatmoDevice, NetatmoRoom
61 NetatmoWeatherModuleEntity,
63 from .helper
import NetatmoArea
65 _LOGGER = logging.getLogger(__name__)
80 """Process health index and return string for display."""
81 if not isinstance(health, int):
88 }.
get(health,
"unhealthy")
92 """Process wifi signal strength and return string for display."""
93 if not isinstance(strength, int):
105 """Process wifi signal strength and return string for display."""
106 if not isinstance(strength, int):
117 @dataclass(frozen=True, kw_only=True)
119 """Describes Netatmo sensor entity."""
122 value_fn: Callable[[StateType], StateType] =
lambda x: x
125 SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
128 netatmo_name=
"temperature",
129 native_unit_of_measurement=UnitOfTemperature.CELSIUS,
130 state_class=SensorStateClass.MEASUREMENT,
131 device_class=SensorDeviceClass.TEMPERATURE,
132 suggested_display_precision=1,
136 netatmo_name=
"temp_trend",
137 entity_registry_enabled_default=
False,
142 native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
143 state_class=SensorStateClass.MEASUREMENT,
144 device_class=SensorDeviceClass.CO2,
148 netatmo_name=
"pressure",
149 native_unit_of_measurement=UnitOfPressure.MBAR,
150 state_class=SensorStateClass.MEASUREMENT,
151 device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
152 suggested_display_precision=1,
155 key=
"pressure_trend",
156 netatmo_name=
"pressure_trend",
157 entity_registry_enabled_default=
False,
161 netatmo_name=
"noise",
162 native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
163 device_class=SensorDeviceClass.SOUND_PRESSURE,
164 state_class=SensorStateClass.MEASUREMENT,
168 netatmo_name=
"humidity",
169 native_unit_of_measurement=PERCENTAGE,
170 state_class=SensorStateClass.MEASUREMENT,
171 device_class=SensorDeviceClass.HUMIDITY,
176 native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
177 device_class=SensorDeviceClass.PRECIPITATION,
178 state_class=SensorStateClass.MEASUREMENT,
182 netatmo_name=
"sum_rain_1",
183 entity_registry_enabled_default=
False,
184 native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
185 device_class=SensorDeviceClass.PRECIPITATION,
186 state_class=SensorStateClass.TOTAL,
187 suggested_display_precision=1,
191 netatmo_name=
"sum_rain_24",
192 native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
193 device_class=SensorDeviceClass.PRECIPITATION,
194 state_class=SensorStateClass.TOTAL_INCREASING,
197 key=
"battery_percent",
198 netatmo_name=
"battery",
199 entity_category=EntityCategory.DIAGNOSTIC,
200 native_unit_of_measurement=PERCENTAGE,
201 state_class=SensorStateClass.MEASUREMENT,
202 device_class=SensorDeviceClass.BATTERY,
206 netatmo_name=
"wind_direction",
207 device_class=SensorDeviceClass.ENUM,
208 options=DIRECTION_OPTIONS,
209 value_fn=
lambda x: x.lower()
if isinstance(x, str)
else None,
212 key=
"windangle_value",
213 netatmo_name=
"wind_angle",
214 entity_registry_enabled_default=
False,
215 native_unit_of_measurement=DEGREE,
216 state_class=SensorStateClass.MEASUREMENT,
220 netatmo_name=
"wind_strength",
221 native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
222 device_class=SensorDeviceClass.WIND_SPEED,
223 state_class=SensorStateClass.MEASUREMENT,
227 netatmo_name=
"gust_direction",
228 entity_registry_enabled_default=
False,
229 device_class=SensorDeviceClass.ENUM,
230 options=DIRECTION_OPTIONS,
231 value_fn=
lambda x: x.lower()
if isinstance(x, str)
else None,
234 key=
"gustangle_value",
235 netatmo_name=
"gust_angle",
236 entity_registry_enabled_default=
False,
237 native_unit_of_measurement=DEGREE,
238 state_class=SensorStateClass.MEASUREMENT,
242 netatmo_name=
"gust_strength",
243 entity_registry_enabled_default=
False,
244 native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
245 device_class=SensorDeviceClass.WIND_SPEED,
246 state_class=SensorStateClass.MEASUREMENT,
250 netatmo_name=
"reachable",
251 entity_registry_enabled_default=
False,
252 entity_category=EntityCategory.DIAGNOSTIC,
256 netatmo_name=
"rf_strength",
257 entity_registry_enabled_default=
False,
258 entity_category=EntityCategory.DIAGNOSTIC,
263 netatmo_name=
"wifi_strength",
264 entity_registry_enabled_default=
False,
265 entity_category=EntityCategory.DIAGNOSTIC,
266 value_fn=process_wifi,
270 netatmo_name=
"health_idx",
271 device_class=SensorDeviceClass.ENUM,
272 options=[
"healthy",
"fine",
"fair",
"poor",
"unhealthy"],
273 value_fn=process_health,
277 netatmo_name=
"power",
278 native_unit_of_measurement=UnitOfPower.WATT,
279 state_class=SensorStateClass.MEASUREMENT,
280 device_class=SensorDeviceClass.POWER,
283 SENSOR_TYPES_KEYS = [desc.key
for desc
in SENSOR_TYPES]
286 @dataclass(frozen=True, kw_only=True)
288 """Describes Netatmo sensor entity."""
290 value_fn: Callable[[PublicWeatherArea], dict[str, Any]]
293 PUBLIC_WEATHER_STATION_TYPES: tuple[
294 NetatmoPublicWeatherSensorEntityDescription, ...
298 native_unit_of_measurement=UnitOfTemperature.CELSIUS,
299 state_class=SensorStateClass.MEASUREMENT,
300 device_class=SensorDeviceClass.TEMPERATURE,
301 suggested_display_precision=1,
302 value_fn=
lambda area: area.get_latest_temperatures(),
306 native_unit_of_measurement=UnitOfPressure.MBAR,
307 state_class=SensorStateClass.MEASUREMENT,
308 device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
309 suggested_display_precision=1,
310 value_fn=
lambda area: area.get_latest_pressures(),
314 native_unit_of_measurement=PERCENTAGE,
315 state_class=SensorStateClass.MEASUREMENT,
316 device_class=SensorDeviceClass.HUMIDITY,
317 value_fn=
lambda area: area.get_latest_humidities(),
321 native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
322 device_class=SensorDeviceClass.PRECIPITATION,
323 state_class=SensorStateClass.MEASUREMENT,
324 value_fn=
lambda area: area.get_latest_rain(),
328 translation_key=
"sum_rain_1",
329 entity_registry_enabled_default=
False,
330 native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
331 device_class=SensorDeviceClass.PRECIPITATION,
332 state_class=SensorStateClass.TOTAL,
333 suggested_display_precision=1,
334 value_fn=
lambda area: area.get_60_min_rain(),
338 translation_key=
"sum_rain_24",
339 native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
340 device_class=SensorDeviceClass.PRECIPITATION,
341 state_class=SensorStateClass.TOTAL_INCREASING,
342 value_fn=
lambda area: area.get_24_h_rain(),
345 key=
"windangle_value",
346 entity_registry_enabled_default=
False,
347 native_unit_of_measurement=DEGREE,
348 state_class=SensorStateClass.MEASUREMENT,
349 value_fn=
lambda area: area.get_latest_wind_angles(),
353 native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
354 device_class=SensorDeviceClass.WIND_SPEED,
355 state_class=SensorStateClass.MEASUREMENT,
356 value_fn=
lambda area: area.get_latest_wind_strengths(),
359 key=
"gustangle_value",
360 translation_key=
"gust_angle",
361 entity_registry_enabled_default=
False,
362 native_unit_of_measurement=DEGREE,
363 state_class=SensorStateClass.MEASUREMENT,
364 value_fn=
lambda area: area.get_latest_gust_angles(),
368 translation_key=
"gust_strength",
369 entity_registry_enabled_default=
False,
370 native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
371 device_class=SensorDeviceClass.WIND_SPEED,
372 state_class=SensorStateClass.MEASUREMENT,
373 value_fn=
lambda area: area.get_latest_gust_strengths(),
379 netatmo_name=
"battery",
380 entity_category=EntityCategory.DIAGNOSTIC,
381 native_unit_of_measurement=PERCENTAGE,
382 state_class=SensorStateClass.MEASUREMENT,
383 device_class=SensorDeviceClass.BATTERY,
388 hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
390 """Set up the Netatmo sensor platform."""
393 def _create_battery_entity(netatmo_device: NetatmoDevice) ->
None:
394 if not hasattr(netatmo_device.device,
"battery"):
399 entry.async_on_unload(
404 def _create_weather_sensor_entity(netatmo_device: NetatmoDevice) ->
None:
407 for description
in SENSOR_TYPES
408 if description.netatmo_name
in netatmo_device.device.features
411 entry.async_on_unload(
413 hass, NETATMO_CREATE_WEATHER_SENSOR, _create_weather_sensor_entity
418 def _create_sensor_entity(netatmo_device: NetatmoDevice) ->
None:
420 "Adding %s sensor %s",
421 netatmo_device.device.device_category,
422 netatmo_device.device.name,
426 for description
in SENSOR_TYPES
427 if description.key
in netatmo_device.device.features
430 entry.async_on_unload(
435 def _create_room_sensor_entity(netatmo_device: NetatmoRoom) ->
None:
436 if not netatmo_device.room.climate_type:
437 msg = f
"No climate type found for this room: {netatmo_device.room.name}"
442 for description
in SENSOR_TYPES
443 if description.key
in netatmo_device.room.features
446 entry.async_on_unload(
448 hass, NETATMO_CREATE_ROOM_SENSOR, _create_room_sensor_entity
452 device_registry = dr.async_get(hass)
453 data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
455 async
def add_public_entities(update: bool =
True) ->
None:
456 """Retrieve Netatmo public weather entities."""
458 device.name: device.id
459 for device
in dr.async_entries_for_config_entry(
460 device_registry, entry.entry_id
462 if device.model ==
"Public Weather station"
465 new_entities: list[NetatmoPublicSensor] = []
467 NetatmoArea(**i)
for i
in entry.options.get(CONF_WEATHER_AREAS, {}).values()
469 signal_name = f
"{PUBLIC}-{area.uuid}"
471 if area.area_name
in entities:
472 entities.pop(area.area_name)
477 f
"netatmo-config-{area.area_name}",
482 await data_handler.subscribe(
490 area_id=
str(area.uuid),
495 for description
in PUBLIC_WEATHER_STATION_TYPES
498 for device_id
in entities.values():
499 device_registry.async_remove_device(device_id)
504 hass, f
"signal-{DOMAIN}-public-update-{entry.entry_id}", add_public_entities
507 await add_public_entities(
False)
511 """Implementation of a Netatmo weather/home coach sensor."""
513 entity_description: NetatmoSensorEntityDescription
517 netatmo_device: NetatmoDevice,
518 description: NetatmoSensorEntityDescription,
520 """Initialize the sensor."""
528 """Return True if entity is available."""
530 self.
devicedevice.reachable
536 """Update the entity's state."""
540 if value
is not None:
547 """Implementation of a Netatmo sensor."""
549 entity_description: NetatmoSensorEntityDescription
550 device: pyatmo.modules.NRV
551 _attr_configuration_url = CONF_URL_ENERGY
553 def __init__(self, netatmo_device: NetatmoDevice) ->
None:
554 """Initialize the sensor."""
558 self._publishers.extend(
562 "home_id": netatmo_device.device.home.entity_id,
563 SIGNAL_NAME: netatmo_device.signal_name,
568 self.
_attr_unique_id_attr_unique_id = f
"{netatmo_device.parent_id}-{self.device.entity_id}-{self.entity_description.key}"
570 identifiers={(DOMAIN, netatmo_device.parent_id)},
571 name=netatmo_device.device.name,
579 """Update the entity's state."""
580 if not self.
devicedevice.reachable:
590 """Implementation of a Netatmo sensor."""
592 entity_description: NetatmoSensorEntityDescription
593 _attr_configuration_url = CONF_URL_ENERGY
597 netatmo_device: NetatmoDevice,
598 description: NetatmoSensorEntityDescription,
600 """Initialize the sensor."""
604 self._publishers.extend(
608 "home_id": self.
homehome.entity_id,
609 SIGNAL_NAME: netatmo_device.signal_name,
615 f
"{self.device.entity_id}-{self.device.entity_id}-{description.key}"
620 """Update the entity's state."""
621 if not self.
devicedevice.reachable:
636 """Implementation of a Netatmo room sensor."""
638 entity_description: NetatmoSensorEntityDescription
642 netatmo_room: NetatmoRoom,
643 description: NetatmoSensorEntityDescription,
645 """Initialize the sensor."""
649 self._publishers.extend(
653 "home_id": self.
homehome.entity_id,
654 SIGNAL_NAME: netatmo_room.signal_name,
660 f
"{self.device.entity_id}-{self.device.entity_id}-{description.key}"
665 """Update the entity's state."""
675 """Represent a single sensor in a Netatmo."""
677 entity_description: NetatmoPublicWeatherSensorEntityDescription
681 data_handler: NetatmoDataHandler,
683 description: NetatmoPublicWeatherSensorEntityDescription,
685 """Initialize the sensor."""
690 self._publishers.append(
693 "lat_ne": area.lat_ne,
694 "lon_ne": area.lon_ne,
695 "lat_sw": area.lat_sw,
696 "lon_sw": area.lon_sw,
697 "area_name": area.area_name,
702 self.
_station_station = data_handler.account.public_weather_areas[
str(area.uuid)]
707 self.
_attr_unique_id_attr_unique_id = f
"{area.area_name.replace(' ', '-')}-{description.key}"
711 ATTR_LATITUDE: (area.lat_ne + area.lat_sw) / 2,
712 ATTR_LONGITUDE: (area.lon_ne + area.lon_sw) / 2,
716 identifiers={(DOMAIN, area.area_name)},
718 model=
"Public Weather station",
719 manufacturer=
"Netatmo",
720 configuration_url=CONF_URL_PUBLIC_WEATHER,
724 """Entity created."""
730 f
"netatmo-config-{self.area.area_name}",
736 """Update the entity's config."""
737 if self.
areaarea == area:
746 self.
_mode_mode = area.mode
760 """Update the entity's state."""
766 "No station provides %s data in the area %s",
768 self.
areaarea.area_name,
774 if values := [x
for x
in data.values()
if x
is not None]:
775 if self.
_mode_mode ==
"avg":
777 elif self.
_mode_mode ==
"max":
779 elif self.
_mode_mode ==
"min":
_attr_extra_state_attributes
None async_update_callback(self)
tuple[str, str] device_description(self)
None async_update_callback(self)
None __init__(self, NetatmoDevice netatmo_device)
None async_added_to_hass(self)
None __init__(self, NetatmoDataHandler data_handler, NetatmoArea area, NetatmoPublicWeatherSensorEntityDescription description)
None async_update_callback(self)
None async_config_update_callback(self, NetatmoArea area)
None __init__(self, NetatmoRoom netatmo_room, NetatmoSensorEntityDescription description)
None async_update_callback(self)
None __init__(self, NetatmoDevice netatmo_device, NetatmoSensorEntityDescription description)
None async_update_callback(self)
None async_update_callback(self)
None __init__(self, NetatmoDevice netatmo_device, NetatmoSensorEntityDescription description)
StateType|date|datetime|Decimal native_value(self)
None async_write_ha_state(self)
None async_on_remove(self, CALLBACK_TYPE func)
web.Response get(self, web.Request request, str config_key)
IssData update(pyiss.ISS iss)
Callable[[], None] subscribe(HomeAssistant hass, str topic, MessageCallbackType msg_callback, int qos=DEFAULT_QOS, str encoding="utf-8")
str|None process_health(StateType health)
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
str|None process_rf(StateType strength)
str|None process_wifi(StateType strength)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)