2015-12-27 11:32:08 +00:00
|
|
|
"""
|
2016-03-07 17:49:31 +00:00
|
|
|
Support for Telldus Live.
|
2015-12-27 11:32:08 +00:00
|
|
|
|
2016-02-02 23:35:53 +00:00
|
|
|
For more details about this component, please refer to the documentation at
|
|
|
|
https://home-assistant.io/components/tellduslive/
|
2015-12-27 11:32:08 +00:00
|
|
|
"""
|
2016-12-12 05:39:37 +00:00
|
|
|
from datetime import datetime, timedelta
|
2015-12-27 11:32:08 +00:00
|
|
|
import logging
|
2016-09-11 07:22:49 +00:00
|
|
|
|
2018-02-11 17:20:28 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2017-06-02 07:02:26 +00:00
|
|
|
from homeassistant.const import (
|
2017-11-28 07:13:30 +00:00
|
|
|
ATTR_BATTERY_LEVEL, DEVICE_DEFAULT_NAME,
|
|
|
|
CONF_TOKEN, CONF_HOST,
|
|
|
|
EVENT_HOMEASSISTANT_START)
|
2016-09-11 07:22:49 +00:00
|
|
|
from homeassistant.helpers import discovery
|
2017-11-28 07:13:30 +00:00
|
|
|
from homeassistant.components.discovery import SERVICE_TELLDUSLIVE
|
2016-09-11 07:22:49 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2016-12-12 05:39:37 +00:00
|
|
|
from homeassistant.helpers.entity import Entity
|
|
|
|
from homeassistant.helpers.event import track_point_in_utc_time
|
|
|
|
from homeassistant.util.dt import utcnow
|
2017-11-28 07:13:30 +00:00
|
|
|
from homeassistant.util.json import load_json, save_json
|
2015-12-27 11:32:08 +00:00
|
|
|
|
2017-11-28 07:13:30 +00:00
|
|
|
APPLICATION_NAME = 'Home Assistant'
|
|
|
|
|
2016-09-11 07:22:49 +00:00
|
|
|
DOMAIN = 'tellduslive'
|
2016-02-03 21:31:28 +00:00
|
|
|
|
2017-12-04 16:26:07 +00:00
|
|
|
REQUIREMENTS = ['tellduslive==0.10.4']
|
2016-02-03 21:31:28 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2017-11-28 07:13:30 +00:00
|
|
|
TELLLDUS_CONFIG_FILE = 'tellduslive.conf'
|
|
|
|
KEY_CONFIG = 'tellduslive_config'
|
|
|
|
|
2016-09-11 07:22:49 +00:00
|
|
|
CONF_TOKEN_SECRET = 'token_secret'
|
2016-12-12 05:39:37 +00:00
|
|
|
CONF_UPDATE_INTERVAL = 'update_interval'
|
2015-12-27 11:32:08 +00:00
|
|
|
|
2017-11-28 07:13:30 +00:00
|
|
|
PUBLIC_KEY = 'THUPUNECH5YEQA3RE6UYUPRUZ2DUGUGA'
|
|
|
|
NOT_SO_PRIVATE_KEY = 'PHES7U2RADREWAFEBUSTUBAWRASWUTUS'
|
|
|
|
|
2016-12-12 05:39:37 +00:00
|
|
|
MIN_UPDATE_INTERVAL = timedelta(seconds=5)
|
|
|
|
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1)
|
2015-12-27 11:32:08 +00:00
|
|
|
|
2016-09-11 07:22:49 +00:00
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
|
|
|
DOMAIN: vol.Schema({
|
2017-11-28 07:13:30 +00:00
|
|
|
vol.Optional(CONF_HOST): cv.string,
|
2016-12-12 05:39:37 +00:00
|
|
|
vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): (
|
|
|
|
vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)))
|
2016-09-11 07:22:49 +00:00
|
|
|
}),
|
|
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
|
|
|
2016-12-12 05:39:37 +00:00
|
|
|
ATTR_LAST_UPDATED = 'time_last_updated'
|
|
|
|
|
2017-11-28 07:13:30 +00:00
|
|
|
CONFIG_INSTRUCTIONS = """
|
|
|
|
To link your TelldusLive account:
|
|
|
|
|
|
|
|
1. Click the link below
|
|
|
|
|
|
|
|
2. Login to Telldus Live
|
|
|
|
|
|
|
|
3. Authorize {app_name}.
|
|
|
|
|
|
|
|
4. Click the Confirm button.
|
|
|
|
|
|
|
|
[Link TelldusLive account]({auth_url})
|
|
|
|
"""
|
2016-12-12 05:39:37 +00:00
|
|
|
|
2016-09-11 07:22:49 +00:00
|
|
|
|
2017-11-28 07:13:30 +00:00
|
|
|
def setup(hass, config, session=None):
|
|
|
|
"""Set up the Telldus Live component."""
|
|
|
|
from tellduslive import Session, supports_local_api
|
|
|
|
config_filename = hass.config.path(TELLLDUS_CONFIG_FILE)
|
|
|
|
conf = load_json(config_filename)
|
|
|
|
|
|
|
|
def request_configuration(host=None):
|
|
|
|
"""Request TelldusLive authorization."""
|
|
|
|
configurator = hass.components.configurator
|
|
|
|
hass.data.setdefault(KEY_CONFIG, {})
|
|
|
|
data_key = host or DOMAIN
|
|
|
|
|
|
|
|
# Configuration already in progress
|
|
|
|
if hass.data[KEY_CONFIG].get(data_key):
|
|
|
|
return
|
|
|
|
|
|
|
|
_LOGGER.info('Configuring TelldusLive %s',
|
|
|
|
'local client: {}'.format(host) if host else
|
|
|
|
'cloud service')
|
|
|
|
|
|
|
|
session = Session(public_key=PUBLIC_KEY,
|
|
|
|
private_key=NOT_SO_PRIVATE_KEY,
|
|
|
|
host=host,
|
|
|
|
application=APPLICATION_NAME)
|
|
|
|
|
|
|
|
auth_url = session.authorize_url
|
|
|
|
if not auth_url:
|
|
|
|
_LOGGER.warning('Failed to retrieve authorization URL')
|
|
|
|
return
|
|
|
|
|
|
|
|
_LOGGER.debug('Got authorization URL %s', auth_url)
|
|
|
|
|
|
|
|
def configuration_callback(callback_data):
|
|
|
|
"""Handle the submitted configuration."""
|
|
|
|
session.authorize()
|
|
|
|
res = setup(hass, config, session)
|
|
|
|
if not res:
|
|
|
|
configurator.notify_errors(
|
|
|
|
hass.data[KEY_CONFIG].get(data_key),
|
|
|
|
'Unable to connect.')
|
|
|
|
return
|
|
|
|
|
|
|
|
conf.update(
|
|
|
|
{host: {CONF_HOST: host,
|
|
|
|
CONF_TOKEN: session.access_token}} if host else
|
|
|
|
{DOMAIN: {CONF_TOKEN: session.access_token,
|
|
|
|
CONF_TOKEN_SECRET: session.access_token_secret}})
|
|
|
|
save_json(config_filename, conf)
|
|
|
|
# Close all open configurators: for now, we only support one
|
|
|
|
# tellstick device, and configuration via either cloud service
|
|
|
|
# or via local API, not both at the same time
|
|
|
|
for instance in hass.data[KEY_CONFIG].values():
|
|
|
|
configurator.request_done(instance)
|
|
|
|
|
|
|
|
hass.data[KEY_CONFIG][data_key] = \
|
|
|
|
configurator.request_config(
|
|
|
|
'TelldusLive ({})'.format(
|
|
|
|
'LocalAPI' if host
|
|
|
|
else 'Cloud service'),
|
|
|
|
configuration_callback,
|
|
|
|
description=CONFIG_INSTRUCTIONS.format(
|
|
|
|
app_name=APPLICATION_NAME,
|
|
|
|
auth_url=auth_url),
|
|
|
|
submit_caption='Confirm',
|
|
|
|
entity_picture='/static/images/logo_tellduslive.png',
|
|
|
|
)
|
|
|
|
|
|
|
|
def tellstick_discovered(service, info):
|
|
|
|
"""Run when a Tellstick is discovered."""
|
|
|
|
_LOGGER.info('Discovered tellstick device')
|
|
|
|
|
|
|
|
if DOMAIN in hass.data:
|
|
|
|
_LOGGER.debug('Tellstick already configured')
|
|
|
|
return
|
|
|
|
|
|
|
|
host, device = info[:2]
|
|
|
|
|
|
|
|
if not supports_local_api(device):
|
|
|
|
_LOGGER.debug('Tellstick does not support local API')
|
|
|
|
# Configure the cloud service
|
|
|
|
hass.async_add_job(request_configuration)
|
|
|
|
return
|
|
|
|
|
|
|
|
_LOGGER.debug('Tellstick does support local API')
|
|
|
|
|
|
|
|
# Ignore any known devices
|
|
|
|
if conf and host in conf:
|
|
|
|
_LOGGER.debug('Discovered already known device: %s', host)
|
|
|
|
return
|
|
|
|
|
|
|
|
# Offer configuration of both live and local API
|
|
|
|
request_configuration()
|
|
|
|
request_configuration(host)
|
|
|
|
|
|
|
|
discovery.listen(hass, SERVICE_TELLDUSLIVE, tellstick_discovered)
|
|
|
|
|
|
|
|
if session:
|
|
|
|
_LOGGER.debug('Continuing setup configured by configurator')
|
|
|
|
elif conf and CONF_HOST in next(iter(conf.values())):
|
|
|
|
# For now, only one local device is supported
|
|
|
|
_LOGGER.debug('Using Local API pre-configured by configurator')
|
|
|
|
session = Session(**next(iter(conf.values())))
|
|
|
|
elif DOMAIN in conf:
|
|
|
|
_LOGGER.debug('Using TelldusLive cloud service '
|
|
|
|
'pre-configured by configurator')
|
|
|
|
session = Session(PUBLIC_KEY, NOT_SO_PRIVATE_KEY,
|
|
|
|
application=APPLICATION_NAME, **conf[DOMAIN])
|
|
|
|
elif config.get(DOMAIN):
|
|
|
|
_LOGGER.info('Found entry in configuration.yaml. '
|
|
|
|
'Requesting TelldusLive cloud service configuration')
|
|
|
|
request_configuration()
|
|
|
|
|
|
|
|
if CONF_HOST in config.get(DOMAIN, {}):
|
|
|
|
_LOGGER.info('Found TelldusLive host entry in configuration.yaml. '
|
|
|
|
'Requesting Telldus Local API configuration')
|
|
|
|
request_configuration(config.get(DOMAIN).get(CONF_HOST))
|
|
|
|
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
_LOGGER.info('Tellstick discovered, awaiting discovery callback')
|
|
|
|
return True
|
|
|
|
|
|
|
|
if not session.is_authorized:
|
2016-09-11 07:22:49 +00:00
|
|
|
_LOGGER.error(
|
2017-11-28 07:13:30 +00:00
|
|
|
'Authentication Error')
|
2016-09-11 07:22:49 +00:00
|
|
|
return False
|
|
|
|
|
2017-11-28 07:13:30 +00:00
|
|
|
client = TelldusLiveClient(hass, config, session)
|
|
|
|
|
2016-12-12 05:39:37 +00:00
|
|
|
hass.data[DOMAIN] = client
|
2017-06-02 07:02:26 +00:00
|
|
|
|
2017-11-28 07:13:30 +00:00
|
|
|
if session:
|
|
|
|
client.update()
|
|
|
|
else:
|
|
|
|
hass.bus.listen(EVENT_HOMEASSISTANT_START, client.update)
|
2016-09-11 07:22:49 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
2016-02-03 21:31:28 +00:00
|
|
|
|
2018-07-20 08:45:20 +00:00
|
|
|
class TelldusLiveClient:
|
2016-03-08 16:55:57 +00:00
|
|
|
"""Get the latest data and update the states."""
|
|
|
|
|
2017-11-28 07:13:30 +00:00
|
|
|
def __init__(self, hass, config, session):
|
2016-03-08 16:55:57 +00:00
|
|
|
"""Initialize the Tellus data object."""
|
2016-12-12 05:39:37 +00:00
|
|
|
self.entities = []
|
2016-02-03 21:31:28 +00:00
|
|
|
|
|
|
|
self._hass = hass
|
|
|
|
self._config = config
|
2015-12-27 11:32:08 +00:00
|
|
|
|
2017-11-28 07:13:30 +00:00
|
|
|
self._interval = config.get(DOMAIN, {}).get(
|
|
|
|
CONF_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL)
|
2016-12-12 05:39:37 +00:00
|
|
|
_LOGGER.debug('Update interval %s', self._interval)
|
2017-11-28 07:13:30 +00:00
|
|
|
self._client = session
|
2016-02-03 21:31:28 +00:00
|
|
|
|
2017-06-02 07:02:26 +00:00
|
|
|
def update(self, *args):
|
2016-12-12 05:39:37 +00:00
|
|
|
"""Periodically poll the servers for current state."""
|
2017-11-28 07:13:30 +00:00
|
|
|
_LOGGER.debug('Updating')
|
2016-02-16 16:36:09 +00:00
|
|
|
try:
|
2016-12-12 05:39:37 +00:00
|
|
|
self._sync()
|
|
|
|
finally:
|
2017-03-01 15:37:48 +00:00
|
|
|
track_point_in_utc_time(
|
|
|
|
self._hass, self.update, utcnow() + self._interval)
|
2016-12-12 05:39:37 +00:00
|
|
|
|
|
|
|
def _sync(self):
|
|
|
|
"""Update local list of devices."""
|
2017-02-23 11:37:25 +00:00
|
|
|
if not self._client.update():
|
2017-11-28 07:13:30 +00:00
|
|
|
_LOGGER.warning('Failed request')
|
2016-12-12 05:39:37 +00:00
|
|
|
|
|
|
|
def identify_device(device):
|
|
|
|
"""Find out what type of HA component to create."""
|
|
|
|
from tellduslive import (DIM, UP, TURNON)
|
|
|
|
if device.methods & DIM:
|
|
|
|
return 'light'
|
2018-07-23 08:16:05 +00:00
|
|
|
if device.methods & UP:
|
2016-12-12 05:39:37 +00:00
|
|
|
return 'cover'
|
2018-07-23 08:16:05 +00:00
|
|
|
if device.methods & TURNON:
|
2016-12-12 05:39:37 +00:00
|
|
|
return 'switch'
|
2018-07-23 08:16:05 +00:00
|
|
|
if device.methods == 0:
|
2017-10-26 13:54:50 +00:00
|
|
|
return 'binary_sensor'
|
2017-07-06 03:02:16 +00:00
|
|
|
_LOGGER.warning(
|
|
|
|
"Unidentified device type (methods: %d)", device.methods)
|
|
|
|
return 'switch'
|
2016-12-12 05:39:37 +00:00
|
|
|
|
|
|
|
def discover(device_id, component):
|
|
|
|
"""Discover the component."""
|
2017-03-01 15:37:48 +00:00
|
|
|
discovery.load_platform(
|
|
|
|
self._hass, component, DOMAIN, [device_id], self._config)
|
2016-12-12 05:39:37 +00:00
|
|
|
|
2017-06-16 19:44:14 +00:00
|
|
|
known_ids = {entity.device_id for entity in self.entities}
|
2016-12-12 05:39:37 +00:00
|
|
|
for device in self._client.devices:
|
|
|
|
if device.device_id in known_ids:
|
|
|
|
continue
|
|
|
|
if device.is_sensor:
|
2017-01-31 19:08:11 +00:00
|
|
|
for item in device.items:
|
|
|
|
discover((device.device_id, item.name, item.scale),
|
2016-12-12 05:39:37 +00:00
|
|
|
'sensor')
|
|
|
|
else:
|
|
|
|
discover(device.device_id,
|
|
|
|
identify_device(device))
|
|
|
|
|
|
|
|
for entity in self.entities:
|
|
|
|
entity.changed()
|
|
|
|
|
|
|
|
def device(self, device_id):
|
|
|
|
"""Return device representation."""
|
2017-01-31 19:08:11 +00:00
|
|
|
return self._client.device(device_id)
|
2016-12-12 05:39:37 +00:00
|
|
|
|
|
|
|
def is_available(self, device_id):
|
|
|
|
"""Return device availability."""
|
|
|
|
return device_id in self._client.device_ids
|
|
|
|
|
|
|
|
|
|
|
|
class TelldusLiveEntity(Entity):
|
|
|
|
"""Base class for all Telldus Live entities."""
|
|
|
|
|
|
|
|
def __init__(self, hass, device_id):
|
|
|
|
"""Initialize the entity."""
|
|
|
|
self._id = device_id
|
|
|
|
self._client = hass.data[DOMAIN]
|
|
|
|
self._client.entities.append(self)
|
2018-08-26 19:25:39 +00:00
|
|
|
self._name = self.device.name
|
2017-11-28 07:13:30 +00:00
|
|
|
_LOGGER.debug('Created device %s', self)
|
2016-12-12 05:39:37 +00:00
|
|
|
|
|
|
|
def changed(self):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Return the property of the device might have changed."""
|
2018-08-26 19:25:39 +00:00
|
|
|
if self.device.name:
|
|
|
|
self._name = self.device.name
|
2016-12-12 05:39:37 +00:00
|
|
|
self.schedule_update_ha_state()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_id(self):
|
|
|
|
"""Return the id of the device."""
|
|
|
|
return self._id
|
|
|
|
|
2018-08-26 19:25:39 +00:00
|
|
|
@property
|
|
|
|
def device(self):
|
|
|
|
"""Return the representation of the device."""
|
|
|
|
return self._client.device(self.device_id)
|
|
|
|
|
2016-12-12 05:39:37 +00:00
|
|
|
@property
|
|
|
|
def _state(self):
|
|
|
|
"""Return the state of the device."""
|
2018-08-26 19:25:39 +00:00
|
|
|
return self.device.state
|
2016-12-12 05:39:37 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def should_poll(self):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Return the polling state."""
|
2016-12-12 05:39:37 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
|
|
|
def assumed_state(self):
|
|
|
|
"""Return true if unable to access real state of entity."""
|
|
|
|
return True
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return name of device."""
|
2017-02-23 11:37:25 +00:00
|
|
|
return self._name or DEVICE_DEFAULT_NAME
|
2016-12-12 05:39:37 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def available(self):
|
|
|
|
"""Return true if device is not offline."""
|
|
|
|
return self._client.is_available(self.device_id)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Return the state attributes."""
|
|
|
|
attrs = {}
|
|
|
|
if self._battery_level:
|
|
|
|
attrs[ATTR_BATTERY_LEVEL] = self._battery_level
|
|
|
|
if self._last_updated:
|
|
|
|
attrs[ATTR_LAST_UPDATED] = self._last_updated
|
|
|
|
return attrs
|
|
|
|
|
|
|
|
@property
|
|
|
|
def _battery_level(self):
|
|
|
|
"""Return the battery level of a device."""
|
2017-11-28 14:32:36 +00:00
|
|
|
from tellduslive import (BATTERY_LOW,
|
|
|
|
BATTERY_UNKNOWN,
|
|
|
|
BATTERY_OK)
|
2018-08-26 19:25:39 +00:00
|
|
|
if self.device.battery == BATTERY_LOW:
|
2017-11-28 14:32:36 +00:00
|
|
|
return 1
|
2018-08-26 19:25:39 +00:00
|
|
|
if self.device.battery == BATTERY_UNKNOWN:
|
2017-11-28 14:32:36 +00:00
|
|
|
return None
|
2018-08-26 19:25:39 +00:00
|
|
|
if self.device.battery == BATTERY_OK:
|
2017-11-28 14:32:36 +00:00
|
|
|
return 100
|
2018-08-26 19:25:39 +00:00
|
|
|
return self.device.battery # Percentage
|
2016-12-12 05:39:37 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def _last_updated(self):
|
|
|
|
"""Return the last update of a device."""
|
2018-08-26 19:25:39 +00:00
|
|
|
return str(datetime.fromtimestamp(self.device.lastUpdated)) \
|
|
|
|
if self.device.lastUpdated else None
|