2019-05-08 06:26:52 +00:00
|
|
|
"""Support for the Netatmo Weather Service."""
|
2016-01-06 02:39:16 +00:00
|
|
|
import logging
|
2018-07-10 10:30:48 +00:00
|
|
|
import threading
|
2019-06-14 19:26:02 +00:00
|
|
|
from datetime import timedelta
|
|
|
|
from time import time
|
2016-09-11 19:27:58 +00:00
|
|
|
|
2019-06-14 19:26:02 +00:00
|
|
|
import requests
|
2016-09-11 19:27:58 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2019-06-14 19:26:02 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2016-09-11 19:27:58 +00:00
|
|
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
2018-05-30 14:53:35 +00:00
|
|
|
from homeassistant.const import (
|
2019-05-08 06:26:52 +00:00
|
|
|
CONF_NAME, CONF_MODE, CONF_MONITORED_CONDITIONS,
|
2018-05-30 14:53:35 +00:00
|
|
|
TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE,
|
2019-01-24 07:20:20 +00:00
|
|
|
DEVICE_CLASS_BATTERY)
|
2016-02-19 05:27:50 +00:00
|
|
|
from homeassistant.helpers.entity import Entity
|
2019-06-14 19:26:02 +00:00
|
|
|
from homeassistant.helpers.event import call_later
|
2019-05-08 06:26:52 +00:00
|
|
|
from homeassistant.util import Throttle
|
2019-04-26 15:15:37 +00:00
|
|
|
from .const import DATA_NETATMO_AUTH
|
2019-04-12 06:37:45 +00:00
|
|
|
|
2016-09-11 19:27:58 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2016-06-22 16:01:53 +00:00
|
|
|
|
2016-09-11 19:27:58 +00:00
|
|
|
CONF_MODULES = 'modules'
|
|
|
|
CONF_STATION = 'station'
|
2019-05-08 06:26:52 +00:00
|
|
|
CONF_AREAS = 'areas'
|
|
|
|
CONF_LAT_NE = 'lat_ne'
|
|
|
|
CONF_LON_NE = 'lon_ne'
|
|
|
|
CONF_LAT_SW = 'lat_sw'
|
|
|
|
CONF_LON_SW = 'lon_sw'
|
2016-09-11 19:27:58 +00:00
|
|
|
|
2019-05-08 06:26:52 +00:00
|
|
|
DEFAULT_MODE = 'avg'
|
|
|
|
MODE_TYPES = {'max', 'avg'}
|
|
|
|
|
|
|
|
DEFAULT_NAME_PUBLIC = 'Netatmo Public Data'
|
|
|
|
|
|
|
|
# This is the Netatmo data upload interval in seconds
|
2018-07-10 10:30:48 +00:00
|
|
|
NETATMO_UPDATE_INTERVAL = 600
|
2016-01-13 02:45:43 +00:00
|
|
|
|
2019-05-08 06:26:52 +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'
|
|
|
|
]
|
|
|
|
|
2016-01-06 02:39:16 +00:00
|
|
|
SENSOR_TYPES = {
|
2019-05-08 06:26:52 +00:00
|
|
|
'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer',
|
2018-05-30 14:53:35 +00:00
|
|
|
DEVICE_CLASS_TEMPERATURE],
|
|
|
|
'co2': ['CO2', 'ppm', 'mdi:cloud', None],
|
|
|
|
'pressure': ['Pressure', 'mbar', 'mdi:gauge', None],
|
|
|
|
'noise': ['Noise', 'dB', 'mdi:volume-high', None],
|
2019-05-08 06:26:52 +00:00
|
|
|
'humidity': ['Humidity', '%', 'mdi:water-percent', DEVICE_CLASS_HUMIDITY],
|
2018-05-30 14:53:35 +00:00
|
|
|
'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],
|
2019-01-20 21:30:17 +00:00
|
|
|
'battery_percent': ['battery_percent', '%', None, DEVICE_CLASS_BATTERY],
|
2018-05-30 14:53:35 +00:00
|
|
|
'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],
|
2019-05-08 06:26:52 +00:00
|
|
|
'windstrength': ['Wind Strength', 'km/h', 'mdi:weather-windy', None],
|
2018-05-30 14:53:35 +00:00
|
|
|
'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],
|
2018-07-10 10:30:48 +00:00
|
|
|
'wifi_status_lvl': ['Wifi_lvl', 'dBm', 'mdi:wifi', None],
|
2019-02-17 10:45:58 +00:00
|
|
|
'health_idx': ['Health', '', 'mdi:cloud', None],
|
2016-01-06 02:39:16 +00:00
|
|
|
}
|
|
|
|
|
2016-09-11 19:27:58 +00:00
|
|
|
MODULE_SCHEMA = vol.Schema({
|
2019-02-14 04:35:12 +00:00
|
|
|
vol.Required(cv.string): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
2016-09-11 19:27:58 +00:00
|
|
|
})
|
2016-01-13 02:45:43 +00:00
|
|
|
|
2016-09-11 19:27:58 +00:00
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
|
|
vol.Optional(CONF_STATION): cv.string,
|
2016-10-08 18:26:01 +00:00
|
|
|
vol.Optional(CONF_MODULES): MODULE_SCHEMA,
|
2019-05-08 06:26:52 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
]),
|
2016-09-11 19:27:58 +00:00
|
|
|
})
|
2016-01-06 02:39:16 +00:00
|
|
|
|
2018-12-01 17:00:49 +00:00
|
|
|
MODULE_TYPE_OUTDOOR = 'NAModule1'
|
|
|
|
MODULE_TYPE_WIND = 'NAModule2'
|
|
|
|
MODULE_TYPE_RAIN = 'NAModule3'
|
|
|
|
MODULE_TYPE_INDOOR = 'NAModule4'
|
|
|
|
|
2016-01-06 02:39:16 +00:00
|
|
|
|
2018-08-24 14:37:30 +00:00
|
|
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Set up the available Netatmo weather sensors."""
|
2016-01-06 02:39:16 +00:00
|
|
|
dev = []
|
2019-04-26 15:15:37 +00:00
|
|
|
auth = hass.data[DATA_NETATMO_AUTH]
|
|
|
|
|
2019-05-08 06:26:52 +00:00
|
|
|
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]
|
|
|
|
))
|
2019-02-17 10:45:58 +00:00
|
|
|
else:
|
2019-06-14 19:26:02 +00:00
|
|
|
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]:
|
2019-05-08 06:26:52 +00:00
|
|
|
data = NetatmoData(auth, data_class, config.get(CONF_STATION))
|
|
|
|
# Test if manually configured
|
|
|
|
if CONF_MODULES in config:
|
|
|
|
module_items = config[CONF_MODULES].items()
|
2019-06-14 19:26:02 +00:00
|
|
|
for module_name, monitored_conditions in module_items:
|
2019-05-08 06:26:52 +00:00
|
|
|
for condition in monitored_conditions:
|
|
|
|
dev.append(NetatmoSensor(
|
2019-05-27 16:07:59 +00:00
|
|
|
data, module_name, condition.lower(),
|
|
|
|
config.get(CONF_STATION)))
|
2019-06-14 19:26:02 +00:00
|
|
|
continue
|
2019-02-17 10:45:58 +00:00
|
|
|
|
2019-06-14 19:26:02 +00:00
|
|
|
# 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))
|
2019-02-17 10:45:58 +00:00
|
|
|
|
2019-05-08 06:26:52 +00:00
|
|
|
if dev:
|
|
|
|
add_entities(dev, True)
|
2019-02-17 10:45:58 +00:00
|
|
|
|
|
|
|
|
2019-06-14 19:26:02 +00:00
|
|
|
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))
|
2016-01-06 02:39:16 +00:00
|
|
|
|
2019-06-14 19:26:02 +00:00
|
|
|
for module_name in not_handled:
|
|
|
|
_LOGGER.error('Module name: "%s" not found', module_name)
|
|
|
|
return dev
|
2016-01-06 02:39:16 +00:00
|
|
|
|
|
|
|
|
2019-05-08 06:26:52 +00:00
|
|
|
class NetatmoSensor(Entity):
|
2016-09-29 14:45:54 +00:00
|
|
|
"""Implementation of a Netatmo sensor."""
|
2016-01-06 02:39:16 +00:00
|
|
|
|
2019-05-27 16:07:59 +00:00
|
|
|
def __init__(self, netatmo_data, module_name, sensor_type, station):
|
2016-03-08 15:46:34 +00:00
|
|
|
"""Initialize the sensor."""
|
2016-09-29 14:45:54 +00:00
|
|
|
self._name = 'Netatmo {} {}'.format(module_name,
|
2016-01-13 07:46:45 +00:00
|
|
|
SENSOR_TYPES[sensor_type][0])
|
2016-01-06 02:39:16 +00:00
|
|
|
self.netatmo_data = netatmo_data
|
|
|
|
self.module_name = module_name
|
|
|
|
self.type = sensor_type
|
2019-05-27 16:07:59 +00:00
|
|
|
self.station_name = station
|
2016-01-06 02:39:16 +00:00
|
|
|
self._state = None
|
2018-05-30 14:53:35 +00:00
|
|
|
self._device_class = SENSOR_TYPES[self.type][3]
|
|
|
|
self._icon = SENSOR_TYPES[self.type][2]
|
|
|
|
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
|
2018-12-01 17:00:49 +00:00
|
|
|
self._module_type = self.netatmo_data. \
|
|
|
|
station_data.moduleByName(module=module_name)['type']
|
|
|
|
module_id = self.netatmo_data. \
|
2019-05-27 16:07:59 +00:00
|
|
|
station_data.moduleByName(station=self.station_name,
|
|
|
|
module=module_name)['_id']
|
2018-12-01 17:00:49 +00:00
|
|
|
self._unique_id = '{}-{}'.format(module_id, self.type)
|
2016-01-06 02:39:16 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
2016-03-08 15:46:34 +00:00
|
|
|
"""Return the name of the sensor."""
|
2016-01-13 07:46:45 +00:00
|
|
|
return self._name
|
2016-01-06 02:39:16 +00:00
|
|
|
|
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."""
|
2018-05-30 14:53:35 +00:00
|
|
|
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
|
|
|
|
2016-01-06 02:39:16 +00:00
|
|
|
@property
|
|
|
|
def state(self):
|
2016-03-08 15:46:34 +00:00
|
|
|
"""Return the state of the device."""
|
2016-01-06 02:39:16 +00:00
|
|
|
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."""
|
2016-01-06 02:39:16 +00:00
|
|
|
return self._unit_of_measurement
|
|
|
|
|
2018-12-01 17:00:49 +00:00
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
"""Return the unique ID for this sensor."""
|
|
|
|
return self._unique_id
|
|
|
|
|
2016-01-06 02:39:16 +00:00
|
|
|
def update(self):
|
2019-05-08 06:26:52 +00:00
|
|
|
"""Get the latest data from Netatmo API and updates the states."""
|
2016-01-06 02:39:16 +00:00
|
|
|
self.netatmo_data.update()
|
2019-02-23 23:41:38 +00:00
|
|
|
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
|
|
|
|
|
2017-01-20 08:07:03 +00:00
|
|
|
data = self.netatmo_data.data.get(self.module_name)
|
|
|
|
|
|
|
|
if data is None:
|
|
|
|
_LOGGER.warning("No data found for %s", self.module_name)
|
2019-01-24 07:20:20 +00:00
|
|
|
self._state = None
|
2017-01-20 08:07:03 +00:00
|
|
|
return
|
2016-01-06 02:39:16 +00:00
|
|
|
|
2019-02-12 11:12:44 +00:00
|
|
|
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"
|
2019-02-17 10:45:58 +00:00
|
|
|
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"
|
2019-02-12 11:12:44 +00:00
|
|
|
except KeyError:
|
|
|
|
_LOGGER.error("No %s data found for %s", self.type,
|
|
|
|
self.module_name)
|
|
|
|
self._state = None
|
|
|
|
return
|
2016-01-06 02:39:16 +00:00
|
|
|
|
|
|
|
|
2019-05-08 06:26:52 +00:00
|
|
|
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."""
|
2016-01-06 02:39:16 +00:00
|
|
|
|
2019-02-17 10:45:58 +00:00
|
|
|
def __init__(self, auth, data_class, station):
|
2016-03-08 15:46:34 +00:00
|
|
|
"""Initialize the data object."""
|
2016-01-06 02:39:16 +00:00
|
|
|
self.auth = auth
|
2019-02-17 10:45:58 +00:00
|
|
|
self.data_class = data_class
|
2019-05-08 06:26:52 +00:00
|
|
|
self.data = {}
|
2016-10-08 18:26:01 +00:00
|
|
|
self.station_data = None
|
2016-05-29 21:25:11 +00:00
|
|
|
self.station = station
|
2018-07-10 10:30:48 +00:00
|
|
|
self._next_update = time()
|
|
|
|
self._update_in_progress = threading.Lock()
|
2016-01-06 02:39:16 +00:00
|
|
|
|
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."""
|
2016-01-11 04:21:38 +00:00
|
|
|
self.update()
|
|
|
|
return self.data.keys()
|
|
|
|
|
2016-01-06 02:39:16 +00:00
|
|
|
def update(self):
|
2018-07-10 10:30:48 +00:00
|
|
|
"""Call the Netatmo API to update the data.
|
2016-05-29 21:25:11 +00:00
|
|
|
|
2018-07-10 10:30:48 +00:00
|
|
|
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:
|
2019-06-14 19:26:02 +00:00
|
|
|
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
|
2018-07-10 10:30:48 +00:00
|
|
|
|
|
|
|
if self.station is not None:
|
2019-05-16 16:07:37 +00:00
|
|
|
data = self.station_data.lastData(
|
2018-07-10 10:30:48 +00:00
|
|
|
station=self.station, exclude=3600)
|
|
|
|
else:
|
2019-05-16 16:07:37 +00:00
|
|
|
data = self.station_data.lastData(exclude=3600)
|
|
|
|
if not data:
|
|
|
|
self._next_update = time() + NETATMO_UPDATE_INTERVAL
|
|
|
|
return
|
|
|
|
self.data = data
|
2018-07-10 10:30:48 +00:00
|
|
|
|
|
|
|
newinterval = 0
|
2019-02-12 11:12:44 +00:00
|
|
|
try:
|
|
|
|
for module in self.data:
|
|
|
|
if 'When' in self.data[module]:
|
|
|
|
newinterval = self.data[module]['When']
|
|
|
|
break
|
|
|
|
except TypeError:
|
2019-02-17 10:45:58 +00:00
|
|
|
_LOGGER.debug("No %s modules found", self.data_class.__name__)
|
2019-02-12 11:12:44 +00:00
|
|
|
|
2018-07-10 10:30:48 +00:00
|
|
|
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:
|
2019-05-08 06:26:52 +00:00
|
|
|
# Never hammer the Netatmo API more than
|
2018-07-10 10:30:48 +00:00
|
|
|
# twice per update interval
|
|
|
|
newinterval = NETATMO_UPDATE_INTERVAL / 2
|
2018-07-24 16:35:57 +00:00
|
|
|
_LOGGER.info(
|
2019-05-08 06:26:52 +00:00
|
|
|
"Netatmo refresh interval reset to %d seconds",
|
2018-07-10 10:30:48 +00:00
|
|
|
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()
|