core/homeassistant/components/netatmo/sensor.py

591 lines
22 KiB
Python
Raw Normal View History

"""Support for the Netatmo Weather Service."""
import logging
import threading
from datetime import timedelta
from time import time
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_NAME, CONF_MODE, CONF_MONITORED_CONDITIONS,
TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_BATTERY)
2016-02-19 05:27:50 +00:00
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import call_later
from homeassistant.util import Throttle
from .const import DATA_NETATMO_AUTH
_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'}
DEFAULT_NAME_PUBLIC = 'Netatmo Public Data'
# This is the Netatmo data upload interval in seconds
NETATMO_UPDATE_INTERVAL = 600
2016-01-13 02:45:43 +00:00
# NetAtmo Public Data is uploaded to server every 10 minutes
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600)
SUPPORTED_PUBLIC_SENSOR_TYPES = [
'temperature', 'pressure', 'humidity', 'rain', 'windstrength',
'guststrength'
]
SENSOR_TYPES = {
'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer',
DEVICE_CLASS_TEMPERATURE],
'co2': ['CO2', 'ppm', 'mdi:cloud', None],
'pressure': ['Pressure', 'mbar', 'mdi:gauge', None],
'noise': ['Noise', 'dB', 'mdi:volume-high', None],
'humidity': ['Humidity', '%', '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', '%', 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', 'km/h', 'mdi:weather-windy', None],
'gustangle': ['Gust Angle', '', 'mdi:compass', None],
'gustangle_value': ['Gust Angle Value', 'º', 'mdi:compass', None],
'guststrength': ['Gust Strength', 'km/h', 'mdi:weather-windy', 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_SCHEMA = vol.Schema({
vol.Required(cv.string): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
2016-01-13 02:45:43 +00:00
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_STATION): cv.string,
vol.Optional(CONF_MODULES): MODULE_SCHEMA,
vol.Optional(CONF_AREAS): vol.All(cv.ensure_list, [
{
vol.Required(CONF_LAT_NE): cv.latitude,
vol.Required(CONF_LAT_SW): cv.latitude,
vol.Required(CONF_LON_NE): cv.longitude,
vol.Required(CONF_LON_SW): cv.longitude,
vol.Required(CONF_MONITORED_CONDITIONS): [vol.In(
SUPPORTED_PUBLIC_SENSOR_TYPES)],
vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(MODE_TYPES),
vol.Optional(CONF_NAME, default=DEFAULT_NAME_PUBLIC): cv.string
}
]),
})
MODULE_TYPE_OUTDOOR = 'NAModule1'
MODULE_TYPE_WIND = 'NAModule2'
MODULE_TYPE_RAIN = 'NAModule3'
MODULE_TYPE_INDOOR = 'NAModule4'
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the available Netatmo weather sensors."""
dev = []
auth = hass.data[DATA_NETATMO_AUTH]
if config.get(CONF_AREAS) is not None:
for area in config[CONF_AREAS]:
data = NetatmoPublicData(
auth,
lat_ne=area[CONF_LAT_NE],
lon_ne=area[CONF_LON_NE],
lat_sw=area[CONF_LAT_SW],
lon_sw=area[CONF_LON_SW]
)
for sensor_type in area[CONF_MONITORED_CONDITIONS]:
dev.append(NetatmoPublicSensor(
area[CONF_NAME],
data,
sensor_type,
area[CONF_MODE]
))
else:
def _retry(_data):
try:
_dev = find_devices(_data)
except requests.exceptions.Timeout:
return call_later(hass, NETATMO_UPDATE_INTERVAL,
lambda _: _retry(_data))
if _dev:
add_entities(_dev, True)
import pyatmo
for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]:
data = NetatmoData(auth, data_class, config.get(CONF_STATION))
# Test if manually configured
if CONF_MODULES in config:
module_items = config[CONF_MODULES].items()
for module_name, monitored_conditions in module_items:
for condition in monitored_conditions:
dev.append(NetatmoSensor(
data, module_name, condition.lower(),
config.get(CONF_STATION)))
continue
# otherwise add all modules and conditions
try:
dev.extend(find_devices(data))
except requests.exceptions.Timeout:
call_later(hass, NETATMO_UPDATE_INTERVAL,
lambda _: _retry(data))
if dev:
add_entities(dev, True)
def find_devices(data):
"""Find all devices."""
dev = []
not_handled = []
for module_name in data.get_module_names():
if (module_name not in data.get_module_names()
and module_name not in not_handled):
not_handled.append(not_handled)
continue
for condition in data.station_data.monitoredConditions(module_name):
dev.append(NetatmoSensor(
data, module_name, condition.lower(), data.station))
for module_name in not_handled:
_LOGGER.error('Module name: "%s" not found', module_name)
return dev
class NetatmoSensor(Entity):
"""Implementation of a Netatmo sensor."""
def __init__(self, netatmo_data, module_name, sensor_type, station):
2016-03-08 15:46:34 +00:00
"""Initialize the sensor."""
self._name = 'Netatmo {} {}'.format(module_name,
SENSOR_TYPES[sensor_type][0])
self.netatmo_data = netatmo_data
self.module_name = module_name
self.type = sensor_type
self.station_name = station
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 = self.netatmo_data. \
station_data.moduleByName(module=module_name)['type']
module_id = self.netatmo_data. \
station_data.moduleByName(station=self.station_name,
module=module_name)['_id']
self._unique_id = '{}-{}'.format(module_id, self.type)
@property
def name(self):
2016-03-08 15:46:34 +00:00
"""Return the name of the sensor."""
return self._name
2016-01-22 20:15:07 +00:00
@property
def icon(self):
2016-02-23 05:21:49 +00:00
"""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
2016-01-22 20:15:07 +00:00
@property
def state(self):
2016-03-08 15:46:34 +00:00
"""Return the state of the device."""
return self._state
@property
def unit_of_measurement(self):
2016-03-08 15:46:34 +00:00
"""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
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 found for %s", self.module_name)
self._state = None
return
data = self.netatmo_data.data.get(self.module_name)
if data is None:
_LOGGER.warning("No data found for %s", self.module_name)
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 = data['sum_rain_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 == '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:
_LOGGER.error("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 = '{} {}'.format(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 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
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.warning("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 == '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
if self._mode == 'avg':
self._state = round(sum(data.values()) / len(data), 1)
elif self._mode == 'max':
self._state = max(data.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."""
import pyatmo
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)
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, data_class, station):
2016-03-08 15:46:34 +00:00
"""Initialize the data object."""
self.auth = auth
self.data_class = data_class
self.data = {}
self.station_data = None
self.station = station
self._next_update = time()
self._update_in_progress = threading.Lock()
2016-01-11 05:57:31 +00:00
def get_module_names(self):
2016-02-23 05:21:49 +00:00
"""Return all module available on the API as a list."""
self.update()
return self.data.keys()
def update(self):
"""Call the Netatmo API to update the data.
This method is not throttled by the builtin Throttle decorator
but with a custom logic, which takes into account the time
of the last update from the cloud.
"""
if time() < self._next_update or \
not self._update_in_progress.acquire(False):
return
try:
from pyatmo import NoDevice
try:
self.station_data = self.data_class(self.auth)
_LOGGER.debug("%s detected!", str(self.data_class.__name__))
except NoDevice:
_LOGGER.warning("No Weather or HomeCoach devices found for %s",
str(self.station)
)
return
except requests.exceptions.Timeout:
_LOGGER.warning("Timed out when connecting to Netatmo server.")
return
if self.station is not None:
data = self.station_data.lastData(
station=self.station, exclude=3600)
else:
data = self.station_data.lastData(exclude=3600)
if not data:
self._next_update = time() + NETATMO_UPDATE_INTERVAL
return
self.data = data
newinterval = 0
try:
for module in self.data:
if 'When' in self.data[module]:
newinterval = self.data[module]['When']
break
except TypeError:
_LOGGER.debug("No %s modules found", self.data_class.__name__)
if newinterval:
# Try and estimate when fresh data will be available
newinterval += NETATMO_UPDATE_INTERVAL - time()
if newinterval > NETATMO_UPDATE_INTERVAL - 30:
newinterval = NETATMO_UPDATE_INTERVAL
else:
if newinterval < NETATMO_UPDATE_INTERVAL / 2:
# Never hammer the Netatmo API more than
# twice per update interval
newinterval = NETATMO_UPDATE_INTERVAL / 2
_LOGGER.info(
"Netatmo refresh interval reset to %d seconds",
newinterval)
else:
# Last update time not found, fall back to default value
newinterval = NETATMO_UPDATE_INTERVAL
self._next_update = time() + newinterval
finally:
self._update_in_progress.release()