core/homeassistant/components/netatmo/sensor.py

564 lines
20 KiB
Python

"""Support for the Netatmo Weather Service."""
from datetime import timedelta
import logging
import pyatmo
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS,
UNIT_PERCENTAGE,
)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from .const import AUTH, DOMAIN, MANUFACTURER, MODELS
_LOGGER = logging.getLogger(__name__)
CONF_MODULES = "modules"
CONF_STATION = "station"
CONF_AREAS = "areas"
CONF_LAT_NE = "lat_ne"
CONF_LON_NE = "lon_ne"
CONF_LAT_SW = "lat_sw"
CONF_LON_SW = "lon_sw"
DEFAULT_MODE = "avg"
MODE_TYPES = {"max", "avg"}
# This is the Netatmo data upload interval in seconds
NETATMO_UPDATE_INTERVAL = 600
# NetAtmo Public Data is uploaded to server every 10 minutes
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=NETATMO_UPDATE_INTERVAL)
SUPPORTED_PUBLIC_SENSOR_TYPES = [
"temperature",
"pressure",
"humidity",
"rain",
"windstrength",
"guststrength",
"sum_rain_1",
"sum_rain_24",
]
SENSOR_TYPES = {
"temperature": [
"Temperature",
TEMP_CELSIUS,
"mdi:thermometer",
DEVICE_CLASS_TEMPERATURE,
],
"co2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, "mdi:periodic-table-co2", None],
"pressure": ["Pressure", "mbar", "mdi:gauge", None],
"noise": ["Noise", "dB", "mdi:volume-high", None],
"humidity": [
"Humidity",
UNIT_PERCENTAGE,
"mdi:water-percent",
DEVICE_CLASS_HUMIDITY,
],
"rain": ["Rain", "mm", "mdi:weather-rainy", None],
"sum_rain_1": ["sum_rain_1", "mm", "mdi:weather-rainy", None],
"sum_rain_24": ["sum_rain_24", "mm", "mdi:weather-rainy", None],
"battery_vp": ["Battery", "", "mdi:battery", None],
"battery_lvl": ["Battery_lvl", "", "mdi:battery", None],
"battery_percent": ["battery_percent", UNIT_PERCENTAGE, None, DEVICE_CLASS_BATTERY],
"min_temp": ["Min Temp.", TEMP_CELSIUS, "mdi:thermometer", None],
"max_temp": ["Max Temp.", TEMP_CELSIUS, "mdi:thermometer", None],
"windangle": ["Angle", "", "mdi:compass", None],
"windangle_value": ["Angle Value", "º", "mdi:compass", None],
"windstrength": [
"Wind Strength",
SPEED_KILOMETERS_PER_HOUR,
"mdi:weather-windy",
None,
],
"gustangle": ["Gust Angle", "", "mdi:compass", None],
"gustangle_value": ["Gust Angle Value", "º", "mdi:compass", None],
"guststrength": [
"Gust Strength",
SPEED_KILOMETERS_PER_HOUR,
"mdi:weather-windy",
None,
],
"reachable": ["Reachability", "", "mdi:signal", None],
"rf_status": ["Radio", "", "mdi:signal", None],
"rf_status_lvl": ["Radio_lvl", "", "mdi:signal", None],
"wifi_status": ["Wifi", "", "mdi:wifi", None],
"wifi_status_lvl": ["Wifi_lvl", "dBm", "mdi:wifi", None],
"health_idx": ["Health", "", "mdi:cloud", None],
}
MODULE_TYPE_OUTDOOR = "NAModule1"
MODULE_TYPE_WIND = "NAModule2"
MODULE_TYPE_RAIN = "NAModule3"
MODULE_TYPE_INDOOR = "NAModule4"
NETATMO_DEVICE_TYPES = {
"WeatherStationData": "weather station",
"HomeCoachData": "home coach",
}
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Netatmo weather and homecoach platform."""
auth = hass.data[DOMAIN][entry.entry_id][AUTH]
def find_entities(data):
"""Find all entities."""
all_module_infos = data.get_module_infos()
entities = []
for module in all_module_infos.values():
_LOGGER.debug("Adding module %s %s", module["module_name"], module["id"])
for condition in data.station_data.monitoredConditions(
moduleId=module["id"]
):
entities.append(NetatmoSensor(data, module, condition.lower()))
return entities
def get_entities():
"""Retrieve Netatmo entities."""
entities = []
for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]:
try:
dc_data = data_class(auth)
_LOGGER.debug("%s detected!", NETATMO_DEVICE_TYPES[data_class.__name__])
data = NetatmoData(auth, dc_data)
except pyatmo.NoDevice:
_LOGGER.debug(
"No %s entities found", NETATMO_DEVICE_TYPES[data_class.__name__]
)
continue
entities.extend(find_entities(data))
return entities
async_add_entities(await hass.async_add_executor_job(get_entities), True)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Netatmo weather and homecoach platform."""
return
class NetatmoSensor(Entity):
"""Implementation of a Netatmo sensor."""
def __init__(self, netatmo_data, module_info, sensor_type):
"""Initialize the sensor."""
self.netatmo_data = netatmo_data
device = self.netatmo_data.station_data.moduleById(mid=module_info["id"])
if not device:
# Assume it's a station if module can't be found
device = self.netatmo_data.station_data.stationById(sid=module_info["id"])
if device["type"] == "NHC":
self.module_name = module_info["station_name"]
else:
self.module_name = (
f"{module_info['station_name']} {module_info['module_name']}"
)
self._name = f"{MANUFACTURER} {self.module_name} {SENSOR_TYPES[sensor_type][0]}"
self.type = sensor_type
self._state = None
self._device_class = SENSOR_TYPES[self.type][3]
self._icon = SENSOR_TYPES[self.type][2]
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
self._module_type = device["type"]
self._module_id = module_info["id"]
self._unique_id = f"{self._module_id}-{self.type}"
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return self._icon
@property
def device_class(self):
"""Return the device class of the sensor."""
return self._device_class
@property
def device_info(self):
"""Return the device info for the sensor."""
return {
"identifiers": {(DOMAIN, self._module_id)},
"name": self.module_name,
"manufacturer": MANUFACTURER,
"model": MODELS[self._module_type],
}
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
@property
def unique_id(self):
"""Return the unique ID for this sensor."""
return self._unique_id
@property
def available(self):
"""Return True if entity is available."""
return self._state is not None
def update(self):
"""Get the latest data from Netatmo API and updates the states."""
self.netatmo_data.update()
if self.netatmo_data.data is None:
if self._state is None:
return
_LOGGER.warning("No data from update")
self._state = None
return
data = self.netatmo_data.data.get(self._module_id)
if data is None:
if self._state:
_LOGGER.debug(
"No data found for %s (%s)", self.module_name, self._module_id
)
_LOGGER.debug("data: %s", self.netatmo_data.data)
self._state = None
return
try:
if self.type == "temperature":
self._state = round(data["Temperature"], 1)
elif self.type == "humidity":
self._state = data["Humidity"]
elif self.type == "rain":
self._state = data["Rain"]
elif self.type == "sum_rain_1":
self._state = round(data["sum_rain_1"], 1)
elif self.type == "sum_rain_24":
self._state = data["sum_rain_24"]
elif self.type == "noise":
self._state = data["Noise"]
elif self.type == "co2":
self._state = data["CO2"]
elif self.type == "pressure":
self._state = round(data["Pressure"], 1)
elif self.type == "battery_percent":
self._state = data["battery_percent"]
elif self.type == "battery_lvl":
self._state = data["battery_vp"]
elif self.type == "battery_vp" and self._module_type == MODULE_TYPE_WIND:
if data["battery_vp"] >= 5590:
self._state = "Full"
elif data["battery_vp"] >= 5180:
self._state = "High"
elif data["battery_vp"] >= 4770:
self._state = "Medium"
elif data["battery_vp"] >= 4360:
self._state = "Low"
elif data["battery_vp"] < 4360:
self._state = "Very Low"
elif self.type == "battery_vp" and self._module_type == MODULE_TYPE_RAIN:
if data["battery_vp"] >= 5500:
self._state = "Full"
elif data["battery_vp"] >= 5000:
self._state = "High"
elif data["battery_vp"] >= 4500:
self._state = "Medium"
elif data["battery_vp"] >= 4000:
self._state = "Low"
elif data["battery_vp"] < 4000:
self._state = "Very Low"
elif self.type == "battery_vp" and self._module_type == MODULE_TYPE_INDOOR:
if data["battery_vp"] >= 5640:
self._state = "Full"
elif data["battery_vp"] >= 5280:
self._state = "High"
elif data["battery_vp"] >= 4920:
self._state = "Medium"
elif data["battery_vp"] >= 4560:
self._state = "Low"
elif data["battery_vp"] < 4560:
self._state = "Very Low"
elif self.type == "battery_vp" and self._module_type == MODULE_TYPE_OUTDOOR:
if data["battery_vp"] >= 5500:
self._state = "Full"
elif data["battery_vp"] >= 5000:
self._state = "High"
elif data["battery_vp"] >= 4500:
self._state = "Medium"
elif data["battery_vp"] >= 4000:
self._state = "Low"
elif data["battery_vp"] < 4000:
self._state = "Very Low"
elif self.type == "min_temp":
self._state = data["min_temp"]
elif self.type == "max_temp":
self._state = data["max_temp"]
elif self.type == "windangle_value":
self._state = data["WindAngle"]
elif self.type == "windangle":
if data["WindAngle"] >= 330:
self._state = "N (%d\xb0)" % data["WindAngle"]
elif data["WindAngle"] >= 300:
self._state = "NW (%d\xb0)" % data["WindAngle"]
elif data["WindAngle"] >= 240:
self._state = "W (%d\xb0)" % data["WindAngle"]
elif data["WindAngle"] >= 210:
self._state = "SW (%d\xb0)" % data["WindAngle"]
elif data["WindAngle"] >= 150:
self._state = "S (%d\xb0)" % data["WindAngle"]
elif data["WindAngle"] >= 120:
self._state = "SE (%d\xb0)" % data["WindAngle"]
elif data["WindAngle"] >= 60:
self._state = "E (%d\xb0)" % data["WindAngle"]
elif data["WindAngle"] >= 30:
self._state = "NE (%d\xb0)" % data["WindAngle"]
elif data["WindAngle"] >= 0:
self._state = "N (%d\xb0)" % data["WindAngle"]
elif self.type == "windstrength":
self._state = data["WindStrength"]
elif self.type == "gustangle_value":
self._state = data["GustAngle"]
elif self.type == "gustangle":
if data["GustAngle"] >= 330:
self._state = "N (%d\xb0)" % data["GustAngle"]
elif data["GustAngle"] >= 300:
self._state = "NW (%d\xb0)" % data["GustAngle"]
elif data["GustAngle"] >= 240:
self._state = "W (%d\xb0)" % data["GustAngle"]
elif data["GustAngle"] >= 210:
self._state = "SW (%d\xb0)" % data["GustAngle"]
elif data["GustAngle"] >= 150:
self._state = "S (%d\xb0)" % data["GustAngle"]
elif data["GustAngle"] >= 120:
self._state = "SE (%d\xb0)" % data["GustAngle"]
elif data["GustAngle"] >= 60:
self._state = "E (%d\xb0)" % data["GustAngle"]
elif data["GustAngle"] >= 30:
self._state = "NE (%d\xb0)" % data["GustAngle"]
elif data["GustAngle"] >= 0:
self._state = "N (%d\xb0)" % data["GustAngle"]
elif self.type == "guststrength":
self._state = data["GustStrength"]
elif self.type == "reachable":
self._state = data["reachable"]
elif self.type == "rf_status_lvl":
self._state = data["rf_status"]
elif self.type == "rf_status":
if data["rf_status"] >= 90:
self._state = "Low"
elif data["rf_status"] >= 76:
self._state = "Medium"
elif data["rf_status"] >= 60:
self._state = "High"
elif data["rf_status"] <= 59:
self._state = "Full"
elif self.type == "wifi_status_lvl":
self._state = data["wifi_status"]
elif self.type == "wifi_status":
if data["wifi_status"] >= 86:
self._state = "Low"
elif data["wifi_status"] >= 71:
self._state = "Medium"
elif data["wifi_status"] >= 56:
self._state = "High"
elif data["wifi_status"] <= 55:
self._state = "Full"
elif self.type == "health_idx":
if data["health_idx"] == 0:
self._state = "Healthy"
elif data["health_idx"] == 1:
self._state = "Fine"
elif data["health_idx"] == 2:
self._state = "Fair"
elif data["health_idx"] == 3:
self._state = "Poor"
elif data["health_idx"] == 4:
self._state = "Unhealthy"
except KeyError:
if self._state:
_LOGGER.info("No %s data found for %s", self.type, self.module_name)
self._state = None
return
class NetatmoPublicSensor(Entity):
"""Represent a single sensor in a Netatmo."""
def __init__(self, area_name, data, sensor_type, mode):
"""Initialize the sensor."""
self.netatmo_data = data
self.type = sensor_type
self._mode = mode
self._name = f"{MANUFACTURER} {area_name} {SENSOR_TYPES[self.type][0]}"
self._area_name = area_name
self._state = None
self._device_class = SENSOR_TYPES[self.type][3]
self._icon = SENSOR_TYPES[self.type][2]
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def icon(self):
"""Icon to use in the frontend."""
return self._icon
@property
def device_class(self):
"""Return the device class of the sensor."""
return self._device_class
@property
def device_info(self):
"""Return the device info for the sensor."""
return {
"identifiers": {(DOMAIN, self._area_name)},
"name": self._area_name,
"manufacturer": MANUFACTURER,
"model": "public",
}
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return self._unit_of_measurement
@property
def available(self):
"""Return True if entity is available."""
return bool(self._state)
def update(self):
"""Get the latest data from Netatmo API and updates the states."""
self.netatmo_data.update()
if self.netatmo_data.data is None:
_LOGGER.info("No data found for %s", self._name)
self._state = None
return
data = None
if self.type == "temperature":
data = self.netatmo_data.data.getLatestTemperatures()
elif self.type == "pressure":
data = self.netatmo_data.data.getLatestPressures()
elif self.type == "humidity":
data = self.netatmo_data.data.getLatestHumidities()
elif self.type == "rain":
data = self.netatmo_data.data.getLatestRain()
elif self.type == "sum_rain_1":
data = self.netatmo_data.data.get60minRain()
elif self.type == "sum_rain_24":
data = self.netatmo_data.data.get24hRain()
elif self.type == "windstrength":
data = self.netatmo_data.data.getLatestWindStrengths()
elif self.type == "guststrength":
data = self.netatmo_data.data.getLatestGustStrengths()
if not data:
_LOGGER.warning(
"No station provides %s data in the area %s", self.type, self._area_name
)
self._state = None
return
values = [x for x in data.values() if x is not None]
if self._mode == "avg":
self._state = round(sum(values) / len(values), 1)
elif self._mode == "max":
self._state = max(values)
class NetatmoPublicData:
"""Get the latest data from Netatmo."""
def __init__(self, auth, lat_ne, lon_ne, lat_sw, lon_sw):
"""Initialize the data object."""
self.auth = auth
self.data = None
self.lat_ne = lat_ne
self.lon_ne = lon_ne
self.lat_sw = lat_sw
self.lon_sw = lon_sw
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Request an update from the Netatmo API."""
try:
data = pyatmo.PublicData(
self.auth,
LAT_NE=self.lat_ne,
LON_NE=self.lon_ne,
LAT_SW=self.lat_sw,
LON_SW=self.lon_sw,
filtering=True,
)
except pyatmo.NoDevice:
data = None
if not data:
_LOGGER.debug("No data received when updating public station data")
return
if data.CountStationInArea() == 0:
_LOGGER.warning("No Stations available in this area.")
return
self.data = data
class NetatmoData:
"""Get the latest data from Netatmo."""
def __init__(self, auth, station_data):
"""Initialize the data object."""
self.data = {}
self.station_data = station_data
self.auth = auth
def get_module_infos(self):
"""Return all modules available on the API as a dict."""
return self.station_data.getModules()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Call the Netatmo API to update the data."""
self.station_data = self.station_data.__class__(self.auth)
data = self.station_data.lastData(exclude=3600, byId=True)
if not data:
_LOGGER.debug("No data received when updating station data")
return
self.data = data