commit
bd536be66d
19
.coveragerc
19
.coveragerc
|
@ -5,6 +5,8 @@ omit =
|
|||
homeassistant/__main__.py
|
||||
|
||||
# omit pieces of code that rely on external devices being present
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
|
@ -15,6 +17,10 @@ omit =
|
|||
homeassistant/components/*/modbus.py
|
||||
|
||||
homeassistant/components/*/tellstick.py
|
||||
|
||||
homeassistant/components/tellduslive.py
|
||||
homeassistant/components/*/tellduslive.py
|
||||
|
||||
homeassistant/components/*/vera.py
|
||||
|
||||
homeassistant/components/ecobee.py
|
||||
|
@ -32,6 +38,12 @@ omit =
|
|||
homeassistant/components/rfxtrx.py
|
||||
homeassistant/components/*/rfxtrx.py
|
||||
|
||||
homeassistant/components/mysensors.py
|
||||
homeassistant/components/*/mysensors.py
|
||||
|
||||
homeassistant/components/rpi_gpio.py
|
||||
homeassistant/components/*/rpi_gpio.py
|
||||
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
|
@ -41,7 +53,6 @@ omit =
|
|||
homeassistant/components/device_tracker/asuswrt.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/geofancy.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
|
@ -70,6 +81,7 @@ omit =
|
|||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/nma.py
|
||||
homeassistant/components/notify/pushbullet.py
|
||||
|
@ -89,10 +101,9 @@ omit =
|
|||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/mysensors.py
|
||||
homeassistant/components/sensor/netatmo.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/rpi_gpio.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
|
@ -108,13 +119,13 @@ omit =
|
|||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_gpio.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wemo.py
|
||||
homeassistant/components/thermostat/heatmiser.py
|
||||
homeassistant/components/thermostat/homematic.py
|
||||
homeassistant/components/thermostat/honeywell.py
|
||||
homeassistant/components/thermostat/nest.py
|
||||
homeassistant/components/thermostat/proliphix.py
|
||||
homeassistant/components/thermostat/radiotherm.py
|
||||
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ Icon
|
|||
dist
|
||||
build
|
||||
eggs
|
||||
.eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
|
|
|
@ -8,9 +8,7 @@ python:
|
|||
- 3.4
|
||||
- 3.5
|
||||
install:
|
||||
# Validate requirements_all.txt on Python 3.5
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then python3 setup.py develop; script/gen_requirements_all.py validate; fi
|
||||
- script/bootstrap_server
|
||||
- "true"
|
||||
script:
|
||||
- script/cibuild
|
||||
matrix:
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Paulus Schoutsen
|
||||
Copyright (c) 2016 Paulus Schoutsen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
|
|
@ -275,7 +275,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
|
|||
datefmt='%y-%m-%d %H:%M:%S'))
|
||||
logger = logging.getLogger('')
|
||||
logger.addHandler(err_handler)
|
||||
logger.setLevel(logging.NOTSET) # this sets the minimum log level
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
else:
|
||||
_LOGGER.error(
|
||||
|
|
|
@ -87,13 +87,21 @@ def setup(hass, config):
|
|||
lambda item: util.split_entity_id(item)[0])
|
||||
|
||||
for domain, ent_ids in by_domain:
|
||||
# We want to block for all calls and only return when all calls
|
||||
# have been processed. If a service does not exist it causes a 10
|
||||
# second delay while we're blocking waiting for a response.
|
||||
# But services can be registered on other HA instances that are
|
||||
# listening to the bus too. So as a in between solution, we'll
|
||||
# block only if the service is defined in the current HA instance.
|
||||
blocking = hass.services.has_service(domain, service.service)
|
||||
|
||||
# Create a new dict for this call
|
||||
data = dict(service.data)
|
||||
|
||||
# ent_ids is a generator, convert it to a list.
|
||||
data[ATTR_ENTITY_ID] = list(ent_ids)
|
||||
|
||||
hass.services.call(domain, service.service, data, True)
|
||||
hass.services.call(domain, service.service, data, blocking)
|
||||
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
"""
|
||||
homeassistant.components.alarm_control_panel.alarmdotcom
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Interfaces with Verisure alarm control panel.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUIREMENTS = ['https://github.com/Xorso/pyalarmdotcom'
|
||||
'/archive/0.0.7.zip'
|
||||
'#pyalarmdotcom==0.0.7']
|
||||
DEFAULT_NAME = 'Alarm.com'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Setup an Alarm.com control panel. """
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
if username is None or password is None:
|
||||
_LOGGER.error('Must specify username and password!')
|
||||
return False
|
||||
|
||||
add_devices([AlarmDotCom(hass,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('code'),
|
||||
username,
|
||||
password)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
# pylint: disable=abstract-method
|
||||
class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
""" Represents a Alarm.com status. """
|
||||
|
||||
def __init__(self, hass, name, code, username, password):
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
self._alarm = Alarmdotcom(username, password, timeout=10)
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._username = username
|
||||
self._password = password
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
""" One or more characters if code is defined. """
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
if self._alarm.state == 'Disarmed':
|
||||
return STATE_ALARM_DISARMED
|
||||
elif self._alarm.state == 'Armed Stay':
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._alarm.state == 'Armed Away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.disarm()
|
||||
self.update_ha_state()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.arm_stay()
|
||||
self.update_ha_state()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
# Open another session to alarm.com to fire off the command
|
||||
_alarm = Alarmdotcom(self._username, self._password, timeout=10)
|
||||
_alarm.arm_away()
|
||||
self.update_ha_state()
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
""" Validate given code. """
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning('Wrong code entered for %s', state)
|
||||
return check
|
|
@ -68,7 +68,8 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
if self._state in (STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) and \
|
||||
if self._state in (STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY) and \
|
||||
self._pending_time and self._state_ts + self._pending_time > \
|
||||
dt_util.utcnow():
|
||||
return STATE_ALARM_PENDING
|
||||
|
|
|
@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
alarms.extend([
|
||||
VerisureAlarm(value)
|
||||
for value in verisure.get_alarm_status().values()
|
||||
for value in verisure.ALARM_STATUS.values()
|
||||
if verisure.SHOW_ALARM
|
||||
])
|
||||
|
||||
|
@ -42,7 +42,6 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||
|
||||
def __init__(self, alarm_status):
|
||||
self._id = alarm_status.id
|
||||
self._device = verisure.MY_PAGES.DEVICE_ALARM
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
|
@ -58,40 +57,40 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||
@property
|
||||
def code_format(self):
|
||||
""" Four digit code required. """
|
||||
return '^\\d{4}$'
|
||||
return '^\\d{%s}$' % verisure.CODE_DIGITS
|
||||
|
||||
def update(self):
|
||||
""" Update alarm status """
|
||||
verisure.update()
|
||||
verisure.update_alarm()
|
||||
|
||||
if verisure.STATUS[self._device][self._id].status == 'unarmed':
|
||||
if verisure.ALARM_STATUS[self._id].status == 'unarmed':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif verisure.STATUS[self._device][self._id].status == 'armedhome':
|
||||
elif verisure.ALARM_STATUS[self._id].status == 'armedhome':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif verisure.STATUS[self._device][self._id].status == 'armedaway':
|
||||
elif verisure.ALARM_STATUS[self._id].status == 'armed':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
elif verisure.STATUS[self._device][self._id].status != 'pending':
|
||||
elif verisure.ALARM_STATUS[self._id].status != 'pending':
|
||||
_LOGGER.error(
|
||||
'Unknown alarm state %s',
|
||||
verisure.STATUS[self._device][self._id].status)
|
||||
verisure.ALARM_STATUS[self._id].status)
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
""" Send disarm command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_DISARMED)
|
||||
_LOGGER.warning('disarming')
|
||||
verisure.MY_PAGES.alarm.set(code, 'DISARMED')
|
||||
_LOGGER.info('verisure alarm disarming')
|
||||
verisure.MY_PAGES.alarm.wait_while_pending()
|
||||
verisure.update_alarm()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
""" Send arm home command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_ARMED_HOME)
|
||||
_LOGGER.warning('arming home')
|
||||
verisure.MY_PAGES.alarm.set(code, 'ARMED_HOME')
|
||||
_LOGGER.info('verisure alarm arming home')
|
||||
verisure.MY_PAGES.alarm.wait_while_pending()
|
||||
verisure.update_alarm()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
""" Send arm away command. """
|
||||
verisure.MY_PAGES.set_alarm_status(
|
||||
code,
|
||||
verisure.MY_PAGES.ALARM_ARMED_AWAY)
|
||||
_LOGGER.warning('arming away')
|
||||
verisure.MY_PAGES.alarm.set(code, 'ARMED_AWAY')
|
||||
_LOGGER.info('verisure alarm arming away')
|
||||
verisure.MY_PAGES.alarm.wait_while_pending()
|
||||
verisure.update_alarm()
|
||||
|
|
|
@ -11,6 +11,7 @@ import logging
|
|||
|
||||
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
|
||||
from homeassistant.util import template
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
|
@ -23,6 +24,7 @@ API_ENDPOINT = '/api/alexa'
|
|||
CONF_INTENTS = 'intents'
|
||||
CONF_CARD = 'card'
|
||||
CONF_SPEECH = 'speech'
|
||||
CONF_ACTION = 'action'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
@ -80,6 +82,7 @@ def _handle_alexa(handler, path_match, data):
|
|||
|
||||
speech = config.get(CONF_SPEECH)
|
||||
card = config.get(CONF_CARD)
|
||||
action = config.get(CONF_ACTION)
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
|
@ -89,6 +92,9 @@ def _handle_alexa(handler, path_match, data):
|
|||
response.add_card(CardType[card['type']], card['title'],
|
||||
card['content'])
|
||||
|
||||
if action is not None:
|
||||
call_from_config(handler.server.hass, action, True)
|
||||
|
||||
handler.write_json(response.as_dict())
|
||||
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@ https://home-assistant.io/components/automation/
|
|||
import logging
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.util import split_entity_id
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.components import logbook
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
|
||||
DOMAIN = 'automation'
|
||||
|
||||
|
@ -19,8 +19,6 @@ DEPENDENCIES = ['group']
|
|||
|
||||
CONF_ALIAS = 'alias'
|
||||
CONF_SERVICE = 'service'
|
||||
CONF_SERVICE_ENTITY_ID = 'entity_id'
|
||||
CONF_SERVICE_DATA = 'data'
|
||||
|
||||
CONF_CONDITION = 'condition'
|
||||
CONF_ACTION = 'action'
|
||||
|
@ -96,22 +94,7 @@ def _get_action(hass, config, name):
|
|||
_LOGGER.info('Executing %s', name)
|
||||
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
|
||||
domain, service = split_entity_id(config[CONF_SERVICE])
|
||||
service_data = config.get(CONF_SERVICE_DATA, {})
|
||||
|
||||
if not isinstance(service_data, dict):
|
||||
_LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA)
|
||||
service_data = {}
|
||||
|
||||
if CONF_SERVICE_ENTITY_ID in config:
|
||||
try:
|
||||
service_data[ATTR_ENTITY_ID] = \
|
||||
config[CONF_SERVICE_ENTITY_ID].split(",")
|
||||
except AttributeError:
|
||||
service_data[ATTR_ENTITY_ID] = \
|
||||
config[CONF_SERVICE_ENTITY_ID]
|
||||
|
||||
hass.services.call(domain, service, service_data)
|
||||
call_from_config(hass, config)
|
||||
|
||||
return action
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ Offers numeric state listening automation rules.
|
|||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#numeric-state-trigger
|
||||
"""
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
|
@ -20,6 +21,14 @@ CONF_ABOVE = "above"
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _renderer(hass, value_template, state):
|
||||
"""Render state value."""
|
||||
if value_template is None:
|
||||
return state.state
|
||||
|
||||
return template.render(hass, value_template, {'state': state})
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
@ -38,12 +47,7 @@ def trigger(hass, config, action):
|
|||
CONF_BELOW, CONF_ABOVE)
|
||||
return False
|
||||
|
||||
if value_template is not None:
|
||||
renderer = lambda value: template.render(hass,
|
||||
value_template,
|
||||
{'state': value})
|
||||
else:
|
||||
renderer = lambda value: value.state
|
||||
renderer = partial(_renderer, hass, value_template)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
|
@ -79,12 +83,7 @@ def if_action(hass, config):
|
|||
CONF_BELOW, CONF_ABOVE)
|
||||
return None
|
||||
|
||||
if value_template is not None:
|
||||
renderer = lambda value: template.render(hass,
|
||||
value_template,
|
||||
{'state': value})
|
||||
else:
|
||||
renderer = lambda value: value.state
|
||||
renderer = partial(_renderer, hass, value_template)
|
||||
|
||||
def if_numeric_state():
|
||||
""" Test numeric state condition. """
|
||||
|
|
|
@ -17,6 +17,10 @@ DEPENDENCIES = ['sun']
|
|||
|
||||
CONF_OFFSET = 'offset'
|
||||
CONF_EVENT = 'event'
|
||||
CONF_BEFORE = "before"
|
||||
CONF_BEFORE_OFFSET = "before_offset"
|
||||
CONF_AFTER = "after"
|
||||
CONF_AFTER_OFFSET = "after_offset"
|
||||
|
||||
EVENT_SUNSET = 'sunset'
|
||||
EVENT_SUNRISE = 'sunrise'
|
||||
|
@ -37,26 +41,9 @@ def trigger(hass, config, action):
|
|||
_LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event)
|
||||
return False
|
||||
|
||||
if CONF_OFFSET in config:
|
||||
raw_offset = config.get(CONF_OFFSET)
|
||||
|
||||
negative_offset = False
|
||||
if raw_offset.startswith('-'):
|
||||
negative_offset = True
|
||||
raw_offset = raw_offset[1:]
|
||||
|
||||
try:
|
||||
(hour, minute, second) = [int(x) for x in raw_offset.split(':')]
|
||||
except ValueError:
|
||||
_LOGGER.error('Could not parse offset %s', raw_offset)
|
||||
return False
|
||||
|
||||
offset = timedelta(hours=hour, minutes=minute, seconds=second)
|
||||
|
||||
if negative_offset:
|
||||
offset *= -1
|
||||
else:
|
||||
offset = timedelta(0)
|
||||
offset = _parse_offset(config.get(CONF_OFFSET))
|
||||
if offset is False:
|
||||
return False
|
||||
|
||||
# Do something to call action
|
||||
if event == EVENT_SUNRISE:
|
||||
|
@ -67,6 +54,77 @@ def trigger(hass, config, action):
|
|||
return True
|
||||
|
||||
|
||||
def if_action(hass, config):
|
||||
""" Wraps action method with sun based condition. """
|
||||
before = config.get(CONF_BEFORE)
|
||||
after = config.get(CONF_AFTER)
|
||||
|
||||
# Make sure required configuration keys are present
|
||||
if before is None and after is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing if-condition configuration key %s or %s",
|
||||
CONF_BEFORE, CONF_AFTER)
|
||||
return None
|
||||
|
||||
# Make sure configuration keys have the right value
|
||||
if before not in (None, EVENT_SUNRISE, EVENT_SUNSET) or \
|
||||
after not in (None, EVENT_SUNRISE, EVENT_SUNSET):
|
||||
logging.getLogger(__name__).error(
|
||||
"%s and %s can only be set to %s or %s",
|
||||
CONF_BEFORE, CONF_AFTER, EVENT_SUNRISE, EVENT_SUNSET)
|
||||
return None
|
||||
|
||||
before_offset = _parse_offset(config.get(CONF_BEFORE_OFFSET))
|
||||
after_offset = _parse_offset(config.get(CONF_AFTER_OFFSET))
|
||||
if before_offset is False or after_offset is False:
|
||||
return None
|
||||
|
||||
if before is None:
|
||||
def before_func():
|
||||
"""Return no point in time."""
|
||||
return None
|
||||
elif before == EVENT_SUNRISE:
|
||||
def before_func():
|
||||
"""Return time before sunrise."""
|
||||
return sun.next_rising(hass) + before_offset
|
||||
else:
|
||||
def before_func():
|
||||
"""Return time before sunset."""
|
||||
return sun.next_setting(hass) + before_offset
|
||||
|
||||
if after is None:
|
||||
def after_func():
|
||||
"""Return no point in time."""
|
||||
return None
|
||||
elif after == EVENT_SUNRISE:
|
||||
def after_func():
|
||||
"""Return time after sunrise."""
|
||||
return sun.next_rising(hass) + after_offset
|
||||
else:
|
||||
def after_func():
|
||||
"""Return time after sunset."""
|
||||
return sun.next_setting(hass) + after_offset
|
||||
|
||||
def time_if():
|
||||
""" Validate time based if-condition """
|
||||
|
||||
now = dt_util.now()
|
||||
before = before_func()
|
||||
after = after_func()
|
||||
|
||||
if before is not None and now > now.replace(hour=before.hour,
|
||||
minute=before.minute):
|
||||
return False
|
||||
|
||||
if after is not None and now < now.replace(hour=after.hour,
|
||||
minute=after.minute):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return time_if
|
||||
|
||||
|
||||
def trigger_sunrise(hass, action, offset):
|
||||
""" Trigger action at next sun rise. """
|
||||
def next_rise():
|
||||
|
@ -103,3 +161,26 @@ def trigger_sunset(hass, action, offset):
|
|||
action()
|
||||
|
||||
track_point_in_utc_time(hass, sunset_automation_listener, next_set())
|
||||
|
||||
|
||||
def _parse_offset(raw_offset):
|
||||
if raw_offset is None:
|
||||
return timedelta(0)
|
||||
|
||||
negative_offset = False
|
||||
if raw_offset.startswith('-'):
|
||||
negative_offset = True
|
||||
raw_offset = raw_offset[1:]
|
||||
|
||||
try:
|
||||
(hour, minute, second) = [int(x) for x in raw_offset.split(':')]
|
||||
except ValueError:
|
||||
_LOGGER.error('Could not parse offset %s', raw_offset)
|
||||
return False
|
||||
|
||||
offset = timedelta(hours=hour, minutes=minute, seconds=second)
|
||||
|
||||
if negative_offset:
|
||||
offset *= -1
|
||||
|
||||
return offset
|
||||
|
|
|
@ -32,8 +32,8 @@ def trigger(hass, config, action):
|
|||
_error_time(config[CONF_AFTER], CONF_AFTER)
|
||||
return False
|
||||
hours, minutes, seconds = after.hour, after.minute, after.second
|
||||
elif (CONF_HOURS in config or CONF_MINUTES in config
|
||||
or CONF_SECONDS in config):
|
||||
elif (CONF_HOURS in config or CONF_MINUTES in config or
|
||||
CONF_SECONDS in config):
|
||||
hours = convert(config.get(CONF_HOURS), int)
|
||||
minutes = convert(config.get(CONF_MINUTES), int)
|
||||
seconds = convert(config.get(CONF_SECONDS), int)
|
||||
|
|
|
@ -6,12 +6,11 @@ The rest binary sensor will consume responses sent by an exposed REST API.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rest/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.util import template, Throttle
|
||||
from homeassistant.util import template
|
||||
from homeassistant.components.sensor.rest import RestData
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -19,61 +18,33 @@ _LOGGER = logging.getLogger(__name__)
|
|||
DEFAULT_NAME = 'REST Binary Sensor'
|
||||
DEFAULT_METHOD = 'GET'
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the REST binary sensor. """
|
||||
|
||||
use_get = False
|
||||
use_post = False
|
||||
|
||||
"""Setup REST binary sensors."""
|
||||
resource = config.get('resource', None)
|
||||
method = config.get('method', DEFAULT_METHOD)
|
||||
payload = config.get('payload', None)
|
||||
verify_ssl = config.get('verify_ssl', True)
|
||||
|
||||
if method == 'GET':
|
||||
use_get = True
|
||||
elif method == 'POST':
|
||||
use_post = True
|
||||
rest = RestData(method, resource, payload, verify_ssl)
|
||||
rest.update()
|
||||
|
||||
try:
|
||||
if use_get:
|
||||
response = requests.get(resource, timeout=10, verify=verify_ssl)
|
||||
elif use_post:
|
||||
response = requests.post(resource, data=payload, timeout=10,
|
||||
verify=verify_ssl)
|
||||
if not response.ok:
|
||||
_LOGGER.error('Response status is "%s"', response.status_code)
|
||||
return False
|
||||
except requests.exceptions.MissingSchema:
|
||||
_LOGGER.error('Missing resource or schema in configuration. '
|
||||
'Add http:// to your URL.')
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('No route to resource/endpoint: %s',
|
||||
resource)
|
||||
if rest.data is None:
|
||||
_LOGGER.error('Unable to fetch Rest data')
|
||||
return False
|
||||
|
||||
if use_get:
|
||||
rest = RestDataGet(resource, verify_ssl)
|
||||
elif use_post:
|
||||
rest = RestDataPost(resource, payload, verify_ssl)
|
||||
|
||||
add_devices([RestBinarySensor(hass,
|
||||
rest,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get(CONF_VALUE_TEMPLATE))])
|
||||
add_devices([RestBinarySensor(
|
||||
hass, rest, config.get('name', DEFAULT_NAME),
|
||||
config.get(CONF_VALUE_TEMPLATE))])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class RestBinarySensor(BinarySensorDevice):
|
||||
""" Implements a REST binary sensor. """
|
||||
"""REST binary sensor."""
|
||||
|
||||
def __init__(self, hass, rest, name, value_template):
|
||||
"""Initialize a REST binary sensor."""
|
||||
self._hass = hass
|
||||
self.rest = rest
|
||||
self._name = name
|
||||
|
@ -83,63 +54,20 @@ class RestBinarySensor(BinarySensorDevice):
|
|||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the binary sensor. """
|
||||
"""Name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if the binary sensor is on. """
|
||||
if self.rest.data is False:
|
||||
"""Return if the binary sensor is on."""
|
||||
if self.rest.data is None:
|
||||
return False
|
||||
else:
|
||||
if self._value_template is not None:
|
||||
self.rest.data = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, self.rest.data, False)
|
||||
return bool(int(self.rest.data))
|
||||
|
||||
if self._value_template is not None:
|
||||
self.rest.data = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, self.rest.data, False)
|
||||
return bool(int(self.rest.data))
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from REST API and updates the state. """
|
||||
"""Get the latest data from REST API and updates the state."""
|
||||
self.rest.update()
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class RestDataGet(object):
|
||||
""" Class for handling the data retrieval with GET method. """
|
||||
|
||||
def __init__(self, resource, verify_ssl):
|
||||
self._resource = resource
|
||||
self._verify_ssl = verify_ssl
|
||||
self.data = False
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from REST service with GET method. """
|
||||
try:
|
||||
response = requests.get(self._resource, timeout=10,
|
||||
verify=self._verify_ssl)
|
||||
self.data = response.text
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
|
||||
self.data = False
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class RestDataPost(object):
|
||||
""" Class for handling the data retrieval with POST method. """
|
||||
|
||||
def __init__(self, resource, payload, verify_ssl):
|
||||
self._resource = resource
|
||||
self._payload = payload
|
||||
self._verify_ssl = verify_ssl
|
||||
self.data = False
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from REST service with POST method. """
|
||||
try:
|
||||
response = requests.post(self._resource, data=self._payload,
|
||||
timeout=10, verify=self._verify_ssl)
|
||||
self.data = response.text
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
|
||||
self.data = False
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
"""
|
||||
homeassistant.components.binary_sensor.rpi_gpio
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to configure a binary_sensor using RPi GPIO.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rpi_gpio/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import homeassistant.components.rpi_gpio as rpi_gpio
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import (DEVICE_DEFAULT_NAME)
|
||||
|
||||
DEFAULT_PULL_MODE = "UP"
|
||||
DEFAULT_BOUNCETIME = 50
|
||||
DEFAULT_INVERT_LOGIC = False
|
||||
|
||||
DEPENDENCIES = ['rpi_gpio']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Raspberry PI GPIO devices. """
|
||||
|
||||
pull_mode = config.get('pull_mode', DEFAULT_PULL_MODE)
|
||||
bouncetime = config.get('bouncetime', DEFAULT_BOUNCETIME)
|
||||
invert_logic = config.get('invert_logic', DEFAULT_INVERT_LOGIC)
|
||||
|
||||
binary_sensors = []
|
||||
ports = config.get('ports')
|
||||
for port_num, port_name in ports.items():
|
||||
binary_sensors.append(RPiGPIOBinarySensor(
|
||||
port_name, port_num, pull_mode, bouncetime, invert_logic))
|
||||
add_devices(binary_sensors)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
""" Represents a binary sensor that uses Raspberry Pi GPIO. """
|
||||
def __init__(self, name, port, pull_mode, bouncetime, invert_logic):
|
||||
# pylint: disable=no-member
|
||||
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._port = port
|
||||
self._pull_mode = pull_mode
|
||||
self._bouncetime = bouncetime
|
||||
self._invert_logic = invert_logic
|
||||
|
||||
rpi_gpio.setup_input(self._port, self._pull_mode)
|
||||
self._state = rpi_gpio.read_input(self._port)
|
||||
|
||||
def read_gpio(port):
|
||||
""" Reads state from GPIO. """
|
||||
self._state = rpi_gpio.read_input(self._port)
|
||||
self.update_ha_state()
|
||||
rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the sensor. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" Returns the state of the entity. """
|
||||
return self._state != self._invert_logic
|
|
@ -58,8 +58,8 @@ class AsusWrtDeviceScanner(object):
|
|||
|
||||
def __init__(self, config):
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
self.username = str(config[CONF_USERNAME])
|
||||
self.password = str(config[CONF_PASSWORD])
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
"""
|
||||
homeassistant.components.device_tracker.geofancy
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Geofancy platform for the device tracker.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.geofancy/
|
||||
"""
|
||||
from homeassistant.const import (
|
||||
HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_SEE = 0
|
||||
|
||||
URL_API_GEOFANCY_ENDPOINT = "/api/geofancy"
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up an endpoint for the Geofancy app. """
|
||||
|
||||
# Use a global variable to keep setup_scanner compact when using a callback
|
||||
global _SEE
|
||||
_SEE = see
|
||||
|
||||
# POST would be semantically better, but that currently does not work
|
||||
# since Geofancy sends the data as key1=value1&key2=value2
|
||||
# in the request body, while Home Assistant expects json there.
|
||||
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_GEOFANCY_ENDPOINT, _handle_get_api_geofancy)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api_geofancy(handler, path_match, data):
|
||||
""" Geofancy message received. """
|
||||
|
||||
if not isinstance(data, dict):
|
||||
handler.write_json_message(
|
||||
"Error while parsing Geofancy message.",
|
||||
HTTP_INTERNAL_SERVER_ERROR)
|
||||
return
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
handler.write_json_message(
|
||||
"Location not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
if 'device' not in data or 'id' not in data:
|
||||
handler.write_json_message(
|
||||
"Device id or location id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
try:
|
||||
gps_coords = (float(data['latitude']), float(data['longitude']))
|
||||
except ValueError:
|
||||
# If invalid latitude / longitude format
|
||||
handler.write_json_message(
|
||||
"Invalid latitude / longitude format.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
# entity id's in Home Assistant must be alphanumerical
|
||||
device_uuid = data['device']
|
||||
device_entity_id = device_uuid.replace('-', '')
|
||||
|
||||
_SEE(dev_id=device_entity_id, gps=gps_coords, location_name=data['id'])
|
||||
|
||||
handler.write_json_message("Geofancy message processed")
|
|
@ -0,0 +1,104 @@
|
|||
"""
|
||||
homeassistant.components.device_tracker.locative
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Locative platform for the device tracker.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.locative/
|
||||
"""
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.const import (
|
||||
HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME)
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
URL_API_LOCATIVE_ENDPOINT = "/api/locative"
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up an endpoint for the Locative app. """
|
||||
|
||||
# POST would be semantically better, but that currently does not work
|
||||
# since Locative sends the data as key1=value1&key2=value2
|
||||
# in the request body, while Home Assistant expects json there.
|
||||
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_LOCATIVE_ENDPOINT,
|
||||
partial(_handle_get_api_locative, hass, see))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api_locative(hass, see, handler, path_match, data):
|
||||
""" Locative message received. """
|
||||
|
||||
if not _check_data(handler, data):
|
||||
return
|
||||
|
||||
device = data['device'].replace('-', '')
|
||||
location_name = data['id'].lower()
|
||||
direction = data['trigger']
|
||||
|
||||
if direction == 'enter':
|
||||
see(dev_id=device, location_name=location_name)
|
||||
handler.write_text("Setting location to {}".format(location_name))
|
||||
|
||||
elif direction == 'exit':
|
||||
current_state = hass.states.get("{}.{}".format(DOMAIN, device))
|
||||
|
||||
if current_state is None or current_state.state == location_name:
|
||||
see(dev_id=device, location_name=STATE_NOT_HOME)
|
||||
handler.write_text("Setting location to not home")
|
||||
else:
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
# aren't currently in. This occurs when a zone is entered before
|
||||
# the previous zone was exited. The enter message will be sent
|
||||
# first, then the exit message will be sent second.
|
||||
handler.write_text(
|
||||
'Ignoring exit from {} (already in {})'.format(
|
||||
location_name, current_state))
|
||||
|
||||
elif direction == 'test':
|
||||
# In the app, a test message can be sent. Just return something to
|
||||
# the user to let them know that it works.
|
||||
handler.write_text("Received test message.")
|
||||
|
||||
else:
|
||||
handler.write_text(
|
||||
"Received unidentified message: {}".format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Received unidentified message from Locative: %s",
|
||||
direction)
|
||||
|
||||
|
||||
def _check_data(handler, data):
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
handler.write_text("Latitude and longitude not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Latitude and longitude not specified.")
|
||||
return False
|
||||
|
||||
if 'device' not in data:
|
||||
handler.write_text("Device id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Device id not specified.")
|
||||
return False
|
||||
|
||||
if 'id' not in data:
|
||||
handler.write_text("Location id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Location id not specified.")
|
||||
return False
|
||||
|
||||
if 'trigger' not in data:
|
||||
handler.write_text("Trigger is not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Trigger is not specified.")
|
||||
return False
|
||||
|
||||
return True
|
|
@ -19,7 +19,7 @@ from homeassistant.components.device_tracker import DOMAIN
|
|||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pynetgear==0.3']
|
||||
REQUIREMENTS = ['pynetgear==0.3.1']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
|
|
|
@ -10,14 +10,17 @@ import json
|
|||
import logging
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import (STATE_HOME, STATE_NOT_HOME)
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
CONF_TRANSITION_EVENTS = 'use_events'
|
||||
LOCATION_TOPIC = 'owntracks/+/+'
|
||||
EVENT_TOPIC = 'owntracks/+/+/event'
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up a OwnTracksks tracker. """
|
||||
""" Set up an OwnTracks tracker. """
|
||||
|
||||
def owntracks_location_update(topic, payload, qos):
|
||||
""" MQTT message received. """
|
||||
|
@ -48,6 +51,56 @@ def setup_scanner(hass, config, see):
|
|||
|
||||
see(**kwargs)
|
||||
|
||||
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
|
||||
def owntracks_event_update(topic, payload, qos):
|
||||
""" MQTT event (geofences) received. """
|
||||
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typetransition
|
||||
try:
|
||||
data = json.loads(payload)
|
||||
except ValueError:
|
||||
# If invalid JSON
|
||||
logging.getLogger(__name__).error(
|
||||
'Unable to parse payload as JSON: %s', payload)
|
||||
return
|
||||
|
||||
if not isinstance(data, dict) or data.get('_type') != 'transition':
|
||||
return
|
||||
|
||||
# check if in "home" fence or other zone
|
||||
location = ''
|
||||
if data['event'] == 'enter':
|
||||
|
||||
if data['desc'].lower() == 'home':
|
||||
location = STATE_HOME
|
||||
else:
|
||||
location = data['desc']
|
||||
|
||||
elif data['event'] == 'leave':
|
||||
location = STATE_NOT_HOME
|
||||
else:
|
||||
logging.getLogger(__name__).error(
|
||||
'Misformatted mqtt msgs, _type=transition, event=%s',
|
||||
data['event'])
|
||||
return
|
||||
|
||||
parts = topic.split('/')
|
||||
kwargs = {
|
||||
'dev_id': '{}_{}'.format(parts[1], parts[2]),
|
||||
'host_name': parts[1],
|
||||
'gps': (data['lat'], data['lon']),
|
||||
'location_name': location,
|
||||
}
|
||||
if 'acc' in data:
|
||||
kwargs['gps_accuracy'] = data['acc']
|
||||
|
||||
see(**kwargs)
|
||||
|
||||
use_events = config.get(CONF_TRANSITION_EVENTS)
|
||||
|
||||
if use_events:
|
||||
mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1)
|
||||
else:
|
||||
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
|
||||
|
||||
return True
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# Describes the format for available device tracker services
|
||||
|
||||
see:
|
||||
description: Control tracked device
|
||||
|
||||
fields:
|
||||
mac:
|
||||
description: MAC address of device
|
||||
example: 'FF:FF:FF:FF:FF:FF'
|
||||
|
||||
dev_id:
|
||||
description: Id of device (find id in known_devices.yaml)
|
||||
example: 'phonedave'
|
||||
|
||||
host_name:
|
||||
description: Hostname of device
|
||||
example: 'Dave'
|
||||
|
||||
location_name:
|
||||
description: Name of location where device is located (not_home is away)
|
||||
example: 'home'
|
||||
|
||||
gps:
|
||||
description: GPS coordinates where device is located (latitude, longitude)
|
||||
example: '[51.509802, -0.086692]'
|
||||
|
||||
gps_accuracy:
|
||||
description: Accuracy of GPS coordinates
|
||||
example: '80'
|
||||
|
||||
battery:
|
||||
description: Battery level of device
|
||||
example: '100'
|
|
@ -105,8 +105,7 @@ class SnmpScanner(object):
|
|||
return
|
||||
if errstatus:
|
||||
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
|
||||
errindex and restable[-1][int(errindex)-1]
|
||||
or '?')
|
||||
errindex and restable[-1][int(errindex)-1] or '?')
|
||||
return
|
||||
|
||||
for resrow in restable:
|
||||
|
|
|
@ -242,8 +242,8 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
|||
|
||||
_LOGGER.info("Loading wireless clients...")
|
||||
|
||||
url = 'http://{}/cgi-bin/luci/;stok={}/admin/wireless?form=statistics' \
|
||||
.format(self.host, self.stok)
|
||||
url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?'
|
||||
'form=statistics').format(self.host, self.stok)
|
||||
referer = 'http://{}/webpages/index.html'.format(self.host)
|
||||
|
||||
response = requests.post(url,
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "be08c5a3ce12040bbdba2db83cb1a568"
|
||||
VERSION = "1003c31441ec44b3db84b49980f736a7"
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
Subproject commit 50aadaf880a9cb36bf144540171ff5fa029e9eaf
|
||||
Subproject commit 2ecd6a818443780dc5d0d981996d165218b2b094
|
|
@ -21,7 +21,7 @@ from urllib.parse import urlparse, parse_qs
|
|||
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, CONTENT_TYPE_JSON,
|
||||
SERVER_PORT, CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN,
|
||||
HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING,
|
||||
HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH,
|
||||
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_EXPIRES, HTTP_OK, HTTP_UNAUTHORIZED,
|
||||
|
@ -112,10 +112,10 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
|||
_LOGGER.info("running http in development mode")
|
||||
|
||||
if ssl_certificate is not None:
|
||||
wrap_kwargs = {'certfile': ssl_certificate}
|
||||
if ssl_key is not None:
|
||||
wrap_kwargs['keyfile'] = ssl_key
|
||||
self.socket = ssl.wrap_socket(self.socket, **wrap_kwargs)
|
||||
context = ssl.create_default_context(
|
||||
purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(ssl_certificate, keyfile=ssl_key)
|
||||
self.socket = context.wrap_socket(self.socket, server_side=True)
|
||||
|
||||
def start(self):
|
||||
""" Starts the HTTP server. """
|
||||
|
@ -198,12 +198,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
self.authenticated = (self.server.api_password is None
|
||||
or self.headers.get(HTTP_HEADER_HA_AUTH) ==
|
||||
self.server.api_password
|
||||
or data.get(DATA_API_PASSWORD) ==
|
||||
self.server.api_password
|
||||
or self.verify_session())
|
||||
self.authenticated = (self.server.api_password is None or
|
||||
self.headers.get(HTTP_HEADER_HA_AUTH) ==
|
||||
self.server.api_password or
|
||||
data.get(DATA_API_PASSWORD) ==
|
||||
self.server.api_password or
|
||||
self.verify_session())
|
||||
|
||||
if '_METHOD' in data:
|
||||
method = data.pop('_METHOD')
|
||||
|
@ -293,6 +293,17 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||
json.dumps(data, indent=4, sort_keys=True,
|
||||
cls=rem.JSONEncoder).encode("UTF-8"))
|
||||
|
||||
def write_text(self, message, status_code=HTTP_OK):
|
||||
""" Helper method to return a text message to the caller. """
|
||||
self.send_response(status_code)
|
||||
self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
|
||||
|
||||
self.set_session_cookie_header()
|
||||
|
||||
self.end_headers()
|
||||
|
||||
self.wfile.write(message.encode("UTF-8"))
|
||||
|
||||
def write_file(self, path, cache_headers=True):
|
||||
""" Returns a file to the user. """
|
||||
try:
|
||||
|
|
|
@ -7,7 +7,6 @@ For more details about this component, please refer to the documentation at
|
|||
https://home-assistant.io/components/influxdb/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.const import (EVENT_STATE_CHANGED, STATE_ON, STATE_OFF,
|
||||
|
@ -77,6 +76,10 @@ def setup(hass, config):
|
|||
_state = 0
|
||||
else:
|
||||
_state = state.state
|
||||
try:
|
||||
_state = float(_state)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
measurement = state.attributes.get('unit_of_measurement', state.domain)
|
||||
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
"""
|
||||
Component to keep track of user controlled booleans for within automation.
|
||||
|
||||
For more details about this component, please refer to the documentation
|
||||
at https://home-assistant.io/components/input_boolean/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DOMAIN = 'input_boolean'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NAME = "name"
|
||||
CONF_INITIAL = "initial"
|
||||
CONF_ICON = "icon"
|
||||
|
||||
|
||||
def is_on(hass, entity_id):
|
||||
"""Test if input_boolean is True."""
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
|
||||
def turn_on(hass, entity_id):
|
||||
"""Set input_boolean to True."""
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id})
|
||||
|
||||
|
||||
def turn_off(hass, entity_id):
|
||||
"""Set input_boolean to False."""
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up input booleans."""
|
||||
if not isinstance(config.get(DOMAIN), dict):
|
||||
_LOGGER.error('Expected %s config to be a dictionary', DOMAIN)
|
||||
return False
|
||||
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
entities = []
|
||||
|
||||
for object_id, cfg in config[DOMAIN].items():
|
||||
if object_id != slugify(object_id):
|
||||
_LOGGER.warning("Found invalid key for boolean input: %s. "
|
||||
"Use %s instead", object_id, slugify(object_id))
|
||||
continue
|
||||
if not cfg:
|
||||
cfg = {}
|
||||
|
||||
name = cfg.get(CONF_NAME)
|
||||
state = cfg.get(CONF_INITIAL, False)
|
||||
icon = cfg.get(CONF_ICON)
|
||||
|
||||
entities.append(InputBoolean(object_id, name, state, icon))
|
||||
|
||||
if not entities:
|
||||
return False
|
||||
|
||||
def toggle_service(service):
|
||||
"""Handle a calls to the input boolean services."""
|
||||
target_inputs = component.extract_from_service(service)
|
||||
|
||||
for input_b in target_inputs:
|
||||
if service.service == SERVICE_TURN_ON:
|
||||
input_b.turn_on()
|
||||
else:
|
||||
input_b.turn_off()
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, toggle_service)
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, toggle_service)
|
||||
|
||||
component.add_entities(entities)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class InputBoolean(ToggleEntity):
|
||||
"""Represent a boolean input within Home Assistant."""
|
||||
|
||||
def __init__(self, object_id, name, state, icon):
|
||||
"""Initialize a boolean input."""
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
self._state = state
|
||||
self._icon = icon
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""If entitiy should be polled."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of the boolean input."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to be used for this entity."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if entity is on."""
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the entity on."""
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the entity off."""
|
||||
self._state = False
|
||||
self.update_ha_state()
|
|
@ -50,6 +50,7 @@ FLASH_LONG = "long"
|
|||
# Apply an effect to the light, can be EFFECT_COLORLOOP
|
||||
ATTR_EFFECT = "effect"
|
||||
EFFECT_COLORLOOP = "colorloop"
|
||||
EFFECT_RANDOM = "random"
|
||||
EFFECT_WHITE = "white"
|
||||
|
||||
LIGHT_PROFILES_FILE = "light_profiles.csv"
|
||||
|
@ -228,7 +229,8 @@ def setup(hass, config):
|
|||
if dat.get(ATTR_FLASH) in (FLASH_SHORT, FLASH_LONG):
|
||||
params[ATTR_FLASH] = dat[ATTR_FLASH]
|
||||
|
||||
if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE):
|
||||
if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE,
|
||||
EFFECT_RANDOM):
|
||||
params[ATTR_EFFECT] = dat[ATTR_EFFECT]
|
||||
|
||||
for light in target_lights:
|
||||
|
|
|
@ -10,17 +10,18 @@ import json
|
|||
import logging
|
||||
import os
|
||||
import socket
|
||||
import random
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME
|
||||
from homeassistant.const import CONF_HOST, CONF_FILENAME, DEVICE_DEFAULT_NAME
|
||||
from homeassistant.components.light import (
|
||||
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP,
|
||||
ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT,
|
||||
ATTR_EFFECT, EFFECT_COLORLOOP, ATTR_RGB_COLOR)
|
||||
ATTR_EFFECT, EFFECT_COLORLOOP, EFFECT_RANDOM, ATTR_RGB_COLOR)
|
||||
|
||||
REQUIREMENTS = ['phue==0.8']
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
@ -34,9 +35,9 @@ _CONFIGURING = {}
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _find_host_from_config(hass):
|
||||
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
|
||||
""" Attempt to detect host based on existing configuration. """
|
||||
path = hass.config.path(PHUE_CONFIG_FILE)
|
||||
path = hass.config.path(filename)
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
|
@ -53,13 +54,14 @@ def _find_host_from_config(hass):
|
|||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Gets the Hue lights. """
|
||||
filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE)
|
||||
if discovery_info is not None:
|
||||
host = urlparse(discovery_info[1]).hostname
|
||||
else:
|
||||
host = config.get(CONF_HOST, None)
|
||||
|
||||
if host is None:
|
||||
host = _find_host_from_config(hass)
|
||||
host = _find_host_from_config(hass, filename)
|
||||
|
||||
if host is None:
|
||||
_LOGGER.error('No host found in configuration')
|
||||
|
@ -69,17 +71,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
if host in _CONFIGURING:
|
||||
return
|
||||
|
||||
setup_bridge(host, hass, add_devices_callback)
|
||||
setup_bridge(host, hass, add_devices_callback, filename)
|
||||
|
||||
|
||||
def setup_bridge(host, hass, add_devices_callback):
|
||||
def setup_bridge(host, hass, add_devices_callback, filename):
|
||||
""" Setup a phue bridge based on host parameter. """
|
||||
import phue
|
||||
|
||||
try:
|
||||
bridge = phue.Bridge(
|
||||
host,
|
||||
config_file_path=hass.config.path(PHUE_CONFIG_FILE))
|
||||
config_file_path=hass.config.path(filename))
|
||||
except ConnectionRefusedError: # Wrong host was given
|
||||
_LOGGER.exception("Error connecting to the Hue bridge at %s", host)
|
||||
|
||||
|
@ -88,7 +90,7 @@ def setup_bridge(host, hass, add_devices_callback):
|
|||
except phue.PhueRegistrationException:
|
||||
_LOGGER.warning("Connected to Hue at %s but not registered.", host)
|
||||
|
||||
request_configuration(host, hass, add_devices_callback)
|
||||
request_configuration(host, hass, add_devices_callback, filename)
|
||||
|
||||
return
|
||||
|
||||
|
@ -120,10 +122,17 @@ def setup_bridge(host, hass, add_devices_callback):
|
|||
|
||||
new_lights = []
|
||||
|
||||
api_name = api.get('config').get('name')
|
||||
if api_name == 'RaspBee-GW':
|
||||
bridge_type = 'deconz'
|
||||
else:
|
||||
bridge_type = 'hue'
|
||||
|
||||
for light_id, info in api_states.items():
|
||||
if light_id not in lights:
|
||||
lights[light_id] = HueLight(int(light_id), info,
|
||||
bridge, update_lights)
|
||||
bridge, update_lights,
|
||||
bridge_type=bridge_type)
|
||||
new_lights.append(lights[light_id])
|
||||
else:
|
||||
lights[light_id].info = info
|
||||
|
@ -134,7 +143,7 @@ def setup_bridge(host, hass, add_devices_callback):
|
|||
update_lights()
|
||||
|
||||
|
||||
def request_configuration(host, hass, add_devices_callback):
|
||||
def request_configuration(host, hass, add_devices_callback, filename):
|
||||
""" Request configuration steps from the user. """
|
||||
configurator = get_component('configurator')
|
||||
|
||||
|
@ -148,7 +157,7 @@ def request_configuration(host, hass, add_devices_callback):
|
|||
# pylint: disable=unused-argument
|
||||
def hue_configuration_callback(data):
|
||||
""" Actions to do when our configuration callback is called. """
|
||||
setup_bridge(host, hass, add_devices_callback)
|
||||
setup_bridge(host, hass, add_devices_callback, filename)
|
||||
|
||||
_CONFIGURING[host] = configurator.request_config(
|
||||
hass, "Philips Hue", hue_configuration_callback,
|
||||
|
@ -162,11 +171,14 @@ def request_configuration(host, hass, add_devices_callback):
|
|||
class HueLight(Light):
|
||||
""" Represents a Hue light """
|
||||
|
||||
def __init__(self, light_id, info, bridge, update_lights):
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, light_id, info, bridge, update_lights,
|
||||
bridge_type='hue'):
|
||||
self.light_id = light_id
|
||||
self.info = info
|
||||
self.bridge = bridge
|
||||
self.update_lights = update_lights
|
||||
self.bridge_type = bridge_type
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
@ -226,14 +238,17 @@ class HueLight(Light):
|
|||
command['alert'] = 'lselect'
|
||||
elif flash == FLASH_SHORT:
|
||||
command['alert'] = 'select'
|
||||
else:
|
||||
elif self.bridge_type == 'hue':
|
||||
command['alert'] = 'none'
|
||||
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
|
||||
if effect == EFFECT_COLORLOOP:
|
||||
command['effect'] = 'colorloop'
|
||||
else:
|
||||
elif effect == EFFECT_RANDOM:
|
||||
command['hue'] = random.randrange(0, 65535)
|
||||
command['sat'] = random.randrange(150, 254)
|
||||
elif self.bridge_type == 'hue':
|
||||
command['effect'] = 'none'
|
||||
|
||||
self.bridge.set_light(self.light_id, command)
|
||||
|
|
|
@ -13,8 +13,9 @@ from homeassistant.components.light import Light
|
|||
from homeassistant.util import slugify
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.components.rfxtrx import ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, \
|
||||
ATTR_NAME, EVENT_BUTTON_PRESSED
|
||||
from homeassistant.components.rfxtrx import (
|
||||
ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID,
|
||||
ATTR_NAME, EVENT_BUTTON_PRESSED)
|
||||
|
||||
|
||||
DEPENDENCIES = ['rfxtrx']
|
||||
|
|
|
@ -42,6 +42,7 @@ turn_on:
|
|||
description: Light effect
|
||||
values:
|
||||
- colorloop
|
||||
- random
|
||||
|
||||
turn_off:
|
||||
description: Turn a light off
|
||||
|
|
|
@ -7,16 +7,15 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/light.vera/
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
from homeassistant.components.switch.vera import VeraSwitch
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
|
||||
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/'
|
||||
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
|
||||
'#python-vera==0.1.1']
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_ON
|
||||
|
||||
REQUIREMENTS = ['pyvera==0.2.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -36,10 +35,19 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
|
||||
device_data = config.get('device_data', {})
|
||||
|
||||
controller = veraApi.VeraController(base_url)
|
||||
vera_controller, created = veraApi.init_controller(base_url)
|
||||
|
||||
if created:
|
||||
def stop_subscription(event):
|
||||
""" Shutdown Vera subscriptions and subscription thread on exit"""
|
||||
_LOGGER.info("Shutting down subscriptions.")
|
||||
vera_controller.stop()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription)
|
||||
|
||||
devices = []
|
||||
try:
|
||||
devices = controller.get_devices([
|
||||
devices = vera_controller.get_devices([
|
||||
'Switch',
|
||||
'On/Off Switch',
|
||||
'Dimmable Switch'])
|
||||
|
@ -50,11 +58,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
|
||||
lights = []
|
||||
for device in devices:
|
||||
extra_data = device_data.get(device.deviceId, {})
|
||||
extra_data = device_data.get(device.device_id, {})
|
||||
exclude = extra_data.get('exclude', False)
|
||||
|
||||
if exclude is not True:
|
||||
lights.append(VeraLight(device, extra_data))
|
||||
lights.append(VeraLight(device, vera_controller, extra_data))
|
||||
|
||||
add_devices_callback(lights)
|
||||
|
||||
|
@ -77,5 +85,5 @@ class VeraLight(VeraSwitch):
|
|||
else:
|
||||
self.vera_device.switch_on()
|
||||
|
||||
self.last_command_send = time.time()
|
||||
self.is_on_status = True
|
||||
self._state = STATE_ON
|
||||
self.update_ha_state(True)
|
||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS
|
|||
from homeassistant.components.wink import WinkToggleDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.3.1']
|
||||
REQUIREMENTS = ['python-wink==0.4.1']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
|
|
@ -11,7 +11,7 @@ import logging
|
|||
from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.3.1']
|
||||
REQUIREMENTS = ['python-wink==0.4.1']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
|
|
@ -28,7 +28,7 @@ QUERY_EVENTS_BETWEEN = """
|
|||
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
|
||||
"""
|
||||
|
||||
EVENT_LOGBOOK_ENTRY = 'LOGBOOK_ENTRY'
|
||||
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
|
||||
|
||||
GROUP_BY_MINUTES = 15
|
||||
|
||||
|
@ -204,7 +204,7 @@ def humanify(events):
|
|||
event.time_fired, "Home Assistant", action,
|
||||
domain=HA_DOMAIN)
|
||||
|
||||
elif event.event_type == EVENT_LOGBOOK_ENTRY:
|
||||
elif event.event_type.lower() == EVENT_LOGBOOK_ENTRY:
|
||||
domain = event.data.get(ATTR_DOMAIN)
|
||||
entity_id = event.data.get(ATTR_ENTITY_ID)
|
||||
if domain is None and entity_id is not None:
|
||||
|
|
|
@ -76,8 +76,12 @@ def setup(hass, config=None):
|
|||
|
||||
logfilter[LOGGER_LOGS] = logs
|
||||
|
||||
logger = logging.getLogger('')
|
||||
logger.setLevel(logging.NOTSET)
|
||||
|
||||
# Set log filter for all log handler
|
||||
for handler in logging.root.handlers:
|
||||
handler.setLevel(logging.NOTSET)
|
||||
handler.addFilter(HomeAssistantLogFilter(logfilter))
|
||||
|
||||
return True
|
||||
|
|
|
@ -72,6 +72,7 @@ SUPPORT_YOUTUBE = 64
|
|||
SUPPORT_TURN_ON = 128
|
||||
SUPPORT_TURN_OFF = 256
|
||||
SUPPORT_PLAY_MEDIA = 512
|
||||
SUPPORT_VOLUME_STEP = 1024
|
||||
|
||||
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ from homeassistant.components.media_player import (
|
|||
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
|
||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
|
||||
|
||||
REQUIREMENTS = ['pychromecast==0.6.13']
|
||||
REQUIREMENTS = ['pychromecast==0.6.14']
|
||||
CONF_IGNORE_CEC = 'ignore_cec'
|
||||
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
|
||||
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
|
|
|
@ -142,10 +142,14 @@ class MpdDevice(MediaPlayerDevice):
|
|||
def media_title(self):
|
||||
""" Title of current playing media. """
|
||||
name = self.currentsong.get('name', None)
|
||||
title = self.currentsong['title']
|
||||
title = self.currentsong.get('title', None)
|
||||
|
||||
if name is None:
|
||||
if name is None and title is None:
|
||||
return "None"
|
||||
elif name is None:
|
||||
return title
|
||||
elif title is None:
|
||||
return name
|
||||
else:
|
||||
return '{}: {}'.format(name, title)
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
|||
|
||||
|
||||
def config_from_file(filename, config=None):
|
||||
''' Small configuration file management function'''
|
||||
""" Small configuration file management function. """
|
||||
if config:
|
||||
# We're writing configuration
|
||||
try:
|
||||
|
@ -85,7 +85,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup_plexserver(host, token, hass, add_devices_callback):
|
||||
''' Setup a plexserver based on host parameter'''
|
||||
""" Setup a plexserver based on host parameter. """
|
||||
import plexapi.server
|
||||
import plexapi.exceptions
|
||||
|
||||
|
|
|
@ -22,9 +22,9 @@ from homeassistant.const import (
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK |\
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||
SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \
|
||||
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
@ -202,11 +202,10 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
|||
""" Image url of current playing media. """
|
||||
if 'artwork_url' in self._status:
|
||||
return self._status['artwork_url']
|
||||
return 'http://{server}:{port}/music/current/cover.jpg?player={player}'\
|
||||
.format(
|
||||
server=self._lms.host,
|
||||
port=self._lms.http_port,
|
||||
player=self._id)
|
||||
return ('http://{server}:{port}/music/current/cover.jpg?'
|
||||
'player={player}').format(server=self._lms.host,
|
||||
port=self._lms.http_port,
|
||||
player=self._id)
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
|
|
|
@ -0,0 +1,438 @@
|
|||
"""
|
||||
homeassistant.components.media_player.universal
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Combines multiple media players into one for a universal controller.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.universal/
|
||||
"""
|
||||
|
||||
# pylint: disable=import-error
|
||||
from copy import copy
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_ON, STATE_OFF, CONF_NAME,
|
||||
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE,
|
||||
SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
|
||||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, DOMAIN,
|
||||
SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
|
||||
SERVICE_PLAY_MEDIA, SERVICE_YOUTUBE_VIDEO,
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS, ATTR_MEDIA_VOLUME_MUTED,
|
||||
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION,
|
||||
ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST, ATTR_MEDIA_ALBUM_NAME,
|
||||
ATTR_MEDIA_TRACK, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_ALBUM_ARTIST,
|
||||
ATTR_MEDIA_SEASON, ATTR_MEDIA_EPISODE, ATTR_MEDIA_CHANNEL,
|
||||
ATTR_MEDIA_PLAYLIST, ATTR_APP_ID, ATTR_APP_NAME, ATTR_MEDIA_VOLUME_LEVEL,
|
||||
ATTR_MEDIA_SEEK_POSITION)
|
||||
|
||||
ATTR_ACTIVE_CHILD = 'active_child'
|
||||
|
||||
CONF_ATTRS = 'attributes'
|
||||
CONF_CHILDREN = 'children'
|
||||
CONF_COMMANDS = 'commands'
|
||||
CONF_PLATFORM = 'platform'
|
||||
CONF_SERVICE = 'service'
|
||||
CONF_SERVICE_DATA = 'service_data'
|
||||
CONF_STATE = 'state'
|
||||
|
||||
OFF_STATES = [STATE_IDLE, STATE_OFF]
|
||||
REQUIREMENTS = []
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" sets up the universal media players """
|
||||
if not validate_config(config):
|
||||
return
|
||||
|
||||
player = UniversalMediaPlayer(hass,
|
||||
config[CONF_NAME],
|
||||
config[CONF_CHILDREN],
|
||||
config[CONF_COMMANDS],
|
||||
config[CONF_ATTRS])
|
||||
|
||||
add_devices([player])
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
""" validate universal media player configuration """
|
||||
del config[CONF_PLATFORM]
|
||||
|
||||
# validate name
|
||||
if CONF_NAME not in config:
|
||||
_LOGGER.error('Universal Media Player configuration requires name')
|
||||
return False
|
||||
|
||||
validate_children(config)
|
||||
validate_commands(config)
|
||||
validate_attributes(config)
|
||||
|
||||
del_keys = []
|
||||
for key in config:
|
||||
if key not in [CONF_NAME, CONF_CHILDREN, CONF_COMMANDS, CONF_ATTRS]:
|
||||
_LOGGER.warning(
|
||||
'Universal Media Player (%s) unrecognized parameter %s',
|
||||
config[CONF_NAME], key)
|
||||
del_keys.append(key)
|
||||
for key in del_keys:
|
||||
del config[key]
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_children(config):
|
||||
""" validate children """
|
||||
if CONF_CHILDREN not in config:
|
||||
_LOGGER.info(
|
||||
'No children under Universal Media Player (%s)', config[CONF_NAME])
|
||||
config[CONF_CHILDREN] = []
|
||||
elif not isinstance(config[CONF_CHILDREN], list):
|
||||
_LOGGER.warning(
|
||||
'Universal Media Player (%s) children not list in config. '
|
||||
'They will be ignored.',
|
||||
config[CONF_NAME])
|
||||
config[CONF_CHILDREN] = []
|
||||
|
||||
|
||||
def validate_commands(config):
|
||||
""" validate commands """
|
||||
if CONF_COMMANDS not in config:
|
||||
config[CONF_COMMANDS] = {}
|
||||
elif not isinstance(config[CONF_COMMANDS], dict):
|
||||
_LOGGER.warning(
|
||||
'Universal Media Player (%s) specified commands not dict in '
|
||||
'config. They will be ignored.',
|
||||
config[CONF_NAME])
|
||||
config[CONF_COMMANDS] = {}
|
||||
|
||||
|
||||
def validate_attributes(config):
|
||||
""" validate attributes """
|
||||
if CONF_ATTRS not in config:
|
||||
config[CONF_ATTRS] = {}
|
||||
elif not isinstance(config[CONF_ATTRS], dict):
|
||||
_LOGGER.warning(
|
||||
'Universal Media Player (%s) specified attributes '
|
||||
'not dict in config. They will be ignored.',
|
||||
config[CONF_NAME])
|
||||
config[CONF_ATTRS] = {}
|
||||
|
||||
for key, val in config[CONF_ATTRS].items():
|
||||
attr = val.split('|', 1)
|
||||
if len(attr) == 1:
|
||||
attr.append(None)
|
||||
config[CONF_ATTRS][key] = attr
|
||||
|
||||
|
||||
class UniversalMediaPlayer(MediaPlayerDevice):
|
||||
""" Represents a universal media player in HA """
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
def __init__(self, hass, name, children, commands, attributes):
|
||||
# pylint: disable=too-many-arguments
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._children = children
|
||||
self._cmds = commands
|
||||
self._attrs = attributes
|
||||
self._child_state = None
|
||||
|
||||
def on_dependency_update(*_):
|
||||
""" update ha state when dependencies update """
|
||||
self.update_ha_state(True)
|
||||
|
||||
depend = copy(children)
|
||||
for entity in attributes.values():
|
||||
depend.append(entity[0])
|
||||
|
||||
track_state_change(hass, depend, on_dependency_update)
|
||||
|
||||
def _entity_lkp(self, entity_id, state_attr=None):
|
||||
""" Looks up an entity state from hass """
|
||||
state_obj = self.hass.states.get(entity_id)
|
||||
|
||||
if state_obj is None:
|
||||
return
|
||||
|
||||
if state_attr:
|
||||
return state_obj.attributes.get(state_attr)
|
||||
return state_obj.state
|
||||
|
||||
def _override_or_child_attr(self, attr_name):
|
||||
""" returns either the override or the active child for attr_name """
|
||||
if attr_name in self._attrs:
|
||||
return self._entity_lkp(self._attrs[attr_name][0],
|
||||
self._attrs[attr_name][1])
|
||||
|
||||
return self._child_attr(attr_name)
|
||||
|
||||
def _child_attr(self, attr_name):
|
||||
""" returns the active child's attr """
|
||||
active_child = self._child_state
|
||||
return active_child.attributes.get(attr_name) if active_child else None
|
||||
|
||||
def _call_service(self, service_name, service_data=None,
|
||||
allow_override=False):
|
||||
""" calls either a specified or active child's service """
|
||||
if allow_override and service_name in self._cmds:
|
||||
call_from_config(
|
||||
self.hass, self._cmds[service_name], blocking=True)
|
||||
return
|
||||
|
||||
if service_data is None:
|
||||
service_data = {}
|
||||
|
||||
active_child = self._child_state
|
||||
service_data[ATTR_ENTITY_ID] = active_child.entity_id
|
||||
|
||||
self.hass.services.call(DOMAIN, service_name, service_data,
|
||||
blocking=True)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Indicates whether HA should poll for updates """
|
||||
return False
|
||||
|
||||
@property
|
||||
def master_state(self):
|
||||
""" gets the master state from entity or none """
|
||||
if CONF_STATE in self._attrs:
|
||||
master_state = self._entity_lkp(self._attrs[CONF_STATE][0],
|
||||
self._attrs[CONF_STATE][1])
|
||||
return master_state if master_state else STATE_OFF
|
||||
else:
|
||||
return None
|
||||
|
||||
def _cache_active_child_state(self):
|
||||
""" The state of the active child or None """
|
||||
for child_name in self._children:
|
||||
child_state = self.hass.states.get(child_name)
|
||||
if child_state and child_state.state not in OFF_STATES:
|
||||
self._child_state = child_state
|
||||
return
|
||||
self._child_state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" name of universal player """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""
|
||||
Current state of media player
|
||||
|
||||
Off if master state is off
|
||||
ELSE Status of first active child
|
||||
ELSE master state or off
|
||||
"""
|
||||
master_state = self.master_state # avoid multiple lookups
|
||||
if master_state == STATE_OFF:
|
||||
return STATE_OFF
|
||||
|
||||
active_child = self._child_state
|
||||
if active_child:
|
||||
return active_child.state
|
||||
|
||||
return master_state if master_state else STATE_OFF
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
""" Volume level of entity specified in attributes or active child """
|
||||
return self._child_attr(ATTR_MEDIA_VOLUME_LEVEL)
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
""" boolean if volume is muted """
|
||||
return self._override_or_child_attr(ATTR_MEDIA_VOLUME_MUTED) \
|
||||
in [True, STATE_ON]
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
""" Content ID of current playing media. """
|
||||
return self._child_attr(ATTR_MEDIA_CONTENT_ID)
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
""" Content type of current playing media. """
|
||||
return self._child_attr(ATTR_MEDIA_CONTENT_TYPE)
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
""" Duration of current playing media in seconds. """
|
||||
return self._child_attr(ATTR_MEDIA_DURATION)
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
""" Image url of current playing media. """
|
||||
return self._child_attr(ATTR_ENTITY_PICTURE)
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
""" Title of current playing media. """
|
||||
return self._child_attr(ATTR_MEDIA_TITLE)
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
""" Artist of current playing media. (Music track only) """
|
||||
return self._child_attr(ATTR_MEDIA_ARTIST)
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
""" Album name of current playing media. (Music track only) """
|
||||
return self._child_attr(ATTR_MEDIA_ALBUM_NAME)
|
||||
|
||||
@property
|
||||
def media_album_artist(self):
|
||||
""" Album arist of current playing media. (Music track only) """
|
||||
return self._child_attr(ATTR_MEDIA_ALBUM_ARTIST)
|
||||
|
||||
@property
|
||||
def media_track(self):
|
||||
""" Track number of current playing media. (Music track only) """
|
||||
return self._child_attr(ATTR_MEDIA_TRACK)
|
||||
|
||||
@property
|
||||
def media_series_title(self):
|
||||
""" Series title of current playing media. (TV Show only)"""
|
||||
return self._child_attr(ATTR_MEDIA_SERIES_TITLE)
|
||||
|
||||
@property
|
||||
def media_season(self):
|
||||
""" Season of current playing media. (TV Show only) """
|
||||
return self._child_attr(ATTR_MEDIA_SEASON)
|
||||
|
||||
@property
|
||||
def media_episode(self):
|
||||
""" Episode of current playing media. (TV Show only) """
|
||||
return self._child_attr(ATTR_MEDIA_EPISODE)
|
||||
|
||||
@property
|
||||
def media_channel(self):
|
||||
""" Channel currently playing. """
|
||||
return self._child_attr(ATTR_MEDIA_CHANNEL)
|
||||
|
||||
@property
|
||||
def media_playlist(self):
|
||||
""" Title of Playlist currently playing. """
|
||||
return self._child_attr(ATTR_MEDIA_PLAYLIST)
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
""" ID of the current running app. """
|
||||
return self._child_attr(ATTR_APP_ID)
|
||||
|
||||
@property
|
||||
def app_name(self):
|
||||
""" Name of the current running app. """
|
||||
return self._child_attr(ATTR_APP_NAME)
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
""" Flags of media commands that are supported. """
|
||||
flags = self._child_attr(ATTR_SUPPORTED_MEDIA_COMMANDS) or 0
|
||||
|
||||
if SERVICE_TURN_ON in self._cmds:
|
||||
flags |= SUPPORT_TURN_ON
|
||||
if SERVICE_TURN_OFF in self._cmds:
|
||||
flags |= SUPPORT_TURN_OFF
|
||||
|
||||
if any([cmd in self._cmds for cmd in [SERVICE_VOLUME_UP,
|
||||
SERVICE_VOLUME_DOWN]]):
|
||||
flags |= SUPPORT_VOLUME_STEP
|
||||
flags &= ~SUPPORT_VOLUME_SET
|
||||
|
||||
if SERVICE_VOLUME_MUTE in self._cmds and \
|
||||
ATTR_MEDIA_VOLUME_MUTED in self._attrs:
|
||||
flags |= SUPPORT_VOLUME_MUTE
|
||||
|
||||
return flags
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Extra attributes a device wants to expose. """
|
||||
active_child = self._child_state
|
||||
return {ATTR_ACTIVE_CHILD: active_child.entity_id} \
|
||||
if active_child else {}
|
||||
|
||||
def turn_on(self):
|
||||
""" turn the media player on. """
|
||||
self._call_service(SERVICE_TURN_ON, allow_override=True)
|
||||
|
||||
def turn_off(self):
|
||||
""" turn the media player off. """
|
||||
self._call_service(SERVICE_TURN_OFF, allow_override=True)
|
||||
|
||||
def mute_volume(self, is_volume_muted):
|
||||
""" mute the volume. """
|
||||
data = {ATTR_MEDIA_VOLUME_MUTED: is_volume_muted}
|
||||
self._call_service(SERVICE_VOLUME_MUTE, data, allow_override=True)
|
||||
|
||||
def set_volume_level(self, volume_level):
|
||||
""" set volume level, range 0..1. """
|
||||
data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level}
|
||||
self._call_service(SERVICE_VOLUME_SET, data)
|
||||
|
||||
def media_play(self):
|
||||
""" Send play commmand. """
|
||||
self._call_service(SERVICE_MEDIA_PLAY)
|
||||
|
||||
def media_pause(self):
|
||||
""" Send pause command. """
|
||||
self._call_service(SERVICE_MEDIA_PAUSE)
|
||||
|
||||
def media_previous_track(self):
|
||||
""" Send previous track command. """
|
||||
self._call_service(SERVICE_MEDIA_PREVIOUS_TRACK)
|
||||
|
||||
def media_next_track(self):
|
||||
""" Send next track command. """
|
||||
self._call_service(SERVICE_MEDIA_NEXT_TRACK)
|
||||
|
||||
def media_seek(self, position):
|
||||
""" Send seek command. """
|
||||
data = {ATTR_MEDIA_SEEK_POSITION: position}
|
||||
self._call_service(SERVICE_MEDIA_SEEK, data)
|
||||
|
||||
def play_youtube(self, media_id):
|
||||
""" Plays a YouTube media. """
|
||||
data = {'media_id': media_id}
|
||||
self._call_service(SERVICE_YOUTUBE_VIDEO, data)
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
""" Plays a piece of media. """
|
||||
data = {'media_type': media_type, 'media_id': media_id}
|
||||
self._call_service(SERVICE_PLAY_MEDIA, data)
|
||||
|
||||
def volume_up(self):
|
||||
""" volume_up media player. """
|
||||
self._call_service(SERVICE_VOLUME_UP, allow_override=True)
|
||||
|
||||
def volume_down(self):
|
||||
""" volume_down media player. """
|
||||
self._call_service(SERVICE_VOLUME_DOWN, allow_override=True)
|
||||
|
||||
def media_play_pause(self):
|
||||
""" media_play_pause media player. """
|
||||
self._call_service(SERVICE_MEDIA_PLAY_PAUSE)
|
||||
|
||||
def update(self):
|
||||
""" event to trigger a state update in HA """
|
||||
for child_name in self._children:
|
||||
child_state = self.hass.states.get(child_name)
|
||||
if child_state and child_state.state not in OFF_STATES:
|
||||
self._child_state = child_state
|
||||
return
|
||||
self._child_state = None
|
|
@ -30,7 +30,7 @@ DEFAULT_QOS = 0
|
|||
DEFAULT_RETAIN = False
|
||||
|
||||
SERVICE_PUBLISH = 'publish'
|
||||
EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED'
|
||||
EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received'
|
||||
|
||||
REQUIREMENTS = ['paho-mqtt==1.1']
|
||||
|
||||
|
@ -149,9 +149,9 @@ class MQTT(object):
|
|||
}
|
||||
|
||||
if client_id is None:
|
||||
self._mqttc = mqtt.Client()
|
||||
self._mqttc = mqtt.Client(protocol=mqtt.MQTTv311)
|
||||
else:
|
||||
self._mqttc = mqtt.Client(client_id)
|
||||
self._mqttc = mqtt.Client(client_id, protocol=mqtt.MQTTv311)
|
||||
|
||||
self._mqttc.user_data_set(self.userdata)
|
||||
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
"""
|
||||
homeassistant.components.mqtt_eventstream
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Connect two Home Assistant instances via mqtt.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the mqtt_eventstream component you will need to add the following to
|
||||
your configuration.yaml file.
|
||||
|
||||
If you do not specify a publish_topic you will not forward events to the queue.
|
||||
If you do not specify a subscribe_topic then you will not receive events from
|
||||
the remote server.
|
||||
|
||||
mqtt_eventstream:
|
||||
publish_topic: MyServerName
|
||||
subscribe_topic: OtherHaServerName
|
||||
"""
|
||||
import json
|
||||
from homeassistant.core import EventOrigin, State
|
||||
from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN
|
||||
from homeassistant.components.mqtt import SERVICE_PUBLISH as MQTT_SVC_PUBLISH
|
||||
from homeassistant.const import (
|
||||
MATCH_ALL,
|
||||
EVENT_TIME_CHANGED,
|
||||
EVENT_CALL_SERVICE,
|
||||
EVENT_SERVICE_EXECUTED,
|
||||
EVENT_STATE_CHANGED,
|
||||
)
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.remote import JSONEncoder
|
||||
|
||||
# The domain of your component. Should be equal to the name of your component
|
||||
DOMAIN = "mqtt_eventstream"
|
||||
|
||||
# List of component names (string) your component depends upon
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup our mqtt_eventstream component. """
|
||||
mqtt = loader.get_component('mqtt')
|
||||
pub_topic = config[DOMAIN].get('publish_topic', None)
|
||||
sub_topic = config[DOMAIN].get('subscribe_topic', None)
|
||||
|
||||
def _event_publisher(event):
|
||||
""" Handle events by publishing them on the mqtt queue. """
|
||||
if event.origin != EventOrigin.local:
|
||||
return
|
||||
if event.event_type == EVENT_TIME_CHANGED:
|
||||
return
|
||||
|
||||
# Filter out the events that were triggered by publishing
|
||||
# to the MQTT topic, or you will end up in an infinite loop.
|
||||
if event.event_type == EVENT_CALL_SERVICE:
|
||||
if (
|
||||
event.data.get('domain') == MQTT_DOMAIN and
|
||||
event.data.get('service') == MQTT_SVC_PUBLISH and
|
||||
event.data.get('topic') == pub_topic
|
||||
):
|
||||
return
|
||||
|
||||
# Filter out all the "event service executed" events because they
|
||||
# are only used internally by core as callbacks for blocking
|
||||
# during the interval while a service is being executed.
|
||||
# They will serve no purpose to the external system,
|
||||
# and thus are unnecessary traffic.
|
||||
# And at any rate it would cause an infinite loop to publish them
|
||||
# because publishing to an MQTT topic itself triggers one.
|
||||
if event.event_type == EVENT_SERVICE_EXECUTED:
|
||||
return
|
||||
|
||||
event_info = {'event_type': event.event_type, 'event_data': event.data}
|
||||
msg = json.dumps(event_info, cls=JSONEncoder)
|
||||
mqtt.publish(hass, pub_topic, msg)
|
||||
|
||||
# Only listen for local events if you are going to publish them
|
||||
if pub_topic:
|
||||
hass.bus.listen(MATCH_ALL, _event_publisher)
|
||||
|
||||
# Process events from a remote server that are received on a queue
|
||||
def _event_receiver(topic, payload, qos):
|
||||
"""
|
||||
Receive events published by the other HA instance and fire
|
||||
them on this hass instance.
|
||||
"""
|
||||
event = json.loads(payload)
|
||||
event_type = event.get('event_type')
|
||||
event_data = event.get('event_data')
|
||||
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
# Copied over from the _handle_api_post_events_event method
|
||||
# of the api component.
|
||||
if event_type == EVENT_STATE_CHANGED and event_data:
|
||||
for key in ('old_state', 'new_state'):
|
||||
state = State.from_dict(event_data.get(key))
|
||||
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
hass.bus.fire(
|
||||
event_type,
|
||||
event_data=event_data,
|
||||
origin=EventOrigin.remote
|
||||
)
|
||||
|
||||
# Only subscribe if you specified a topic
|
||||
if sub_topic:
|
||||
mqtt.subscribe(hass, sub_topic, _event_receiver)
|
||||
|
||||
hass.states.set('{domain}.initialized'.format(domain=DOMAIN), True)
|
||||
# return boolean to indicate that initialization was successful
|
||||
return True
|
|
@ -0,0 +1,230 @@
|
|||
"""
|
||||
homeassistant.components.mysensors
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
MySensors component that connects to a MySensors gateway via pymysensors
|
||||
API.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.mysensors.html
|
||||
|
||||
|
||||
New features:
|
||||
|
||||
New MySensors component.
|
||||
Updated MySensors Sensor platform.
|
||||
New MySensors Switch platform. Currently only in optimistic mode (compare
|
||||
with MQTT).
|
||||
Multiple gateways are now supported.
|
||||
|
||||
Configuration.yaml:
|
||||
|
||||
mysensors:
|
||||
gateways:
|
||||
- port: '/dev/ttyUSB0'
|
||||
persistence_file: 'path/mysensors.json'
|
||||
- port: '/dev/ttyACM1'
|
||||
persistence_file: 'path/mysensors2.json'
|
||||
debug: true
|
||||
persistence: true
|
||||
version: '1.5'
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED,
|
||||
TEMP_CELCIUS,)
|
||||
|
||||
CONF_GATEWAYS = 'gateways'
|
||||
CONF_PORT = 'port'
|
||||
CONF_DEBUG = 'debug'
|
||||
CONF_PERSISTENCE = 'persistence'
|
||||
CONF_PERSISTENCE_FILE = 'persistence_file'
|
||||
CONF_VERSION = 'version'
|
||||
DEFAULT_VERSION = '1.4'
|
||||
|
||||
DOMAIN = 'mysensors'
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/theolind/pymysensors/archive/'
|
||||
'005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ATTR_NODE_ID = 'node_id'
|
||||
ATTR_CHILD_ID = 'child_id'
|
||||
ATTR_PORT = 'port'
|
||||
|
||||
GATEWAYS = None
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
DISCOVER_SENSORS = "mysensors.sensors"
|
||||
DISCOVER_SWITCHES = "mysensors.switches"
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_COMPONENTS = [
|
||||
('sensor', DISCOVER_SENSORS),
|
||||
('switch', DISCOVER_SWITCHES),
|
||||
]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the MySensors component."""
|
||||
# pylint: disable=too-many-locals
|
||||
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_GATEWAYS]},
|
||||
_LOGGER):
|
||||
return False
|
||||
|
||||
import mysensors.mysensors as mysensors
|
||||
|
||||
version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION))
|
||||
is_metric = (hass.config.temperature_unit == TEMP_CELCIUS)
|
||||
|
||||
def setup_gateway(port, persistence, persistence_file, version):
|
||||
"""Return gateway after setup of the gateway."""
|
||||
gateway = mysensors.SerialGateway(port, event_callback=None,
|
||||
persistence=persistence,
|
||||
persistence_file=persistence_file,
|
||||
protocol_version=version)
|
||||
gateway.metric = is_metric
|
||||
gateway.debug = config[DOMAIN].get(CONF_DEBUG, False)
|
||||
gateway = GatewayWrapper(gateway, version)
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
gateway.event_callback = gateway.callback_factory()
|
||||
|
||||
def gw_start(event):
|
||||
"""Callback to trigger start of gateway and any persistence."""
|
||||
gateway.start()
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
lambda event: gateway.stop())
|
||||
if persistence:
|
||||
for node_id in gateway.sensors:
|
||||
gateway.event_callback('persistence', node_id)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, gw_start)
|
||||
|
||||
return gateway
|
||||
|
||||
# Setup all ports from config
|
||||
global GATEWAYS
|
||||
GATEWAYS = {}
|
||||
conf_gateways = config[DOMAIN][CONF_GATEWAYS]
|
||||
if isinstance(conf_gateways, dict):
|
||||
conf_gateways = [conf_gateways]
|
||||
persistence = config[DOMAIN].get(CONF_PERSISTENCE, True)
|
||||
|
||||
for index, gway in enumerate(conf_gateways):
|
||||
port = gway[CONF_PORT]
|
||||
persistence_file = gway.get(
|
||||
CONF_PERSISTENCE_FILE,
|
||||
hass.config.path('mysensors{}.pickle'.format(index + 1)))
|
||||
GATEWAYS[port] = setup_gateway(
|
||||
port, persistence, persistence_file, version)
|
||||
|
||||
for (component, discovery_service) in DISCOVERY_COMPONENTS:
|
||||
# Ensure component is loaded
|
||||
if not bootstrap.setup_component(hass, component, config):
|
||||
return False
|
||||
# Fire discovery event
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: discovery_service,
|
||||
ATTR_DISCOVERED: {}})
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def pf_callback_factory(
|
||||
s_types, v_types, devices, add_devices, entity_class):
|
||||
"""Return a new callback for the platform."""
|
||||
def mysensors_callback(gateway, node_id):
|
||||
"""Callback for mysensors platform."""
|
||||
if gateway.sensors[node_id].sketch_name is None:
|
||||
_LOGGER.info('No sketch_name: node %s', node_id)
|
||||
return
|
||||
# previously discovered, just update state with latest info
|
||||
if node_id in devices:
|
||||
for entity in devices[node_id]:
|
||||
entity.update_ha_state(True)
|
||||
return
|
||||
|
||||
# First time we see this node, detect sensors
|
||||
for child in gateway.sensors[node_id].children.values():
|
||||
name = '{} {}.{}'.format(
|
||||
gateway.sensors[node_id].sketch_name, node_id, child.id)
|
||||
|
||||
for value_type in child.values.keys():
|
||||
if child.type not in s_types or value_type not in v_types:
|
||||
continue
|
||||
|
||||
devices[node_id].append(
|
||||
entity_class(gateway, node_id, child.id, name, value_type))
|
||||
if devices[node_id]:
|
||||
_LOGGER.info('adding new devices: %s', devices[node_id])
|
||||
add_devices(devices[node_id])
|
||||
for entity in devices[node_id]:
|
||||
entity.update_ha_state(True)
|
||||
return mysensors_callback
|
||||
|
||||
|
||||
class GatewayWrapper(object):
|
||||
"""Gateway wrapper class, by subclassing serial gateway."""
|
||||
|
||||
def __init__(self, gateway, version):
|
||||
"""Setup class attributes on instantiation.
|
||||
|
||||
Args:
|
||||
gateway (mysensors.SerialGateway): Gateway to wrap.
|
||||
version (str): Version of mysensors API.
|
||||
|
||||
Attributes:
|
||||
_wrapped_gateway (mysensors.SerialGateway): Wrapped gateway.
|
||||
version (str): Version of mysensors API.
|
||||
platform_callbacks (list): Callback functions, one per platform.
|
||||
const (module): Mysensors API constants.
|
||||
__initialised (bool): True if GatewayWrapper is initialised.
|
||||
"""
|
||||
self._wrapped_gateway = gateway
|
||||
self.version = version
|
||||
self.platform_callbacks = []
|
||||
self.const = self.get_const()
|
||||
self.__initialised = True
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""See if this object has attribute name."""
|
||||
# Do not use hasattr, it goes into infinite recurrsion
|
||||
if name in self.__dict__:
|
||||
# this object has it
|
||||
return getattr(self, name)
|
||||
# proxy to the wrapped object
|
||||
return getattr(self._wrapped_gateway, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
"""See if this object has attribute name then set to value."""
|
||||
if '_GatewayWrapper__initialised' not in self.__dict__:
|
||||
return object.__setattr__(self, name, value)
|
||||
elif name in self.__dict__:
|
||||
object.__setattr__(self, name, value)
|
||||
else:
|
||||
object.__setattr__(self._wrapped_gateway, name, value)
|
||||
|
||||
def get_const(self):
|
||||
"""Get mysensors API constants."""
|
||||
if self.version == '1.5':
|
||||
import mysensors.const_15 as const
|
||||
else:
|
||||
import mysensors.const_14 as const
|
||||
return const
|
||||
|
||||
def callback_factory(self):
|
||||
"""Return a new callback function."""
|
||||
def node_update(update_type, node_id):
|
||||
"""Callback for node updates from the MySensors gateway."""
|
||||
_LOGGER.info('update %s: node %s', update_type, node_id)
|
||||
for callback in self.platform_callbacks:
|
||||
callback(self, node_id)
|
||||
|
||||
return node_update
|
|
@ -0,0 +1,51 @@
|
|||
"""
|
||||
homeassistant.components.notify.free_mobile
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Free Mobile SMS platform for notify component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.free_mobile/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.notify import (
|
||||
DOMAIN, BaseNotificationService)
|
||||
from homeassistant.const import CONF_USERNAME, CONF_ACCESS_TOKEN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['freesms==0.1.0']
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
""" Get the Free Mobile SMS notification service. """
|
||||
|
||||
if not validate_config({DOMAIN: config},
|
||||
{DOMAIN: [CONF_USERNAME,
|
||||
CONF_ACCESS_TOKEN]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
return FreeSMSNotificationService(config[CONF_USERNAME],
|
||||
config[CONF_ACCESS_TOKEN])
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class FreeSMSNotificationService(BaseNotificationService):
|
||||
""" Implements notification service for the Free Mobile SMS service. """
|
||||
|
||||
def __init__(self, username, access_token):
|
||||
from freesms import FreeClient
|
||||
self.free_client = FreeClient(username, access_token)
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
""" Send a message to the Free Mobile user cell. """
|
||||
resp = self.free_client.send_sms(message)
|
||||
|
||||
if resp.status_code == 400:
|
||||
_LOGGER.error("At least one parameter is missing")
|
||||
elif resp.status_code == 402:
|
||||
_LOGGER.error("Too much SMS send in a few time")
|
||||
elif resp.status_code == 403:
|
||||
_LOGGER.error("Wrong Username/Password")
|
||||
elif resp.status_code == 500:
|
||||
_LOGGER.error("Server error, try later")
|
|
@ -7,59 +7,62 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/notify.syslog/
|
||||
"""
|
||||
import logging
|
||||
import syslog
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.notify import (
|
||||
DOMAIN, ATTR_TITLE, BaseNotificationService)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
FACILITIES = {'kernel': syslog.LOG_KERN,
|
||||
'user': syslog.LOG_USER,
|
||||
'mail': syslog.LOG_MAIL,
|
||||
'daemon': syslog.LOG_DAEMON,
|
||||
'auth': syslog.LOG_KERN,
|
||||
'LPR': syslog.LOG_LPR,
|
||||
'news': syslog.LOG_NEWS,
|
||||
'uucp': syslog.LOG_UUCP,
|
||||
'cron': syslog.LOG_CRON,
|
||||
'syslog': syslog.LOG_SYSLOG,
|
||||
'local0': syslog.LOG_LOCAL0,
|
||||
'local1': syslog.LOG_LOCAL1,
|
||||
'local2': syslog.LOG_LOCAL2,
|
||||
'local3': syslog.LOG_LOCAL3,
|
||||
'local4': syslog.LOG_LOCAL4,
|
||||
'local5': syslog.LOG_LOCAL5,
|
||||
'local6': syslog.LOG_LOCAL6,
|
||||
'local7': syslog.LOG_LOCAL7}
|
||||
|
||||
OPTIONS = {'pid': syslog.LOG_PID,
|
||||
'cons': syslog.LOG_CONS,
|
||||
'ndelay': syslog.LOG_NDELAY,
|
||||
'nowait': syslog.LOG_NOWAIT,
|
||||
'perror': syslog.LOG_PERROR}
|
||||
|
||||
PRIORITIES = {5: syslog.LOG_EMERG,
|
||||
4: syslog.LOG_ALERT,
|
||||
3: syslog.LOG_CRIT,
|
||||
2: syslog.LOG_ERR,
|
||||
1: syslog.LOG_WARNING,
|
||||
0: syslog.LOG_NOTICE,
|
||||
-1: syslog.LOG_INFO,
|
||||
-2: syslog.LOG_DEBUG}
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
""" Get the mail notification service. """
|
||||
|
||||
"""Get the syslog notification service."""
|
||||
if not validate_config({DOMAIN: config},
|
||||
{DOMAIN: ['facility', 'option', 'priority']},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
_facility = FACILITIES.get(config['facility'], 40)
|
||||
_option = OPTIONS.get(config['option'], 10)
|
||||
_priority = PRIORITIES.get(config['priority'], -1)
|
||||
import syslog
|
||||
|
||||
_facility = {
|
||||
'kernel': syslog.LOG_KERN,
|
||||
'user': syslog.LOG_USER,
|
||||
'mail': syslog.LOG_MAIL,
|
||||
'daemon': syslog.LOG_DAEMON,
|
||||
'auth': syslog.LOG_KERN,
|
||||
'LPR': syslog.LOG_LPR,
|
||||
'news': syslog.LOG_NEWS,
|
||||
'uucp': syslog.LOG_UUCP,
|
||||
'cron': syslog.LOG_CRON,
|
||||
'syslog': syslog.LOG_SYSLOG,
|
||||
'local0': syslog.LOG_LOCAL0,
|
||||
'local1': syslog.LOG_LOCAL1,
|
||||
'local2': syslog.LOG_LOCAL2,
|
||||
'local3': syslog.LOG_LOCAL3,
|
||||
'local4': syslog.LOG_LOCAL4,
|
||||
'local5': syslog.LOG_LOCAL5,
|
||||
'local6': syslog.LOG_LOCAL6,
|
||||
'local7': syslog.LOG_LOCAL7,
|
||||
}.get(config['facility'], 40)
|
||||
|
||||
_option = {
|
||||
'pid': syslog.LOG_PID,
|
||||
'cons': syslog.LOG_CONS,
|
||||
'ndelay': syslog.LOG_NDELAY,
|
||||
'nowait': syslog.LOG_NOWAIT,
|
||||
'perror': syslog.LOG_PERROR
|
||||
}.get(config['option'], 10)
|
||||
|
||||
_priority = {
|
||||
5: syslog.LOG_EMERG,
|
||||
4: syslog.LOG_ALERT,
|
||||
3: syslog.LOG_CRIT,
|
||||
2: syslog.LOG_ERR,
|
||||
1: syslog.LOG_WARNING,
|
||||
0: syslog.LOG_NOTICE,
|
||||
-1: syslog.LOG_INFO,
|
||||
-2: syslog.LOG_DEBUG
|
||||
}.get(config['priority'], -1)
|
||||
|
||||
return SyslogNotificationService(_facility, _option, _priority)
|
||||
|
||||
|
@ -76,6 +79,7 @@ class SyslogNotificationService(BaseNotificationService):
|
|||
|
||||
def send_message(self, message="", **kwargs):
|
||||
""" Send a message to a user. """
|
||||
import syslog
|
||||
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import json
|
|||
import atexit
|
||||
|
||||
from homeassistant.core import Event, EventOrigin, State
|
||||
import homeassistant.util.dt as date_util
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.remote import JSONEncoder
|
||||
from homeassistant.const import (
|
||||
MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
|
||||
|
@ -62,8 +62,8 @@ def row_to_state(row):
|
|||
try:
|
||||
return State(
|
||||
row[1], row[2], json.loads(row[3]),
|
||||
date_util.utc_from_timestamp(row[4]),
|
||||
date_util.utc_from_timestamp(row[5]))
|
||||
dt_util.utc_from_timestamp(row[4]),
|
||||
dt_util.utc_from_timestamp(row[5]))
|
||||
except ValueError:
|
||||
# When json.loads fails
|
||||
_LOGGER.exception("Error converting row to state: %s", row)
|
||||
|
@ -74,7 +74,7 @@ def row_to_event(row):
|
|||
""" Convert a databse row to an event. """
|
||||
try:
|
||||
return Event(row[1], json.loads(row[2]), EventOrigin(row[3]),
|
||||
date_util.utc_from_timestamp(row[5]))
|
||||
dt_util.utc_from_timestamp(row[5]))
|
||||
except ValueError:
|
||||
# When json.loads fails
|
||||
_LOGGER.exception("Error converting row to event: %s", row)
|
||||
|
@ -116,10 +116,10 @@ class RecorderRun(object):
|
|||
self.start = _INSTANCE.recording_start
|
||||
self.closed_incorrect = False
|
||||
else:
|
||||
self.start = date_util.utc_from_timestamp(row[1])
|
||||
self.start = dt_util.utc_from_timestamp(row[1])
|
||||
|
||||
if row[2] is not None:
|
||||
self.end = date_util.utc_from_timestamp(row[2])
|
||||
self.end = dt_util.utc_from_timestamp(row[2])
|
||||
|
||||
self.closed_incorrect = bool(row[3])
|
||||
|
||||
|
@ -169,8 +169,8 @@ class Recorder(threading.Thread):
|
|||
self.queue = queue.Queue()
|
||||
self.quit_object = object()
|
||||
self.lock = threading.Lock()
|
||||
self.recording_start = date_util.utcnow()
|
||||
self.utc_offset = date_util.now().utcoffset().total_seconds()
|
||||
self.recording_start = dt_util.utcnow()
|
||||
self.utc_offset = dt_util.now().utcoffset().total_seconds()
|
||||
|
||||
def start_recording(event):
|
||||
""" Start recording. """
|
||||
|
@ -217,10 +217,11 @@ class Recorder(threading.Thread):
|
|||
def shutdown(self, event):
|
||||
""" Tells the recorder to shut down. """
|
||||
self.queue.put(self.quit_object)
|
||||
self.block_till_done()
|
||||
|
||||
def record_state(self, entity_id, state, event_id):
|
||||
""" Save a state to the database. """
|
||||
now = date_util.utcnow()
|
||||
now = dt_util.utcnow()
|
||||
|
||||
# State got deleted
|
||||
if state is None:
|
||||
|
@ -247,7 +248,7 @@ class Recorder(threading.Thread):
|
|||
""" Save an event to the database. """
|
||||
info = (
|
||||
event.event_type, json.dumps(event.data, cls=JSONEncoder),
|
||||
str(event.origin), date_util.utcnow(), event.time_fired,
|
||||
str(event.origin), dt_util.utcnow(), event.time_fired,
|
||||
self.utc_offset
|
||||
)
|
||||
|
||||
|
@ -307,7 +308,7 @@ class Recorder(threading.Thread):
|
|||
def save_migration(migration_id):
|
||||
""" Save and commit a migration to the database. """
|
||||
cur.execute('INSERT INTO schema_version VALUES (?, ?)',
|
||||
(migration_id, date_util.utcnow()))
|
||||
(migration_id, dt_util.utcnow()))
|
||||
self.conn.commit()
|
||||
_LOGGER.info("Database migrated to version %d", migration_id)
|
||||
|
||||
|
@ -420,18 +421,18 @@ class Recorder(threading.Thread):
|
|||
self.query(
|
||||
"""INSERT INTO recorder_runs (start, created, utc_offset)
|
||||
VALUES (?, ?, ?)""",
|
||||
(self.recording_start, date_util.utcnow(), self.utc_offset))
|
||||
(self.recording_start, dt_util.utcnow(), self.utc_offset))
|
||||
|
||||
def _close_run(self):
|
||||
""" Save end time for current run. """
|
||||
self.query(
|
||||
"UPDATE recorder_runs SET end=? WHERE start=?",
|
||||
(date_util.utcnow(), self.recording_start))
|
||||
(dt_util.utcnow(), self.recording_start))
|
||||
|
||||
|
||||
def _adapt_datetime(datetimestamp):
|
||||
""" Turn a datetime into an integer for in the DB. """
|
||||
return date_util.as_utc(datetimestamp.replace(microsecond=0)).timestamp()
|
||||
return dt_util.as_utc(datetimestamp.replace(microsecond=0)).timestamp()
|
||||
|
||||
|
||||
def _verify_instance():
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
"""
|
||||
homeassistant.components.rpi_gpio
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to control the GPIO pins of a Raspberry Pi.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/rpi_gpio/
|
||||
"""
|
||||
|
||||
import logging
|
||||
try:
|
||||
import RPi.GPIO as GPIO
|
||||
except ImportError:
|
||||
GPIO = None
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
REQUIREMENTS = ['RPi.GPIO==0.6.1']
|
||||
DOMAIN = "rpi_gpio"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
def setup(hass, config):
|
||||
""" Sets up the Raspberry PI GPIO component. """
|
||||
if GPIO is None:
|
||||
_LOGGER.error('RPi.GPIO not available. rpi_gpio ports ignored.')
|
||||
return False
|
||||
|
||||
def cleanup_gpio(event):
|
||||
""" Stuff to do before stop home assistant. """
|
||||
GPIO.cleanup()
|
||||
|
||||
def prepare_gpio(event):
|
||||
""" Stuff to do when home assistant starts. """
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio)
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
return True
|
||||
|
||||
|
||||
def setup_output(port):
|
||||
""" Setup a GPIO as output. """
|
||||
GPIO.setup(port, GPIO.OUT)
|
||||
|
||||
|
||||
def setup_input(port, pull_mode):
|
||||
""" Setup a GPIO as input. """
|
||||
GPIO.setup(port, GPIO.IN,
|
||||
GPIO.PUD_DOWN if pull_mode == 'DOWN' else GPIO.PUD_UP)
|
||||
|
||||
|
||||
def write_output(port, value):
|
||||
""" Write a value to a GPIO. """
|
||||
GPIO.output(port, value)
|
||||
|
||||
|
||||
def read_input(port):
|
||||
""" Read a value from a GPIO. """
|
||||
return GPIO.input(port)
|
||||
|
||||
|
||||
def edge_detect(port, event_callback, bounce):
|
||||
""" Adds detection for RISING and FALLING events. """
|
||||
GPIO.add_event_detect(
|
||||
port,
|
||||
GPIO.BOTH,
|
||||
callback=event_callback,
|
||||
bouncetime=bounce)
|
|
@ -73,8 +73,9 @@ def _process_config(scene_config):
|
|||
|
||||
for entity_id in c_entities:
|
||||
if isinstance(c_entities[entity_id], dict):
|
||||
state = c_entities[entity_id].pop('state', None)
|
||||
attributes = c_entities[entity_id]
|
||||
entity_attrs = c_entities[entity_id].copy()
|
||||
state = entity_attrs.pop('state', None)
|
||||
attributes = entity_attrs
|
||||
else:
|
||||
state = c_entities[entity_id]
|
||||
attributes = {}
|
||||
|
|
|
@ -81,7 +81,7 @@ def setup(hass, config):
|
|||
object_id)
|
||||
continue
|
||||
alias = cfg.get(CONF_ALIAS, object_id)
|
||||
script = Script(hass, object_id, alias, cfg[CONF_SEQUENCE])
|
||||
script = Script(object_id, alias, cfg[CONF_SEQUENCE])
|
||||
component.add_entities((script,))
|
||||
hass.services.register(DOMAIN, object_id, service_handler)
|
||||
|
||||
|
@ -106,8 +106,7 @@ def setup(hass, config):
|
|||
class Script(ToggleEntity):
|
||||
""" Represents a script. """
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, hass, object_id, name, sequence):
|
||||
self.hass = hass
|
||||
def __init__(self, object_id, name, sequence):
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
self.sequence = sequence
|
||||
|
|
|
@ -9,7 +9,8 @@ https://home-assistant.io/components/sensor/
|
|||
import logging
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.components import wink, zwave, isy994, verisure, ecobee
|
||||
from homeassistant.components import (
|
||||
wink, zwave, isy994, verisure, ecobee, tellduslive, mysensors)
|
||||
|
||||
DOMAIN = 'sensor'
|
||||
SCAN_INTERVAL = 30
|
||||
|
@ -22,7 +23,9 @@ DISCOVERY_PLATFORMS = {
|
|||
zwave.DISCOVER_SENSORS: 'zwave',
|
||||
isy994.DISCOVER_SENSORS: 'isy994',
|
||||
verisure.DISCOVER_SENSORS: 'verisure',
|
||||
ecobee.DISCOVER_SENSORS: 'ecobee'
|
||||
ecobee.DISCOVER_SENSORS: 'ecobee',
|
||||
tellduslive.DISCOVER_SENSORS: 'tellduslive',
|
||||
mysensors.DISCOVER_SENSORS: 'mysensors',
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ import logging
|
|||
|
||||
import requests
|
||||
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, \
|
||||
DEVICE_DEFAULT_NAME
|
||||
from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE,
|
||||
DEVICE_DEFAULT_NAME)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import template, Throttle
|
||||
|
|
|
@ -53,6 +53,11 @@ class EliqSensor(Entity):
|
|||
""" Returns the name. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
""" Returns icon. """
|
||||
return "mdi:speedometer"
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Unit of measurement of this entity, if any. """
|
||||
|
@ -65,5 +70,8 @@ class EliqSensor(Entity):
|
|||
|
||||
def update(self):
|
||||
""" Gets the latest data. """
|
||||
response = self.api.get_data_now(channelid=self.channel_id)
|
||||
self._state = int(response.power)
|
||||
try:
|
||||
response = self.api.get_data_now(channelid=self.channel_id)
|
||||
self._state = int(response.power)
|
||||
except TypeError: # raised by eliqonline library on any HTTP error
|
||||
pass
|
||||
|
|
|
@ -7,150 +7,177 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/sensor.mysensors/
|
||||
"""
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP,
|
||||
ATTR_BATTERY_LEVEL,
|
||||
TEMP_CELCIUS, TEMP_FAHRENHEIT,
|
||||
STATE_ON, STATE_OFF)
|
||||
|
||||
CONF_PORT = "port"
|
||||
CONF_DEBUG = "debug"
|
||||
CONF_PERSISTENCE = "persistence"
|
||||
CONF_PERSISTENCE_FILE = "persistence_file"
|
||||
CONF_VERSION = "version"
|
||||
|
||||
ATTR_NODE_ID = "node_id"
|
||||
ATTR_CHILD_ID = "child_id"
|
||||
import homeassistant.components.mysensors as mysensors
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['https://github.com/theolind/pymysensors/archive/'
|
||||
'd4b809c2167650691058d1e29bfd2c4b1792b4b0.zip'
|
||||
'#pymysensors==0.3']
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Setup the mysensors platform. """
|
||||
"""Setup the mysensors platform for sensors."""
|
||||
# Only act if loaded via mysensors by discovery event.
|
||||
# Otherwise gateway is not setup.
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
import mysensors.mysensors as mysensors
|
||||
import mysensors.const_14 as const
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||
# states.
|
||||
s_types = [
|
||||
gateway.const.Presentation.S_TEMP,
|
||||
gateway.const.Presentation.S_HUM,
|
||||
gateway.const.Presentation.S_BARO,
|
||||
gateway.const.Presentation.S_WIND,
|
||||
gateway.const.Presentation.S_RAIN,
|
||||
gateway.const.Presentation.S_UV,
|
||||
gateway.const.Presentation.S_WEIGHT,
|
||||
gateway.const.Presentation.S_POWER,
|
||||
gateway.const.Presentation.S_DISTANCE,
|
||||
gateway.const.Presentation.S_LIGHT_LEVEL,
|
||||
gateway.const.Presentation.S_IR,
|
||||
gateway.const.Presentation.S_WATER,
|
||||
gateway.const.Presentation.S_AIR_QUALITY,
|
||||
gateway.const.Presentation.S_CUSTOM,
|
||||
gateway.const.Presentation.S_DUST,
|
||||
gateway.const.Presentation.S_SCENE_CONTROLLER,
|
||||
]
|
||||
not_v_types = [
|
||||
gateway.const.SetReq.V_ARMED,
|
||||
gateway.const.SetReq.V_LIGHT,
|
||||
gateway.const.SetReq.V_LOCK_STATUS,
|
||||
]
|
||||
if float(gateway.version) >= 1.5:
|
||||
s_types.extend([
|
||||
gateway.const.Presentation.S_COLOR_SENSOR,
|
||||
gateway.const.Presentation.S_MULTIMETER,
|
||||
])
|
||||
not_v_types.extend([gateway.const.SetReq.V_STATUS, ])
|
||||
v_types = [member for member in gateway.const.SetReq
|
||||
if member.value not in not_v_types]
|
||||
|
||||
devices = {} # keep track of devices added to HA
|
||||
# Just assume celcius means that the user wants metric for now.
|
||||
# It may make more sense to make this a global config option in the future.
|
||||
is_metric = (hass.config.temperature_unit == TEMP_CELCIUS)
|
||||
|
||||
def sensor_update(update_type, nid):
|
||||
""" Callback for sensor updates from the MySensors gateway. """
|
||||
_LOGGER.info("sensor_update %s: node %s", update_type, nid)
|
||||
sensor = gateway.sensors[nid]
|
||||
if sensor.sketch_name is None:
|
||||
return
|
||||
if nid not in devices:
|
||||
devices[nid] = {}
|
||||
|
||||
node = devices[nid]
|
||||
new_devices = []
|
||||
for child_id, child in sensor.children.items():
|
||||
if child_id not in node:
|
||||
node[child_id] = {}
|
||||
for value_type, value in child.values.items():
|
||||
if value_type not in node[child_id]:
|
||||
name = '{} {}.{}'.format(sensor.sketch_name, nid, child.id)
|
||||
node[child_id][value_type] = \
|
||||
MySensorsNodeValue(
|
||||
nid, child_id, name, value_type, is_metric, const)
|
||||
new_devices.append(node[child_id][value_type])
|
||||
else:
|
||||
node[child_id][value_type].update_sensor(
|
||||
value, sensor.battery_level)
|
||||
|
||||
if new_devices:
|
||||
_LOGGER.info("adding new devices: %s", new_devices)
|
||||
add_devices(new_devices)
|
||||
|
||||
port = config.get(CONF_PORT)
|
||||
if port is None:
|
||||
_LOGGER.error("Missing required key 'port'")
|
||||
return False
|
||||
|
||||
persistence = config.get(CONF_PERSISTENCE, True)
|
||||
persistence_file = config.get(CONF_PERSISTENCE_FILE,
|
||||
hass.config.path('mysensors.pickle'))
|
||||
version = config.get(CONF_VERSION, '1.4')
|
||||
|
||||
gateway = mysensors.SerialGateway(port, sensor_update,
|
||||
persistence=persistence,
|
||||
persistence_file=persistence_file,
|
||||
protocol_version=version)
|
||||
gateway.metric = is_metric
|
||||
gateway.debug = config.get(CONF_DEBUG, False)
|
||||
gateway.start()
|
||||
|
||||
if persistence:
|
||||
for nid in gateway.sensors:
|
||||
sensor_update('sensor_update', nid)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
lambda event: gateway.stop())
|
||||
devices = defaultdict(list)
|
||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||
s_types, v_types, devices, add_devices, MySensorsSensor))
|
||||
|
||||
|
||||
class MySensorsNodeValue(Entity):
|
||||
""" Represents the value of a MySensors child node. """
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
def __init__(self, node_id, child_id, name, value_type, metric, const):
|
||||
self._name = name
|
||||
class MySensorsSensor(Entity):
|
||||
"""Represent the value of a MySensors child node."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
|
||||
def __init__(self, gateway, node_id, child_id, name, value_type):
|
||||
"""Setup class attributes on instantiation.
|
||||
|
||||
Args:
|
||||
gateway (GatewayWrapper): Gateway object.
|
||||
node_id (str): Id of node.
|
||||
child_id (str): Id of child.
|
||||
name (str): Entity name.
|
||||
value_type (str): Value type of child. Value is entity state.
|
||||
|
||||
Attributes:
|
||||
gateway (GatewayWrapper): Gateway object.
|
||||
node_id (str): Id of node.
|
||||
child_id (str): Id of child.
|
||||
_name (str): Entity name.
|
||||
value_type (str): Value type of child. Value is entity state.
|
||||
battery_level (int): Node battery level.
|
||||
_values (dict): Child values. Non state values set as state attributes.
|
||||
"""
|
||||
self.gateway = gateway
|
||||
self.node_id = node_id
|
||||
self.child_id = child_id
|
||||
self.battery_level = 0
|
||||
self._name = name
|
||||
self.value_type = value_type
|
||||
self.metric = metric
|
||||
self._value = ''
|
||||
self.const = const
|
||||
self.battery_level = 0
|
||||
self._values = {}
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" MySensor gateway pushes its state to HA. """
|
||||
"""MySensor gateway pushes its state to HA."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of this sensor. """
|
||||
"""The name of this entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._value
|
||||
"""Return the state of the device."""
|
||||
if not self._values:
|
||||
return ''
|
||||
return self._values[self.value_type]
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Unit of measurement of this entity. """
|
||||
if self.value_type == self.const.SetReq.V_TEMP:
|
||||
return TEMP_CELCIUS if self.metric else TEMP_FAHRENHEIT
|
||||
elif self.value_type == self.const.SetReq.V_HUM or \
|
||||
self.value_type == self.const.SetReq.V_DIMMER or \
|
||||
self.value_type == self.const.SetReq.V_LIGHT_LEVEL:
|
||||
"""Unit of measurement of this entity."""
|
||||
# pylint:disable=too-many-return-statements
|
||||
if self.value_type == self.gateway.const.SetReq.V_TEMP:
|
||||
return TEMP_CELCIUS if self.gateway.metric else TEMP_FAHRENHEIT
|
||||
elif self.value_type == self.gateway.const.SetReq.V_HUM or \
|
||||
self.value_type == self.gateway.const.SetReq.V_DIMMER or \
|
||||
self.value_type == self.gateway.const.SetReq.V_PERCENTAGE or \
|
||||
self.value_type == self.gateway.const.SetReq.V_LIGHT_LEVEL:
|
||||
return '%'
|
||||
elif self.value_type == self.gateway.const.SetReq.V_WATT:
|
||||
return 'W'
|
||||
elif self.value_type == self.gateway.const.SetReq.V_KWH:
|
||||
return 'kWh'
|
||||
elif self.value_type == self.gateway.const.SetReq.V_VOLTAGE:
|
||||
return 'V'
|
||||
elif self.value_type == self.gateway.const.SetReq.V_CURRENT:
|
||||
return 'A'
|
||||
elif self.value_type == self.gateway.const.SetReq.V_IMPEDANCE:
|
||||
return 'ohm'
|
||||
elif self.gateway.const.SetReq.V_UNIT_PREFIX in self._values:
|
||||
return self._values[self.gateway.const.SetReq.V_UNIT_PREFIX]
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
device_attr = dict(self._values)
|
||||
device_attr.pop(self.value_type, None)
|
||||
return device_attr
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns the state attributes. """
|
||||
return {
|
||||
ATTR_NODE_ID: self.node_id,
|
||||
ATTR_CHILD_ID: self.child_id,
|
||||
"""Return the state attributes."""
|
||||
data = {
|
||||
mysensors.ATTR_PORT: self.gateway.port,
|
||||
mysensors.ATTR_NODE_ID: self.node_id,
|
||||
mysensors.ATTR_CHILD_ID: self.child_id,
|
||||
ATTR_BATTERY_LEVEL: self.battery_level,
|
||||
}
|
||||
|
||||
def update_sensor(self, value, battery_level):
|
||||
""" Update a sensor with the latest value from the controller. """
|
||||
_LOGGER.info("%s value = %s", self._name, value)
|
||||
if self.value_type == self.const.SetReq.V_TRIPPED or \
|
||||
self.value_type == self.const.SetReq.V_ARMED:
|
||||
self._value = STATE_ON if int(value) == 1 else STATE_OFF
|
||||
else:
|
||||
self._value = value
|
||||
self.battery_level = battery_level
|
||||
self.update_ha_state()
|
||||
device_attr = self.device_state_attributes
|
||||
|
||||
if device_attr is not None:
|
||||
data.update(device_attr)
|
||||
|
||||
return data
|
||||
|
||||
def update(self):
|
||||
"""Update the controller with the latest values from a sensor."""
|
||||
node = self.gateway.sensors[self.node_id]
|
||||
child = node.children[self.child_id]
|
||||
for value_type, value in child.values.items():
|
||||
_LOGGER.info(
|
||||
"%s: value_type %s, value = %s", self._name, value_type, value)
|
||||
if value_type == self.gateway.const.SetReq.V_TRIPPED:
|
||||
self._values[value_type] = STATE_ON if int(
|
||||
value) == 1 else STATE_OFF
|
||||
else:
|
||||
self._values[value_type] = value
|
||||
|
||||
self.battery_level = node.battery_level
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
"""
|
||||
homeassistant.components.sensor.netatmo
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
NetAtmo Weather Service service.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.netatmo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from homeassistant.components.sensor import DOMAIN
|
||||
from homeassistant.const import (CONF_API_KEY, CONF_USERNAME, CONF_PASSWORD,
|
||||
TEMP_CELCIUS)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/HydrelioxGitHub/netatmo-api-python/archive/'
|
||||
'43ff238a0122b0939a0dc4e8836b6782913fb6e2.zip'
|
||||
'#lnetatmo==0.4.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'temperature': ['Temperature', TEMP_CELCIUS],
|
||||
'co2': ['CO2', 'ppm'],
|
||||
'pressure': ['Pressure', 'mbar'],
|
||||
'noise': ['Noise', 'dB'],
|
||||
'humidity': ['Humidity', '%']
|
||||
}
|
||||
|
||||
CONF_SECRET_KEY = 'secret_key'
|
||||
ATTR_MODULE = 'modules'
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
# NetAtmo Data is uploaded to server every 10mn
|
||||
# so this time should not be under
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the NetAtmo sensor. """
|
||||
|
||||
if not validate_config({DOMAIN: config},
|
||||
{DOMAIN: [CONF_API_KEY,
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_SECRET_KEY]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
import lnetatmo
|
||||
|
||||
authorization = lnetatmo.ClientAuth(config.get(CONF_API_KEY, None),
|
||||
config.get(CONF_SECRET_KEY, None),
|
||||
config.get(CONF_USERNAME, None),
|
||||
config.get(CONF_PASSWORD, None))
|
||||
|
||||
if not authorization:
|
||||
_LOGGER.error(
|
||||
"Connection error "
|
||||
"Please check your settings for NatAtmo API.")
|
||||
return False
|
||||
|
||||
data = NetAtmoData(authorization)
|
||||
|
||||
dev = []
|
||||
try:
|
||||
# Iterate each module
|
||||
for module_name, monitored_conditions in config[ATTR_MODULE].items():
|
||||
# Test if module exist """
|
||||
if module_name not in data.get_module_names():
|
||||
_LOGGER.error('Module name: "%s" not found', module_name)
|
||||
continue
|
||||
# Only create sensor for monitored """
|
||||
for variable in monitored_conditions:
|
||||
if variable not in SENSOR_TYPES:
|
||||
_LOGGER.error('Sensor type: "%s" does not exist', variable)
|
||||
else:
|
||||
dev.append(
|
||||
NetAtmoSensor(data, module_name, variable))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class NetAtmoSensor(Entity):
|
||||
""" Implements a NetAtmo sensor. """
|
||||
|
||||
def __init__(self, netatmo_data, module_name, sensor_type):
|
||||
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._state = None
|
||||
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Unit of measurement of this entity, if any. """
|
||||
return self._unit_of_measurement
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def update(self):
|
||||
""" Gets the latest data from NetAtmo API and updates the states. """
|
||||
|
||||
self.netatmo_data.update()
|
||||
data = self.netatmo_data.data[self.module_name]
|
||||
|
||||
if self.type == 'temperature':
|
||||
self._state = round(data['Temperature'], 1)
|
||||
elif self.type == 'humidity':
|
||||
self._state = data['Humidity']
|
||||
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)
|
||||
|
||||
|
||||
class NetAtmoData(object):
|
||||
""" Gets the latest data from NetAtmo. """
|
||||
|
||||
def __init__(self, auth):
|
||||
self.auth = auth
|
||||
self.data = None
|
||||
|
||||
def get_module_names(self):
|
||||
""" Return all module available on the API as a list. """
|
||||
self.update()
|
||||
return self.data.keys()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Call the NetAtmo API to update the data. """
|
||||
import lnetatmo
|
||||
# Gets the latest data from NetAtmo. """
|
||||
dev_list = lnetatmo.DeviceList(self.auth)
|
||||
self.data = dev_list.lastData(exclude=3600)
|
|
@ -13,14 +13,14 @@ from homeassistant.util import Throttle
|
|||
from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['pyowm==2.2.1']
|
||||
REQUIREMENTS = ['pyowm==2.3.0']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES = {
|
||||
'weather': ['Condition', ''],
|
||||
'temperature': ['Temperature', ''],
|
||||
'wind_speed': ['Wind speed', 'm/s'],
|
||||
'humidity': ['Humidity', '%'],
|
||||
'pressure': ['Pressure', 'hPa'],
|
||||
'pressure': ['Pressure', 'mbar'],
|
||||
'clouds': ['Cloud coverage', '%'],
|
||||
'rain': ['Rain', 'mm'],
|
||||
'snow': ['Snow', 'mm']
|
||||
|
|
|
@ -10,7 +10,7 @@ from datetime import timedelta
|
|||
import logging
|
||||
import requests
|
||||
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.const import (CONF_VALUE_TEMPLATE, STATE_UNKNOWN)
|
||||
from homeassistant.util import template, Throttle
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
|
@ -26,48 +26,21 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
|||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the REST sensor. """
|
||||
|
||||
use_get = False
|
||||
use_post = False
|
||||
|
||||
resource = config.get('resource', None)
|
||||
method = config.get('method', DEFAULT_METHOD)
|
||||
payload = config.get('payload', None)
|
||||
verify_ssl = config.get('verify_ssl', True)
|
||||
|
||||
if method == 'GET':
|
||||
use_get = True
|
||||
elif method == 'POST':
|
||||
use_post = True
|
||||
rest = RestData(method, resource, payload, verify_ssl)
|
||||
rest.update()
|
||||
|
||||
try:
|
||||
if use_get:
|
||||
response = requests.get(resource, timeout=10, verify=verify_ssl)
|
||||
elif use_post:
|
||||
response = requests.post(resource, data=payload, timeout=10,
|
||||
verify=verify_ssl)
|
||||
if not response.ok:
|
||||
_LOGGER.error('Response status is "%s"', response.status_code)
|
||||
return False
|
||||
except requests.exceptions.MissingSchema:
|
||||
_LOGGER.error('Missing resource or schema in configuration. '
|
||||
'Add http:// to your URL.')
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('No route to resource/endpoint. '
|
||||
'Please check the URL in the configuration file.')
|
||||
if rest.data is None:
|
||||
_LOGGER.error('Unable to fetch Rest data')
|
||||
return False
|
||||
|
||||
if use_get:
|
||||
rest = RestDataGet(resource, verify_ssl)
|
||||
elif use_post:
|
||||
rest = RestDataPost(resource, payload, verify_ssl)
|
||||
|
||||
add_devices([RestSensor(hass,
|
||||
rest,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get('unit_of_measurement'),
|
||||
config.get(CONF_VALUE_TEMPLATE))])
|
||||
add_devices([RestSensor(
|
||||
hass, rest, config.get('name', DEFAULT_NAME),
|
||||
config.get('unit_of_measurement'), config.get(CONF_VALUE_TEMPLATE))])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
|
@ -78,7 +51,7 @@ class RestSensor(Entity):
|
|||
self._hass = hass
|
||||
self.rest = rest
|
||||
self._name = name
|
||||
self._state = 'n/a'
|
||||
self._state = STATE_UNKNOWN
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._value_template = value_template
|
||||
self.update()
|
||||
|
@ -103,57 +76,33 @@ class RestSensor(Entity):
|
|||
self.rest.update()
|
||||
value = self.rest.data
|
||||
|
||||
if 'error' in value:
|
||||
self._state = value['error']
|
||||
else:
|
||||
if self._value_template is not None:
|
||||
value = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, value, 'N/A')
|
||||
self._state = value
|
||||
if value is None:
|
||||
value = STATE_UNKNOWN
|
||||
elif self._value_template is not None:
|
||||
value = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, value, STATE_UNKNOWN)
|
||||
|
||||
self._state = value
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class RestDataGet(object):
|
||||
""" Class for handling the data retrieval with GET method. """
|
||||
class RestData(object):
|
||||
"""Class for handling the data retrieval."""
|
||||
|
||||
def __init__(self, resource, verify_ssl):
|
||||
self._resource = resource
|
||||
def __init__(self, method, resource, data, verify_ssl):
|
||||
self._request = requests.Request(method, resource, data=data).prepare()
|
||||
self._verify_ssl = verify_ssl
|
||||
self.data = dict()
|
||||
self.data = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from REST service with GET method. """
|
||||
try:
|
||||
response = requests.get(self._resource, timeout=10,
|
||||
verify=self._verify_ssl)
|
||||
if 'error' in self.data:
|
||||
del self.data['error']
|
||||
with requests.Session() as sess:
|
||||
response = sess.send(self._request, timeout=10,
|
||||
verify=self._verify_ssl)
|
||||
|
||||
self.data = response.text
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to resource/endpoint.")
|
||||
self.data['error'] = 'N/A'
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class RestDataPost(object):
|
||||
""" Class for handling the data retrieval with POST method. """
|
||||
|
||||
def __init__(self, resource, payload, verify_ssl):
|
||||
self._resource = resource
|
||||
self._payload = payload
|
||||
self._verify_ssl = verify_ssl
|
||||
self.data = dict()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from REST service with POST method. """
|
||||
try:
|
||||
response = requests.post(self._resource, data=self._payload,
|
||||
timeout=10, verify=self._verify_ssl)
|
||||
if 'error' in self.data:
|
||||
del self.data['error']
|
||||
self.data = response.text
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to resource/endpoint.")
|
||||
self.data['error'] = 'N/A'
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.error("Error fetching data: %s", self._request)
|
||||
self.data = None
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
"""
|
||||
homeassistant.components.sensor.rpi_gpio
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to configure a binary state sensor using RPi GPIO.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.rpi_gpio/
|
||||
"""
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from homeassistant.const import (DEVICE_DEFAULT_NAME,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
DEFAULT_PULL_MODE = "UP"
|
||||
DEFAULT_VALUE_HIGH = "HIGH"
|
||||
DEFAULT_VALUE_LOW = "LOW"
|
||||
DEFAULT_BOUNCETIME = 50
|
||||
|
||||
REQUIREMENTS = ['RPi.GPIO==0.5.11']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Raspberry PI GPIO ports. """
|
||||
import RPi.GPIO as GPIO
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
|
||||
sensors = []
|
||||
pull_mode = config.get('pull_mode', DEFAULT_PULL_MODE)
|
||||
value_high = config.get('value_high', DEFAULT_VALUE_HIGH)
|
||||
value_low = config.get('value_low', DEFAULT_VALUE_LOW)
|
||||
bouncetime = config.get('bouncetime', DEFAULT_BOUNCETIME)
|
||||
ports = config.get('ports')
|
||||
for port_num, port_name in ports.items():
|
||||
sensors.append(RPiGPIOSensor(
|
||||
port_name, port_num, pull_mode,
|
||||
value_high, value_low, bouncetime))
|
||||
add_devices(sensors)
|
||||
|
||||
def cleanup_gpio(event):
|
||||
""" Stuff to do before stop home assistant. """
|
||||
# pylint: disable=no-member
|
||||
GPIO.cleanup()
|
||||
|
||||
def prepare_gpio(event):
|
||||
""" Stuff to do when home assistant starts. """
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
class RPiGPIOSensor(Entity):
|
||||
""" Sets up the Raspberry PI GPIO ports. """
|
||||
def __init__(self, port_name, port_num, pull_mode,
|
||||
value_high, value_low, bouncetime):
|
||||
# pylint: disable=no-member
|
||||
import RPi.GPIO as GPIO
|
||||
self._name = port_name or DEVICE_DEFAULT_NAME
|
||||
self._port = port_num
|
||||
self._pull = GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP
|
||||
self._vhigh = value_high
|
||||
self._vlow = value_low
|
||||
self._bouncetime = bouncetime
|
||||
GPIO.setup(self._port, GPIO.IN, pull_up_down=self._pull)
|
||||
self._state = self._vhigh if GPIO.input(self._port) else self._vlow
|
||||
|
||||
def edge_callback(channel):
|
||||
""" port changed state """
|
||||
# pylint: disable=no-member
|
||||
self._state = self._vhigh if GPIO.input(channel) else self._vlow
|
||||
self.update_ha_state()
|
||||
|
||||
GPIO.add_event_detect(
|
||||
self._port,
|
||||
GPIO.BOTH,
|
||||
callback=edge_callback,
|
||||
bouncetime=self._bouncetime)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the sensor. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the entity. """
|
||||
return self._state
|
|
@ -14,25 +14,25 @@ from homeassistant.const import STATE_ON, STATE_OFF
|
|||
|
||||
REQUIREMENTS = ['psutil==3.2.2']
|
||||
SENSOR_TYPES = {
|
||||
'disk_use_percent': ['Disk Use', '%'],
|
||||
'disk_use': ['Disk Use', 'GiB'],
|
||||
'disk_free': ['Disk Free', 'GiB'],
|
||||
'memory_use_percent': ['RAM Use', '%'],
|
||||
'memory_use': ['RAM Use', 'MiB'],
|
||||
'memory_free': ['RAM Free', 'MiB'],
|
||||
'processor_use': ['CPU Use', '%'],
|
||||
'process': ['Process', ''],
|
||||
'swap_use_percent': ['Swap Use', '%'],
|
||||
'swap_use': ['Swap Use', 'GiB'],
|
||||
'swap_free': ['Swap Free', 'GiB'],
|
||||
'network_out': ['Sent', 'MiB'],
|
||||
'network_in': ['Recieved', 'MiB'],
|
||||
'packets_out': ['Packets sent', ''],
|
||||
'packets_in': ['Packets recieved', ''],
|
||||
'ipv4_address': ['IPv4 address', ''],
|
||||
'ipv6_address': ['IPv6 address', ''],
|
||||
'last_boot': ['Last Boot', ''],
|
||||
'since_last_boot': ['Since Last Boot', '']
|
||||
'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'],
|
||||
'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'],
|
||||
'disk_free': ['Disk Free', 'GiB', 'mdi:harddisk'],
|
||||
'memory_use_percent': ['RAM Use', '%', 'mdi:memory'],
|
||||
'memory_use': ['RAM Use', 'MiB', 'mdi:memory'],
|
||||
'memory_free': ['RAM Free', 'MiB', 'mdi:memory'],
|
||||
'processor_use': ['CPU Use', '%', 'mdi:memory'],
|
||||
'process': ['Process', '', 'mdi:memory'],
|
||||
'swap_use_percent': ['Swap Use', '%', 'mdi:harddisk'],
|
||||
'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'],
|
||||
'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'],
|
||||
'network_out': ['Sent', 'MiB', 'mdi:server-network'],
|
||||
'network_in': ['Recieved', 'MiB', 'mdi:server-network'],
|
||||
'packets_out': ['Packets sent', '', 'mdi:server-network'],
|
||||
'packets_in': ['Packets recieved', '', 'mdi:server-network'],
|
||||
'ipv4_address': ['IPv4 address', '', 'mdi:server-network'],
|
||||
'ipv6_address': ['IPv6 address', '', 'mdi:server-network'],
|
||||
'last_boot': ['Last Boot', '', 'mdi:clock'],
|
||||
'since_last_boot': ['Since Last Boot', '', 'mdi:clock']
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -69,6 +69,10 @@ class SystemMonitorSensor(Entity):
|
|||
def name(self):
|
||||
return self._name.rstrip()
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
return SENSOR_TYPES[self.type][2]
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
"""
|
||||
homeassistant.components.sensor.tellduslive
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Shows sensor values from Tellstick Net/Telstick Live.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.tellduslive/
|
||||
|
||||
"""
|
||||
import logging
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from homeassistant.const import TEMP_CELCIUS, ATTR_BATTERY_LEVEL
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components import tellduslive
|
||||
|
||||
ATTR_LAST_UPDATED = "time_last_updated"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['tellduslive']
|
||||
|
||||
SENSOR_TYPE_TEMP = "temp"
|
||||
SENSOR_TYPE_HUMIDITY = "humidity"
|
||||
SENSOR_TYPE_RAINRATE = "rrate"
|
||||
SENSOR_TYPE_RAINTOTAL = "rtot"
|
||||
SENSOR_TYPE_WINDDIRECTION = "wdir"
|
||||
SENSOR_TYPE_WINDAVERAGE = "wavg"
|
||||
SENSOR_TYPE_WINDGUST = "wgust"
|
||||
SENSOR_TYPE_WATT = "watt"
|
||||
|
||||
SENSOR_TYPES = {
|
||||
SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"],
|
||||
SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"],
|
||||
SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"],
|
||||
SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"],
|
||||
SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""],
|
||||
SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""],
|
||||
SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""],
|
||||
SENSOR_TYPE_WATT: ['Watt', 'W', ""],
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up Tellstick sensors. """
|
||||
sensors = tellduslive.NETWORK.get_sensors()
|
||||
devices = []
|
||||
|
||||
for component in sensors:
|
||||
for sensor in component["data"]:
|
||||
# one component can have more than one sensor
|
||||
# (e.g. both humidity and temperature)
|
||||
devices.append(TelldusLiveSensor(component["id"],
|
||||
component["name"],
|
||||
sensor["name"]))
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class TelldusLiveSensor(Entity):
|
||||
""" Represents a Telldus Live sensor. """
|
||||
|
||||
def __init__(self, sensor_id, sensor_name, sensor_type):
|
||||
self._sensor_id = sensor_id
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0]
|
||||
self._last_update = None
|
||||
self._battery_level = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attrs = dict()
|
||||
if self._battery_level is not None:
|
||||
attrs[ATTR_BATTERY_LEVEL] = self._battery_level
|
||||
if self._last_update is not None:
|
||||
attrs[ATTR_LAST_UPDATED] = self._last_update
|
||||
return attrs
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
return SENSOR_TYPES[self._sensor_type][1]
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
return SENSOR_TYPES[self._sensor_type][2]
|
||||
|
||||
def update(self):
|
||||
values = tellduslive.NETWORK.get_sensor_value(self._sensor_id,
|
||||
self._sensor_type)
|
||||
self._state, self._battery_level, self._last_update = values
|
||||
|
||||
self._state = float(self._state)
|
||||
if self._sensor_type == SENSOR_TYPE_TEMP:
|
||||
self._state = round(self._state, 1)
|
||||
elif self._sensor_type == SENSOR_TYPE_HUMIDITY:
|
||||
self._state = int(round(self._state))
|
||||
|
||||
self._battery_level = round(self._battery_level * 100 / 255) # percent
|
||||
|
||||
self._last_update = str(datetime.fromtimestamp(self._last_update))
|
|
@ -13,11 +13,9 @@ import homeassistant.util.dt as dt_util
|
|||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME,
|
||||
TEMP_CELCIUS, TEMP_FAHRENHEIT)
|
||||
TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/'
|
||||
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
|
||||
'#python-vera==0.1.1']
|
||||
REQUIREMENTS = ['pyvera==0.2.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -37,8 +35,20 @@ def get_devices(hass, config):
|
|||
|
||||
device_data = config.get('device_data', {})
|
||||
|
||||
vera_controller = veraApi.VeraController(base_url)
|
||||
categories = ['Temperature Sensor', 'Light Sensor', 'Sensor']
|
||||
vera_controller, created = veraApi.init_controller(base_url)
|
||||
|
||||
if created:
|
||||
def stop_subscription(event):
|
||||
""" Shutdown Vera subscriptions and subscription thread on exit"""
|
||||
_LOGGER.info("Shutting down subscriptions.")
|
||||
vera_controller.stop()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription)
|
||||
|
||||
categories = ['Temperature Sensor',
|
||||
'Light Sensor',
|
||||
'Humidity Sensor',
|
||||
'Sensor']
|
||||
devices = []
|
||||
try:
|
||||
devices = vera_controller.get_devices(categories)
|
||||
|
@ -49,11 +59,12 @@ def get_devices(hass, config):
|
|||
|
||||
vera_sensors = []
|
||||
for device in devices:
|
||||
extra_data = device_data.get(device.deviceId, {})
|
||||
extra_data = device_data.get(device.device_id, {})
|
||||
exclude = extra_data.get('exclude', False)
|
||||
|
||||
if exclude is not True:
|
||||
vera_sensors.append(VeraSensor(device, extra_data))
|
||||
vera_sensors.append(
|
||||
VeraSensor(device, vera_controller, extra_data))
|
||||
|
||||
return vera_sensors
|
||||
|
||||
|
@ -66,8 +77,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class VeraSensor(Entity):
|
||||
""" Represents a Vera Sensor. """
|
||||
|
||||
def __init__(self, vera_device, extra_data=None):
|
||||
def __init__(self, vera_device, controller, extra_data=None):
|
||||
self.vera_device = vera_device
|
||||
self.controller = controller
|
||||
self.extra_data = extra_data
|
||||
if self.extra_data and self.extra_data.get('name'):
|
||||
self._name = self.extra_data.get('name')
|
||||
|
@ -76,8 +88,15 @@ class VeraSensor(Entity):
|
|||
self.current_value = ''
|
||||
self._temperature_units = None
|
||||
|
||||
self.controller.register(vera_device, self._update_callback)
|
||||
self.update()
|
||||
|
||||
def _update_callback(self, _device):
|
||||
""" Called by the vera device callback to update state. """
|
||||
self.update_ha_state(True)
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state)
|
||||
return "%s %s %s" % (self.name, self.vera_device.device_id, self.state)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
@ -91,7 +110,12 @@ class VeraSensor(Entity):
|
|||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Unit of measurement of this entity, if any. """
|
||||
return self._temperature_units
|
||||
if self.vera_device.category == "Temperature Sensor":
|
||||
return self._temperature_units
|
||||
elif self.vera_device.category == "Light Sensor":
|
||||
return 'lux'
|
||||
elif self.vera_device.category == "Humidity Sensor":
|
||||
return '%'
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
|
@ -100,28 +124,33 @@ class VeraSensor(Entity):
|
|||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
||||
|
||||
if self.vera_device.is_armable:
|
||||
armed = self.vera_device.refresh_value('Armed')
|
||||
attr[ATTR_ARMED] = 'True' if armed == '1' else 'False'
|
||||
armed = self.vera_device.is_armed
|
||||
attr[ATTR_ARMED] = 'True' if armed else 'False'
|
||||
|
||||
if self.vera_device.is_trippable:
|
||||
last_tripped = self.vera_device.refresh_value('LastTrip')
|
||||
last_tripped = self.vera_device.last_trip
|
||||
if last_tripped is not None:
|
||||
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
|
||||
attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str(
|
||||
utc_time)
|
||||
else:
|
||||
attr[ATTR_LAST_TRIP_TIME] = None
|
||||
tripped = self.vera_device.refresh_value('Tripped')
|
||||
attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False'
|
||||
tripped = self.vera_device.is_tripped
|
||||
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
|
||||
|
||||
attr['Vera Device Id'] = self.vera_device.vera_device_id
|
||||
return attr
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Tells Home Assistant not to poll this entity. """
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
if self.vera_device.category == "Temperature Sensor":
|
||||
self.vera_device.refresh_value('CurrentTemperature')
|
||||
current_temp = self.vera_device.get_value('CurrentTemperature')
|
||||
vera_temp_units = self.vera_device.veraController.temperature_units
|
||||
current_temp = self.vera_device.temperature
|
||||
vera_temp_units = (
|
||||
self.vera_device.vera_controller.temperature_units)
|
||||
|
||||
if vera_temp_units == 'F':
|
||||
self._temperature_units = TEMP_FAHRENHEIT
|
||||
|
@ -137,10 +166,11 @@ class VeraSensor(Entity):
|
|||
|
||||
self.current_value = current_temp
|
||||
elif self.vera_device.category == "Light Sensor":
|
||||
self.vera_device.refresh_value('CurrentLevel')
|
||||
self.current_value = self.vera_device.get_value('CurrentLevel')
|
||||
self.current_value = self.vera_device.light
|
||||
elif self.vera_device.category == "Humidity Sensor":
|
||||
self.current_value = self.vera_device.humidity
|
||||
elif self.vera_device.category == "Sensor":
|
||||
tripped = self.vera_device.refresh_value('Tripped')
|
||||
self.current_value = 'Tripped' if tripped == '1' else 'Not Tripped'
|
||||
tripped = self.vera_device.is_tripped
|
||||
self.current_value = 'Tripped' if tripped else 'Not Tripped'
|
||||
else:
|
||||
self.current_value = 'Unknown'
|
||||
|
|
|
@ -27,14 +27,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
sensors.extend([
|
||||
VerisureThermometer(value)
|
||||
for value in verisure.get_climate_status().values()
|
||||
for value in verisure.CLIMATE_STATUS.values()
|
||||
if verisure.SHOW_THERMOMETERS and
|
||||
hasattr(value, 'temperature') and value.temperature
|
||||
])
|
||||
|
||||
sensors.extend([
|
||||
VerisureHygrometer(value)
|
||||
for value in verisure.get_climate_status().values()
|
||||
for value in verisure.CLIMATE_STATUS.values()
|
||||
if verisure.SHOW_HYGROMETERS and
|
||||
hasattr(value, 'humidity') and value.humidity
|
||||
])
|
||||
|
@ -47,20 +47,19 @@ class VerisureThermometer(Entity):
|
|||
|
||||
def __init__(self, climate_status):
|
||||
self._id = climate_status.id
|
||||
self._device = verisure.MY_PAGES.DEVICE_CLIMATE
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return '{} {}'.format(
|
||||
verisure.STATUS[self._device][self._id].location,
|
||||
verisure.CLIMATE_STATUS[self._id].location,
|
||||
"Temperature")
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
# remove ° character
|
||||
return verisure.STATUS[self._device][self._id].temperature[:-1]
|
||||
return verisure.CLIMATE_STATUS[self._id].temperature[:-1]
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
|
@ -69,7 +68,7 @@ class VerisureThermometer(Entity):
|
|||
|
||||
def update(self):
|
||||
''' update sensor '''
|
||||
verisure.update()
|
||||
verisure.update_climate()
|
||||
|
||||
|
||||
class VerisureHygrometer(Entity):
|
||||
|
@ -77,20 +76,19 @@ class VerisureHygrometer(Entity):
|
|||
|
||||
def __init__(self, climate_status):
|
||||
self._id = climate_status.id
|
||||
self._device = verisure.MY_PAGES.DEVICE_CLIMATE
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return '{} {}'.format(
|
||||
verisure.STATUS[self._device][self._id].location,
|
||||
verisure.CLIMATE_STATUS[self._id].location,
|
||||
"Humidity")
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
# remove % character
|
||||
return verisure.STATUS[self._device][self._id].humidity[:-1]
|
||||
return verisure.CLIMATE_STATUS[self._id].humidity[:-1]
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
|
@ -99,4 +97,4 @@ class VerisureHygrometer(Entity):
|
|||
|
||||
def update(self):
|
||||
''' update sensor '''
|
||||
verisure.update()
|
||||
verisure.update_climate()
|
||||
|
|
|
@ -11,7 +11,7 @@ import logging
|
|||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.3.1']
|
||||
REQUIREMENTS = ['python-wink==0.4.1']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
"""
|
||||
homeassistant.components.sensor.yr
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Yr.no weather service.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.yr/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_PICTURE
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import location, dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUIREMENTS = ['xmltodict']
|
||||
|
||||
# Sensor types are defined like so:
|
||||
SENSOR_TYPES = {
|
||||
'symbol': ['Symbol', ''],
|
||||
'precipitation': ['Condition', 'mm'],
|
||||
'temperature': ['Temperature', '°C'],
|
||||
'windSpeed': ['Wind speed', 'm/s'],
|
||||
'pressure': ['Pressure', 'mbar'],
|
||||
'windDirection': ['Wind direction', '°'],
|
||||
'humidity': ['Humidity', '%'],
|
||||
'fog': ['Fog', '%'],
|
||||
'cloudiness': ['Cloudiness', '%'],
|
||||
'lowClouds': ['Low clouds', '%'],
|
||||
'mediumClouds': ['Medium clouds', '%'],
|
||||
'highClouds': ['High clouds', '%'],
|
||||
'dewpointTemperature': ['Dewpoint temperature', '°C'],
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the Yr.no sensor. """
|
||||
|
||||
if None in (hass.config.latitude, hass.config.longitude):
|
||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||
return False
|
||||
|
||||
elevation = config.get('elevation')
|
||||
|
||||
if elevation is None:
|
||||
elevation = location.elevation(hass.config.latitude,
|
||||
hass.config.longitude)
|
||||
|
||||
coordinates = dict(lat=hass.config.latitude,
|
||||
lon=hass.config.longitude, msl=elevation)
|
||||
|
||||
weather = YrData(coordinates)
|
||||
|
||||
dev = []
|
||||
if 'monitored_conditions' in config:
|
||||
for variable in config['monitored_conditions']:
|
||||
if variable not in SENSOR_TYPES:
|
||||
_LOGGER.error('Sensor type: "%s" does not exist', variable)
|
||||
else:
|
||||
dev.append(YrSensor(variable, weather))
|
||||
|
||||
# add symbol as default sensor
|
||||
if len(dev) == 0:
|
||||
dev.append(YrSensor("symbol", weather))
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class YrSensor(Entity):
|
||||
""" Implements an Yr.no sensor. """
|
||||
|
||||
def __init__(self, sensor_type, weather):
|
||||
self.client_name = 'yr'
|
||||
self._name = SENSOR_TYPES[sensor_type][0]
|
||||
self.type = sensor_type
|
||||
self._state = None
|
||||
self._weather = weather
|
||||
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
|
||||
self._update = None
|
||||
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return '{} {}'.format(self.client_name, self._name)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns state attributes. """
|
||||
data = {
|
||||
'about': "Weather forecast from yr.no, delivered by the"
|
||||
" Norwegian Meteorological Institute and the NRK"
|
||||
}
|
||||
if self.type == 'symbol':
|
||||
symbol_nr = self._state
|
||||
data[ATTR_ENTITY_PICTURE] = \
|
||||
"http://api.met.no/weatherapi/weathericon/1.1/" \
|
||||
"?symbol={0};content_type=image/png".format(symbol_nr)
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Unit of measurement of this entity, if any. """
|
||||
return self._unit_of_measurement
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from yr.no and updates the states. """
|
||||
|
||||
now = dt_util.utcnow()
|
||||
# check if data should be updated
|
||||
if self._update is not None and now <= self._update:
|
||||
return
|
||||
|
||||
self._weather.update()
|
||||
|
||||
# find sensor
|
||||
for time_entry in self._weather.data['product']['time']:
|
||||
valid_from = dt_util.str_to_datetime(
|
||||
time_entry['@from'], "%Y-%m-%dT%H:%M:%SZ")
|
||||
valid_to = dt_util.str_to_datetime(
|
||||
time_entry['@to'], "%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
loc_data = time_entry['location']
|
||||
|
||||
if self.type not in loc_data or now >= valid_to:
|
||||
continue
|
||||
|
||||
self._update = valid_to
|
||||
|
||||
if self.type == 'precipitation' and valid_from < now:
|
||||
self._state = loc_data[self.type]['@value']
|
||||
break
|
||||
elif self.type == 'symbol' and valid_from < now:
|
||||
self._state = loc_data[self.type]['@number']
|
||||
break
|
||||
elif self.type == ('temperature', 'pressure', 'humidity',
|
||||
'dewpointTemperature'):
|
||||
self._state = loc_data[self.type]['@value']
|
||||
break
|
||||
elif self.type == 'windSpeed':
|
||||
self._state = loc_data[self.type]['@mps']
|
||||
break
|
||||
elif self.type == 'windDirection':
|
||||
self._state = float(loc_data[self.type]['@deg'])
|
||||
break
|
||||
elif self.type in ('fog', 'cloudiness', 'lowClouds',
|
||||
'mediumClouds', 'highClouds'):
|
||||
self._state = loc_data[self.type]['@percent']
|
||||
break
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class YrData(object):
|
||||
""" Gets the latest data and updates the states. """
|
||||
|
||||
def __init__(self, coordinates):
|
||||
self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \
|
||||
'lat={lat};lon={lon};msl={msl}'.format(**coordinates)
|
||||
|
||||
self._nextrun = None
|
||||
self.data = {}
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from yr.no """
|
||||
# check if new will be available
|
||||
if self._nextrun is not None and dt_util.utcnow() <= self._nextrun:
|
||||
return
|
||||
try:
|
||||
with requests.Session() as sess:
|
||||
response = sess.get(self._url)
|
||||
except requests.RequestException:
|
||||
return
|
||||
if response.status_code != 200:
|
||||
return
|
||||
data = response.text
|
||||
|
||||
import xmltodict
|
||||
self.data = xmltodict.parse(data)['weatherdata']
|
||||
model = self.data['meta']['model']
|
||||
if '@nextrun' not in model:
|
||||
model = model[0]
|
||||
self._nextrun = dt_util.str_to_datetime(model['@nextrun'],
|
||||
"%Y-%m-%dT%H:%M:%SZ")
|
|
@ -74,6 +74,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
value.type == zwave.TYPE_DECIMAL):
|
||||
add_devices([ZWaveMultilevelSensor(value)])
|
||||
|
||||
elif value.command_class == zwave.COMMAND_CLASS_ALARM:
|
||||
add_devices([ZWaveAlarmSensor(value)])
|
||||
|
||||
|
||||
class ZWaveSensor(Entity):
|
||||
""" Represents a Z-Wave sensor. """
|
||||
|
@ -216,3 +219,19 @@ class ZWaveMultilevelSensor(ZWaveSensor):
|
|||
return TEMP_FAHRENHEIT
|
||||
else:
|
||||
return unit
|
||||
|
||||
|
||||
class ZWaveAlarmSensor(ZWaveSensor):
|
||||
""" A Z-wave sensor that sends Alarm alerts
|
||||
|
||||
Examples include certain Multisensors that have motion and
|
||||
vibration capabilities. Z-Wave defines various alarm types
|
||||
such as Smoke, Flood, Burglar, CarbonMonoxide, etc.
|
||||
|
||||
This wraps these alarms and allows you to use them to
|
||||
trigger things, etc.
|
||||
|
||||
COMMAND_CLASS_ALARM is what we get here.
|
||||
"""
|
||||
# Empty subclass for now. Allows for later customizations
|
||||
pass
|
||||
|
|
|
@ -8,10 +8,9 @@ https://home-assistant.io/components/sun/
|
|||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import urllib
|
||||
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util import location as location_util, dt as dt_util
|
||||
from homeassistant.helpers.event import (
|
||||
track_point_in_utc_time, track_utc_time_change)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
@ -19,7 +18,6 @@ from homeassistant.helpers.entity import Entity
|
|||
REQUIREMENTS = ['astral==0.8.1']
|
||||
DOMAIN = "sun"
|
||||
ENTITY_ID = "sun.sun"
|
||||
ENTITY_ID_ELEVATION = "sun.elevation"
|
||||
|
||||
CONF_ELEVATION = 'elevation'
|
||||
|
||||
|
@ -34,21 +32,21 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
""" Returns if the sun is currently up based on the statemachine. """
|
||||
"""Test if the sun is currently up based on the statemachine."""
|
||||
entity_id = entity_id or ENTITY_ID
|
||||
|
||||
return hass.states.is_state(entity_id, STATE_ABOVE_HORIZON)
|
||||
|
||||
|
||||
def next_setting(hass, entity_id=None):
|
||||
""" Returns the local datetime object of the next sun setting. """
|
||||
"""Local datetime object of the next sun setting."""
|
||||
utc_next = next_setting_utc(hass, entity_id)
|
||||
|
||||
return dt_util.as_local(utc_next) if utc_next else None
|
||||
|
||||
|
||||
def next_setting_utc(hass, entity_id=None):
|
||||
""" Returns the UTC datetime object of the next sun setting. """
|
||||
"""UTC datetime object of the next sun setting."""
|
||||
entity_id = entity_id or ENTITY_ID
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
|
@ -63,14 +61,14 @@ def next_setting_utc(hass, entity_id=None):
|
|||
|
||||
|
||||
def next_rising(hass, entity_id=None):
|
||||
""" Returns the local datetime object of the next sun rising. """
|
||||
"""Local datetime object of the next sun rising."""
|
||||
utc_next = next_rising_utc(hass, entity_id)
|
||||
|
||||
return dt_util.as_local(utc_next) if utc_next else None
|
||||
|
||||
|
||||
def next_rising_utc(hass, entity_id=None):
|
||||
""" Returns the UTC datetime object of the next sun rising. """
|
||||
"""UTC datetime object of the next sun rising."""
|
||||
entity_id = entity_id or ENTITY_ID
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
|
@ -85,7 +83,7 @@ def next_rising_utc(hass, entity_id=None):
|
|||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Tracks the state of the sun. """
|
||||
"""Track the state of the sun in HA."""
|
||||
if None in (hass.config.latitude, hass.config.longitude):
|
||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||
return False
|
||||
|
@ -111,21 +109,13 @@ def setup(hass, config):
|
|||
platform_config = config.get(DOMAIN, {})
|
||||
|
||||
elevation = platform_config.get(CONF_ELEVATION)
|
||||
if elevation is None:
|
||||
elevation = location_util.elevation(latitude, longitude)
|
||||
|
||||
from astral import Location, GoogleGeocoder
|
||||
from astral import Location
|
||||
|
||||
location = Location(('', '', latitude, longitude, hass.config.time_zone,
|
||||
elevation or 0))
|
||||
|
||||
if elevation is None:
|
||||
google = GoogleGeocoder()
|
||||
try:
|
||||
google._get_elevation(location) # pylint: disable=protected-access
|
||||
_LOGGER.info(
|
||||
'Retrieved elevation from Google: %s', location.elevation)
|
||||
except urllib.error.URLError:
|
||||
# If no internet connection available etc.
|
||||
pass
|
||||
elevation))
|
||||
|
||||
sun = Sun(hass, location)
|
||||
sun.point_in_time_listener(dt_util.utcnow())
|
||||
|
@ -134,7 +124,7 @@ def setup(hass, config):
|
|||
|
||||
|
||||
class Sun(Entity):
|
||||
""" Represents the Sun. """
|
||||
"""Represents the Sun."""
|
||||
|
||||
entity_id = ENTITY_ID
|
||||
|
||||
|
@ -167,12 +157,12 @@ class Sun(Entity):
|
|||
|
||||
@property
|
||||
def next_change(self):
|
||||
""" Returns the datetime when the next change to the state is. """
|
||||
"""Datetime when the next change to the state is."""
|
||||
return min(self.next_rising, self.next_setting)
|
||||
|
||||
@property
|
||||
def solar_elevation(self):
|
||||
""" Returns the angle the sun is above the horizon"""
|
||||
"""Angle the sun is above the horizon."""
|
||||
from astral import Astral
|
||||
return Astral().solar_elevation(
|
||||
dt_util.utcnow(),
|
||||
|
|
|
@ -17,7 +17,7 @@ from homeassistant.helpers.entity import ToggleEntity
|
|||
from homeassistant.const import (
|
||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
from homeassistant.components import (
|
||||
group, discovery, wink, isy994, verisure, zwave)
|
||||
group, discovery, wink, isy994, verisure, zwave, tellduslive, mysensors)
|
||||
|
||||
DOMAIN = 'switch'
|
||||
SCAN_INTERVAL = 30
|
||||
|
@ -40,6 +40,8 @@ DISCOVERY_PLATFORMS = {
|
|||
isy994.DISCOVER_SWITCHES: 'isy994',
|
||||
verisure.DISCOVER_SWITCHES: 'verisure',
|
||||
zwave.DISCOVER_SWITCHES: 'zwave',
|
||||
tellduslive.DISCOVER_SWITCHES: 'tellduslive',
|
||||
mysensors.DISCOVER_SWITCHES: 'mysensors',
|
||||
}
|
||||
|
||||
PROP_TO_ATTR = {
|
||||
|
|
|
@ -10,6 +10,8 @@ import logging
|
|||
import subprocess
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.util import template
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -24,20 +26,30 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
for dev_name, properties in switches.items():
|
||||
devices.append(
|
||||
CommandSwitch(
|
||||
hass,
|
||||
properties.get('name', dev_name),
|
||||
properties.get('oncmd', 'true'),
|
||||
properties.get('offcmd', 'true')))
|
||||
properties.get('offcmd', 'true'),
|
||||
properties.get('statecmd', False),
|
||||
properties.get(CONF_VALUE_TEMPLATE, False)))
|
||||
|
||||
add_devices_callback(devices)
|
||||
|
||||
|
||||
class CommandSwitch(SwitchDevice):
|
||||
""" Represents a switch that can be togggled using shell commands. """
|
||||
def __init__(self, name, command_on, command_off):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, name, command_on, command_off,
|
||||
command_state, value_template):
|
||||
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._command_on = command_on
|
||||
self._command_off = command_off
|
||||
self._command_state = command_state
|
||||
self._value_template = value_template
|
||||
|
||||
@staticmethod
|
||||
def _switch(command):
|
||||
|
@ -51,10 +63,27 @@ class CommandSwitch(SwitchDevice):
|
|||
|
||||
return success
|
||||
|
||||
@staticmethod
|
||||
def _query_state_value(command):
|
||||
""" Execute state command for return value. """
|
||||
_LOGGER.info('Running state command: %s', command)
|
||||
|
||||
try:
|
||||
return_value = subprocess.check_output(command, shell=True)
|
||||
return return_value.strip().decode('utf-8')
|
||||
except subprocess.CalledProcessError:
|
||||
_LOGGER.error('Command failed: %s', command)
|
||||
|
||||
@staticmethod
|
||||
def _query_state_code(command):
|
||||
""" Execute state command for return code. """
|
||||
_LOGGER.info('Running state command: %s', command)
|
||||
return subprocess.call(command, shell=True) == 0
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed. """
|
||||
return False
|
||||
""" Only poll if we have statecmd. """
|
||||
return self._command_state is not None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -66,14 +95,34 @@ class CommandSwitch(SwitchDevice):
|
|||
""" True if device is on. """
|
||||
return self._state
|
||||
|
||||
def _query_state(self):
|
||||
""" Query for state. """
|
||||
if not self._command_state:
|
||||
_LOGGER.error('No state command specified')
|
||||
return
|
||||
if self._value_template:
|
||||
return CommandSwitch._query_state_value(self._command_state)
|
||||
return CommandSwitch._query_state_code(self._command_state)
|
||||
|
||||
def update(self):
|
||||
""" Update device state. """
|
||||
if self._command_state:
|
||||
payload = str(self._query_state())
|
||||
if self._value_template:
|
||||
payload = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, payload)
|
||||
self._state = (payload.lower() == "true")
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
if CommandSwitch._switch(self._command_on):
|
||||
if (CommandSwitch._switch(self._command_on) and
|
||||
not self._command_state):
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
if CommandSwitch._switch(self._command_off):
|
||||
if (CommandSwitch._switch(self._command_off) and
|
||||
not self._command_state):
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
self.update_ha_state()
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
"""
|
||||
homeassistant.components.switch.mysensors
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for MySensors switches.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.mysensors.html
|
||||
"""
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL,
|
||||
STATE_ON, STATE_OFF)
|
||||
|
||||
import homeassistant.components.mysensors as mysensors
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors platform for switches."""
|
||||
# Only act if loaded via mysensors by discovery event.
|
||||
# Otherwise gateway is not setup.
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||
# states.
|
||||
s_types = [
|
||||
gateway.const.Presentation.S_DOOR,
|
||||
gateway.const.Presentation.S_MOTION,
|
||||
gateway.const.Presentation.S_SMOKE,
|
||||
gateway.const.Presentation.S_LIGHT,
|
||||
gateway.const.Presentation.S_LOCK,
|
||||
]
|
||||
v_types = [
|
||||
gateway.const.SetReq.V_ARMED,
|
||||
gateway.const.SetReq.V_LIGHT,
|
||||
gateway.const.SetReq.V_LOCK_STATUS,
|
||||
]
|
||||
if float(gateway.version) >= 1.5:
|
||||
s_types.extend([
|
||||
gateway.const.Presentation.S_BINARY,
|
||||
gateway.const.Presentation.S_SPRINKLER,
|
||||
gateway.const.Presentation.S_WATER_LEAK,
|
||||
gateway.const.Presentation.S_SOUND,
|
||||
gateway.const.Presentation.S_VIBRATION,
|
||||
gateway.const.Presentation.S_MOISTURE,
|
||||
])
|
||||
v_types.extend([gateway.const.SetReq.V_STATUS, ])
|
||||
|
||||
devices = defaultdict(list)
|
||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||
s_types, v_types, devices, add_devices, MySensorsSwitch))
|
||||
|
||||
|
||||
class MySensorsSwitch(SwitchDevice):
|
||||
"""Represent the value of a MySensors child node."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
|
||||
def __init__(self, gateway, node_id, child_id, name, value_type):
|
||||
"""Setup class attributes on instantiation.
|
||||
|
||||
Args:
|
||||
gateway (GatewayWrapper): Gateway object.
|
||||
node_id (str): Id of node.
|
||||
child_id (str): Id of child.
|
||||
name (str): Entity name.
|
||||
value_type (str): Value type of child. Value is entity state.
|
||||
|
||||
Attributes:
|
||||
gateway (GatewayWrapper): Gateway object
|
||||
node_id (str): Id of node.
|
||||
child_id (str): Id of child.
|
||||
_name (str): Entity name.
|
||||
value_type (str): Value type of child. Value is entity state.
|
||||
battery_level (int): Node battery level.
|
||||
_values (dict): Child values. Non state values set as state attributes.
|
||||
"""
|
||||
self.gateway = gateway
|
||||
self.node_id = node_id
|
||||
self.child_id = child_id
|
||||
self._name = name
|
||||
self.value_type = value_type
|
||||
self.battery_level = 0
|
||||
self._values = {}
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""MySensor gateway pushes its state to HA."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of this entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
device_attr = dict(self._values)
|
||||
device_attr.pop(self.value_type, None)
|
||||
return device_attr
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
data = {
|
||||
mysensors.ATTR_PORT: self.gateway.port,
|
||||
mysensors.ATTR_NODE_ID: self.node_id,
|
||||
mysensors.ATTR_CHILD_ID: self.child_id,
|
||||
ATTR_BATTERY_LEVEL: self.battery_level,
|
||||
}
|
||||
|
||||
device_attr = self.device_state_attributes
|
||||
|
||||
if device_attr is not None:
|
||||
data.update(device_attr)
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if switch is on."""
|
||||
if self.value_type in self._values:
|
||||
return self._values[self.value_type] == STATE_ON
|
||||
return False
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn the switch on."""
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, self.value_type, 1)
|
||||
self._values[self.value_type] = STATE_ON
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the switch off."""
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, self.value_type, 0)
|
||||
self._values[self.value_type] = STATE_OFF
|
||||
self.update_ha_state()
|
||||
|
||||
def update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
node = self.gateway.sensors[self.node_id]
|
||||
child = node.children[self.child_id]
|
||||
for value_type, value in child.values.items():
|
||||
_LOGGER.info(
|
||||
"%s: value_type %s, value = %s", self._name, value_type, value)
|
||||
if value_type == self.gateway.const.SetReq.V_ARMED or \
|
||||
value_type == self.gateway.const.SetReq.V_STATUS or \
|
||||
value_type == self.gateway.const.SetReq.V_LIGHT or \
|
||||
value_type == self.gateway.const.SetReq.V_LOCK_STATUS:
|
||||
self._values[value_type] = (
|
||||
STATE_ON if int(value) == 1 else STATE_OFF)
|
||||
else:
|
||||
self._values[value_type] = value
|
||||
self.battery_level = node.battery_level
|
|
@ -18,7 +18,7 @@ DEFAULT_BODY_ON = "ON"
|
|||
DEFAULT_BODY_OFF = "OFF"
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=unused-argument,
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Get REST switch. """
|
||||
|
||||
|
@ -32,11 +32,10 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
requests.get(resource, timeout=10)
|
||||
except requests.exceptions.MissingSchema:
|
||||
_LOGGER.error("Missing resource or schema in configuration. "
|
||||
"Add http:// to your URL.")
|
||||
"Add http:// or https:// to your URL")
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to resource/endpoint. "
|
||||
"Please check the IP address in the configuration file.")
|
||||
_LOGGER.error("No route to resource/endpoint: %s", resource)
|
||||
return False
|
||||
|
||||
add_devices_callback([RestSwitch(
|
||||
|
|
|
@ -13,8 +13,9 @@ from homeassistant.components.switch import SwitchDevice
|
|||
from homeassistant.util import slugify
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.components.rfxtrx import ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, \
|
||||
ATTR_NAME, EVENT_BUTTON_PRESSED
|
||||
from homeassistant.components.rfxtrx import (
|
||||
ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID,
|
||||
ATTR_NAME, EVENT_BUTTON_PRESSED)
|
||||
|
||||
|
||||
DEPENDENCIES = ['rfxtrx']
|
||||
|
|
|
@ -1,69 +1,48 @@
|
|||
"""
|
||||
homeassistant.components.switch.rpi_gpio
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Allows to control the GPIO pins of a Raspberry Pi.
|
||||
Allows to configure a switch using RPi GPIO.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.rpi_gpio/
|
||||
"""
|
||||
|
||||
import logging
|
||||
try:
|
||||
import RPi.GPIO as GPIO
|
||||
except ImportError:
|
||||
GPIO = None
|
||||
import homeassistant.components.rpi_gpio as rpi_gpio
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.const import (DEVICE_DEFAULT_NAME,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.const import (DEVICE_DEFAULT_NAME)
|
||||
|
||||
DEFAULT_INVERT_LOGIC = False
|
||||
|
||||
REQUIREMENTS = ['RPi.GPIO==0.5.11']
|
||||
DEPENDENCIES = ['rpi_gpio']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Raspberry PI GPIO ports. """
|
||||
if GPIO is None:
|
||||
_LOGGER.error('RPi.GPIO not available. rpi_gpio ports ignored.')
|
||||
return
|
||||
# pylint: disable=no-member
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
""" Sets up the Raspberry PI GPIO devices. """
|
||||
|
||||
invert_logic = config.get('invert_logic', DEFAULT_INVERT_LOGIC)
|
||||
|
||||
switches = []
|
||||
invert_logic = config.get('invert_logic', DEFAULT_INVERT_LOGIC)
|
||||
ports = config.get('ports')
|
||||
for port_num, port_name in ports.items():
|
||||
switches.append(RPiGPIOSwitch(port_name, port_num, invert_logic))
|
||||
for port, name in ports.items():
|
||||
switches.append(RPiGPIOSwitch(name, port, invert_logic))
|
||||
add_devices(switches)
|
||||
|
||||
def cleanup_gpio(event):
|
||||
""" Stuff to do before stop home assistant. """
|
||||
# pylint: disable=no-member
|
||||
GPIO.cleanup()
|
||||
|
||||
def prepare_gpio(event):
|
||||
""" Stuff to do when home assistant starts. """
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio)
|
||||
|
||||
|
||||
class RPiGPIOSwitch(ToggleEntity):
|
||||
""" Represents a port that can be toggled using Raspberry Pi GPIO. """
|
||||
|
||||
def __init__(self, name, gpio, invert_logic):
|
||||
""" Represents a switch that can be toggled using Raspberry Pi GPIO. """
|
||||
def __init__(self, name, port, invert_logic):
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._gpio = gpio
|
||||
self._active_state = not invert_logic
|
||||
self._state = not self._active_state
|
||||
# pylint: disable=no-member
|
||||
GPIO.setup(gpio, GPIO.OUT)
|
||||
self._port = port
|
||||
self._invert_logic = invert_logic
|
||||
self._state = False
|
||||
rpi_gpio.setup_output(self._port)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the port. """
|
||||
""" The name of the switch. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
|
@ -76,41 +55,14 @@ class RPiGPIOSwitch(ToggleEntity):
|
|||
""" True if device is on. """
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
def turn_on(self):
|
||||
""" Turn the device on. """
|
||||
if self._switch(self._active_state):
|
||||
self._state = True
|
||||
rpi_gpio.write_output(self._port, 0 if self._invert_logic else 1)
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
def turn_off(self):
|
||||
""" Turn the device off. """
|
||||
if self._switch(not self._active_state):
|
||||
self._state = False
|
||||
rpi_gpio.write_output(self._port, 1 if self._invert_logic else 0)
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
|
||||
def _switch(self, new_state):
|
||||
""" Change the output value to Raspberry Pi GPIO port. """
|
||||
_LOGGER.info('Setting GPIO %s to %s', self._gpio, new_state)
|
||||
# pylint: disable=bare-except
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
GPIO.output(self._gpio, 1 if new_state else 0)
|
||||
except:
|
||||
_LOGGER.error('GPIO "%s" output failed', self._gpio)
|
||||
return False
|
||||
return True
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Returns device specific state attributes. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
data = {}
|
||||
device_attr = self.device_state_attributes
|
||||
if device_attr is not None:
|
||||
data.update(device_attr)
|
||||
return data
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
"""
|
||||
homeassistant.components.switch.tellduslive
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Support for Tellstick switches using Tellstick Net and
|
||||
the Telldus Live online service.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.tellduslive/
|
||||
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
||||
from homeassistant.components import tellduslive
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['tellduslive']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Find and return Tellstick switches. """
|
||||
switches = tellduslive.NETWORK.get_switches()
|
||||
add_devices([TelldusLiveSwitch(switch["name"],
|
||||
switch["id"])
|
||||
for switch in switches if switch["type"] == "device"])
|
||||
|
||||
|
||||
class TelldusLiveSwitch(ToggleEntity):
|
||||
""" Represents a Tellstick switch. """
|
||||
|
||||
def __init__(self, name, switch_id):
|
||||
self._name = name
|
||||
self._id = switch_id
|
||||
self._state = STATE_UNKNOWN
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Tells Home Assistant to poll this entity. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the switch if any. """
|
||||
return self._name
|
||||
|
||||
def update(self):
|
||||
from tellive.live import const
|
||||
state = tellduslive.NETWORK.get_switch_state(self._id)
|
||||
if state == const.TELLSTICK_TURNON:
|
||||
self._state = STATE_ON
|
||||
elif state == const.TELLSTICK_TURNOFF:
|
||||
self._state = STATE_OFF
|
||||
else:
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if switch is on. """
|
||||
return self._state == STATE_ON
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
if tellduslive.NETWORK.turn_switch_on(self._id):
|
||||
self._state = STATE_ON
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turns the switch off. """
|
||||
if tellduslive.NETWORK.turn_switch_off(self._id):
|
||||
self._state = STATE_OFF
|
||||
self.update_ha_state()
|
|
@ -7,17 +7,21 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/switch.vera/
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
from requests.exceptions import RequestException
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME)
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
|
||||
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/'
|
||||
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
|
||||
'#python-vera==0.1.1']
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL,
|
||||
ATTR_TRIPPED,
|
||||
ATTR_ARMED,
|
||||
ATTR_LAST_TRIP_TIME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_ON,
|
||||
STATE_OFF)
|
||||
|
||||
REQUIREMENTS = ['pyvera==0.2.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -37,7 +41,16 @@ def get_devices(hass, config):
|
|||
|
||||
device_data = config.get('device_data', {})
|
||||
|
||||
vera_controller = veraApi.VeraController(base_url)
|
||||
vera_controller, created = veraApi.init_controller(base_url)
|
||||
|
||||
if created:
|
||||
def stop_subscription(event):
|
||||
""" Shutdown Vera subscriptions and subscription thread on exit"""
|
||||
_LOGGER.info("Shutting down subscriptions.")
|
||||
vera_controller.stop()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription)
|
||||
|
||||
devices = []
|
||||
try:
|
||||
devices = vera_controller.get_devices([
|
||||
|
@ -49,11 +62,12 @@ def get_devices(hass, config):
|
|||
|
||||
vera_switches = []
|
||||
for device in devices:
|
||||
extra_data = device_data.get(device.deviceId, {})
|
||||
extra_data = device_data.get(device.device_id, {})
|
||||
exclude = extra_data.get('exclude', False)
|
||||
|
||||
if exclude is not True:
|
||||
vera_switches.append(VeraSwitch(device, extra_data))
|
||||
vera_switches.append(
|
||||
VeraSwitch(device, vera_controller, extra_data))
|
||||
|
||||
return vera_switches
|
||||
|
||||
|
@ -63,19 +77,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
add_devices(get_devices(hass, config))
|
||||
|
||||
|
||||
class VeraSwitch(ToggleEntity):
|
||||
class VeraSwitch(SwitchDevice):
|
||||
""" Represents a Vera Switch. """
|
||||
|
||||
def __init__(self, vera_device, extra_data=None):
|
||||
def __init__(self, vera_device, controller, extra_data=None):
|
||||
self.vera_device = vera_device
|
||||
self.extra_data = extra_data
|
||||
self.controller = controller
|
||||
if self.extra_data and self.extra_data.get('name'):
|
||||
self._name = self.extra_data.get('name')
|
||||
else:
|
||||
self._name = self.vera_device.name
|
||||
self.is_on_status = False
|
||||
# for debouncing status check after command is sent
|
||||
self.last_command_send = 0
|
||||
self._state = STATE_OFF
|
||||
|
||||
self.controller.register(vera_device, self._update_callback)
|
||||
self.update()
|
||||
|
||||
def _update_callback(self, _device):
|
||||
self.update_ha_state(True)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -90,44 +109,47 @@ class VeraSwitch(ToggleEntity):
|
|||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
||||
|
||||
if self.vera_device.is_armable:
|
||||
armed = self.vera_device.refresh_value('Armed')
|
||||
attr[ATTR_ARMED] = 'True' if armed == '1' else 'False'
|
||||
armed = self.vera_device.is_armed
|
||||
attr[ATTR_ARMED] = 'True' if armed else 'False'
|
||||
|
||||
if self.vera_device.is_trippable:
|
||||
last_tripped = self.vera_device.refresh_value('LastTrip')
|
||||
last_tripped = self.vera_device.last_trip
|
||||
if last_tripped is not None:
|
||||
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
|
||||
attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str(
|
||||
utc_time)
|
||||
else:
|
||||
attr[ATTR_LAST_TRIP_TIME] = None
|
||||
tripped = self.vera_device.refresh_value('Tripped')
|
||||
attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False'
|
||||
tripped = self.vera_device.is_tripped
|
||||
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
|
||||
|
||||
attr['Vera Device Id'] = self.vera_device.vera_device_id
|
||||
|
||||
return attr
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
self.last_command_send = time.time()
|
||||
self.vera_device.switch_on()
|
||||
self.is_on_status = True
|
||||
self._state = STATE_ON
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
self.last_command_send = time.time()
|
||||
self.vera_device.switch_off()
|
||||
self.is_on_status = False
|
||||
self._state = STATE_OFF
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Tells Home Assistant not to poll this entity. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return self.is_on_status
|
||||
return self._state == STATE_ON
|
||||
|
||||
def update(self):
|
||||
# We need to debounce the status call after turning switch on or off
|
||||
# because the vera has some lag in updating the device status
|
||||
try:
|
||||
if (self.last_command_send + 5) < time.time():
|
||||
self.is_on_status = self.vera_device.is_switched_on()
|
||||
except RequestException:
|
||||
_LOGGER.warning('Could not update status for %s', self.name)
|
||||
""" Called by the vera device callback to update state. """
|
||||
if self.vera_device.is_switched_on():
|
||||
self._state = STATE_ON
|
||||
else:
|
||||
self._state = STATE_OFF
|
||||
|
|
|
@ -25,7 +25,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
switches.extend([
|
||||
VerisureSmartplug(value)
|
||||
for value in verisure.get_smartplug_status().values()
|
||||
for value in verisure.SMARTPLUG_STATUS.values()
|
||||
if verisure.SHOW_SMARTPLUGS
|
||||
])
|
||||
|
||||
|
@ -36,31 +36,29 @@ class VerisureSmartplug(SwitchDevice):
|
|||
""" Represents a Verisure smartplug. """
|
||||
def __init__(self, smartplug_status):
|
||||
self._id = smartplug_status.id
|
||||
self.status_on = verisure.MY_PAGES.SMARTPLUG_ON
|
||||
self.status_off = verisure.MY_PAGES.SMARTPLUG_OFF
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Get the name (location) of the smartplug. """
|
||||
return verisure.get_smartplug_status()[self._id].location
|
||||
return verisure.SMARTPLUG_STATUS[self._id].location
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" Returns True if on """
|
||||
plug_status = verisure.get_smartplug_status()[self._id].status
|
||||
return plug_status == self.status_on
|
||||
plug_status = verisure.SMARTPLUG_STATUS[self._id].status
|
||||
return plug_status == 'on'
|
||||
|
||||
def turn_on(self):
|
||||
""" Set smartplug status on. """
|
||||
verisure.MY_PAGES.set_smartplug_status(
|
||||
self._id,
|
||||
self.status_on)
|
||||
verisure.MY_PAGES.smartplug.set(self._id, 'on')
|
||||
verisure.MY_PAGES.smartplug.wait_while_updating(self._id, 'on')
|
||||
verisure.update_smartplug()
|
||||
|
||||
def turn_off(self):
|
||||
""" Set smartplug status off. """
|
||||
verisure.MY_PAGES.set_smartplug_status(
|
||||
self._id,
|
||||
self.status_off)
|
||||
verisure.MY_PAGES.smartplug.set(self._id, 'off')
|
||||
verisure.MY_PAGES.smartplug.wait_while_updating(self._id, 'off')
|
||||
verisure.update_smartplug()
|
||||
|
||||
def update(self):
|
||||
verisure.update()
|
||||
verisure.update_smartplug()
|
||||
|
|
|
@ -9,11 +9,14 @@ https://home-assistant.io/components/switch.wemo/
|
|||
import logging
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
REQUIREMENTS = ['pywemo==0.3.3']
|
||||
REQUIREMENTS = ['pywemo==0.3.8']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_WEMO_SUBSCRIPTION_REGISTRY = None
|
||||
|
||||
|
||||
# pylint: disable=unused-argument, too-many-function-args
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
@ -21,6 +24,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
import pywemo
|
||||
import pywemo.discovery as discovery
|
||||
|
||||
global _WEMO_SUBSCRIPTION_REGISTRY
|
||||
if _WEMO_SUBSCRIPTION_REGISTRY is None:
|
||||
_WEMO_SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry()
|
||||
_WEMO_SUBSCRIPTION_REGISTRY.start()
|
||||
|
||||
def stop_wemo(event):
|
||||
""" Shutdown Wemo subscriptions and subscription thread on exit"""
|
||||
_LOGGER.info("Shutting down subscriptions.")
|
||||
_WEMO_SUBSCRIPTION_REGISTRY.stop()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo)
|
||||
|
||||
if discovery_info is not None:
|
||||
location = discovery_info[2]
|
||||
mac = discovery_info[3]
|
||||
|
@ -47,6 +62,22 @@ class WemoSwitch(SwitchDevice):
|
|||
self.insight_params = None
|
||||
self.maker_params = None
|
||||
|
||||
_WEMO_SUBSCRIPTION_REGISTRY.register(wemo)
|
||||
_WEMO_SUBSCRIPTION_REGISTRY.on(
|
||||
wemo, None, self._update_callback)
|
||||
|
||||
def _update_callback(self, _device, _params):
|
||||
""" Called by the wemo device callback to update state. """
|
||||
_LOGGER.info(
|
||||
'Subscription update for %s',
|
||||
_device)
|
||||
self.update_ha_state(True)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed with subscriptions """
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
""" Returns the id of this WeMo switch """
|
||||
|
|
|
@ -11,7 +11,7 @@ import logging
|
|||
from homeassistant.components.wink import WinkToggleDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.3.1']
|
||||
REQUIREMENTS = ['python-wink==0.4.1']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -30,3 +30,5 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
pywink.set_bearer_token(token)
|
||||
|
||||
add_devices(WinkToggleDevice(switch) for switch in pywink.get_switches())
|
||||
add_devices(WinkToggleDevice(switch) for switch in
|
||||
pywink.get_powerstrip_outlets())
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
"""
|
||||
homeassistant.components.tellduslive
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tellduslive Component
|
||||
|
||||
This component adds support for the Telldus Live service.
|
||||
Telldus Live is the online service used with Tellstick Net devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.tellduslive/
|
||||
|
||||
Developer access to the Telldus Live service is neccessary
|
||||
API keys can be aquired from https://api.telldus.com/keys/index
|
||||
|
||||
Tellstick Net devices can be auto discovered using the method described in:
|
||||
https://developer.telldus.com/doxygen/html/TellStickNet.html
|
||||
|
||||
It might be possible to communicate with the Tellstick Net device
|
||||
directly, bypassing the Tellstick Live service.
|
||||
This however is poorly documented and yet not fully supported (?) according to
|
||||
http://developer.telldus.se/ticket/114 and
|
||||
https://developer.telldus.com/doxygen/html/TellStickNet.html
|
||||
|
||||
API requests to certain methods, as described in
|
||||
https://api.telldus.com/explore/sensor/info
|
||||
are limited to one request every 10 minutes
|
||||
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.const import (
|
||||
EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED)
|
||||
|
||||
|
||||
DOMAIN = "tellduslive"
|
||||
DISCOVER_SWITCHES = "tellduslive.switches"
|
||||
DISCOVER_SENSORS = "tellduslive.sensors"
|
||||
|
||||
CONF_PUBLIC_KEY = "public_key"
|
||||
CONF_PRIVATE_KEY = "private_key"
|
||||
CONF_TOKEN = "token"
|
||||
CONF_TOKEN_SECRET = "token_secret"
|
||||
|
||||
REQUIREMENTS = ['tellive-py==0.5.2']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NETWORK = None
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600)
|
||||
|
||||
|
||||
class TelldusLiveData(object):
|
||||
""" Gets the latest data and update the states. """
|
||||
|
||||
def __init__(self, hass, config):
|
||||
|
||||
public_key = config[DOMAIN].get(CONF_PUBLIC_KEY)
|
||||
private_key = config[DOMAIN].get(CONF_PRIVATE_KEY)
|
||||
token = config[DOMAIN].get(CONF_TOKEN)
|
||||
token_secret = config[DOMAIN].get(CONF_TOKEN_SECRET)
|
||||
|
||||
from tellive.client import LiveClient
|
||||
from tellive.live import TelldusLive
|
||||
|
||||
self._sensors = []
|
||||
self._switches = []
|
||||
|
||||
self._client = LiveClient(public_key=public_key,
|
||||
private_key=private_key,
|
||||
access_token=token,
|
||||
access_secret=token_secret)
|
||||
self._api = TelldusLive(self._client)
|
||||
|
||||
def update(self, hass, config):
|
||||
""" Send discovery event if component not yet discovered """
|
||||
self._update_sensors()
|
||||
self._update_switches()
|
||||
for component_name, found_devices, discovery_type in \
|
||||
(('sensor', self._sensors, DISCOVER_SENSORS),
|
||||
('switch', self._switches, DISCOVER_SWITCHES)):
|
||||
if len(found_devices):
|
||||
component = get_component(component_name)
|
||||
bootstrap.setup_component(hass, component.DOMAIN, config)
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
|
||||
{ATTR_SERVICE: discovery_type,
|
||||
ATTR_DISCOVERED: {}})
|
||||
|
||||
def _request(self, what, **params):
|
||||
""" Sends a request to the tellstick live API """
|
||||
|
||||
from tellive.live import const
|
||||
|
||||
supported_methods = const.TELLSTICK_TURNON \
|
||||
| const.TELLSTICK_TURNOFF \
|
||||
| const.TELLSTICK_TOGGLE
|
||||
|
||||
default_params = {'supportedMethods': supported_methods,
|
||||
"includeValues": 1,
|
||||
"includeScale": 1}
|
||||
|
||||
params.update(default_params)
|
||||
|
||||
# room for improvement: the telllive library doesn't seem to
|
||||
# re-use sessions, instead it opens a new session for each request
|
||||
# this needs to be fixed
|
||||
response = self._client.request(what, params)
|
||||
return response
|
||||
|
||||
def check_request(self, what, **params):
|
||||
""" Make request, check result if successful """
|
||||
response = self._request(what, **params)
|
||||
return response['status'] == "success"
|
||||
|
||||
def validate_session(self):
|
||||
""" Make a dummy request to see if the session is valid """
|
||||
try:
|
||||
response = self._request("user/profile")
|
||||
return 'email' in response
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_sensors(self):
|
||||
""" Get the latest sensor data from Telldus Live """
|
||||
_LOGGER.info("Updating sensors from Telldus Live")
|
||||
self._sensors = self._request("sensors/list")["sensor"]
|
||||
|
||||
def _update_switches(self):
|
||||
""" Get the configured switches from Telldus Live"""
|
||||
_LOGGER.info("Updating switches from Telldus Live")
|
||||
self._switches = self._request("devices/list")["device"]
|
||||
# filter out any group of switches
|
||||
self._switches = [switch for switch in self._switches
|
||||
if switch["type"] == "device"]
|
||||
|
||||
def get_sensors(self):
|
||||
""" Get the configured sensors """
|
||||
self._update_sensors()
|
||||
return self._sensors
|
||||
|
||||
def get_switches(self):
|
||||
""" Get the configured switches """
|
||||
self._update_switches()
|
||||
return self._switches
|
||||
|
||||
def get_sensor_value(self, sensor_id, sensor_name):
|
||||
""" Get the latest (possibly cached) sensor value """
|
||||
self._update_sensors()
|
||||
for component in self._sensors:
|
||||
if component["id"] == sensor_id:
|
||||
for sensor in component["data"]:
|
||||
if sensor["name"] == sensor_name:
|
||||
return (sensor["value"],
|
||||
component["battery"],
|
||||
component["lastUpdated"])
|
||||
|
||||
def get_switch_state(self, switch_id):
|
||||
""" returns state of switch. """
|
||||
_LOGGER.info("Updating switch state from Telldus Live")
|
||||
response = self._request("device/info", id=switch_id)["state"]
|
||||
return int(response)
|
||||
|
||||
def turn_switch_on(self, switch_id):
|
||||
""" turn switch off """
|
||||
return self.check_request("device/turnOn", id=switch_id)
|
||||
|
||||
def turn_switch_off(self, switch_id):
|
||||
""" turn switch on """
|
||||
return self.check_request("device/turnOff", id=switch_id)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup the tellduslive component """
|
||||
|
||||
# fixme: aquire app key and provide authentication
|
||||
# using username + password
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_PUBLIC_KEY,
|
||||
CONF_PRIVATE_KEY,
|
||||
CONF_TOKEN,
|
||||
CONF_TOKEN_SECRET]},
|
||||
_LOGGER):
|
||||
_LOGGER.error(
|
||||
"Configuration Error: "
|
||||
"Please make sure you have configured your keys "
|
||||
"that can be aquired from https://api.telldus.com/keys/index")
|
||||
return False
|
||||
|
||||
global NETWORK
|
||||
NETWORK = TelldusLiveData(hass, config)
|
||||
|
||||
if not NETWORK.validate_session():
|
||||
_LOGGER.error(
|
||||
"Authentication Error: "
|
||||
"Please make sure you have configured your keys "
|
||||
"that can be aquired from https://api.telldus.com/keys/index")
|
||||
return False
|
||||
|
||||
NETWORK.update(hass, config)
|
||||
|
||||
return True
|
|
@ -224,12 +224,12 @@ class ThermostatDevice(Entity):
|
|||
@property
|
||||
def min_temp(self):
|
||||
""" Return minimum temperature. """
|
||||
return convert(7, TEMP_CELCIUS, self.unit_of_measurement)
|
||||
return round(convert(7, TEMP_CELCIUS, self.unit_of_measurement))
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
""" Return maxmum temperature. """
|
||||
return convert(35, TEMP_CELCIUS, self.unit_of_measurement)
|
||||
return round(convert(35, TEMP_CELCIUS, self.unit_of_measurement))
|
||||
|
||||
def _convert(self, temp, round_dec=None):
|
||||
""" Convert temperature from this thermost into user preferred
|
||||
|
|
|
@ -46,8 +46,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
return
|
||||
data = ecobee.NETWORK
|
||||
hold_temp = discovery_info['hold_temp']
|
||||
_LOGGER.info("Loading ecobee thermostat component with hold_temp set to "
|
||||
+ str(hold_temp))
|
||||
_LOGGER.info(
|
||||
"Loading ecobee thermostat component with hold_temp set to %s",
|
||||
hold_temp)
|
||||
add_devices(Thermostat(data, index, hold_temp)
|
||||
for index in range(len(data.ecobee.thermostats)))
|
||||
|
||||
|
|
|
@ -6,8 +6,9 @@ Adds support for Honeywell Round Connected and Honeywell Evohome thermostats.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/thermostat.honeywell/
|
||||
"""
|
||||
import socket
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from homeassistant.components.thermostat import ThermostatDevice
|
||||
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS)
|
||||
|
||||
|
@ -15,6 +16,8 @@ REQUIREMENTS = ['evohomeclient==0.2.4']
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AWAY_TEMP = "away_temperature"
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -23,17 +26,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
try:
|
||||
away_temp = float(config.get(CONF_AWAY_TEMP, 16))
|
||||
except ValueError:
|
||||
_LOGGER.error("value entered for item %s should convert to a number",
|
||||
CONF_AWAY_TEMP)
|
||||
return False
|
||||
if username is None or password is None:
|
||||
_LOGGER.error("Missing required configuration items %s or %s",
|
||||
CONF_USERNAME, CONF_PASSWORD)
|
||||
return False
|
||||
|
||||
evo_api = EvohomeClient(username, password)
|
||||
|
||||
try:
|
||||
zones = evo_api.temperatures(force_refresh=True)
|
||||
for i, zone in enumerate(zones):
|
||||
add_devices([RoundThermostat(evo_api, zone['id'], i == 0)])
|
||||
add_devices([RoundThermostat(evo_api,
|
||||
zone['id'],
|
||||
i == 0,
|
||||
away_temp)])
|
||||
except socket.error:
|
||||
_LOGGER.error(
|
||||
"Connection error logging into the honeywell evohome web service"
|
||||
|
@ -44,7 +56,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class RoundThermostat(ThermostatDevice):
|
||||
""" Represents a Honeywell Round Connected thermostat. """
|
||||
|
||||
def __init__(self, device, zone_id, master):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, device, zone_id, master, away_temp):
|
||||
self.device = device
|
||||
self._current_temperature = None
|
||||
self._target_temperature = None
|
||||
|
@ -52,6 +65,8 @@ class RoundThermostat(ThermostatDevice):
|
|||
self._id = zone_id
|
||||
self._master = master
|
||||
self._is_dhw = False
|
||||
self._away_temp = away_temp
|
||||
self._away = False
|
||||
self.update()
|
||||
|
||||
@property
|
||||
|
@ -80,6 +95,25 @@ class RoundThermostat(ThermostatDevice):
|
|||
""" Set new target temperature """
|
||||
self.device.set_temperature(self._name, temperature)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
""" Returns if away mode is on. """
|
||||
return self._away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
""" Turns away on.
|
||||
Evohome does have a proprietary away mode, but it doesn't really work
|
||||
the way it should. For example: If you set a temperature manually
|
||||
it doesn't get overwritten when away mode is switched on.
|
||||
"""
|
||||
self._away = True
|
||||
self.device.set_temperature(self._name, self._away_temp)
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
""" Turns away off. """
|
||||
self._away = False
|
||||
self.device.cancel_temp_override(self._name)
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
# Only refresh if this is the "master" device,
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
"""
|
||||
homeassistant.components.thermostat.proliphix
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The Proliphix NT10e Thermostat is an ethernet connected thermostat.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/thermostat.proliphix/
|
||||
"""
|
||||
from homeassistant.components.thermostat import (ThermostatDevice, STATE_COOL,
|
||||
STATE_IDLE, STATE_HEAT)
|
||||
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_HOST, TEMP_FAHRENHEIT)
|
||||
|
||||
REQUIREMENTS = ['proliphix==0.1.0']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the Proliphix thermostats. """
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
host = config.get(CONF_HOST)
|
||||
|
||||
import proliphix
|
||||
|
||||
pdp = proliphix.PDP(host, username, password)
|
||||
|
||||
add_devices([
|
||||
ProliphixThermostat(pdp)
|
||||
])
|
||||
|
||||
|
||||
class ProliphixThermostat(ThermostatDevice):
|
||||
""" Represents a Proliphix thermostat. """
|
||||
|
||||
def __init__(self, pdp):
|
||||
self._pdp = pdp
|
||||
# initial data
|
||||
self._pdp.update()
|
||||
self._name = self._pdp.name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Polling needed for thermostat.. """
|
||||
return True
|
||||
|
||||
def update(self):
|
||||
""" Update the data from the thermostat. """
|
||||
self._pdp.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the thermostat. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Returns device specific state attributes. """
|
||||
return {
|
||||
"fan": self._pdp.fan_state
|
||||
}
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Returns the unit of measurement. """
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
""" Returns the current temperature. """
|
||||
return self._pdp.cur_temp
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
""" Returns the temperature we try to reach. """
|
||||
return self._pdp.setback_heat
|
||||
|
||||
@property
|
||||
def operation(self):
|
||||
""" Returns the current state of the thermostat. """
|
||||
state = self._pdp.hvac_state
|
||||
if state in (1, 2):
|
||||
return STATE_IDLE
|
||||
elif state == 3:
|
||||
return STATE_HEAT
|
||||
elif state == 6:
|
||||
return STATE_COOL
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
""" Set new target temperature. """
|
||||
self._pdp.setback_heat = temperature
|
|
@ -7,6 +7,8 @@ For more details about this component, please refer to the documentation at
|
|||
https://home-assistant.io/components/verisure/
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant import bootstrap
|
||||
|
@ -26,15 +28,14 @@ DISCOVER_SWITCHES = 'verisure.switches'
|
|||
DISCOVER_ALARMS = 'verisure.alarm_control_panel'
|
||||
|
||||
DEPENDENCIES = ['alarm_control_panel']
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/persandstrom/python-verisure/archive/'
|
||||
'9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6'
|
||||
]
|
||||
REQUIREMENTS = ['vsure==0.4.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MY_PAGES = None
|
||||
STATUS = {}
|
||||
ALARM_STATUS = {}
|
||||
SMARTPLUG_STATUS = {}
|
||||
CLIMATE_STATUS = {}
|
||||
|
||||
VERISURE_LOGIN_ERROR = None
|
||||
VERISURE_ERROR = None
|
||||
|
@ -43,11 +44,12 @@ SHOW_THERMOMETERS = True
|
|||
SHOW_HYGROMETERS = True
|
||||
SHOW_ALARM = True
|
||||
SHOW_SMARTPLUGS = True
|
||||
CODE_DIGITS = 4
|
||||
|
||||
# if wrong password was given don't try again
|
||||
WRONG_PASSWORD_GIVEN = False
|
||||
|
||||
MIN_TIME_BETWEEN_REQUESTS = timedelta(seconds=5)
|
||||
MIN_TIME_BETWEEN_REQUESTS = timedelta(seconds=1)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
@ -60,15 +62,13 @@ def setup(hass, config):
|
|||
|
||||
from verisure import MyPages, LoginError, Error
|
||||
|
||||
STATUS[MyPages.DEVICE_ALARM] = {}
|
||||
STATUS[MyPages.DEVICE_CLIMATE] = {}
|
||||
STATUS[MyPages.DEVICE_SMARTPLUG] = {}
|
||||
|
||||
global SHOW_THERMOMETERS, SHOW_HYGROMETERS, SHOW_ALARM, SHOW_SMARTPLUGS
|
||||
global SHOW_THERMOMETERS, SHOW_HYGROMETERS,\
|
||||
SHOW_ALARM, SHOW_SMARTPLUGS, CODE_DIGITS
|
||||
SHOW_THERMOMETERS = int(config[DOMAIN].get('thermometers', '1'))
|
||||
SHOW_HYGROMETERS = int(config[DOMAIN].get('hygrometers', '1'))
|
||||
SHOW_ALARM = int(config[DOMAIN].get('alarm', '1'))
|
||||
SHOW_SMARTPLUGS = int(config[DOMAIN].get('smartplugs', '1'))
|
||||
CODE_DIGITS = int(config[DOMAIN].get('code_digits', '4'))
|
||||
|
||||
global MY_PAGES
|
||||
MY_PAGES = MyPages(
|
||||
|
@ -84,7 +84,9 @@ def setup(hass, config):
|
|||
_LOGGER.error('Could not log in to verisure mypages, %s', ex)
|
||||
return False
|
||||
|
||||
update()
|
||||
update_alarm()
|
||||
update_climate()
|
||||
update_smartplug()
|
||||
|
||||
# Load components for the devices in the ISY controller that we support
|
||||
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
|
||||
|
@ -101,24 +103,10 @@ def setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
def get_alarm_status():
|
||||
""" Return a list of status overviews for alarm components. """
|
||||
return STATUS[MY_PAGES.DEVICE_ALARM]
|
||||
|
||||
|
||||
def get_climate_status():
|
||||
""" Return a list of status overviews for alarm components. """
|
||||
return STATUS[MY_PAGES.DEVICE_CLIMATE]
|
||||
|
||||
|
||||
def get_smartplug_status():
|
||||
""" Return a list of status overviews for alarm components. """
|
||||
return STATUS[MY_PAGES.DEVICE_SMARTPLUG]
|
||||
|
||||
|
||||
def reconnect():
|
||||
""" Reconnect to verisure mypages. """
|
||||
try:
|
||||
time.sleep(1)
|
||||
MY_PAGES.login()
|
||||
except VERISURE_LOGIN_ERROR as ex:
|
||||
_LOGGER.error("Could not login to Verisure mypages, %s", ex)
|
||||
|
@ -129,19 +117,31 @@ def reconnect():
|
|||
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_REQUESTS)
|
||||
def update():
|
||||
def update_alarm():
|
||||
""" Updates the status of alarms. """
|
||||
update_component(MY_PAGES.alarm.get, ALARM_STATUS)
|
||||
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_REQUESTS)
|
||||
def update_climate():
|
||||
""" Updates the status of climate sensors. """
|
||||
update_component(MY_PAGES.climate.get, CLIMATE_STATUS)
|
||||
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_REQUESTS)
|
||||
def update_smartplug():
|
||||
""" Updates the status of smartplugs. """
|
||||
update_component(MY_PAGES.smartplug.get, SMARTPLUG_STATUS)
|
||||
|
||||
|
||||
def update_component(get_function, status):
|
||||
""" Updates the status of verisure components. """
|
||||
if WRONG_PASSWORD_GIVEN:
|
||||
_LOGGER.error('Wrong password')
|
||||
return
|
||||
|
||||
try:
|
||||
for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_ALARM):
|
||||
STATUS[MY_PAGES.DEVICE_ALARM][overview.id] = overview
|
||||
for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_CLIMATE):
|
||||
STATUS[MY_PAGES.DEVICE_CLIMATE][overview.id] = overview
|
||||
for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_SMARTPLUG):
|
||||
STATUS[MY_PAGES.DEVICE_SMARTPLUG][overview.id] = overview
|
||||
except ConnectionError as ex:
|
||||
for overview in get_function():
|
||||
status[overview.id] = overview
|
||||
except (ConnectionError, VERISURE_ERROR) as ex:
|
||||
_LOGGER.error('Caught connection error %s, tries to reconnect', ex)
|
||||
reconnect()
|
||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.const import (
|
|||
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
|
||||
|
||||
DOMAIN = "wink"
|
||||
REQUIREMENTS = ['python-wink==0.3.1']
|
||||
REQUIREMENTS = ['python-wink==0.4.1']
|
||||
|
||||
DISCOVER_LIGHTS = "wink.lights"
|
||||
DISCOVER_SWITCHES = "wink.switches"
|
||||
|
@ -37,9 +37,10 @@ def setup(hass, config):
|
|||
# Load components for the devices in the Wink that we support
|
||||
for component_name, func_exists, discovery_type in (
|
||||
('light', pywink.get_bulbs, DISCOVER_LIGHTS),
|
||||
('switch', pywink.get_switches, DISCOVER_SWITCHES),
|
||||
('sensor', lambda: pywink.get_sensors or pywink.get_eggtrays,
|
||||
DISCOVER_SENSORS),
|
||||
('switch', lambda: pywink.get_switches or
|
||||
pywink.get_powerstrip_outlets, DISCOVER_SWITCHES),
|
||||
('sensor', lambda: pywink.get_sensors or
|
||||
pywink.get_eggtrays, DISCOVER_SENSORS),
|
||||
('lock', pywink.get_locks, DISCOVER_LOCKS)):
|
||||
|
||||
if func_exists():
|
||||
|
|
|
@ -26,6 +26,9 @@ CONF_POLLING_INTERVAL = "polling_interval"
|
|||
DEFAULT_ZWAVE_CONFIG_PATH = os.path.join(sys.prefix, 'share',
|
||||
'python-openzwave', 'config')
|
||||
|
||||
SERVICE_ADD_NODE = "add_node"
|
||||
SERVICE_REMOVE_NODE = "remove_node"
|
||||
|
||||
DISCOVER_SENSORS = "zwave.sensors"
|
||||
DISCOVER_SWITCHES = "zwave.switch"
|
||||
DISCOVER_LIGHTS = "zwave.light"
|
||||
|
@ -37,6 +40,7 @@ COMMAND_CLASS_SENSOR_BINARY = 48
|
|||
COMMAND_CLASS_SENSOR_MULTILEVEL = 49
|
||||
COMMAND_CLASS_METER = 50
|
||||
COMMAND_CLASS_BATTERY = 128
|
||||
COMMAND_CLASS_ALARM = 113 # 0x71
|
||||
|
||||
GENRE_WHATEVER = None
|
||||
GENRE_USER = "User"
|
||||
|
@ -53,7 +57,8 @@ DISCOVERY_COMPONENTS = [
|
|||
DISCOVER_SENSORS,
|
||||
[COMMAND_CLASS_SENSOR_BINARY,
|
||||
COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
COMMAND_CLASS_METER],
|
||||
COMMAND_CLASS_METER,
|
||||
COMMAND_CLASS_ALARM],
|
||||
TYPE_WHATEVER,
|
||||
GENRE_USER),
|
||||
('light',
|
||||
|
@ -176,6 +181,14 @@ def setup(hass, config):
|
|||
dispatcher.connect(
|
||||
value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False)
|
||||
|
||||
def add_node(event):
|
||||
""" Switch into inclusion mode """
|
||||
NETWORK.controller.begin_command_add_device()
|
||||
|
||||
def remove_node(event):
|
||||
""" Switch into exclusion mode"""
|
||||
NETWORK.controller.begin_command_remove_device()
|
||||
|
||||
def stop_zwave(event):
|
||||
""" Stop Z-wave. """
|
||||
NETWORK.stop()
|
||||
|
@ -190,6 +203,11 @@ def setup(hass, config):
|
|||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zwave)
|
||||
|
||||
# register add / remove node services for zwave sticks without
|
||||
# hardware inclusion button
|
||||
hass.services.register(DOMAIN, SERVICE_ADD_NODE, add_node)
|
||||
hass.services.register(DOMAIN, SERVICE_REMOVE_NODE, remove_node)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_zwave)
|
||||
|
||||
return True
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# coding: utf-8
|
||||
""" Constants used by Home Assistant components. """
|
||||
|
||||
__version__ = "0.10.1"
|
||||
__version__ = "0.11.0"
|
||||
|
||||
# Can be used to specify a catch all when registering state or event listeners.
|
||||
MATCH_ALL = '*'
|
||||
|
@ -24,6 +24,7 @@ CONF_USERNAME = "username"
|
|||
CONF_PASSWORD = "password"
|
||||
CONF_API_KEY = "api_key"
|
||||
CONF_ACCESS_TOKEN = "access_token"
|
||||
CONF_FILENAME = "filename"
|
||||
|
||||
CONF_VALUE_TEMPLATE = "value_template"
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
"""
|
||||
homeassistant
|
||||
~~~~~~~~~~~~~
|
||||
Core components of Home Assistant.
|
||||
|
||||
Home Assistant is a Home Automation framework for observing the state
|
||||
of entities and react to changes.
|
||||
|
@ -53,9 +52,10 @@ _MockHA = namedtuple("MockHomeAssistant", ['bus'])
|
|||
|
||||
|
||||
class HomeAssistant(object):
|
||||
""" Core class to route all communication to right components. """
|
||||
"""Root object of the Home Assistant home automation."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize new Home Assistant object."""
|
||||
self.pool = pool = create_worker_pool()
|
||||
self.bus = EventBus(pool)
|
||||
self.services = ServiceRegistry(self.bus, pool)
|
||||
|
@ -63,7 +63,7 @@ class HomeAssistant(object):
|
|||
self.config = Config()
|
||||
|
||||
def start(self):
|
||||
""" Start home assistant. """
|
||||
"""Start home assistant."""
|
||||
_LOGGER.info(
|
||||
"Starting Home Assistant (%d threads)", self.pool.worker_count)
|
||||
|
||||
|
@ -71,12 +71,11 @@ class HomeAssistant(object):
|
|||
self.bus.fire(EVENT_HOMEASSISTANT_START)
|
||||
|
||||
def block_till_stopped(self):
|
||||
""" Will register service homeassistant/stop and
|
||||
will block until called. """
|
||||
"""Register service homeassistant/stop and will block until called."""
|
||||
request_shutdown = threading.Event()
|
||||
|
||||
def stop_homeassistant(*args):
|
||||
""" Stops Home Assistant. """
|
||||
"""Stop Home Assistant."""
|
||||
request_shutdown.set()
|
||||
|
||||
self.services.register(
|
||||
|
@ -98,7 +97,7 @@ class HomeAssistant(object):
|
|||
self.stop()
|
||||
|
||||
def stop(self):
|
||||
""" Stops Home Assistant and shuts down all threads. """
|
||||
"""Stop Home Assistant and shuts down all threads."""
|
||||
_LOGGER.info("Stopping")
|
||||
|
||||
self.bus.fire(EVENT_HOMEASSISTANT_STOP)
|
||||
|
@ -150,8 +149,7 @@ class HomeAssistant(object):
|
|||
|
||||
|
||||
class JobPriority(util.OrderedEnum):
|
||||
""" Provides priorities for bus events. """
|
||||
# pylint: disable=no-init,too-few-public-methods
|
||||
"""Provides job priorities for event bus jobs."""
|
||||
|
||||
EVENT_CALLBACK = 0
|
||||
EVENT_SERVICE = 1
|
||||
|
@ -161,7 +159,7 @@ class JobPriority(util.OrderedEnum):
|
|||
|
||||
@staticmethod
|
||||
def from_event_type(event_type):
|
||||
""" Returns a priority based on event type. """
|
||||
"""Return a priority based on event type."""
|
||||
if event_type == EVENT_TIME_CHANGED:
|
||||
return JobPriority.EVENT_TIME
|
||||
elif event_type == EVENT_STATE_CHANGED:
|
||||
|
@ -175,8 +173,7 @@ class JobPriority(util.OrderedEnum):
|
|||
|
||||
|
||||
class EventOrigin(enum.Enum):
|
||||
""" Distinguish between origin of event. """
|
||||
# pylint: disable=no-init,too-few-public-methods
|
||||
"""Represents origin of an event."""
|
||||
|
||||
local = "LOCAL"
|
||||
remote = "REMOTE"
|
||||
|
@ -185,14 +182,15 @@ class EventOrigin(enum.Enum):
|
|||
return self.value
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class Event(object):
|
||||
""" Represents an event within the Bus. """
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Represents an event within the Bus."""
|
||||
|
||||
__slots__ = ['event_type', 'data', 'origin', 'time_fired']
|
||||
|
||||
def __init__(self, event_type, data=None, origin=EventOrigin.local,
|
||||
time_fired=None):
|
||||
"""Initialize a new event."""
|
||||
self.event_type = event_type
|
||||
self.data = data or {}
|
||||
self.origin = origin
|
||||
|
@ -200,7 +198,7 @@ class Event(object):
|
|||
time_fired or dt_util.utcnow())
|
||||
|
||||
def as_dict(self):
|
||||
""" Returns a dict representation of this Event. """
|
||||
"""Create a dict representation of this Event."""
|
||||
return {
|
||||
'event_type': self.event_type,
|
||||
'data': dict(self.data),
|
||||
|
@ -227,26 +225,23 @@ class Event(object):
|
|||
|
||||
|
||||
class EventBus(object):
|
||||
""" Class that allows different components to communicate via services
|
||||
and events.
|
||||
"""
|
||||
"""Allows firing of and listening for events."""
|
||||
|
||||
def __init__(self, pool=None):
|
||||
"""Initialize a new event bus."""
|
||||
self._listeners = {}
|
||||
self._lock = threading.Lock()
|
||||
self._pool = pool or create_worker_pool()
|
||||
|
||||
@property
|
||||
def listeners(self):
|
||||
""" Dict with events that is being listened for and the number
|
||||
of listeners.
|
||||
"""
|
||||
"""Dict with events and the number of listeners."""
|
||||
with self._lock:
|
||||
return {key: len(self._listeners[key])
|
||||
for key in self._listeners}
|
||||
|
||||
def fire(self, event_type, event_data=None, origin=EventOrigin.local):
|
||||
""" Fire an event. """
|
||||
"""Fire an event."""
|
||||
if not self._pool.running:
|
||||
raise HomeAssistantError('Home Assistant has shut down.')
|
||||
|
||||
|
@ -271,7 +266,7 @@ class EventBus(object):
|
|||
self._pool.add_job(job_priority, (func, event))
|
||||
|
||||
def listen(self, event_type, listener):
|
||||
""" Listen for all events or events of a specific type.
|
||||
"""Listen for all events or events of a specific type.
|
||||
|
||||
To listen to all events specify the constant ``MATCH_ALL``
|
||||
as event_type.
|
||||
|
@ -283,7 +278,7 @@ class EventBus(object):
|
|||
self._listeners[event_type] = [listener]
|
||||
|
||||
def listen_once(self, event_type, listener):
|
||||
""" Listen once for event of a specific type.
|
||||
"""Listen once for event of a specific type.
|
||||
|
||||
To listen to all events specify the constant ``MATCH_ALL``
|
||||
as event_type.
|
||||
|
@ -292,7 +287,7 @@ class EventBus(object):
|
|||
"""
|
||||
@ft.wraps(listener)
|
||||
def onetime_listener(event):
|
||||
""" Removes listener from eventbus and then fires listener. """
|
||||
"""Remove listener from eventbus and then fires listener."""
|
||||
if hasattr(onetime_listener, 'run'):
|
||||
return
|
||||
# Set variable so that we will never run twice.
|
||||
|
@ -311,7 +306,7 @@ class EventBus(object):
|
|||
return onetime_listener
|
||||
|
||||
def remove_listener(self, event_type, listener):
|
||||
""" Removes a listener of a specific event_type. """
|
||||
"""Remove a listener of a specific event_type."""
|
||||
with self._lock:
|
||||
try:
|
||||
self._listeners[event_type].remove(listener)
|
||||
|
@ -343,6 +338,7 @@ class State(object):
|
|||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, entity_id, state, attributes=None, last_changed=None,
|
||||
last_updated=None):
|
||||
"""Initialize a new state."""
|
||||
if not ENTITY_ID_PATTERN.match(entity_id):
|
||||
raise InvalidEntityFormatError((
|
||||
"Invalid entity id encountered: {}. "
|
||||
|
@ -363,31 +359,33 @@ class State(object):
|
|||
|
||||
@property
|
||||
def domain(self):
|
||||
""" Returns domain of this state. """
|
||||
"""Domain of this state."""
|
||||
return util.split_entity_id(self.entity_id)[0]
|
||||
|
||||
@property
|
||||
def object_id(self):
|
||||
""" Returns object_id of this state. """
|
||||
"""Object id of this state."""
|
||||
return util.split_entity_id(self.entity_id)[1]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Name to represent this state. """
|
||||
"""Name of this state."""
|
||||
return (
|
||||
self.attributes.get(ATTR_FRIENDLY_NAME) or
|
||||
self.object_id.replace('_', ' '))
|
||||
|
||||
def copy(self):
|
||||
""" Creates a copy of itself. """
|
||||
"""Return a copy of the state."""
|
||||
return State(self.entity_id, self.state,
|
||||
dict(self.attributes), self.last_changed,
|
||||
self.last_updated)
|
||||
|
||||
def as_dict(self):
|
||||
""" Converts State to a dict to be used within JSON.
|
||||
Ensures: state == State.from_dict(state.as_dict()) """
|
||||
"""Return a dict representation of the State.
|
||||
|
||||
To be used for JSON serialization.
|
||||
Ensures: state == State.from_dict(state.as_dict())
|
||||
"""
|
||||
return {'entity_id': self.entity_id,
|
||||
'state': self.state,
|
||||
'attributes': self.attributes,
|
||||
|
@ -396,11 +394,11 @@ class State(object):
|
|||
|
||||
@classmethod
|
||||
def from_dict(cls, json_dict):
|
||||
""" Static method to create a state from a dict.
|
||||
Ensures: state == State.from_json_dict(state.to_json_dict()) """
|
||||
"""Initialize a state from a dict.
|
||||
|
||||
if not (json_dict and
|
||||
'entity_id' in json_dict and
|
||||
Ensures: state == State.from_json_dict(state.to_json_dict())
|
||||
"""
|
||||
if not (json_dict and 'entity_id' in json_dict and
|
||||
'state' in json_dict):
|
||||
return None
|
||||
|
||||
|
@ -433,15 +431,16 @@ class State(object):
|
|||
|
||||
|
||||
class StateMachine(object):
|
||||
""" Helper class that tracks the state of different entities. """
|
||||
"""Helper class that tracks the state of different entities."""
|
||||
|
||||
def __init__(self, bus):
|
||||
"""Initialize state machine."""
|
||||
self._states = {}
|
||||
self._bus = bus
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def entity_ids(self, domain_filter=None):
|
||||
""" List of entity ids that are being tracked. """
|
||||
"""List of entity ids that are being tracked."""
|
||||
if domain_filter is None:
|
||||
return list(self._states.keys())
|
||||
|
||||
|
@ -451,35 +450,43 @@ class StateMachine(object):
|
|||
if state.domain == domain_filter]
|
||||
|
||||
def all(self):
|
||||
""" Returns a list of all states. """
|
||||
"""Create a list of all states."""
|
||||
with self._lock:
|
||||
return [state.copy() for state in self._states.values()]
|
||||
|
||||
def get(self, entity_id):
|
||||
""" Returns the state of the specified entity. """
|
||||
"""Retrieve state of entity_id or None if not found."""
|
||||
state = self._states.get(entity_id.lower())
|
||||
|
||||
# Make a copy so people won't mutate the state
|
||||
return state.copy() if state else None
|
||||
|
||||
def is_state(self, entity_id, state):
|
||||
""" Returns True if entity exists and is specified state. """
|
||||
"""Test if entity exists and is specified state."""
|
||||
entity_id = entity_id.lower()
|
||||
|
||||
return (entity_id in self._states and
|
||||
self._states[entity_id].state == state)
|
||||
|
||||
def remove(self, entity_id):
|
||||
""" Removes an entity from the state machine.
|
||||
def is_state_attr(self, entity_id, name, value):
|
||||
"""Test if entity exists and has a state attribute set to value."""
|
||||
entity_id = entity_id.lower()
|
||||
|
||||
Returns boolean to indicate if an entity was removed. """
|
||||
return (entity_id in self._states and
|
||||
self._states[entity_id].attributes.get(name, None) == value)
|
||||
|
||||
def remove(self, entity_id):
|
||||
"""Remove the state of an entity.
|
||||
|
||||
Returns boolean to indicate if an entity was removed.
|
||||
"""
|
||||
entity_id = entity_id.lower()
|
||||
|
||||
with self._lock:
|
||||
return self._states.pop(entity_id, None) is not None
|
||||
|
||||
def set(self, entity_id, new_state, attributes=None):
|
||||
""" Set the state of an entity, add entity if it does not exist.
|
||||
"""Set the state of an entity, add entity if it does not exist.
|
||||
|
||||
Attributes is an optional dict to specify attributes of this state.
|
||||
|
||||
|
@ -514,9 +521,7 @@ class StateMachine(object):
|
|||
self._bus.fire(EVENT_STATE_CHANGED, event_data)
|
||||
|
||||
def track_change(self, entity_ids, action, from_state=None, to_state=None):
|
||||
"""
|
||||
DEPRECATED AS OF 8/4/2015
|
||||
"""
|
||||
"""DEPRECATED AS OF 8/4/2015."""
|
||||
_LOGGER.warning(
|
||||
'hass.states.track_change is deprecated. '
|
||||
'Use homeassistant.helpers.event.track_state_change instead.')
|
||||
|
@ -527,33 +532,36 @@ class StateMachine(object):
|
|||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class Service(object):
|
||||
""" Represents a service. """
|
||||
"""Represents a callable service."""
|
||||
|
||||
__slots__ = ['func', 'description', 'fields']
|
||||
|
||||
def __init__(self, func, description, fields):
|
||||
"""Initialize a service."""
|
||||
self.func = func
|
||||
self.description = description or ''
|
||||
self.fields = fields or {}
|
||||
|
||||
def as_dict(self):
|
||||
""" Return dictionary representation of this service. """
|
||||
"""Return dictionary representation of this service."""
|
||||
return {
|
||||
'description': self.description,
|
||||
'fields': self.fields,
|
||||
}
|
||||
|
||||
def __call__(self, call):
|
||||
"""Execute the service."""
|
||||
self.func(call)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class ServiceCall(object):
|
||||
""" Represents a call to a service. """
|
||||
"""Represents a call to a service."""
|
||||
|
||||
__slots__ = ['domain', 'service', 'data']
|
||||
|
||||
def __init__(self, domain, service, data=None):
|
||||
"""Initialize a service call."""
|
||||
self.domain = domain
|
||||
self.service = service
|
||||
self.data = data or {}
|
||||
|
@ -567,9 +575,10 @@ class ServiceCall(object):
|
|||
|
||||
|
||||
class ServiceRegistry(object):
|
||||
""" Offers services over the eventbus. """
|
||||
"""Offers services over the eventbus."""
|
||||
|
||||
def __init__(self, bus, pool=None):
|
||||
"""Initialize a service registry."""
|
||||
self._services = {}
|
||||
self._lock = threading.Lock()
|
||||
self._pool = pool or create_worker_pool()
|
||||
|
@ -579,14 +588,14 @@ class ServiceRegistry(object):
|
|||
|
||||
@property
|
||||
def services(self):
|
||||
""" Dict with per domain a list of available services. """
|
||||
"""Dict with per domain a list of available services."""
|
||||
with self._lock:
|
||||
return {domain: {key: value.as_dict() for key, value
|
||||
in self._services[domain].items()}
|
||||
for domain in self._services}
|
||||
|
||||
def has_service(self, domain, service):
|
||||
""" Returns True if specified service exists. """
|
||||
"""Test if specified service exists."""
|
||||
return service in self._services.get(domain, [])
|
||||
|
||||
def register(self, domain, service, service_func, description=None):
|
||||
|
@ -611,7 +620,8 @@ class ServiceRegistry(object):
|
|||
|
||||
def call(self, domain, service, service_data=None, blocking=False):
|
||||
"""
|
||||
Calls specified service.
|
||||
Call a service.
|
||||
|
||||
Specify blocking=True to wait till service is executed.
|
||||
Waits a maximum of SERVICE_CALL_LIMIT.
|
||||
|
||||
|
@ -635,10 +645,7 @@ class ServiceRegistry(object):
|
|||
executed_event = threading.Event()
|
||||
|
||||
def service_executed(call):
|
||||
"""
|
||||
Called when a service is executed.
|
||||
Will set the event if matches our service call.
|
||||
"""
|
||||
"""Callback method that is called when service is executed."""
|
||||
if call.data[ATTR_SERVICE_CALL_ID] == call_id:
|
||||
executed_event.set()
|
||||
|
||||
|
@ -653,7 +660,7 @@ class ServiceRegistry(object):
|
|||
return success
|
||||
|
||||
def _event_to_service_call(self, event):
|
||||
""" Calls a service from an event. """
|
||||
"""Callback for SERVICE_CALLED events from the event bus."""
|
||||
service_data = dict(event.data)
|
||||
domain = service_data.pop(ATTR_DOMAIN, None)
|
||||
service = service_data.pop(ATTR_SERVICE, None)
|
||||
|
@ -670,7 +677,7 @@ class ServiceRegistry(object):
|
|||
(service_handler, service_call)))
|
||||
|
||||
def _execute_service(self, service_and_call):
|
||||
""" Executes a service and fires a SERVICE_EXECUTED event. """
|
||||
"""Execute a service and fires a SERVICE_EXECUTED event."""
|
||||
service, call = service_and_call
|
||||
service(call)
|
||||
|
||||
|
@ -680,16 +687,17 @@ class ServiceRegistry(object):
|
|||
{ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]})
|
||||
|
||||
def _generate_unique_id(self):
|
||||
""" Generates a unique service call id. """
|
||||
"""Generate a unique service call id."""
|
||||
self._cur_id += 1
|
||||
return "{}-{}".format(id(self), self._cur_id)
|
||||
|
||||
|
||||
class Config(object):
|
||||
""" Configuration settings for Home Assistant. """
|
||||
"""Configuration settings for Home Assistant."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self):
|
||||
"""Initialize a new config object."""
|
||||
self.latitude = None
|
||||
self.longitude = None
|
||||
self.temperature_unit = None
|
||||
|
@ -709,15 +717,15 @@ class Config(object):
|
|||
self.config_dir = get_default_config_dir()
|
||||
|
||||
def distance(self, lat, lon):
|
||||
""" Calculate distance from Home Assistant in meters. """
|
||||
"""Calculate distance from Home Assistant in meters."""
|
||||
return location.distance(self.latitude, self.longitude, lat, lon)
|
||||
|
||||
def path(self, *path):
|
||||
""" Returns path to the file within the config dir. """
|
||||
"""Generate path to the file within the config dir."""
|
||||
return os.path.join(self.config_dir, *path)
|
||||
|
||||
def temperature(self, value, unit):
|
||||
""" Converts temperature to user preferred unit if set. """
|
||||
"""Convert temperature to user preferred unit if set."""
|
||||
if not (unit in (TEMP_CELCIUS, TEMP_FAHRENHEIT) and
|
||||
self.temperature_unit and unit != self.temperature_unit):
|
||||
return value, unit
|
||||
|
@ -732,7 +740,7 @@ class Config(object):
|
|||
self.temperature_unit)
|
||||
|
||||
def as_dict(self):
|
||||
""" Converts config to a dictionary. """
|
||||
"""Create a dict representation of this dict."""
|
||||
time_zone = self.time_zone or dt_util.UTC
|
||||
|
||||
return {
|
||||
|
@ -747,7 +755,7 @@ class Config(object):
|
|||
|
||||
|
||||
def create_timer(hass, interval=TIMER_INTERVAL):
|
||||
""" Creates a timer. Timer will start on HOMEASSISTANT_START. """
|
||||
"""Create a timer that will start on HOMEASSISTANT_START."""
|
||||
# We want to be able to fire every time a minute starts (seconds=0).
|
||||
# We want this so other modules can use that to make sure they fire
|
||||
# every minute.
|
||||
|
@ -810,12 +818,12 @@ def create_timer(hass, interval=TIMER_INTERVAL):
|
|||
|
||||
|
||||
def create_worker_pool(worker_count=None):
|
||||
""" Creates a worker pool to be used. """
|
||||
"""Create a worker pool."""
|
||||
if worker_count is None:
|
||||
worker_count = MIN_WORKER_THREAD
|
||||
|
||||
def job_handler(job):
|
||||
""" Called whenever a job is available to do. """
|
||||
"""Called whenever a job is available to do."""
|
||||
try:
|
||||
func, arg = job
|
||||
func(arg)
|
||||
|
@ -825,8 +833,7 @@ def create_worker_pool(worker_count=None):
|
|||
_LOGGER.exception("BusHandler:Exception doing job")
|
||||
|
||||
def busy_callback(worker_count, current_jobs, pending_jobs_count):
|
||||
""" Callback to be called when the pool queue gets too big. """
|
||||
|
||||
"""Callback to be called when the pool queue gets too big."""
|
||||
_LOGGER.warning(
|
||||
"WorkerPool:All %d threads are busy and %d jobs pending",
|
||||
worker_count, pending_jobs_count)
|
||||
|
|
|
@ -36,7 +36,7 @@ def extract_entity_ids(hass, service):
|
|||
service_ent_id = service.data[ATTR_ENTITY_ID]
|
||||
|
||||
if isinstance(service_ent_id, str):
|
||||
return group.expand_entity_ids(hass, [service_ent_id.lower()])
|
||||
return group.expand_entity_ids(hass, [service_ent_id])
|
||||
|
||||
return [ent_id for ent_id in group.expand_entity_ids(hass, service_ent_id)]
|
||||
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
"""Service calling related helpers."""
|
||||
import logging
|
||||
|
||||
from homeassistant.util import split_entity_id
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
|
||||
CONF_SERVICE = 'service'
|
||||
CONF_SERVICE_ENTITY_ID = 'entity_id'
|
||||
CONF_SERVICE_DATA = 'data'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def call_from_config(hass, config, blocking=False):
|
||||
"""Call a service based on a config hash."""
|
||||
if not isinstance(config, dict) or CONF_SERVICE not in config:
|
||||
_LOGGER.error('Missing key %s: %s', CONF_SERVICE, config)
|
||||
return
|
||||
|
||||
try:
|
||||
domain, service = split_entity_id(config[CONF_SERVICE])
|
||||
except ValueError:
|
||||
_LOGGER.error('Invalid service specified: %s', config[CONF_SERVICE])
|
||||
return
|
||||
|
||||
service_data = config.get(CONF_SERVICE_DATA)
|
||||
|
||||
if service_data is None:
|
||||
service_data = {}
|
||||
elif isinstance(service_data, dict):
|
||||
service_data = dict(service_data)
|
||||
else:
|
||||
_LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA)
|
||||
service_data = {}
|
||||
|
||||
entity_id = config.get(CONF_SERVICE_ENTITY_ID)
|
||||
if isinstance(entity_id, str):
|
||||
service_data[ATTR_ENTITY_ID] = [ent.strip() for ent in
|
||||
entity_id.split(",")]
|
||||
elif entity_id is not None:
|
||||
service_data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(domain, service, service_data, blocking)
|
|
@ -1,9 +1,6 @@
|
|||
"""
|
||||
homeassistant.helpers.state
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Helpers that help with state related things.
|
||||
"""
|
||||
"""Helpers that help with state related things."""
|
||||
from collections import defaultdict
|
||||
import json
|
||||
import logging
|
||||
|
||||
from homeassistant.core import State
|
||||
|
@ -25,32 +22,36 @@ class TrackStates(object):
|
|||
that have changed since the start time to the return list when with-block
|
||||
is exited.
|
||||
"""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize a TrackStates block."""
|
||||
self.hass = hass
|
||||
self.states = []
|
||||
|
||||
def __enter__(self):
|
||||
"""Record time from which to track changes."""
|
||||
self.now = dt_util.utcnow()
|
||||
return self.states
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
"""Add changes states to changes list."""
|
||||
self.states.extend(get_changed_since(self.hass.states.all(), self.now))
|
||||
|
||||
|
||||
def get_changed_since(states, utc_point_in_time):
|
||||
"""
|
||||
Returns all states that have been changed since utc_point_in_time.
|
||||
"""
|
||||
"""List of states that have been changed since utc_point_in_time."""
|
||||
point_in_time = dt_util.strip_microseconds(utc_point_in_time)
|
||||
|
||||
return [state for state in states if state.last_updated >= point_in_time]
|
||||
|
||||
|
||||
def reproduce_state(hass, states, blocking=False):
|
||||
""" Takes in a state and will try to have the entity reproduce it. """
|
||||
"""Reproduce given state."""
|
||||
if isinstance(states, State):
|
||||
states = [states]
|
||||
|
||||
to_call = defaultdict(list)
|
||||
|
||||
for state in states:
|
||||
current_state = hass.states.get(state.entity_id)
|
||||
|
||||
|
@ -76,7 +77,18 @@ def reproduce_state(hass, states, blocking=False):
|
|||
state)
|
||||
continue
|
||||
|
||||
service_data = dict(state.attributes)
|
||||
service_data[ATTR_ENTITY_ID] = state.entity_id
|
||||
if state.domain == 'group':
|
||||
service_domain = 'homeassistant'
|
||||
else:
|
||||
service_domain = state.domain
|
||||
|
||||
hass.services.call(state.domain, service, service_data, blocking)
|
||||
# We group service calls for entities by service call
|
||||
# json used to create a hashable version of dict with maybe lists in it
|
||||
key = (service_domain, service,
|
||||
json.dumps(state.attributes, sort_keys=True))
|
||||
to_call[key].append(state.entity_id)
|
||||
|
||||
for (service_domain, service, service_data), entity_ids in to_call.items():
|
||||
data = json.loads(service_data)
|
||||
data[ATTR_ENTITY_ID] = entity_ids
|
||||
hass.services.call(service_domain, service, data, blocking)
|
||||
|
|
|
@ -53,8 +53,12 @@ def color_xy_brightness_to_RGB(vX, vY, brightness):
|
|||
return (0, 0, 0)
|
||||
|
||||
Y = brightness
|
||||
X = (Y / vY) * vX
|
||||
Z = (Y / vY) * (1 - vX - vY)
|
||||
if vY != 0:
|
||||
X = (Y / vY) * vX
|
||||
Z = (Y / vY) * (1 - vX - vY)
|
||||
else:
|
||||
X = 0
|
||||
Z = 0
|
||||
|
||||
# Convert to RGB using Wide RGB D65 conversion.
|
||||
r = X * 1.612 - Y * 0.203 - Z * 0.302
|
||||
|
|
|
@ -108,14 +108,14 @@ def datetime_to_date_str(dattim):
|
|||
return dattim.strftime(DATE_STR_FORMAT)
|
||||
|
||||
|
||||
def str_to_datetime(dt_str):
|
||||
def str_to_datetime(dt_str, dt_format=DATETIME_STR_FORMAT):
|
||||
""" Converts a string to a UTC datetime object.
|
||||
|
||||
@rtype: datetime
|
||||
"""
|
||||
try:
|
||||
return dt.datetime.strptime(
|
||||
dt_str, DATETIME_STR_FORMAT).replace(tzinfo=pytz.utc)
|
||||
dt_str, dt_format).replace(tzinfo=pytz.utc)
|
||||
except ValueError: # If dt_str did not match our format
|
||||
return None
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import collections
|
|||
import requests
|
||||
from vincenty import vincenty
|
||||
|
||||
ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json'
|
||||
|
||||
|
||||
LocationInfo = collections.namedtuple(
|
||||
"LocationInfo",
|
||||
|
@ -34,3 +36,20 @@ def detect_location_info():
|
|||
def distance(lat1, lon1, lat2, lon2):
|
||||
""" Calculate the distance in meters between two points. """
|
||||
return vincenty((lat1, lon1), (lat2, lon2)) * 1000
|
||||
|
||||
|
||||
def elevation(latitude, longitude):
|
||||
""" Return elevation for given latitude and longitude. """
|
||||
|
||||
req = requests.get(ELEVATION_URL, params={
|
||||
'locations': '{},{}'.format(latitude, longitude),
|
||||
'sensor': 'false',
|
||||
})
|
||||
|
||||
if req.status_code != 200:
|
||||
return 0
|
||||
|
||||
try:
|
||||
return int(float(req.json()['results'][0]['elevation']))
|
||||
except (ValueError, KeyError):
|
||||
return 0
|
||||
|
|
|
@ -43,7 +43,8 @@ def render(hass, template, variables=None, **kwargs):
|
|||
try:
|
||||
return ENV.from_string(template, {
|
||||
'states': AllStates(hass),
|
||||
'is_state': hass.states.is_state
|
||||
'is_state': hass.states.is_state,
|
||||
'is_state_attr': hass.states.is_state_attr
|
||||
}).render(kwargs).strip()
|
||||
except jinja2.TemplateError as err:
|
||||
raise TemplateError(err)
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
[pytest]
|
||||
testpaths = tests
|
|
@ -1,5 +0,0 @@
|
|||
requests>=2,<3
|
||||
pyyaml>=3.11,<4
|
||||
pytz>=2015.4
|
||||
pip>=7.0.0
|
||||
vincenty==0.1.3
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue