Merge pull request #1995 from home-assistant/dev

0.19
This commit is contained in:
Paulus Schoutsen 2016-05-07 12:55:27 -07:00
commit 6b1f9a32dd
143 changed files with 5715 additions and 8650 deletions

View File

@ -14,6 +14,9 @@ omit =
homeassistant/components/bloomsky.py
homeassistant/components/*/bloomsky.py
homeassistant/components/dweet.py
homeassistant/components/*/dweet.py
homeassistant/components/ecobee.py
homeassistant/components/*/ecobee.py
@ -32,6 +35,9 @@ omit =
homeassistant/components/nest.py
homeassistant/components/*/nest.py
homeassistant/components/octoprint.py
homeassistant/components/*/octoprint.py
homeassistant/components/rpi_gpio.py
homeassistant/components/*/rpi_gpio.py
@ -110,6 +116,7 @@ omit =
homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py
homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/samsungtv.py
homeassistant/components/media_player/snapcast.py
@ -139,11 +146,12 @@ omit =
homeassistant/components/sensor/cpuspeed.py
homeassistant/components/sensor/deutsche_bahn.py
homeassistant/components/sensor/dht.py
homeassistant/components/sensor/dweet.py
homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/glances.py
homeassistant/components/sensor/google_travel_time.py
homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/netatmo.py
@ -164,6 +172,7 @@ omit =
homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py
homeassistant/components/sensor/worldclock.py
homeassistant/components/switch/acer_projector.py
homeassistant/components/switch/arest.py
homeassistant/components/switch/dlink.py
homeassistant/components/switch/edimax.py
@ -172,6 +181,7 @@ omit =
homeassistant/components/switch/orvibo.py
homeassistant/components/switch/pulseaudio_loopback.py
homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_rf.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/wake_on_lan.py
homeassistant/components/thermostat/eq3btsmart.py

View File

@ -1,4 +1,4 @@
Feature requests should go in the forum: https://community.home-assistant.io/c/feature-requests
Make sure you run the latest version before reporting an issue. Feature requests should go in the forum: https://community.home-assistant.io/c/feature-requests
**Home Assistant release (`hass --version`):**

1
.gitignore vendored
View File

@ -78,6 +78,7 @@ nosetests.xml
pyvenv.cfg
pip-selfcheck.json
venv
.venv
# vimmy stuff
*.swp

View File

@ -1,5 +1,5 @@
Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant|
===========================================================================================================
Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant| |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs|
==================================================================================================================
Home Assistant is a home automation platform running on Python 3. The
goal of Home Assistant is to be able to track and control all devices at
@ -80,9 +80,9 @@ Built home automation on top of your devices:
The system is built modular so support for other devices or actions can
be implemented easily. See also the `section on
architecture <https://home-assistant.io/developers/architecture.html>`__
architecture <https://home-assistant.io/developers/architecture/>`__
and the `section on creating your own
components <https://home-assistant.io/developers/creating_components.html>`__.
components <https://home-assistant.io/developers/creating_components/>`__.
If you run into issues while using Home Assistant or during development
of a component, check the `Home Assistant help
@ -92,7 +92,9 @@ section <https://home-assistant.io/help/>`__ how to reach us.
:target: https://travis-ci.org/home-assistant/home-assistant
.. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
.. |Join the chat at https://gitter.im/home-assistant/home-assistant| image:: https://badges.gitter.im/Join%20Chat.svg
.. |Join the chat at https://gitter.im/home-assistant/home-assistant| image:: https://img.shields.io/badge/gitter-general-blue.svg
:target: https://gitter.im/home-assistant/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs| image:: https://img.shields.io/badge/gitter-development-yellowgreen.svg
:target: https://gitter.im/home-assistant/home-assistant/devs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
:target: https://home-assistant.io/demo/

View File

@ -14,6 +14,7 @@ import homeassistant.components as core_components
import homeassistant.components.group as group
import homeassistant.config as config_util
import homeassistant.core as core
import homeassistant.helpers.config_validation as cv
import homeassistant.loader as loader
import homeassistant.util.dt as date_util
import homeassistant.util.location as loc_util
@ -103,7 +104,7 @@ def _setup_component(hass, domain, config):
try:
config = component.CONFIG_SCHEMA(config)
except vol.MultipleInvalid as ex:
_LOGGER.error('Invalid config for [%s]: %s', domain, ex)
cv.log_exception(_LOGGER, ex, domain)
return False
elif hasattr(component, 'PLATFORM_SCHEMA'):
@ -113,8 +114,7 @@ def _setup_component(hass, domain, config):
try:
p_validated = component.PLATFORM_SCHEMA(p_config)
except vol.MultipleInvalid as ex:
_LOGGER.error('Invalid platform config for [%s]: %s. %s',
domain, ex, p_config)
cv.log_exception(_LOGGER, ex, domain)
return False
# Not all platform components follow same pattern for platforms
@ -135,9 +135,8 @@ def _setup_component(hass, domain, config):
try:
p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.MultipleInvalid as ex:
_LOGGER.error(
'Invalid platform config for [%s.%s]: %s. %s',
domain, p_name, ex, p_config)
cv.log_exception(_LOGGER, ex, '{}.{}'
.format(domain, p_name))
return False
platforms.append(p_validated)
@ -233,7 +232,7 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA(
config.get(core.DOMAIN, {})))
except vol.MultipleInvalid as ex:
_LOGGER.error('Invalid config for [homeassistant]: %s', ex)
cv.log_exception(_LOGGER, ex, 'homeassistant')
return None
process_ha_config_upgrade(hass)

View File

@ -48,6 +48,11 @@ class VerisureAlarm(alarm.AlarmControlPanel):
"""Return the state of the device."""
return self._state
@property
def available(self):
"""Return True if entity is available."""
return hub.available
@property
def code_format(self):
"""The code format as regex."""

View File

@ -8,8 +8,7 @@ import enum
import logging
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
from homeassistant.helpers.service import call_from_config
from homeassistant.helpers import template
from homeassistant.helpers import template, script
DOMAIN = 'alexa'
DEPENDENCIES = ['http']
@ -27,7 +26,14 @@ CONF_ACTION = 'action'
def setup(hass, config):
"""Activate Alexa component."""
_CONFIG.update(config[DOMAIN].get(CONF_INTENTS, {}))
intents = config[DOMAIN].get(CONF_INTENTS, {})
for name, intent in intents.items():
if CONF_ACTION in intent:
intent[CONF_ACTION] = script.Script(hass, intent[CONF_ACTION],
"Alexa intent {}".format(name))
_CONFIG.update(intents)
hass.http.register_path('POST', API_ENDPOINT, _handle_alexa, True)
@ -91,7 +97,7 @@ def _handle_alexa(handler, path_match, data):
card['content'])
if action is not None:
call_from_config(handler.server.hass, action, True)
action.run(response.variables)
handler.write_json(response.as_dict())

View File

@ -16,9 +16,10 @@ from homeassistant.const import (
CONTENT_TYPE_TEXT_PLAIN, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_HEADER_CONTENT_TYPE, HTTP_NOT_FOUND,
HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_ERROR_LOG, URL_API_EVENT_FORWARD, URL_API_EVENTS,
URL_API_LOG_OUT, URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY,
URL_API_STREAM, URL_API_TEMPLATE)
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_LOG_OUT, URL_API_SERVICES,
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
__version__)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import TrackStates
from homeassistant.helpers import template
@ -37,13 +38,18 @@ def setup(hass, config):
# /api - for validation purposes
hass.http.register_path('GET', URL_API, _handle_get_api)
# /api/stream
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
# /api/config
hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)
# /states
# /api/discovery_info
hass.http.register_path('GET', URL_API_DISCOVERY_INFO,
_handle_get_api_discovery_info,
require_auth=False)
# /api/stream
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
# /api/states
hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
hass.http.register_path(
'GET', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
@ -58,13 +64,13 @@ def setup(hass, config):
'DELETE', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_handle_delete_state_entity)
# /events
# /api/events
hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events)
hass.http.register_path(
'POST', re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
_handle_api_post_events_event)
# /services
# /api/services
hass.http.register_path('GET', URL_API_SERVICES, _handle_get_api_services)
hass.http.register_path(
'POST',
@ -73,23 +79,23 @@ def setup(hass, config):
r'(?P<service>[a-zA-Z\._0-9]+)')),
_handle_post_api_services_domain_service)
# /event_forwarding
# /api/event_forwarding
hass.http.register_path(
'POST', URL_API_EVENT_FORWARD, _handle_post_api_event_forward)
hass.http.register_path(
'DELETE', URL_API_EVENT_FORWARD, _handle_delete_api_event_forward)
# /components
# /api/components
hass.http.register_path(
'GET', URL_API_COMPONENTS, _handle_get_api_components)
# /error_log
# /api/error_log
hass.http.register_path('GET', URL_API_ERROR_LOG,
_handle_get_api_error_log)
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
# /template
# /api/template
hass.http.register_path('POST', URL_API_TEMPLATE,
_handle_post_api_template)
@ -176,6 +182,17 @@ def _handle_get_api_config(handler, path_match, data):
handler.write_json(handler.server.hass.config.as_dict())
def _handle_get_api_discovery_info(handler, path_match, data):
needs_auth = (handler.server.hass.config.api.api_password is not None)
params = {
'base_url': handler.server.hass.config.api.base_url,
'location_name': handler.server.hass.config.location_name,
'requires_api_password': needs_auth,
'version': __version__
}
handler.write_json(params)
def _handle_get_api_states(handler, path_match, data):
"""Return a dict containing all entity ids and their state."""
handler.write_json(handler.server.hass.states.all())

View File

@ -11,7 +11,7 @@ from homeassistant.const import (
from homeassistant.helpers import validate_config
DOMAIN = "arduino"
REQUIREMENTS = ['PyMata==2.07a']
REQUIREMENTS = ['PyMata==2.12']
BOARD = None
_LOGGER = logging.getLogger(__name__)
@ -69,7 +69,7 @@ class ArduinoBoard(object):
self._board.ANALOG)
elif mode == 'digital' and direction == 'in':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.INPUT,
self._board.DIGITAL)
elif mode == 'digital' and direction == 'out':
self._board.set_pin_mode(pin,

View File

@ -9,10 +9,10 @@ import logging
import voluptuous as vol
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.const import CONF_PLATFORM
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.components import logbook
from homeassistant.helpers import extract_domain_configs
from homeassistant.helpers.service import call_from_config
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import extract_domain_configs, script, condition
from homeassistant.loader import get_platform
import homeassistant.helpers.config_validation as cv
@ -74,10 +74,11 @@ _CONDITION_SCHEMA = vol.Any(
[
vol.All(
vol.Schema({
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN),
CONF_PLATFORM: str,
CONF_CONDITION: str,
}, extra=vol.ALLOW_EXTRA),
_platform_validator(METHOD_IF_ACTION, 'IF_ACTION_SCHEMA'),
)
cv.has_at_least_one_key(CONF_PLATFORM, CONF_CONDITION),
),
]
)
)
@ -88,21 +89,23 @@ PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE):
vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)),
CONF_CONDITION: _CONDITION_SCHEMA,
vol.Required(CONF_ACTION): cv.SERVICE_SCHEMA,
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
})
def setup(hass, config):
"""Setup the automation."""
success = False
for config_key in extract_domain_configs(config, DOMAIN):
conf = config[config_key]
for list_no, config_block in enumerate(conf):
name = config_block.get(CONF_ALIAS, "{}, {}".format(config_key,
list_no))
_setup_automation(hass, config_block, name, config)
success = (_setup_automation(hass, config_block, name, config) or
success)
return True
return success
def _setup_automation(hass, config_block, name, config):
@ -122,12 +125,13 @@ def _setup_automation(hass, config_block, name, config):
def _get_action(hass, config, name):
"""Return an action based on a configuration."""
def action():
script_obj = script.Script(hass, config, name)
def action(variables=None):
"""Action to be executed."""
_LOGGER.info('Executing %s', name)
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
call_from_config(hass, config)
script_obj.run(variables)
return action
@ -137,6 +141,11 @@ def _process_if(hass, config, p_config, action):
cond_type = p_config.get(CONF_CONDITION_TYPE,
DEFAULT_CONDITION_TYPE).lower()
# Deprecated since 0.19 - 5/5/2016
if cond_type != DEFAULT_CONDITION_TYPE:
_LOGGER.warning('Using condition_type: %s is deprecated. Please use '
'"condition: or" instead.')
if_configs = p_config.get(CONF_CONDITION)
use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES
@ -145,38 +154,47 @@ def _process_if(hass, config, p_config, action):
checks = []
for if_config in if_configs:
platform = _resolve_platform(METHOD_IF_ACTION, hass, config,
if_config.get(CONF_PLATFORM))
if platform is None:
continue
# Deprecated except for used by use_trigger_values
# since 0.19 - 5/5/2016
if CONF_PLATFORM in if_config:
if not use_trigger:
_LOGGER.warning("Please switch your condition configuration "
"to use 'condition' instead of 'platform'.")
if_config = dict(if_config)
if_config[CONF_CONDITION] = if_config.pop(CONF_PLATFORM)
check = platform.if_action(hass, if_config)
# To support use_trigger_values with state trigger accepting
# multiple entity_ids to monitor.
if_entity_id = if_config.get(ATTR_ENTITY_ID)
if isinstance(if_entity_id, list) and len(if_entity_id) == 1:
if_config[ATTR_ENTITY_ID] = if_entity_id[0]
# Invalid conditions are allowed if we base it on trigger
if check is None and not use_trigger:
return None
checks.append(check)
try:
checks.append(condition.from_config(if_config))
except HomeAssistantError as ex:
# Invalid conditions are allowed if we base it on trigger
if use_trigger:
_LOGGER.warning('Ignoring invalid condition: %s', ex)
else:
_LOGGER.warning('Invalid condition: %s', ex)
return None
if cond_type == CONDITION_TYPE_AND:
def if_action():
def if_action(variables=None):
"""AND all conditions."""
if all(check() for check in checks):
action()
if all(check(hass, variables) for check in checks):
action(variables)
else:
def if_action():
def if_action(variables=None):
"""OR all conditions."""
if any(check() for check in checks):
action()
if any(check(hass, variables) for check in checks):
action(variables)
return if_action
def _process_trigger(hass, config, trigger_configs, name, action):
"""Setup the triggers."""
if isinstance(trigger_configs, dict):
trigger_configs = [trigger_configs]
for conf in trigger_configs:
platform = _resolve_platform(METHOD_TRIGGER, hass, config,
conf.get(CONF_PLATFORM))

View File

@ -6,27 +6,38 @@ at https://home-assistant.io/components/automation/#event-trigger
"""
import logging
import voluptuous as vol
from homeassistant.const import CONF_PLATFORM
from homeassistant.helpers import config_validation as cv
CONF_EVENT_TYPE = "event_type"
CONF_EVENT_DATA = "event_data"
_LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'event',
vol.Required(CONF_EVENT_TYPE): cv.string,
vol.Optional(CONF_EVENT_DATA): dict,
})
def trigger(hass, config, action):
"""Listen for events based on configuration."""
event_type = config.get(CONF_EVENT_TYPE)
if event_type is None:
_LOGGER.error("Missing configuration key %s", CONF_EVENT_TYPE)
return False
event_data = config.get(CONF_EVENT_DATA)
def handle_event(event):
"""Listen for events and calls the action when data matches."""
if not event_data or all(val == event.data.get(key) for key, val
in event_data.items()):
action()
action({
'trigger': {
'platform': 'event',
'event': event,
},
})
hass.bus.listen(event_type, handle_event)
return True

View File

@ -30,7 +30,14 @@ def trigger(hass, config, action):
def mqtt_automation_listener(msg_topic, msg_payload, qos):
"""Listen for MQTT messages."""
if payload is None or payload == msg_payload:
action()
action({
'trigger': {
'platform': 'mqtt',
'topic': msg_topic,
'payload': msg_payload,
'qos': qos,
}
})
mqtt.subscribe(hass, topic, mqtt_automation_listener)

View File

@ -5,101 +5,65 @@ For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#numeric-state-trigger
"""
import logging
from functools import partial
from homeassistant.const import CONF_VALUE_TEMPLATE
import voluptuous as vol
from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
CONF_BELOW, CONF_ABOVE)
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers import template
from homeassistant.helpers import condition, config_validation as cv
CONF_ENTITY_ID = "entity_id"
CONF_BELOW = "below"
CONF_ABOVE = "above"
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'numeric_state',
vol.Required(CONF_ENTITY_ID): cv.entity_id,
CONF_BELOW: vol.Coerce(float),
CONF_ABOVE: vol.Coerce(float),
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
}), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE))
_LOGGER = logging.getLogger(__name__)
def _renderer(hass, value_template, state):
"""Render the 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 configuration."""
entity_id = config.get(CONF_ENTITY_ID)
if entity_id is None:
_LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID)
return False
below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE)
value_template = config.get(CONF_VALUE_TEMPLATE)
if below is None and above is None:
_LOGGER.error("Missing configuration key."
" One of %s or %s is required",
CONF_BELOW, CONF_ABOVE)
return False
renderer = partial(_renderer, hass, value_template)
# pylint: disable=unused-argument
def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action."""
# Fire action if we go from outside range into range
if _in_range(above, below, renderer(to_s)) and \
(from_s is None or not _in_range(above, below, renderer(from_s))):
action()
if to_s is None:
return
variables = {
'trigger': {
'platform': 'numeric_state',
'entity_id': entity_id,
'below': below,
'above': above,
}
}
# If new one doesn't match, nothing to do
if not condition.numeric_state(
hass, to_s, below, above, value_template, variables):
return
# Only match if old didn't exist or existed but didn't match
# Written as: skip if old one did exist and matched
if from_s is not None and condition.numeric_state(
hass, from_s, below, above, value_template, variables):
return
variables['trigger']['from_state'] = from_s
variables['trigger']['to_state'] = to_s
action(variables)
track_state_change(
hass, entity_id, state_automation_listener)
return True
def if_action(hass, config):
"""Wrap action method with state based condition."""
entity_id = config.get(CONF_ENTITY_ID)
if entity_id is None:
_LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID)
return None
below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE)
value_template = config.get(CONF_VALUE_TEMPLATE)
if below is None and above is None:
_LOGGER.error("Missing configuration key."
" One of %s or %s is required",
CONF_BELOW, CONF_ABOVE)
return None
renderer = partial(_renderer, hass, value_template)
def if_numeric_state():
"""Test numeric state condition."""
state = hass.states.get(entity_id)
return state is not None and _in_range(above, below, renderer(state))
return if_numeric_state
def _in_range(range_start, range_end, value):
"""Check if value is inside the range."""
try:
value = float(value)
except ValueError:
_LOGGER.warning("Value returned from template is not a number: %s",
value)
return False
if range_start is not None and range_end is not None:
return float(range_start) <= value < float(range_end)
elif range_end is not None:
return value < float(range_end)
else:
return float(range_start) <= value

View File

@ -4,15 +4,11 @@ Offer state listening automation rules.
For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#state-trigger
"""
from datetime import timedelta
import voluptuous as vol
import homeassistant.util.dt as dt_util
from homeassistant.const import (
EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, CONF_PLATFORM)
from homeassistant.components.automation.time import (
CONF_HOURS, CONF_MINUTES, CONF_SECONDS)
from homeassistant.helpers.event import track_state_change, track_point_in_time
import homeassistant.helpers.config_validation as cv
@ -22,46 +18,19 @@ CONF_TO = "to"
CONF_STATE = "state"
CONF_FOR = "for"
BASE_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'state',
vol.Required(CONF_ENTITY_ID): cv.entity_id,
# These are str on purpose. Want to catch YAML conversions
CONF_STATE: str,
CONF_FOR: vol.All(vol.Schema({
CONF_HOURS: vol.Coerce(int),
CONF_MINUTES: vol.Coerce(int),
CONF_SECONDS: vol.Coerce(int),
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS)),
})
TRIGGER_SCHEMA = vol.Schema(vol.All(
BASE_SCHEMA.extend({
TRIGGER_SCHEMA = vol.All(
vol.Schema({
vol.Required(CONF_PLATFORM): 'state',
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
# These are str on purpose. Want to catch YAML conversions
CONF_FROM: str,
CONF_TO: str,
CONF_STATE: str,
CONF_FOR: vol.All(cv.time_period, cv.positive_timedelta),
}),
vol.Any(cv.key_dependency(CONF_FOR, CONF_TO),
cv.key_dependency(CONF_FOR, CONF_STATE))
))
IF_ACTION_SCHEMA = vol.Schema(vol.All(
BASE_SCHEMA,
cv.key_dependency(CONF_FOR, CONF_STATE)
))
def get_time_config(config):
"""Helper function to extract the time specified in the configuration."""
if CONF_FOR not in config:
return None
hours = config[CONF_FOR].get(CONF_HOURS)
minutes = config[CONF_FOR].get(CONF_MINUTES)
seconds = config[CONF_FOR].get(CONF_SECONDS)
return timedelta(hours=(hours or 0.0),
minutes=(minutes or 0.0),
seconds=(seconds or 0.0))
)
def trigger(hass, config, action):
@ -69,52 +38,48 @@ def trigger(hass, config, action):
entity_id = config.get(CONF_ENTITY_ID)
from_state = config.get(CONF_FROM, MATCH_ALL)
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
time_delta = get_time_config(config)
time_delta = config.get(CONF_FOR)
def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action."""
def call_action():
"""Call action with right context."""
action({
'trigger': {
'platform': 'state',
'entity_id': entity,
'from_state': from_s,
'to_state': to_s,
'for': time_delta,
}
})
if time_delta is None:
call_action()
return
def state_for_listener(now):
"""Fire on state changes after a delay and calls action."""
hass.bus.remove_listener(
EVENT_STATE_CHANGED, for_state_listener)
action()
EVENT_STATE_CHANGED, attached_state_for_cancel)
call_action()
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
"""Fire on changes and cancel for listener if changed."""
if inner_to_s == to_s:
if inner_to_s.state == to_s.state:
return
hass.bus.remove_listener(EVENT_TIME_CHANGED, for_time_listener)
hass.bus.remove_listener(
EVENT_STATE_CHANGED, for_state_listener)
hass.bus.remove_listener(EVENT_TIME_CHANGED,
attached_state_for_listener)
hass.bus.remove_listener(EVENT_STATE_CHANGED,
attached_state_for_cancel)
if time_delta is not None:
target_tm = dt_util.utcnow() + time_delta
for_time_listener = track_point_in_time(
hass, state_for_listener, target_tm)
for_state_listener = track_state_change(
hass, entity_id, state_for_cancel_listener,
MATCH_ALL, MATCH_ALL)
else:
action()
attached_state_for_listener = track_point_in_time(
hass, state_for_listener, dt_util.utcnow() + time_delta)
attached_state_for_cancel = track_state_change(
hass, entity, state_for_cancel_listener)
track_state_change(
hass, entity_id, state_automation_listener, from_state, to_state)
return True
def if_action(hass, config):
"""Wrap action method with state based condition."""
entity_id = config.get(CONF_ENTITY_ID)
state = config.get(CONF_STATE)
time_delta = get_time_config(config)
def if_state():
"""Test if condition."""
is_state = hass.states.is_state(entity_id, state)
return (time_delta is None and is_state or
time_delta is not None and
dt_util.utcnow() - time_delta >
hass.states.get(entity_id).last_changed)
return if_state

View File

@ -9,108 +9,41 @@ import logging
import voluptuous as vol
from homeassistant.const import CONF_PLATFORM
import homeassistant.util.dt as dt_util
from homeassistant.components import sun
from homeassistant.const import (
CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE)
from homeassistant.helpers.event import track_sunrise, track_sunset
import homeassistant.helpers.config_validation as cv
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'
_LOGGER = logging.getLogger(__name__)
_SUN_EVENT = vol.All(vol.Lower, vol.Any(EVENT_SUNRISE, EVENT_SUNSET))
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'sun',
vol.Required(CONF_EVENT): _SUN_EVENT,
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_offset,
vol.Required(CONF_EVENT): cv.sun_event,
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period,
})
IF_ACTION_SCHEMA = vol.All(
vol.Schema({
vol.Required(CONF_PLATFORM): 'sun',
CONF_BEFORE: _SUN_EVENT,
CONF_AFTER: _SUN_EVENT,
vol.Required(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_offset,
vol.Required(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_offset,
}),
cv.has_at_least_one_key(CONF_BEFORE, CONF_AFTER),
)
def trigger(hass, config, action):
"""Listen for events based on configuration."""
event = config.get(CONF_EVENT)
offset = config.get(CONF_OFFSET)
def call_action():
"""Call action with right context."""
action({
'trigger': {
'platform': 'sun',
'event': event,
'offset': offset,
},
})
# Do something to call action
if event == EVENT_SUNRISE:
track_sunrise(hass, action, offset)
if event == SUN_EVENT_SUNRISE:
track_sunrise(hass, call_action, offset)
else:
track_sunset(hass, action, offset)
track_sunset(hass, call_action, offset)
return True
def if_action(hass, config):
"""Wrap action method with sun based condition."""
before = config.get(CONF_BEFORE)
after = config.get(CONF_AFTER)
before_offset = config.get(CONF_BEFORE_OFFSET)
after_offset = config.get(CONF_AFTER_OFFSET)
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

View File

@ -9,9 +9,9 @@ import logging
import voluptuous as vol
from homeassistant.const import (
CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED, CONF_PLATFORM)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
CONF_VALUE_TEMPLATE, CONF_PLATFORM, MATCH_ALL)
from homeassistant.helpers import condition
from homeassistant.helpers.event import track_state_change
import homeassistant.helpers.config_validation as cv
@ -30,40 +30,24 @@ def trigger(hass, config, action):
# Local variable to keep track of if the action has already been triggered
already_triggered = False
def event_listener(event):
def state_changed_listener(entity_id, from_s, to_s):
"""Listen for state changes and calls action."""
nonlocal already_triggered
template_result = _check_template(hass, value_template)
template_result = condition.template(hass, value_template)
# Check to see if template returns true
if template_result and not already_triggered:
already_triggered = True
action()
action({
'trigger': {
'platform': 'template',
'entity_id': entity_id,
'from_state': from_s,
'to_state': to_s,
},
})
elif not template_result:
already_triggered = False
hass.bus.listen(EVENT_STATE_CHANGED, event_listener)
track_state_change(hass, MATCH_ALL, state_changed_listener)
return True
def if_action(hass, config):
"""Wrap action method with state based condition."""
value_template = config.get(CONF_VALUE_TEMPLATE)
return lambda: _check_template(hass, value_template)
def _check_template(hass, value_template):
"""Check if result of template is true."""
try:
value = template.render(hass, value_template, {})
except TemplateError as ex:
if ex.args and ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"):
# Common during HA startup - so just a warning
_LOGGER.warning(ex)
else:
_LOGGER.error(ex)
return False
return value.lower() == 'true'

View File

@ -6,99 +6,48 @@ at https://home-assistant.io/components/automation/#time-trigger
"""
import logging
import homeassistant.util.dt as dt_util
import voluptuous as vol
from homeassistant.const import CONF_AFTER, CONF_PLATFORM
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_time_change
CONF_HOURS = "hours"
CONF_MINUTES = "minutes"
CONF_SECONDS = "seconds"
CONF_BEFORE = "before"
CONF_AFTER = "after"
CONF_WEEKDAY = "weekday"
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
_LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'time',
CONF_AFTER: cv.time,
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES,
CONF_SECONDS, CONF_AFTER))
def trigger(hass, config, action):
"""Listen for state changes based on configuration."""
if CONF_AFTER in config:
after = dt_util.parse_time(config[CONF_AFTER])
if after is None:
_error_time(config[CONF_AFTER], CONF_AFTER)
return False
after = config.get(CONF_AFTER)
hours, minutes, seconds = after.hour, after.minute, after.second
elif (CONF_HOURS in config or CONF_MINUTES in config or
CONF_SECONDS in config):
else:
hours = config.get(CONF_HOURS)
minutes = config.get(CONF_MINUTES)
seconds = config.get(CONF_SECONDS)
else:
_LOGGER.error('One of %s, %s, %s OR %s needs to be specified',
CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AFTER)
return False
def time_automation_listener(now):
"""Listen for time changes and calls action."""
action()
action({
'trigger': {
'platform': 'time',
'now': now,
},
})
track_time_change(hass, time_automation_listener,
hour=hours, minute=minutes, second=seconds)
return True
def if_action(hass, config):
"""Wrap action method with time based condition."""
before = config.get(CONF_BEFORE)
after = config.get(CONF_AFTER)
weekday = config.get(CONF_WEEKDAY)
if before is None and after is None and weekday is None:
_LOGGER.error(
"Missing if-condition configuration key %s, %s or %s",
CONF_BEFORE, CONF_AFTER, CONF_WEEKDAY)
return None
if before is not None:
before = dt_util.parse_time(before)
if before is None:
_error_time(before, CONF_BEFORE)
return None
if after is not None:
after = dt_util.parse_time(after)
if after is None:
_error_time(after, CONF_AFTER)
return None
def time_if():
"""Validate time based if-condition."""
now = dt_util.now()
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
if weekday is not None:
now_weekday = WEEKDAYS[now.weekday()]
if isinstance(weekday, str) and weekday != now_weekday or \
now_weekday not in weekday:
return False
return True
return time_if
def _error_time(value, key):
"""Helper method to print error."""
_LOGGER.error(
"Received invalid value for '%s': %s", key, value)
if isinstance(value, int):
_LOGGER.error('Make sure you wrap time values in quotes')

View File

@ -6,33 +6,24 @@ at https://home-assistant.io/components/automation/#zone-trigger
"""
import voluptuous as vol
from homeassistant.components import zone
from homeassistant.const import (
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL, CONF_PLATFORM)
CONF_EVENT, CONF_ENTITY_ID, CONF_ZONE, MATCH_ALL, CONF_PLATFORM)
from homeassistant.helpers.event import track_state_change
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import (
condition, config_validation as cv, location)
CONF_ENTITY_ID = "entity_id"
CONF_ZONE = "zone"
CONF_EVENT = "event"
EVENT_ENTER = "enter"
EVENT_LEAVE = "leave"
DEFAULT_EVENT = EVENT_ENTER
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'zone',
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
vol.Required(CONF_ZONE): cv.entity_id,
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
vol.Any(EVENT_ENTER, EVENT_LEAVE),
})
IF_ACTION_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'zone',
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_ZONE): cv.entity_id,
})
def trigger(hass, config, action):
"""Listen for state changes based on configuration."""
@ -42,46 +33,32 @@ def trigger(hass, config, action):
def zone_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action."""
if from_s and None in (from_s.attributes.get(ATTR_LATITUDE),
from_s.attributes.get(ATTR_LONGITUDE)) or \
None in (to_s.attributes.get(ATTR_LATITUDE),
to_s.attributes.get(ATTR_LONGITUDE)):
if from_s and not location.has_location(from_s) or \
not location.has_location(to_s):
return
from_match = _in_zone(hass, zone_entity_id, from_s) if from_s else None
to_match = _in_zone(hass, zone_entity_id, to_s)
zone_state = hass.states.get(zone_entity_id)
if from_s:
from_match = condition.zone(hass, zone_state, from_s)
else:
from_match = False
to_match = condition.zone(hass, zone_state, to_s)
# pylint: disable=too-many-boolean-expressions
if event == EVENT_ENTER and not from_match and to_match or \
event == EVENT_LEAVE and from_match and not to_match:
action()
action({
'trigger': {
'platform': 'zone',
'entity_id': entity,
'from_state': from_s,
'to_state': to_s,
'zone': zone_state,
'event': event,
},
})
track_state_change(
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL)
return True
def if_action(hass, config):
"""Wrap action method with zone based condition."""
entity_id = config.get(CONF_ENTITY_ID)
zone_entity_id = config.get(CONF_ZONE)
def if_in_zone():
"""Test if condition."""
return _in_zone(hass, zone_entity_id, hass.states.get(entity_id))
return if_in_zone
def _in_zone(hass, zone_entity_id, state):
"""Check if state is in zone."""
if not state or None in (state.attributes.get(ATTR_LATITUDE),
state.attributes.get(ATTR_LONGITUDE)):
return False
zone_state = hass.states.get(zone_entity_id)
return zone_state and zone.in_zone(
zone_state, state.attributes.get(ATTR_LATITUDE),
state.attributes.get(ATTR_LONGITUDE),
state.attributes.get(ATTR_GPS_ACCURACY, 0))

View File

@ -1,13 +1,14 @@
"""
Support for custom shell commands to to retrieve values.
Support for custom shell commands to retrieve values.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.command/
https://home-assistant.io/components/binary_sensor.command_line/
"""
import logging
from datetime import timedelta
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.binary_sensor import (BinarySensorDevice,
SENSOR_CLASSES)
from homeassistant.components.sensor.command_line import CommandSensorData
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.helpers import template
@ -15,6 +16,7 @@ from homeassistant.helpers import template
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Binary Command Sensor"
DEFAULT_SENSOR_CLASS = None
DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_PAYLOAD_OFF = 'OFF'
@ -29,28 +31,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error('Missing required variable: "command"')
return False
sensor_class = config.get('sensor_class')
if sensor_class not in SENSOR_CLASSES:
_LOGGER.warning('Unknown sensor class: %s', sensor_class)
sensor_class = DEFAULT_SENSOR_CLASS
data = CommandSensorData(config.get('command'))
add_devices([CommandBinarySensor(
hass,
data,
config.get('name', DEFAULT_NAME),
sensor_class,
config.get('payload_on', DEFAULT_PAYLOAD_ON),
config.get('payload_off', DEFAULT_PAYLOAD_OFF),
config.get(CONF_VALUE_TEMPLATE)
)])
# pylint: disable=too-many-arguments
# pylint: disable=too-many-arguments, too-many-instance-attributes
class CommandBinarySensor(BinarySensorDevice):
"""Represent a command line binary sensor."""
def __init__(self, hass, data, name, payload_on,
def __init__(self, hass, data, name, sensor_class, payload_on,
payload_off, value_template):
"""Initialize the Command line binary sensor."""
self._hass = hass
self.data = data
self._name = name
self._sensor_class = sensor_class
self._state = False
self._payload_on = payload_on
self._payload_off = payload_off
@ -67,6 +76,11 @@ class CommandBinarySensor(BinarySensorDevice):
"""Return true if the binary sensor is on."""
return self._state
@ property
def sensor_class(self):
"""Return the class of the binary sensor."""
return self._sensor_class
def update(self):
"""Get the latest data and updates the state."""
self.data.update()

View File

@ -6,10 +6,10 @@ https://home-assistant.io/components/binary_sensor.mysensors/
"""
import logging
from homeassistant.components import mysensors
from homeassistant.components.binary_sensor import (SENSOR_CLASSES,
BinarySensorDevice)
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON
from homeassistant.loader import get_component
from homeassistant.const import STATE_ON
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = []
@ -22,8 +22,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None:
return
mysensors = get_component('mysensors')
for gateway in mysensors.GATEWAYS.values():
# Define the S_TYPES and V_TYPES that the platform should handle as
# states. Map them in a dict of lists.
@ -48,81 +46,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
map_sv_types, devices, add_devices, MySensorsBinarySensor))
class MySensorsBinarySensor(BinarySensorDevice):
"""Represent the value of a MySensors child node."""
# pylint: disable=too-many-arguments,too-many-instance-attributes
def __init__(
self, gateway, node_id, child_id, name, value_type, child_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.
child_type (str): Child type of child.
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.
child_type (str): Child type of child.
battery_level (int): Node battery level.
_values (dict): Child values. Non state values set as state attributes.
mysensors (module): Mysensors main component module.
"""
self.gateway = gateway
self.node_id = node_id
self.child_id = child_id
self._name = name
self.value_type = value_type
self.child_type = child_type
self.battery_level = 0
self._values = {}
self.mysensors = get_component('mysensors')
@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."""
address = getattr(self.gateway, 'server_address', None)
if address:
device = '{}:{}'.format(address[0], address[1])
else:
device = self.gateway.port
attr = {
self.mysensors.ATTR_DEVICE: device,
self.mysensors.ATTR_NODE_ID: self.node_id,
self.mysensors.ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level,
}
set_req = self.gateway.const.SetReq
for value_type, value in self._values.items():
if value_type != self.value_type:
try:
attr[set_req(value_type).name] = value
except ValueError:
_LOGGER.error('value_type %s is not valid for mysensors '
'version %s', value_type,
self.gateway.version)
return attr
class MySensorsBinarySensor(
mysensors.MySensorsDeviceEntity, BinarySensorDevice):
"""Represent the value of a MySensors Binary Sensor child node."""
@property
def is_on(self):
@ -150,23 +76,3 @@ class MySensorsBinarySensor(BinarySensorDevice):
})
if class_map.get(self.child_type) in SENSOR_CLASSES:
return class_map.get(self.child_type)
@property
def available(self):
"""Return True if entity is available."""
return self.value_type in self._values
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.debug(
"%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

View File

@ -0,0 +1,109 @@
"""
Support for monitoring OctoPrint binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.octoprint/
"""
import logging
import requests
from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.loader import get_component
DEPENDENCIES = ["octoprint"]
SENSOR_TYPES = {
# API Endpoint, Group, Key, unit
"Printing": ["printer", "state", "printing", None],
"Printing Error": ["printer", "state", "error", None]
}
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the available OctoPrint binary sensors."""
octoprint = get_component('octoprint')
name = config.get(CONF_NAME, "OctoPrint")
monitored_conditions = config.get("monitored_conditions",
SENSOR_TYPES.keys())
devices = []
for octo_type in monitored_conditions:
if octo_type in SENSOR_TYPES:
new_sensor = OctoPrintBinarySensor(octoprint.OCTOPRINT,
octo_type,
SENSOR_TYPES[octo_type][2],
name,
SENSOR_TYPES[octo_type][3],
SENSOR_TYPES[octo_type][0],
SENSOR_TYPES[octo_type][1],
"flags")
devices.append(new_sensor)
else:
_LOGGER.error("Unknown OctoPrint sensor type: %s", octo_type)
add_devices(devices)
# pylint: disable=too-many-instance-attributes
class OctoPrintBinarySensor(BinarySensorDevice):
"""Representation an OctoPrint binary sensor."""
# pylint: disable=too-many-arguments
def __init__(self, api, condition, sensor_type, sensor_name,
unit, endpoint, group, tool=None):
"""Initialize a new OctoPrint sensor."""
self.sensor_name = sensor_name
if tool is None:
self._name = sensor_name + ' ' + condition
else:
self._name = sensor_name + ' ' + condition
self.sensor_type = sensor_type
self.api = api
self._state = False
self._unit_of_measurement = unit
self.api_endpoint = endpoint
self.api_group = group
self.api_tool = tool
# Set initial state
self.update()
_LOGGER.debug("Created OctoPrint binary sensor %r", self)
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self.is_on
@property
def is_on(self):
"""Return true if binary sensor is on."""
if self._state:
return STATE_ON
else:
return STATE_OFF
@property
def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
return None
def update(self):
"""Update state of sensor."""
try:
self._state = self.api.update(self.sensor_type,
self.api_endpoint,
self.api_group,
self.api_tool)
except requests.exceptions.ConnectionError:
# Error calling the api, already logged in api.update()
return
if self._state is None:
_LOGGER.warning("Unable to locate value for %s", self.sensor_type)

View File

@ -7,10 +7,10 @@ at https://home-assistant.io/components/sensor.wink/
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['python-wink==0.7.4']
REQUIREMENTS = ['python-wink==0.7.6']
# These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = {
@ -48,6 +48,7 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity):
"""Initialize the Wink binary sensor."""
self.wink = wink
self._unit_of_measurement = self.wink.UNIT
self._battery = self.wink.battery_level
self.capability = self.wink.capability()
@property
@ -85,3 +86,16 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity):
def update(self):
"""Update state of the sensor."""
self.wink.update_state()
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100

View File

@ -12,7 +12,7 @@ from homeassistant.helpers.event import track_utc_time_change
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyicloud==0.7.2']
REQUIREMENTS = ['pyicloud==0.8.3']
CONF_INTERVAL = 'interval'
DEFAULT_INTERVAL = 8

View File

@ -101,7 +101,7 @@ def setup_scanner(hass, config, see):
"""Execute enter event."""
zone = hass.states.get("zone.{}".format(location))
with LOCK:
if zone is None and data['t'] == 'b':
if zone is None and data.get('t') == 'b':
# Not a HA zone, and a beacon so assume mobile
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
if location not in beacons:

View File

@ -12,7 +12,8 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
# Unifi package doesn't list urllib3 as a requirement
REQUIREMENTS = ['urllib3', 'unifi==1.2.4']
REQUIREMENTS = ['urllib3', 'unifi==1.2.5']
_LOGGER = logging.getLogger(__name__)
CONF_PORT = 'port'

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
EVENT_PLATFORM_DISCOVERED)
DOMAIN = "discovery"
REQUIREMENTS = ['netdisco==0.6.4']
REQUIREMENTS = ['netdisco==0.6.6']
SCAN_INTERVAL = 300 # seconds

View File

@ -0,0 +1,73 @@
"""
A component which allows you to send data to Dweet.io.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/dweet/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNKNOWN
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import state as state_helper
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
DOMAIN = "dweet"
DEPENDENCIES = []
REQUIREMENTS = ['dweepy==0.2.0']
CONF_NAME = 'name'
CONF_WHITELIST = 'whitelist'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_WHITELIST): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
# pylint: disable=too-many-locals
def setup(hass, config):
"""Setup the Dweet.io component."""
conf = config[DOMAIN]
name = conf[CONF_NAME]
whitelist = conf.get(CONF_WHITELIST, [])
json_body = {}
def dweet_event_listener(event):
"""Listen for new messages on the bus and sends them to Dweet.io."""
state = event.data.get('new_state')
if state is None or state.state in (STATE_UNKNOWN, '') \
or state.entity_id not in whitelist:
return
try:
_state = state_helper.state_as_number(state)
except ValueError:
_state = state.state
json_body[state.attributes.get('friendly_name')] = _state
send_data(name, json_body)
hass.bus.listen(EVENT_STATE_CHANGED, dweet_event_listener)
return True
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def send_data(name, msg):
"""Send the collected data to Dweet.io."""
import dweepy
try:
dweepy.dweet_for(name, msg)
except dweepy.DweepyError:
_LOGGER.error("Error saving data '%s' to Dweet.io", msg)

View File

@ -1,7 +1,13 @@
"""RSS/Atom feed reader for Home Assistant."""
"""
Support for RSS/Atom feed.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/feedreader/
"""
from datetime import datetime
from logging import getLogger
import voluptuous as vol
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.event import track_utc_time_change
REQUIREMENTS = ['feedparser==5.2.1']
@ -14,6 +20,7 @@ CONFIG_SCHEMA = vol.Schema({
'urls': [vol.Url()],
}
}, extra=vol.ALLOW_EXTRA)
MAX_ENTRIES = 20
# pylint: disable=too-few-public-methods
@ -25,52 +32,75 @@ class FeedManager(object):
self._url = url
self._feed = None
self._hass = hass
self._firstrun = True
# Initialize last entry timestamp as epoch time
self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple()
_LOGGER.debug('Loading feed %s', self._url)
self._update()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
lambda _: self._update())
track_utc_time_change(hass, lambda now: self._update(),
minute=0, second=0)
def _log_no_entries(self):
"""Send no entries log at debug level."""
_LOGGER.debug('No new entries in feed %s', self._url)
_LOGGER.debug('No new entries in feed "%s"', self._url)
def _update(self):
"""Update the feed and publish new entries in the event bus."""
"""Update the feed and publish new entries to the event bus."""
import feedparser
_LOGGER.info('Fetching new data from feed %s', self._url)
_LOGGER.info('Fetching new data from feed "%s"', self._url)
self._feed = feedparser.parse(self._url,
etag=None if not self._feed
else self._feed.get('etag'),
modified=None if not self._feed
else self._feed.get('modified'))
if not self._feed:
_LOGGER.error('Error fetching feed data from %s', self._url)
_LOGGER.error('Error fetching feed data from "%s"', self._url)
else:
if self._feed.bozo != 0:
_LOGGER.error('Error parsing feed %s', self._url)
_LOGGER.error('Error parsing feed "%s"', self._url)
# Using etag and modified, if there's no new data available,
# the entries list will be empty
elif len(self._feed.entries) > 0:
_LOGGER.debug('Entries available in feed %s', self._url)
_LOGGER.debug('%s entri(es) available in feed "%s"',
len(self._feed.entries),
self._url)
if len(self._feed.entries) > MAX_ENTRIES:
_LOGGER.debug('Publishing only the first %s entries '
'in feed "%s"', MAX_ENTRIES, self._url)
self._feed.entries = self._feed.entries[0:MAX_ENTRIES]
self._publish_new_entries()
self._last_entry_timestamp = \
self._feed.entries[0].published_parsed
else:
self._log_no_entries()
_LOGGER.info('Fetch from feed "%s" completed', self._url)
def _update_and_fire_entry(self, entry):
"""Update last_entry_timestamp and fire entry."""
# We are lucky, `published_parsed` data available,
# let's make use of it to publish only new available
# entries since the last run
if 'published_parsed' in entry.keys():
self._last_entry_timestamp = max(entry.published_parsed,
self._last_entry_timestamp)
else:
_LOGGER.debug('No `published_parsed` info available '
'for entry "%s"', entry.title)
entry.update({'feed_url': self._url})
self._hass.bus.fire(EVENT_FEEDREADER, entry)
def _publish_new_entries(self):
"""Publish new entries to the event bus."""
new_entries = False
for entry in self._feed.entries:
# Consider only entries newer then the latest parsed one
if entry.published_parsed > self._last_entry_timestamp:
if self._firstrun or (
'published_parsed' in entry.keys() and
entry.published_parsed > self._last_entry_timestamp):
self._update_and_fire_entry(entry)
new_entries = True
entry.update({'feed_url': self._url})
self._hass.bus.fire(EVENT_FEEDREADER, entry)
else:
_LOGGER.debug('Entry "%s" already processed', entry.title)
if not new_entries:
self._log_no_entries()
self._firstrun = False
def setup(hass, config):

View File

@ -28,20 +28,55 @@
left: 0;
right: 0;
bottom: 0;
margin-bottom: 123px;
margin-bottom: 97px;
font-family: Roboto, sans-serif;
font-size: 0pt;
transition: font-size 2s;
}
#ha-init-skeleton paper-spinner {
height: 28px;
}
#ha-init-skeleton a {
color: #03A9F4;
text-decoration: none;
font-weight: bold;
}
#ha-init-skeleton.error {
font-size: 16px;
}
#ha-init-skeleton.error img,
#ha-init-skeleton.error paper-spinner {
display: none;
}
</style>
<link rel='import' href='/static/{{ app_url }}' async>
<script>
function initError() {
document
.getElementById('ha-init-skeleton')
.classList.add('error');
}
</script>
<link rel='import' href='/static/{{ app_url }}' onerror='initError()' async>
</head>
<body fullbleed>
<div id='ha-init-skeleton'><img src='/static/favicon-192x192.png' height='192'></div>
<div id='ha-init-skeleton'>
<img src='/static/favicon-192x192.png' height='192'>
<paper-spinner active></paper-spinner>
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
</div>
<script>
var webComponentsSupported = ('registerElement' in document &&
'import' in document.createElement('link') &&
'content' in document.createElement('template'))
var webComponentsSupported = (
'registerElement' in document &&
'import' in document.createElement('link') &&
'content' in document.createElement('template'));
if (!webComponentsSupported) {
var script = document.createElement('script')
script.async = true
script.onerror = initError;
script.src = '/static/webcomponents-lite.min.js'
document.head.appendChild(script)
}

View File

@ -1,2 +1,2 @@
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
VERSION = "af8a531f1c2e477c07c4b3394bd1ce13"
VERSION = "1baebe8155deb447230866d7ae854bd9"

View File

@ -1,2 +1,2 @@
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
VERSION = "ffd8a1bde5ba13f300c3d6ad32036526"
VERSION = "77c51c270b0241ce7ba0d1df2d254d6f"

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 11311809c1eba0ed3b7e26d07a0fdb81b7959e3a
Subproject commit 6a8e6a5a081415690bf89e87697d15b6ce9ebf8b

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1 @@
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={exports:{},id:r,loaded:!1};return e[r].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([/*!*************************************!*\
!*** ./src/service-worker/index.js ***!
\*************************************/
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}]);
//# sourceMappingURL=service_worker.js.map
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={i:r,l:!1,exports:{}};return e[r].call(s.exports,s,s.exports,t),s.l=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(t.s=192)}({192:function(e,t,n){var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}});

File diff suppressed because one or more lines are too long

View File

@ -7,9 +7,9 @@ https://home-assistant.io/components/garage_door.wink/
import logging
from homeassistant.components.garage_door import GarageDoorDevice
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
REQUIREMENTS = ['python-wink==0.7.4']
REQUIREMENTS = ['python-wink==0.7.6']
def setup_platform(hass, config, add_devices, discovery_info=None):
@ -37,6 +37,7 @@ class WinkGarageDoorDevice(GarageDoorDevice):
def __init__(self, wink):
"""Initialize the garage door."""
self.wink = wink
self._battery = self.wink.battery_level
@property
def unique_id(self):
@ -69,3 +70,16 @@ class WinkGarageDoorDevice(GarageDoorDevice):
def open_door(self):
"""Open the door."""
self.wink.set_state(1)
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100

View File

@ -16,18 +16,23 @@ from http import cookies
from http.server import HTTPServer, SimpleHTTPRequestHandler
from socketserver import ThreadingMixIn
from urllib.parse import parse_qs, urlparse
import voluptuous as vol
import homeassistant.bootstrap as bootstrap
import homeassistant.core as ha
import homeassistant.remote as rem
import homeassistant.util as util
import homeassistant.util.dt as date_util
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, HTTP_HEADER_ACCEPT_ENCODING,
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_CONTENT_ENCODING,
HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_EXPIRES,
HTTP_HEADER_HA_AUTH, HTTP_HEADER_VARY, HTTP_METHOD_NOT_ALLOWED,
HTTP_HEADER_HA_AUTH, HTTP_HEADER_VARY,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, HTTP_METHOD_NOT_ALLOWED,
HTTP_NOT_FOUND, HTTP_OK, HTTP_UNAUTHORIZED, HTTP_UNPROCESSABLE_ENTITY,
ALLOWED_CORS_HEADERS,
SERVER_PORT, URL_ROOT, URL_API_EVENT_FORWARD)
DOMAIN = "http"
@ -38,6 +43,7 @@ CONF_SERVER_PORT = "server_port"
CONF_DEVELOPMENT = "development"
CONF_SSL_CERTIFICATE = 'ssl_certificate'
CONF_SSL_KEY = 'ssl_key'
CONF_CORS_ORIGINS = 'cors_allowed_origins'
DATA_API_PASSWORD = 'api_password'
@ -48,6 +54,19 @@ SESSION_KEY = 'sessionId'
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_API_PASSWORD): cv.string,
vol.Optional(CONF_SERVER_HOST): cv.string,
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT):
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
vol.Optional(CONF_DEVELOPMENT): cv.string,
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
vol.Optional(CONF_SSL_KEY): cv.isfile,
vol.Optional(CONF_CORS_ORIGINS): cv.ensure_list
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the HTTP API and debug interface."""
@ -61,11 +80,12 @@ def setup(hass, config):
development = str(conf.get(CONF_DEVELOPMENT, "")) == "1"
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
ssl_key = conf.get(CONF_SSL_KEY)
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
try:
server = HomeAssistantHTTPServer(
(server_host, server_port), RequestHandler, hass, api_password,
development, ssl_certificate, ssl_key)
development, ssl_certificate, ssl_key, cors_origins)
except OSError:
# If address already in use
_LOGGER.exception("Error setting up HTTP server")
@ -96,7 +116,8 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
# pylint: disable=too-many-arguments
def __init__(self, server_address, request_handler_class,
hass, api_password, development, ssl_certificate, ssl_key):
hass, api_password, development, ssl_certificate, ssl_key,
cors_origins):
"""Initialize the server."""
super().__init__(server_address, request_handler_class)
@ -107,6 +128,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
self.paths = []
self.sessions = SessionStore()
self.use_ssl = ssl_certificate is not None
self.cors_origins = cors_origins
# We will lazy init this one if needed
self.event_forwarder = None
@ -351,6 +373,16 @@ class RequestHandler(SimpleHTTPRequestHandler):
self.send_header(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT_ENCODING)
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(content)))
cors_check = (self.headers.get("Origin") in self.server.cors_origins)
cors_headers = ", ".join(ALLOWED_CORS_HEADERS)
if self.server.cors_origins and cors_check:
self.send_header(HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
self.headers.get("Origin"))
self.send_header(HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
cors_headers)
self.end_headers()
if self.command == 'HEAD':

View File

@ -0,0 +1,491 @@
"""
Provides functionality to interact with hvacs.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/hvac/
"""
import logging
import os
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.config import load_yaml_config_file
import homeassistant.util as util
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.temperature import convert
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components import zwave
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
TEMP_CELCIUS)
DOMAIN = "hvac"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
SCAN_INTERVAL = 60
SERVICE_SET_AWAY_MODE = "set_away_mode"
SERVICE_SET_AUX_HEAT = "set_aux_heat"
SERVICE_SET_TEMPERATURE = "set_temperature"
SERVICE_SET_FAN_MODE = "set_fan_mode"
SERVICE_SET_OPERATION_MODE = "set_operation_mode"
SERVICE_SET_SWING = "set_swing_mode"
SERVICE_SET_HUMIDITY = "set_humidity"
STATE_HEAT = "heat"
STATE_COOL = "cool"
STATE_IDLE = "idle"
STATE_AUTO = "auto"
STATE_DRY = "dry"
STATE_FAN_ONLY = "fan_only"
ATTR_CURRENT_TEMPERATURE = "current_temperature"
ATTR_CURRENT_HUMIDITY = "current_humidity"
ATTR_HUMIDITY = "humidity"
ATTR_AWAY_MODE = "away_mode"
ATTR_AUX_HEAT = "aux_heat"
ATTR_FAN = "fan"
ATTR_FAN_LIST = "fan_list"
ATTR_MAX_TEMP = "max_temp"
ATTR_MIN_TEMP = "min_temp"
ATTR_MAX_HUMIDITY = "max_humidity"
ATTR_MIN_HUMIDITY = "min_humidity"
ATTR_OPERATION = "operation_mode"
ATTR_OPERATION_LIST = "operation_list"
ATTR_SWING_MODE = "swing_mode"
ATTR_SWING_LIST = "swing_list"
_LOGGER = logging.getLogger(__name__)
DISCOVERY_PLATFORMS = {
zwave.DISCOVER_HVAC: 'zwave'
}
def set_away_mode(hass, away_mode, entity_id=None):
"""Turn all or specified hvac away mode on."""
data = {
ATTR_AWAY_MODE: away_mode
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data)
def set_aux_heat(hass, aux_heat, entity_id=None):
"""Turn all or specified hvac auxillary heater on."""
data = {
ATTR_AUX_HEAT: aux_heat
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
def set_temperature(hass, temperature, entity_id=None):
"""Set new target temperature."""
data = {ATTR_TEMPERATURE: temperature}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, data)
def set_humidity(hass, humidity, entity_id=None):
"""Set new target humidity."""
data = {ATTR_HUMIDITY: humidity}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data)
def set_fan_mode(hass, fan, entity_id=None):
"""Turn all or specified hvac fan mode on."""
data = {ATTR_FAN: fan}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data)
def set_operation_mode(hass, operation_mode, entity_id=None):
"""Set new target operation mode."""
data = {ATTR_OPERATION: operation_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data)
def set_swing_mode(hass, swing_mode, entity_id=None):
"""Set new target swing mode."""
data = {ATTR_SWING_MODE: swing_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_SWING, data)
# pylint: disable=too-many-branches
def setup(hass, config):
"""Setup hvacs."""
component = EntityComponent(_LOGGER, DOMAIN, hass,
SCAN_INTERVAL, DISCOVERY_PLATFORMS)
component.setup(config)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def away_mode_set_service(service):
"""Set away mode on target hvacs."""
target_hvacs = component.extract_from_service(service)
away_mode = service.data.get(ATTR_AWAY_MODE)
if away_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE)
return
for hvac in target_hvacs:
if away_mode:
hvac.turn_away_mode_on()
else:
hvac.turn_away_mode_off()
if hvac.should_poll:
hvac.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service,
descriptions.get(SERVICE_SET_AWAY_MODE))
def aux_heat_set_service(service):
"""Set auxillary heater on target hvacs."""
target_hvacs = component.extract_from_service(service)
aux_heat = service.data.get(ATTR_AUX_HEAT)
if aux_heat is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT)
return
for hvac in target_hvacs:
if aux_heat:
hvac.turn_aux_heat_on()
else:
hvac.turn_aux_heat_off()
if hvac.should_poll:
hvac.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_AUX_HEAT, aux_heat_set_service,
descriptions.get(SERVICE_SET_AUX_HEAT))
def temperature_set_service(service):
"""Set temperature on the target hvacs."""
target_hvacs = component.extract_from_service(service)
temperature = util.convert(
service.data.get(ATTR_TEMPERATURE), float)
if temperature is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE)
return
for hvac in target_hvacs:
hvac.set_temperature(convert(
temperature, hass.config.temperature_unit,
hvac.unit_of_measurement))
if hvac.should_poll:
hvac.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service,
descriptions.get(SERVICE_SET_TEMPERATURE))
def humidity_set_service(service):
"""Set humidity on the target hvacs."""
target_hvacs = component.extract_from_service(service)
humidity = service.data.get(ATTR_HUMIDITY)
if humidity is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_HUMIDITY, ATTR_HUMIDITY)
return
for hvac in target_hvacs:
hvac.set_humidity(humidity)
if hvac.should_poll:
hvac.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_HUMIDITY, humidity_set_service,
descriptions.get(SERVICE_SET_HUMIDITY))
def fan_mode_set_service(service):
"""Set fan mode on target hvacs."""
target_hvacs = component.extract_from_service(service)
fan = service.data.get(ATTR_FAN)
if fan is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_FAN_MODE, ATTR_FAN)
return
for hvac in target_hvacs:
hvac.set_fan_mode(fan)
if hvac.should_poll:
hvac.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service,
descriptions.get(SERVICE_SET_FAN_MODE))
def operation_set_service(service):
"""Set operating mode on the target hvacs."""
target_hvacs = component.extract_from_service(service)
operation_mode = service.data.get(ATTR_OPERATION)
if operation_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_OPERATION_MODE, ATTR_OPERATION)
return
for hvac in target_hvacs:
hvac.set_operation(operation_mode)
if hvac.should_poll:
hvac.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_OPERATION_MODE, operation_set_service,
descriptions.get(SERVICE_SET_OPERATION_MODE))
def swing_set_service(service):
"""Set swing mode on the target hvacs."""
target_hvacs = component.extract_from_service(service)
swing_mode = service.data.get(ATTR_SWING_MODE)
if swing_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_SWING, ATTR_SWING_MODE)
return
for hvac in target_hvacs:
hvac.set_swing(swing_mode)
if hvac.should_poll:
hvac.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_SWING, swing_set_service,
descriptions.get(SERVICE_SET_SWING))
return True
class HvacDevice(Entity):
"""Representation of a hvac."""
# pylint: disable=too-many-public-methods,no-self-use
@property
def state(self):
"""Return the current state."""
return self.current_operation or STATE_UNKNOWN
@property
def state_attributes(self):
"""Return the optional state attributes."""
data = {
ATTR_CURRENT_TEMPERATURE:
self._convert_for_display(self.current_temperature),
ATTR_MIN_TEMP: self._convert_for_display(self.min_temp),
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp),
ATTR_TEMPERATURE:
self._convert_for_display(self.target_temperature),
ATTR_HUMIDITY: self.target_humidity,
ATTR_CURRENT_HUMIDITY: self.current_humidity,
ATTR_MIN_HUMIDITY: self.min_humidity,
ATTR_MAX_HUMIDITY: self.max_humidity,
ATTR_FAN_LIST: self.fan_list,
ATTR_OPERATION_LIST: self.operation_list,
ATTR_SWING_LIST: self.swing_list,
ATTR_OPERATION: self.current_operation,
ATTR_FAN: self.current_fan_mode,
ATTR_SWING_MODE: self.current_swing_mode,
}
is_away = self.is_away_mode_on
if is_away is not None:
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
is_aux_heat = self.is_aux_heat_on
if is_aux_heat is not None:
data[ATTR_AUX_HEAT] = STATE_ON if is_aux_heat else STATE_OFF
return data
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
raise NotImplementedError
@property
def current_humidity(self):
"""Return the current humidity."""
return None
@property
def target_humidity(self):
"""Return the humidity we try to reach."""
return None
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return None
@property
def operation_list(self):
"""List of available operation modes."""
return None
@property
def current_temperature(self):
"""Return the current temperature."""
return None
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
raise NotImplementedError
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return None
@property
def is_aux_heat_on(self):
"""Return true if away mode is on."""
return None
@property
def current_fan_mode(self):
"""Return the fan setting."""
return None
@property
def fan_list(self):
"""List of available fan modes."""
return None
@property
def current_swing_mode(self):
"""Return the fan setting."""
return None
@property
def swing_list(self):
"""List of available swing modes."""
return None
def set_temperature(self, temperature):
"""Set new target temperature."""
pass
def set_humidity(self, humidity):
"""Set new target humidity."""
pass
def set_fan_mode(self, fan):
"""Set new target fan mode."""
pass
def set_operation(self, operation_mode):
"""Set new target operation mode."""
pass
def set_swing(self, swing_mode):
"""Set new target swing operation."""
pass
def turn_away_mode_on(self):
"""Turn away mode on."""
pass
def turn_away_mode_off(self):
"""Turn away mode off."""
pass
def turn_aux_heat_on(self):
"""Turn auxillary heater on."""
pass
def turn_aux_heat_off(self):
"""Turn auxillary heater off."""
pass
@property
def min_temp(self):
"""Return the minimum temperature."""
return self._convert_for_display(7)
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._convert_for_display(35)
@property
def min_humidity(self):
"""Return the minimum humidity."""
return 30
@property
def max_humidity(self):
"""Return the maximum humidity."""
return 99
def _convert_for_display(self, temp):
"""Convert temperature into preferred units for display purposes."""
if temp is None:
return None
value = convert(temp, self.unit_of_measurement,
self.hass.config.temperature_unit)
if self.hass.config.temperature_unit is TEMP_CELCIUS:
decimal_count = 1
else:
# Users of fahrenheit generally expect integer units.
decimal_count = 0
return round(value, decimal_count)

View File

@ -0,0 +1,164 @@
"""
Demo platform that offers a fake hvac.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.hvac import HvacDevice
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Demo hvacs."""
add_devices([
DemoHvac("HeatPump", 68, TEMP_FAHRENHEIT, None, 77, "Auto Low",
None, None, "Auto", "Heat", None),
DemoHvac("Hvac", 21, TEMP_CELSIUS, True, 22, "On High",
67, 54, "Off", "Cool", False),
])
# pylint: disable=too-many-arguments, too-many-public-methods
class DemoHvac(HvacDevice):
"""Representation of a demo hvac."""
# pylint: disable=too-many-instance-attributes
def __init__(self, name, target_temperature, unit_of_measurement,
away, current_temperature, current_fan_mode,
target_humidity, current_humidity, current_swing_mode,
current_operation, aux):
"""Initialize the hvac."""
self._name = name
self._target_temperature = target_temperature
self._target_humidity = target_humidity
self._unit_of_measurement = unit_of_measurement
self._away = away
self._current_temperature = current_temperature
self._current_humidity = current_humidity
self._current_fan_mode = current_fan_mode
self._current_operation = current_operation
self._aux = aux
self._current_swing_mode = current_swing_mode
self._fan_list = ["On Low", "On High", "Auto Low", "Auto High", "Off"]
self._operation_list = ["Heat", "Cool", "Auto Changeover", "Off"]
self._swing_list = ["Auto", 1, 2, 3, "Off"]
@property
def should_poll(self):
"""Polling not needed for a demo hvac."""
return False
@property
def name(self):
"""Return the name of the hvac."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
@property
def current_humidity(self):
"""Return the current humidity."""
return self._current_humidity
@property
def target_humidity(self):
"""Return the humidity we try to reach."""
return self._target_humidity
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self._current_operation
@property
def operation_list(self):
"""List of available operation modes."""
return self._operation_list
@property
def is_away_mode_on(self):
"""Return if away mode is on."""
return self._away
@property
def is_aux_heat_on(self):
"""Return true if away mode is on."""
return self._aux
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self._current_fan_mode
@property
def fan_list(self):
"""List of available fan modes."""
return self._fan_list
def set_temperature(self, temperature):
"""Set new target temperature."""
self._target_temperature = temperature
self.update_ha_state()
def set_humidity(self, humidity):
"""Set new target temperature."""
self._target_humidity = humidity
self.update_ha_state()
def set_swing(self, swing_mode):
"""Set new target temperature."""
self._current_swing_mode = swing_mode
self.update_ha_state()
def set_fan_mode(self, fan):
"""Set new target temperature."""
self._current_fan_mode = fan
self.update_ha_state()
def set_operation(self, operation_mode):
"""Set new target temperature."""
self._current_operation = operation_mode
self.update_ha_state()
@property
def current_swing_mode(self):
"""Return the swing setting."""
return self._current_swing_mode
@property
def swing_list(self):
"""List of available swing modes."""
return self._swing_list
def turn_away_mode_on(self):
"""Turn away mode on."""
self._away = True
self.update_ha_state()
def turn_away_mode_off(self):
"""Turn away mode off."""
self._away = False
self.update_ha_state()
def turn_aux_heat_on(self):
"""Turn away auxillary heater on."""
self._aux = True
self.update_ha_state()
def turn_aux_heat_off(self):
"""Turn auxillary heater off."""
self._aux = False
self.update_ha_state()

View File

@ -0,0 +1,84 @@
set_aux_heat:
description: Turn auxillary heater on/off for hvac
fields:
entity_id:
description: Name(s) of entities to change
example: 'hvac.kitchen'
aux_heat:
description: New value of axillary heater
example: true
set_away_mode:
description: Turn away mode on/off for hvac
fields:
entity_id:
description: Name(s) of entities to change
example: 'hvac.kitchen'
away_mode:
description: New value of away mode
example: true
set_temperature:
description: Set target temperature of hvac
fields:
entity_id:
description: Name(s) of entities to change
example: 'hvac.kitchen'
temperature:
description: New target temperature for hvac
example: 25
set_humidity:
description: Set target humidity of hvac
fields:
entity_id:
description: Name(s) of entities to change
example: 'hvac.kitchen'
humidity:
description: New target humidity for hvac
example: 60
set_fan_mode:
description: Set fan operation for hvac
fields:
entity_id:
description: Name(s) of entities to change
example: 'hvac.nest'
fan:
description: New value of fan mode
example: On Low
set_operation_mode:
description: Set operation mode for hvac
fields:
entity_id:
description: Name(s) of entities to change
example: 'hvac.nest'
operation_mode:
description: New value of operation mode
example: Heat
set_swing_mode:
description: Set swing operation for hvac
fields:
entity_id:
description: Name(s) of entities to change
example: 'hvac.nest'
swing_mode:
description: New value of swing mode
example: 1

View File

@ -0,0 +1,228 @@
"""ZWave Hvac device."""
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import logging
from homeassistant.components.hvac import DOMAIN
from homeassistant.components.hvac import HvacDevice
from homeassistant.components.zwave import (
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity)
from homeassistant.components import zwave
from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS)
_LOGGER = logging.getLogger(__name__)
CONF_NAME = 'name'
DEFAULT_NAME = 'ZWave Hvac'
REMOTEC = 0x5254
REMOTEC_ZXT_120 = 0x8377
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0)
WORKAROUND_ZXT_120 = 'zxt_120'
DEVICE_MAPPINGS = {
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
}
ZXT_120_SET_TEMP = {
'Heat': 1,
'Cool': 2,
'Dry Air': 8,
'Auto Changeover': 10
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the ZWave Hvac devices."""
if discovery_info is None or zwave.NETWORK is None:
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
discovery_info, zwave.NETWORK)
return
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
value = node.values[discovery_info[ATTR_VALUE_ID]]
value.set_change_verified(False)
add_devices([ZWaveHvac(value)])
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
discovery_info, zwave.NETWORK)
# pylint: disable=too-many-arguments
class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
"""Represents a HeatControl hvac."""
# pylint: disable=too-many-public-methods, too-many-instance-attributes
def __init__(self, value):
"""Initialize the zwave hvac."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._node = value.node
self._target_temperature = None
self._current_temperature = None
self._current_operation = None
self._operation_list = None
self._current_operation_state = None
self._current_fan_mode = None
self._fan_list = None
self._current_swing_mode = None
self._swing_list = None
self._unit = None
self._zxt_120 = None
self.update_properties()
# register listener
dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
# Make sure that we have values for the key before converting to int
if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16),
int(value.node.product_id, 16),
value.index)
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120:
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat as HVAC")
self._zxt_120 = 1
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.node == value.node:
self.update_properties()
self.update_ha_state(True)
_LOGGER.debug("Value changed on network %s", value)
def update_properties(self):
"""Callback on data change for the registered node/value pair."""
# Set point
for value in self._node.get_values(class_id=0x43).values():
if int(value.data) != 0:
self._target_temperature = int(value.data)
# Operation Mode
for value in self._node.get_values(class_id=0x40).values():
self._current_operation = value.data
self._operation_list = list(value.data_items)
_LOGGER.debug("self._operation_list=%s", self._operation_list)
# Current Temp
for value in self._node.get_values(class_id=0x31).values():
self._current_temperature = int(value.data)
self._unit = value.units
# Fan Mode
fan_class_id = 0x44 if self._zxt_120 else 0x42
_LOGGER.debug("fan_class_id=%s", fan_class_id)
for value in self._node.get_values(class_id=fan_class_id).values():
self._current_operation_state = value.data
self._fan_list = list(value.data_items)
_LOGGER.debug("self._fan_list=%s", self._fan_list)
_LOGGER.debug("self._current_operation_state=%s",
self._current_operation_state)
# Swing mode
if self._zxt_120 == 1:
for value in self._node.get_values(class_id=0x70).values():
if value.command_class == 112 and value.index == 33:
self._current_swing_mode = value.data
self._swing_list = [0, 1]
_LOGGER.debug("self._swing_list=%s", self._swing_list)
@property
def should_poll(self):
"""No polling on ZWave."""
return False
@property
def current_fan_mode(self):
"""Return the fan speed set."""
return self._current_operation_state
@property
def fan_list(self):
"""List of available fan modes."""
return self._fan_list
@property
def current_swing_mode(self):
"""Return the swing mode set."""
return self._current_swing_mode
@property
def swing_list(self):
"""List of available swing modes."""
return self._swing_list
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
unit = self._unit
if unit == 'C':
return TEMP_CELSIUS
elif unit == 'F':
return TEMP_FAHRENHEIT
else:
_LOGGER.exception("unit_of_measurement=%s is not valid",
unit)
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def current_operation(self):
"""Return the current operation mode."""
return self._current_operation
@property
def operation_list(self):
"""List of available operation modes."""
return self._operation_list
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def set_temperature(self, temperature):
"""Set new target temperature."""
for value in self._node.get_values(class_id=0x43).values():
if value.command_class != 67:
continue
if self._zxt_120:
# ZXT-120 does not support get setpoint
self._target_temperature = temperature
if ZXT_120_SET_TEMP.get(self._current_operation) \
!= value.index:
continue
# ZXT-120 responds only to whole int
value.data = int(round(temperature, 0))
else:
value.data = int(temperature)
def set_fan_mode(self, fan):
"""Set new target fan mode."""
for value in self._node.get_values(class_id=0x44).values():
if value.command_class == 68 and value.index == 0:
value.data = bytes(fan, 'utf-8')
def set_operation(self, operation_mode):
"""Set new target operation mode."""
for value in self._node.get_values(class_id=0x40).values():
if value.command_class == 64 and value.index == 0:
value.data = bytes(operation_mode, 'utf-8')
def set_swing(self, swing_mode):
"""Set new target swing mode."""
if self._zxt_120 == 1:
for value in self._node.get_values(class_id=0x70).values():
if value.command_class == 112 and value.index == 33:
value.data = int(swing_mode)
@property
def min_temp(self):
"""Return the minimum temperature."""
return self._convert_for_display(19)
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._convert_for_display(30)

View File

@ -6,10 +6,10 @@ https://home-assistant.io/components/light.mysensors/
"""
import logging
from homeassistant.components import mysensors
from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
Light)
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON
from homeassistant.loader import get_component
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.util.color import rgb_hex_to_rgb_list
_LOGGER = logging.getLogger(__name__)
@ -25,8 +25,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None:
return
mysensors = get_component('mysensors')
for gateway in mysensors.GATEWAYS.values():
# Define the S_TYPES and V_TYPES that the platform should handle as
# states. Map them in a dict of lists.
@ -52,35 +50,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
map_sv_types, devices, add_devices, device_class_map))
class MySensorsLight(Light):
"""Represent the value of a MySensors child node."""
class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
"""Represent the value of a MySensors Light child node."""
# pylint: disable=too-many-arguments,too-many-instance-attributes
def __init__(
self, gateway, node_id, child_id, name, value_type, child_type):
def __init__(self, *args):
"""Setup instance 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 = {}
mysensors.MySensorsDeviceEntity.__init__(self, *args)
self._state = None
self._brightness = None
self._rgb = None
self._white = None
self.mysensors = get_component('mysensors')
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of this entity."""
return self._name
@property
def brightness(self):
@ -97,29 +76,6 @@ class MySensorsLight(Light):
"""Return the white value in RGBW, value between 0..255."""
return self._white
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
address = getattr(self.gateway, 'server_address', None)
if address:
device = '{}:{}'.format(address[0], address[1])
else:
device = self.gateway.port
attr = {
self.mysensors.ATTR_DEVICE: device,
self.mysensors.ATTR_NODE_ID: self.node_id,
self.mysensors.ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level,
}
for value_type, value in self._values.items():
attr[self.gateway.const.SetReq(value_type).name] = value
return attr
@property
def available(self):
"""Return true if entity is available."""
return self.value_type in self._values
@property
def assumed_state(self):
"""Return true if unable to access real state of entity."""
@ -319,28 +275,11 @@ class MySensorsLightRGB(MySensorsLight):
self._update_rgb_or_w()
class MySensorsLightRGBW(MySensorsLight):
"""RGBW child class to MySensorsLight."""
class MySensorsLightRGBW(MySensorsLightRGB):
"""RGBW child class to MySensorsLightRGB."""
def turn_on(self, **kwargs):
"""Turn the device on."""
self._turn_on_light()
self._turn_on_dimmer(**kwargs)
self._turn_on_rgb_and_w('%02x%02x%02x%02x', **kwargs)
def turn_off(self, **kwargs):
"""Turn the device off."""
ret = self._turn_off_rgb_or_w()
ret = self._turn_off_dimmer(
value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE])
ret = self._turn_off_light(
value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE])
self._turn_off_main(
value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE])
def update(self):
"""Update the controller with the latest value from a sensor."""
self._update_main()
self._update_light()
self._update_dimmer()
self._update_rgb_or_w()

View File

@ -4,12 +4,16 @@ Support for Tellstick lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.tellstick/
"""
import voluptuous as vol
from homeassistant.components import tellstick
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS,
ATTR_DISCOVER_DEVICES,
ATTR_DISCOVER_CONFIG)
PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): tellstick.DOMAIN})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -13,7 +13,7 @@ from homeassistant.util import color as color_util
from homeassistant.util.color import \
color_temperature_mired_to_kelvin as mired_to_kelvin
REQUIREMENTS = ['python-wink==0.7.4']
REQUIREMENTS = ['python-wink==0.7.6']
def setup_platform(hass, config, add_devices_callback, discovery_info=None):

View File

@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED,
STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK)
from homeassistant.components import (group, verisure, wink)
from homeassistant.components import (group, verisure, wink, zwave)
DOMAIN = 'lock'
SCAN_INTERVAL = 30
@ -33,7 +33,8 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
wink.DISCOVER_LOCKS: 'wink',
verisure.DISCOVER_LOCKS: 'verisure'
verisure.DISCOVER_LOCKS: 'verisure',
zwave.DISCOVER_LOCKS: 'zwave',
}
LOCK_SERVICE_SCHEMA = vol.Schema({

View File

@ -46,6 +46,11 @@ class VerisureDoorlock(LockDevice):
"""Return the state of the lock."""
return self._state
@property
def available(self):
"""Return True if entity is available."""
return hub.available
@property
def code_format(self):
"""Return the required six digit code."""

View File

@ -7,9 +7,9 @@ https://home-assistant.io/components/lock.wink/
import logging
from homeassistant.components.lock import LockDevice
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
REQUIREMENTS = ['python-wink==0.7.4']
REQUIREMENTS = ['python-wink==0.7.6']
def setup_platform(hass, config, add_devices, discovery_info=None):
@ -36,6 +36,7 @@ class WinkLockDevice(LockDevice):
def __init__(self, wink):
"""Initialize the lock."""
self.wink = wink
self._battery = self.wink.battery_level
@property
def unique_id(self):
@ -68,3 +69,16 @@ class WinkLockDevice(LockDevice):
def unlock(self, **kwargs):
"""Unlock the device."""
self.wink.set_state(False)
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100

View File

@ -0,0 +1,64 @@
"""
Zwave platform that handles simple door locks.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/lock.zwave/
"""
# Because we do not compile openzwave on CI
# pylint: disable=import-error
from homeassistant.components.lock import DOMAIN, LockDevice
from homeassistant.components import zwave
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Z-Wave switches."""
if discovery_info is None or zwave.NETWORK is None:
return
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
if value.command_class != zwave.COMMAND_CLASS_DOOR_LOCK:
return
if value.type != zwave.TYPE_BOOL:
return
if value.genre != zwave.GENRE_USER:
return
value.set_change_verified(False)
add_devices([ZwaveLock(value)])
class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
"""Representation of a Z-Wave switch."""
def __init__(self, value):
"""Initialize the Z-Wave switch device."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._state = value.data
dispatcher.connect(
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
def _value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id:
self._state = value.data
self.update_ha_state()
@property
def is_locked(self):
"""Return true if device is locked."""
return self._state
def lock(self, **kwargs):
"""Lock the device."""
self._value.data = True
def unlock(self, **kwargs):
"""Unlock the device."""
self._value.data = False

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE,
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
SERVICE_VOLUME_MUTE, SERVICE_TOGGLE,
SERVICE_VOLUME_MUTE, SERVICE_TOGGLE, SERVICE_MEDIA_STOP,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
@ -82,6 +82,7 @@ SUPPORT_TURN_OFF = 256
SUPPORT_PLAY_MEDIA = 512
SUPPORT_VOLUME_STEP = 1024
SUPPORT_SELECT_SOURCE = 2048
SUPPORT_STOP = 4096
# simple services that only take entity_id(s) as optional argument
SERVICE_TO_METHOD = {
@ -93,6 +94,7 @@ SERVICE_TO_METHOD = {
SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause',
SERVICE_MEDIA_PLAY: 'media_play',
SERVICE_MEDIA_PAUSE: 'media_pause',
SERVICE_MEDIA_STOP: 'media_stop',
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
SERVICE_SELECT_SOURCE: 'select_source'
@ -228,6 +230,12 @@ def media_pause(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data)
def media_stop(hass, entity_id=None):
"""Send the media player the stop command."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_MEDIA_STOP, data)
def media_next_track(hass, entity_id=None):
"""Send the media player the command for next track."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
@ -510,6 +518,10 @@ class MediaPlayerDevice(Entity):
"""Send pause command."""
raise NotImplementedError()
def media_stop(self):
"""Send stop command."""
raise NotImplementedError()
def media_previous_track(self):
"""Send previous track command."""
raise NotImplementedError()
@ -536,6 +548,11 @@ class MediaPlayerDevice(Entity):
"""Boolean if pause is supported."""
return bool(self.supported_media_commands & SUPPORT_PAUSE)
@property
def support_stop(self):
"""Boolean if stop is supported."""
return bool(self.supported_media_commands & SUPPORT_STOP)
@property
def support_seek(self):
"""Boolean if seek is supported."""

View File

@ -9,17 +9,17 @@ import urllib
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
MediaPlayerDevice)
from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['jsonrpc-requests==0.1']
REQUIREMENTS = ['jsonrpc-requests==0.2']
SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
SUPPORT_PLAY_MEDIA
SUPPORT_PLAY_MEDIA | SUPPORT_STOP
def setup_platform(hass, config, add_devices, discovery_info=None):
@ -229,6 +229,13 @@ class KodiDevice(MediaPlayerDevice):
"""Pause the media player."""
self._set_play_state(False)
def media_stop(self):
"""Stop the media player."""
players = self._get_players()
if len(players) != 0:
self._server.Player.Stop(players[0]['playerid'])
def _goto(self, direction):
"""Helper method used for previous/next track."""
players = self._get_players()

View File

@ -0,0 +1,199 @@
"""
Support for Pioneer Network Receivers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.pioneer/
"""
import logging
import telnetlib
from homeassistant.components.media_player import (
DOMAIN, SUPPORT_PAUSE, SUPPORT_SELECT_SOURCE,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN,
CONF_NAME)
_LOGGER = logging.getLogger(__name__)
SUPPORT_PIONEER = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
MAX_VOLUME = 185
MAX_SOURCE_NUMBERS = 60
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Pioneer platform."""
if not config.get(CONF_HOST):
_LOGGER.error(
"Missing required configuration items in %s: %s",
DOMAIN,
CONF_HOST)
return False
pioneer = PioneerDevice(
config.get(CONF_NAME, "Pioneer AVR"),
config.get(CONF_HOST)
)
if pioneer.update():
add_devices([pioneer])
return True
else:
return False
class PioneerDevice(MediaPlayerDevice):
"""Representation of a Pioneer device."""
# pylint: disable=too-many-public-methods, abstract-method
# pylint: disable=too-many-instance-attributes
def __init__(self, name, host):
"""Initialize the Pioneer device."""
self._name = name
self._host = host
self._pwstate = "PWR1"
self._volume = 0
self._muted = False
self._selected_source = ''
self._source_name_to_number = {}
self._source_number_to_name = {}
@classmethod
def telnet_request(cls, telnet, command, expected_prefix):
"""Execute `command` and return the response."""
telnet.write(command.encode("ASCII") + b"\r")
# The receiver will randomly send state change updates, make sure
# we get the response we are looking for
for _ in range(3):
result = telnet.read_until(b"\r\n", timeout=0.2).decode("ASCII") \
.strip()
if result.startswith(expected_prefix):
return result
return None
def telnet_command(self, command):
"""Establish a telnet connection and sends `command`."""
telnet = telnetlib.Telnet(self._host)
telnet.write(command.encode("ASCII") + b"\r")
telnet.read_very_eager() # skip response
telnet.close()
def update(self):
"""Get the latest details from the device."""
try:
telnet = telnetlib.Telnet(self._host)
except ConnectionRefusedError:
return False
self._pwstate = self.telnet_request(telnet, "?P", "PWR")
volume_str = self.telnet_request(telnet, "?V", "VOL")
self._volume = int(volume_str[3:]) / MAX_VOLUME if volume_str else None
muted_value = self.telnet_request(telnet, "?M", "MUT")
self._muted = (muted_value == "MUT0") if muted_value else None
# Build the source name dictionaries if necessary
if not self._source_name_to_number:
for i in range(MAX_SOURCE_NUMBERS):
result = self.telnet_request(telnet,
"?RGB" + str(i).zfill(2),
"RGB")
if not result:
continue
source_name = result[6:]
source_number = str(i).zfill(2)
self._source_name_to_number[source_name] = source_number
self._source_number_to_name[source_number] = source_name
source_number = self.telnet_request(telnet, "?F", "FN")
if source_number:
self._selected_source = self._source_number_to_name \
.get(source_number[2:])
else:
self._selected_source = None
telnet.close()
return True
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
if self._pwstate == "PWR1":
return STATE_OFF
if self._pwstate == "PWR0":
return STATE_ON
return STATE_UNKNOWN
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self._volume
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._muted
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_PIONEER
@property
def source(self):
"""Return the current input source."""
return self._selected_source
@property
def source_list(self):
"""List of available input sources."""
return list(self._source_name_to_number.keys())
@property
def media_title(self):
"""Title of current playing media."""
return self._selected_source
def turn_off(self):
"""Turn off media player."""
self.telnet_command("PF")
def volume_up(self):
"""Volume up media player."""
self.telnet_command("VU")
def volume_down(self):
"""Volume down media player."""
self.telnet_command("VD")
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
# 60dB max
self.telnet_command(str(round(volume * MAX_VOLUME)).zfill(3) + "VL")
def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player."""
self.telnet_command("MO" if mute else "MF")
def turn_on(self):
"""Turn the media player on."""
self.telnet_command("PO")
def select_source(self, source):
"""Select input source."""
self.telnet_command(self._source_name_to_number.get(source) + "FN")

View File

@ -86,6 +86,14 @@ media_pause:
description: Name(s) of entities to pause on
example: 'media_player.living_room_sonos'
media_stop:
description: Send the media player the stop command.
fields:
entity_id:
description: Name(s) of entities to stop on
example: 'media_player.living_room_sonos'
media_next_track:
description: Send the media player the command for next track.

View File

@ -9,12 +9,16 @@ import logging
import socket
from homeassistant.components.media_player import (
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
from homeassistant.const import STATE_OFF, STATE_ON
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE,
MediaPlayerDevice)
from homeassistant.const import (
STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN)
SUPPORT_SNAPCAST = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_SELECT_SOURCE
SUPPORT_SNAPCAST = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
DOMAIN = 'snapcast'
REQUIREMENTS = ['snapcast==1.1.1']
REQUIREMENTS = ['snapcast==1.2.1']
_LOGGER = logging.getLogger(__name__)
@ -67,9 +71,23 @@ class SnapcastDevice(MediaPlayerDevice):
@property
def state(self):
"""Return the state of the player."""
if self._client.connected:
return STATE_ON
return STATE_OFF
if not self._client.connected:
return STATE_OFF
return {
'idle': STATE_IDLE,
'playing': STATE_PLAYING,
'unkown': STATE_UNKNOWN,
}.get(self._client.stream.status, STATE_UNKNOWN)
@property
def source(self):
"""Return the current input source."""
return self._client.stream.identifier
@property
def source_list(self):
"""List of available input sources."""
return self._client.available_streams()
def mute_volume(self, mute):
"""Send the mute command."""
@ -78,3 +96,7 @@ class SnapcastDevice(MediaPlayerDevice):
def set_volume_level(self, volume):
"""Set the volume level."""
self._client.volume = round(volume * 100)
def select_source(self, source):
"""Set input source."""
self._client.stream = source

View File

@ -25,7 +25,8 @@ from homeassistant.const import (
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF,
SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_IDLE, STATE_OFF, STATE_ON)
SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_IDLE, STATE_OFF, STATE_ON,
SERVICE_MEDIA_STOP)
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers.service import call_from_config
@ -384,6 +385,10 @@ class UniversalMediaPlayer(MediaPlayerDevice):
"""Send pause command."""
self._call_service(SERVICE_MEDIA_PAUSE)
def media_stop(self):
"""Send stop command."""
self._call_service(SERVICE_MEDIA_STOP)
def media_previous_track(self):
"""Send previous track command."""
self._call_service(SERVICE_MEDIA_PREVIOUS_TRACK)

View File

@ -69,7 +69,7 @@ def setup_tv(host, hass, add_devices):
_LOGGER.warning(
'Connected to LG WebOS TV at %s but not paired.', host)
return
except ConnectionRefusedError:
except OSError:
_LOGGER.error('Unable to connect to host %s.', host)
return
else:
@ -158,7 +158,7 @@ class LgWebOSDevice(MediaPlayerDevice):
if source['appId'] == self._current_source_id:
self._current_source = source['label']
except ConnectionRefusedError:
except OSError:
self._state = STATE_OFF
@property
@ -208,6 +208,7 @@ class LgWebOSDevice(MediaPlayerDevice):
def turn_off(self):
"""Turn off media player."""
self._state = STATE_OFF
self._client.power_off()
def volume_up(self):

View File

@ -16,21 +16,30 @@ REQUIREMENTS = ['rxv==0.1.11']
_LOGGER = logging.getLogger(__name__)
SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
CONF_SOURCE_NAMES = 'source_names'
CONF_SOURCE_IGNORE = 'source_ignore'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Yamaha platform."""
import rxv
add_devices(YamahaDevice(config.get("name"), receiver)
for receiver in rxv.find())
source_ignore = config.get(CONF_SOURCE_IGNORE, [])
source_names = config.get(CONF_SOURCE_NAMES, {})
add_devices(
YamahaDevice(config.get("name"), receiver, source_ignore, source_names)
for receiver in rxv.find())
class YamahaDevice(MediaPlayerDevice):
"""Representation of a Yamaha device."""
# pylint: disable=too-many-public-methods, abstract-method
def __init__(self, name, receiver):
# pylint: disable=too-many-instance-attributes
def __init__(self, name, receiver, source_ignore, source_names):
"""Initialize the Yamaha Receiver."""
self._receiver = receiver
self._muted = False
@ -38,6 +47,9 @@ class YamahaDevice(MediaPlayerDevice):
self._pwstate = STATE_OFF
self._current_source = None
self._source_list = None
self._source_ignore = source_ignore
self._source_names = source_names
self._reverse_mapping = None
self.update()
self._name = name
@ -48,9 +60,24 @@ class YamahaDevice(MediaPlayerDevice):
else:
self._pwstate = STATE_OFF
self._muted = self._receiver.mute
self._volume = (self._receiver.volume/100) + 1
self._current_source = self._receiver.input
self._source_list = list(self._receiver.inputs().keys())
self._volume = (self._receiver.volume / 100) + 1
if self.source_list is None:
self.build_source_list()
current_source = self._receiver.input
self._current_source = self._source_names.get(current_source,
current_source)
def build_source_list(self):
"""Build the source list."""
self._reverse_mapping = {alias: source for source, alias in
self._source_names.items()}
self._source_list = sorted(
self._source_names.get(source, source) for source in
self._receiver.inputs()
if source not in self._source_ignore)
@property
def name(self):
@ -93,7 +120,7 @@ class YamahaDevice(MediaPlayerDevice):
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
receiver_vol = 100-(volume * 100)
receiver_vol = 100 - (volume * 100)
negative_receiver_vol = -receiver_vol
self._receiver.volume = negative_receiver_vol
@ -104,8 +131,9 @@ class YamahaDevice(MediaPlayerDevice):
def turn_on(self):
"""Turn the media player on."""
self._receiver.on = True
self._volume = (self._receiver.volume/100) + 1
self._volume = (self._receiver.volume / 100) + 1
def select_source(self, source):
"""Select input source."""
self._receiver.input = source
self._receiver.input = self._reverse_mapping.get(source,
source)

View File

@ -39,6 +39,9 @@ CONF_KEEPALIVE = 'keepalive'
CONF_USERNAME = 'username'
CONF_PASSWORD = 'password'
CONF_CERTIFICATE = 'certificate'
CONF_CLIENT_KEY = 'client_key'
CONF_CLIENT_CERT = 'client_cert'
CONF_TLS_INSECURE = 'tls_insecure'
CONF_PROTOCOL = 'protocol'
CONF_STATE_TOPIC = 'state_topic'
@ -78,6 +81,9 @@ def valid_publish_topic(value):
_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2]))
_HBMQTT_CONFIG_SCHEMA = vol.Schema(dict)
CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \
'the mqtt broker config'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_CLIENT_ID): cv.string,
@ -89,6 +95,11 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_CERTIFICATE): cv.isfile,
vol.Inclusive(CONF_CLIENT_KEY, 'client_key_auth',
msg=CLIENT_KEY_AUTH_MSG): cv.isfile,
vol.Inclusive(CONF_CLIENT_CERT, 'client_key_auth',
msg=CLIENT_KEY_AUTH_MSG): cv.isfile,
vol.Optional(CONF_TLS_INSECURE): cv.boolean,
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL):
vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])),
vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA,
@ -192,20 +203,26 @@ def setup(hass, config):
broker_config = _setup_server(hass, config)
broker_in_conf = True if CONF_BROKER in conf else False
# Only auto config if no server config was passed in
if broker_config and CONF_EMBEDDED not in conf:
broker, port, username, password, certificate, protocol = broker_config
elif not broker_config and (CONF_EMBEDDED in conf or
CONF_BROKER not in conf):
# Embedded broker doesn't have some ssl variables
client_key, client_cert, tls_insecure = None, None, None
elif not broker_config and CONF_BROKER not in conf:
_LOGGER.error('Unable to start broker and auto-configure MQTT.')
return False
if CONF_BROKER in conf:
if broker_in_conf:
broker = conf[CONF_BROKER]
port = conf[CONF_PORT]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
certificate = conf.get(CONF_CERTIFICATE)
client_key = conf.get(CONF_CLIENT_KEY)
client_cert = conf.get(CONF_CLIENT_CERT)
tls_insecure = conf.get(CONF_TLS_INSECURE)
protocol = conf[CONF_PROTOCOL]
# For cloudmqtt.com, secured connection, auto fill in certificate
@ -216,8 +233,9 @@ def setup(hass, config):
global MQTT_CLIENT
try:
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username,
password, certificate, protocol)
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive,
username, password, certificate, client_key,
client_cert, tls_insecure, protocol)
except socket.error:
_LOGGER.exception("Can't connect to the broker. "
"Please check your settings and the broker "
@ -268,7 +286,8 @@ class MQTT(object):
"""Home Assistant MQTT client."""
def __init__(self, hass, broker, port, client_id, keepalive, username,
password, certificate, protocol):
password, certificate, client_key, client_cert,
tls_insecure, protocol):
"""Initialize Home Assistant MQTT client."""
import paho.mqtt.client as mqtt
@ -288,8 +307,13 @@ class MQTT(object):
if username is not None:
self._mqttc.username_pw_set(username, password)
if certificate is not None:
self._mqttc.tls_set(certificate)
self._mqttc.tls_set(certificate, certfile=client_cert,
keyfile=client_key)
if tls_insecure is not None:
self._mqttc.tls_insecure_set(tls_insecure)
self._mqttc.on_subscribe = self._mqtt_on_subscribe
self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe

View File

@ -12,7 +12,7 @@ import threading
from homeassistant.components.mqtt import PROTOCOL_311
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
REQUIREMENTS = ['hbmqtt==0.6.3']
REQUIREMENTS = ['hbmqtt==0.7.1']
DEPENDENCIES = ['http']

View File

@ -8,10 +8,12 @@ import logging
import socket
import homeassistant.bootstrap as bootstrap
from homeassistant.const import (ATTR_DISCOVERED, ATTR_SERVICE,
CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START,
from homeassistant.const import (ATTR_BATTERY_LEVEL, ATTR_DISCOVERED,
ATTR_SERVICE, CONF_OPTIMISTIC,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
EVENT_PLATFORM_DISCOVERED, TEMP_CELSIUS)
EVENT_PLATFORM_DISCOVERED, STATE_OFF,
STATE_ON, TEMP_CELSIUS)
from homeassistant.helpers import validate_config
CONF_GATEWAYS = 'gateways'
@ -170,6 +172,8 @@ def pf_callback_factory(map_sv_types, devices, add_devices, entity_class):
class GatewayWrapper(object):
"""Gateway wrapper class."""
# pylint: disable=too-few-public-methods
def __init__(self, gateway, version, optimistic):
"""Setup class attributes on instantiation.
@ -182,14 +186,12 @@ class GatewayWrapper(object):
_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.
optimistic (bool): Send values to actuators without feedback state.
__initialised (bool): True if GatewayWrapper is initialised.
"""
self._wrapped_gateway = gateway
self.version = version
self.platform_callbacks = []
self.const = self.get_const()
self.optimistic = optimistic
self.__initialised = True
@ -197,9 +199,9 @@ class GatewayWrapper(object):
"""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
# This object has the attribute.
return getattr(self, name)
# proxy to the wrapped object
# The wrapped object has the attribute.
return getattr(self._wrapped_gateway, name)
def __setattr__(self, name, value):
@ -211,14 +213,6 @@ class GatewayWrapper(object):
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):
@ -228,3 +222,99 @@ class GatewayWrapper(object):
callback(self, node_id)
return node_update
class MySensorsDeviceEntity(object):
"""Represent a MySensors entity."""
# pylint: disable=too-many-arguments,too-many-instance-attributes
def __init__(
self, gateway, node_id, child_id, name, value_type, child_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.
child_type (str): Child type of child.
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.
child_type (str): Child type of child.
battery_level (int): Node battery level.
_values (dict): Child values. Non state values set as state attributes.
mysensors (module): Mysensors main component module.
"""
self.gateway = gateway
self.node_id = node_id
self.child_id = child_id
self._name = name
self.value_type = value_type
self.child_type = child_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."""
address = getattr(self.gateway, 'server_address', None)
if address:
device = '{}:{}'.format(address[0], address[1])
else:
device = self.gateway.port
attr = {
ATTR_DEVICE: device,
ATTR_NODE_ID: self.node_id,
ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level,
}
set_req = self.gateway.const.SetReq
for value_type, value in self._values.items():
try:
attr[set_req(value_type).name] = value
except ValueError:
_LOGGER.error('value_type %s is not valid for mysensors '
'version %s', value_type,
self.gateway.version)
return attr
@property
def available(self):
"""Return True if entity is available."""
return self.value_type in self._values
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]
self.battery_level = node.battery_level
set_req = self.gateway.const.SetReq
for value_type, value in child.values.items():
_LOGGER.debug(
"%s: value_type %s, value = %s", self._name, value_type, value)
if value_type in (set_req.V_ARMED, set_req.V_LIGHT,
set_req.V_LOCK_STATUS, set_req.V_TRIPPED):
self._values[value_type] = (
STATE_ON if int(value) == 1 else STATE_OFF)
else:
self._values[value_type] = value

View File

@ -14,7 +14,7 @@ from homeassistant.helpers import validate_config
CONF_SENDER = 'sender'
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['messagebird==1.1.1']
REQUIREMENTS = ['messagebird==1.2.0']
def is_valid_sender(sender):

View File

@ -11,7 +11,7 @@ from homeassistant.components.notify import (
from homeassistant.const import CONF_API_KEY
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pushbullet.py==0.9.0']
REQUIREMENTS = ['pushbullet.py==0.10.0']
# pylint: disable=unused-argument

View File

@ -10,7 +10,7 @@ from homeassistant.components.notify import DOMAIN, BaseNotificationService
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import validate_config
REQUIREMENTS = ['slacker==0.6.8']
REQUIREMENTS = ['slacker==0.9.10']
_LOGGER = logging.getLogger(__name__)
@ -51,7 +51,7 @@ class SlackNotificationService(BaseNotificationService):
"""Send a message to a user."""
import slacker
channel = kwargs.get('channel', self._default_channel)
channel = kwargs.get('target', self._default_channel)
try:
self.slack.chat.post_message(channel, message)
except slacker.Error:

View File

@ -14,7 +14,7 @@ from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-telegram-bot==3.4']
REQUIREMENTS = ['python-telegram-bot==4.0.1']
def get_service(hass, config):

View File

@ -11,7 +11,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['TwitterAPI==2.3.6']
REQUIREMENTS = ['TwitterAPI==2.4.1']
CONF_CONSUMER_KEY = "consumer_key"
CONF_CONSUMER_SECRET = "consumer_secret"

View File

@ -10,6 +10,10 @@ from homeassistant.components.notify import (BaseNotificationService, DOMAIN)
from homeassistant.const import (CONF_HOST, CONF_NAME)
from homeassistant.helpers import validate_config
REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv'
'/archive/v0.1.2.zip'
'#pylgtv==0.1.2']
_LOGGER = logging.getLogger(__name__)
@ -35,7 +39,7 @@ def get_service(hass, config):
except PyLGTVPairException:
_LOGGER.error('Pairing failed.')
return None
except ConnectionRefusedError:
except OSError:
_LOGGER.error('Host unreachable.')
return None
@ -58,5 +62,5 @@ class LgWebOSNotificationService(BaseNotificationService):
self._client.send_message(message)
except PyLGTVPairException:
_LOGGER.error('Pairing failed.')
except ConnectionRefusedError:
except OSError:
_LOGGER.error('Host unreachable.')

View File

@ -0,0 +1,120 @@
"""
Support for monitoring OctoPrint 3D printers.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/octoprint/
"""
import logging
import time
import requests
from homeassistant.components import discovery
from homeassistant.const import CONF_API_KEY, CONF_HOST
from homeassistant.helpers import validate_config
DOMAIN = "octoprint"
OCTOPRINT = None
_LOGGER = logging.getLogger(__name__)
DISCOVER_SENSORS = 'octoprint.sensors'
DISCOVER_BINARY_SENSORS = 'octoprint.binary_sensor'
def setup(hass, config):
"""Set up OctoPrint API."""
if not validate_config(config, {DOMAIN: [CONF_API_KEY],
DOMAIN: [CONF_HOST]},
_LOGGER):
return False
base_url = config[DOMAIN][CONF_HOST] + "/api/"
api_key = config[DOMAIN][CONF_API_KEY]
global OCTOPRINT
try:
OCTOPRINT = OctoPrintAPI(base_url, api_key)
OCTOPRINT.get("printer")
OCTOPRINT.get("job")
except requests.exceptions.RequestException as conn_err:
_LOGGER.error("Error setting up OctoPrint API: %r", conn_err)
return False
for component, discovery_service in (
('sensor', DISCOVER_SENSORS),
('binary_sensor', DISCOVER_BINARY_SENSORS)):
discovery.discover(hass, discovery_service, component=component,
hass_config=config)
return True
class OctoPrintAPI(object):
"""Simple JSON wrapper for OctoPrint's API."""
def __init__(self, api_url, key):
"""Initialize OctoPrint API and set headers needed later."""
self.api_url = api_url
self.headers = {'content-type': 'application/json',
'X-Api-Key': key}
self.printer_last_reading = [{}, None]
self.job_last_reading = [{}, None]
def get_tools(self):
"""Get the dynamic list of tools that temperature is monitored on."""
tools = self.printer_last_reading[0]['temperature']
return tools.keys()
def get(self, endpoint):
"""Send a get request, and return the response as a dict."""
now = time.time()
if endpoint == "job":
last_time = self.job_last_reading[1]
if last_time is not None:
if now - last_time < 30.0:
return self.job_last_reading[0]
elif endpoint == "printer":
last_time = self.printer_last_reading[1]
if last_time is not None:
if now - last_time < 30.0:
return self.printer_last_reading[0]
url = self.api_url + endpoint
try:
response = requests.get(url,
headers=self.headers,
timeout=30)
response.raise_for_status()
if endpoint == "job":
self.job_last_reading[0] = response.json()
self.job_last_reading[1] = time.time()
elif endpoint == "printer":
self.printer_last_reading[0] = response.json()
self.printer_last_reading[1] = time.time()
return response.json()
except requests.exceptions.ConnectionError as conn_exc:
_LOGGER.error("Failed to update OctoPrint status. Error: %s",
conn_exc)
raise
def update(self, sensor_type, end_point, group, tool=None):
"""Return the value for sensor_type from the provided endpoint."""
try:
return get_value_from_json(self.get(end_point), sensor_type,
group, tool)
except requests.exceptions.ConnectionError:
raise
# pylint: disable=unused-variable
def get_value_from_json(json_dict, sensor_type, group, tool):
"""Return the value for sensor_type from the JSON."""
if group in json_dict:
if sensor_type in json_dict[group]:
if sensor_type == "target" and json_dict[sensor_type] is None:
return 0
else:
return json_dict[group][sensor_type]
elif tool is not None:
if sensor_type in json_dict[group][tool]:
return json_dict[group][tool][sensor_type]

View File

@ -5,61 +5,104 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/rfxtrx/
"""
import logging
from collections import OrderedDict
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS)
REQUIREMENTS = ['pyRFXtrx==0.6.5']
DOMAIN = "rfxtrx"
DEFAULT_SIGNAL_REPETITIONS = 1
ATTR_AUTOMATIC_ADD = 'automatic_add'
ATTR_DEVICE = 'device'
ATTR_DEBUG = 'debug'
ATTR_STATE = 'state'
ATTR_NAME = 'name'
ATTR_PACKETID = 'packetid'
ATTR_FIREEVENT = 'fire_event'
ATTR_DATA_TYPE = 'data_type'
ATTR_DUMMY = 'dummy'
CONF_SIGNAL_REPETITIONS = 'signal_repetitions'
CONF_DEVICES = 'devices'
DEFAULT_SIGNAL_REPETITIONS = 1
EVENT_BUTTON_PRESSED = 'button_pressed'
DATA_TYPES = OrderedDict([
('Temperature', TEMP_CELSIUS),
('Humidity', '%'),
('Barometer', ''),
('Wind direction', ''),
('Rain rate', ''),
('Energy usage', 'W'),
('Total usage', 'W'),
('Sensor Status', ''),
('Unknown', '')])
RECEIVED_EVT_SUBSCRIBERS = []
RFX_DEVICES = {}
_LOGGER = logging.getLogger(__name__)
RFXOBJECT = None
def validate_packetid(value):
"""Validate that value is a valid packet id for rfxtrx."""
if get_rfx_object(value):
return value
else:
raise vol.Invalid('invalid packet id for {}'.format(value))
def _valid_device(value, device_type):
"""Validate a dictionary of devices definitions."""
config = OrderedDict()
for key, device in value.items():
# Share between rfxtrx platforms
VALID_DEVICE_ID = vol.All(cv.string, vol.Lower)
VALID_SENSOR_DEVICE_ID = vol.All(VALID_DEVICE_ID,
vol.truth(lambda val:
val.startswith('sensor_')))
# Still accept old configuration
if 'packetid' in device.keys():
msg = 'You are using an outdated configuration of the rfxtrx ' +\
'device, {}.'.format(key) +\
' Your new config should be:\n {}: \n name: {}'\
.format(device.get('packetid'),
device.get(ATTR_NAME, 'deivce_name'))
_LOGGER.warning(msg)
key = device.get('packetid')
device.pop('packetid')
if get_rfx_object(key) is None:
raise vol.Invalid('Rfxtrx device {} is invalid: '
'Invalid device id for {}'.format(key, value))
if device_type == 'sensor':
config[key] = DEVICE_SCHEMA_SENSOR(device)
elif device_type == 'light_switch':
config[key] = DEVICE_SCHEMA(device)
else:
raise vol.Invalid('Rfxtrx device is invalid')
if not config[key][ATTR_NAME]:
config[key][ATTR_NAME] = key
return config
def valid_sensor(value):
"""Validate sensor configuration."""
return _valid_device(value, "sensor")
def _valid_light_switch(value):
return _valid_device(value, "light_switch")
DEVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_PACKETID): validate_packetid,
vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean,
})
DEVICE_SCHEMA_SENSOR = vol.Schema({
vol.Optional(ATTR_NAME, default=None): cv.string,
vol.Optional(ATTR_DATA_TYPE, default=[]):
vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]),
})
DEFAULT_SCHEMA = vol.Schema({
vol.Required("platform"): DOMAIN,
vol.Required(CONF_DEVICES): {cv.slug: DEVICE_SCHEMA},
vol.Optional(CONF_DEVICES, default={}): vol.All(dict, _valid_light_switch),
vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean,
vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS):
vol.Coerce(int),
@ -82,11 +125,7 @@ def setup(hass, config):
# Log RFXCOM event
if not event.device.id_string:
return
entity_id = slugify(event.device.id_string.lower())
packet_id = "".join("{0:02x}".format(x) for x in event.data)
entity_name = "%s : %s" % (entity_id, packet_id)
_LOGGER.info("Receive RFXCOM event from %s => %s",
event.device, entity_name)
_LOGGER.info("Receive RFXCOM event from %s", event.device)
# Callback to HA registered components.
for subscriber in RECEIVED_EVT_SUBSCRIBERS:
@ -121,19 +160,16 @@ def get_rfx_object(packetid):
import RFXtrx as rfxtrxmod
binarypacket = bytearray.fromhex(packetid)
pkt = rfxtrxmod.lowlevel.parse(binarypacket)
if pkt is not None:
if isinstance(pkt, rfxtrxmod.lowlevel.SensorPacket):
obj = rfxtrxmod.SensorEvent(pkt)
elif isinstance(pkt, rfxtrxmod.lowlevel.Status):
obj = rfxtrxmod.StatusEvent(pkt)
else:
obj = rfxtrxmod.ControlEvent(pkt)
return obj
return None
if pkt is None:
return None
if isinstance(pkt, rfxtrxmod.lowlevel.SensorPacket):
obj = rfxtrxmod.SensorEvent(pkt)
elif isinstance(pkt, rfxtrxmod.lowlevel.Status):
obj = rfxtrxmod.StatusEvent(pkt)
else:
obj = rfxtrxmod.ControlEvent(pkt)
return obj
def get_devices_from_config(config, device):
@ -141,7 +177,9 @@ def get_devices_from_config(config, device):
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
devices = []
for device_id, entity_info in config[CONF_DEVICES].items():
for packet_id, entity_info in config[CONF_DEVICES].items():
event = get_rfx_object(packet_id)
device_id = slugify(event.device.id_string.lower())
if device_id in RFX_DEVICES:
continue
_LOGGER.info("Add %s rfxtrx", entity_info[ATTR_NAME])
@ -150,8 +188,7 @@ def get_devices_from_config(config, device):
fire_event = entity_info[ATTR_FIREEVENT]
datas = {ATTR_STATE: False, ATTR_FIREEVENT: fire_event}
rfxobject = get_rfx_object(entity_info[ATTR_PACKETID])
new_device = device(entity_info[ATTR_NAME], rfxobject, datas,
new_device = device(entity_info[ATTR_NAME], event, datas,
signal_repetitions)
RFX_DEVICES[device_id] = new_device
devices.append(new_device)
@ -161,83 +198,80 @@ def get_devices_from_config(config, device):
def get_new_device(event, config, device):
"""Add entity if not exist and the automatic_add is True."""
device_id = slugify(event.device.id_string.lower())
if device_id not in RFX_DEVICES:
automatic_add = config[ATTR_AUTOMATIC_ADD]
if not automatic_add:
return
if device_id in RFX_DEVICES:
return
_LOGGER.info(
"Automatic add %s rfxtrx device (Class: %s Sub: %s)",
device_id,
event.device.__class__.__name__,
event.device.subtype
)
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
entity_name = "%s : %s" % (device_id, pkt_id)
datas = {ATTR_STATE: False, ATTR_FIREEVENT: False}
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
new_device = device(entity_name, event, datas,
signal_repetitions)
RFX_DEVICES[device_id] = new_device
return new_device
if not config[ATTR_AUTOMATIC_ADD]:
return
_LOGGER.info(
"Automatic add %s rfxtrx device (Class: %s Sub: %s)",
device_id,
event.device.__class__.__name__,
event.device.subtype
)
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
datas = {ATTR_STATE: False, ATTR_FIREEVENT: False}
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
new_device = device(pkt_id, event, datas,
signal_repetitions)
RFX_DEVICES[device_id] = new_device
return new_device
def apply_received_command(event):
"""Apply command from rfxtrx."""
device_id = slugify(event.device.id_string.lower())
# Check if entity exists or previously added automatically
if device_id in RFX_DEVICES:
_LOGGER.debug(
"EntityID: %s device_update. Command: %s",
device_id,
event.values['Command']
if device_id not in RFX_DEVICES:
return
_LOGGER.debug(
"EntityID: %s device_update. Command: %s",
device_id,
event.values['Command']
)
if event.values['Command'] == 'On'\
or event.values['Command'] == 'Off':
# Update the rfxtrx device state
is_on = event.values['Command'] == 'On'
RFX_DEVICES[device_id].update_state(is_on)
elif hasattr(RFX_DEVICES[device_id], 'brightness')\
and event.values['Command'] == 'Set level':
_brightness = (event.values['Dim level'] * 255 // 100)
# Update the rfxtrx device state
is_on = _brightness > 0
RFX_DEVICES[device_id].update_state(is_on, _brightness)
# Fire event
if RFX_DEVICES[device_id].should_fire_event:
RFX_DEVICES[device_id].hass.bus.fire(
EVENT_BUTTON_PRESSED, {
ATTR_ENTITY_ID:
RFX_DEVICES[device_id].entity_id,
ATTR_STATE: event.values['Command'].lower()
}
)
if event.values['Command'] == 'On'\
or event.values['Command'] == 'Off':
# Update the rfxtrx device state
is_on = event.values['Command'] == 'On'
# pylint: disable=protected-access
RFX_DEVICES[device_id]._state = is_on
RFX_DEVICES[device_id].update_ha_state()
elif hasattr(RFX_DEVICES[device_id], 'brightness')\
and event.values['Command'] == 'Set level':
# pylint: disable=protected-access
RFX_DEVICES[device_id]._brightness = \
(event.values['Dim level'] * 255 // 100)
# Update the rfxtrx device state
is_on = RFX_DEVICES[device_id]._brightness > 0
RFX_DEVICES[device_id]._state = is_on
RFX_DEVICES[device_id].update_ha_state()
# Fire event
if RFX_DEVICES[device_id].should_fire_event:
RFX_DEVICES[device_id].hass.bus.fire(
EVENT_BUTTON_PRESSED, {
ATTR_ENTITY_ID:
RFX_DEVICES[device_id].entity_id,
ATTR_STATE: event.values['Command'].lower()
}
)
class RfxtrxDevice(Entity):
"""Represents a Rfxtrx device.
Contains the common logic for all Rfxtrx devices.
Contains the common logic for Rfxtrx lights and switches.
"""
def __init__(self, name, event, datas, signal_repetitions):
"""Initialize the device."""
self.signal_repetitions = signal_repetitions
self._name = name
self._event = event
self._state = datas[ATTR_STATE]
self._should_fire_event = datas[ATTR_FIREEVENT]
self.signal_repetitions = signal_repetitions
self._brightness = 0
@property
@ -269,6 +303,12 @@ class RfxtrxDevice(Entity):
"""Turn the device off."""
self._send_command("turn_off")
def update_state(self, state, brightness=0):
"""Update det state of the device."""
self._state = state
self._brightness = brightness
self.update_ha_state()
def _send_command(self, command, brightness=0):
if not self._event:
return

View File

@ -2,7 +2,7 @@
Support for command roller shutters.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/rollershutter.command_rollershutter/
https://home-assistant.io/components/rollershutter.command_line/
"""
import logging
import subprocess

View File

@ -8,110 +8,43 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/script/
"""
import logging
import threading
from datetime import timedelta
from itertools import islice
import voluptuous as vol
import homeassistant.util.dt as date_util
from homeassistant.const import (
ATTR_ENTITY_ID, EVENT_TIME_CHANGED, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_TOGGLE, STATE_ON)
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_TOGGLE, STATE_ON, CONF_ALIAS)
from homeassistant.helpers.entity import ToggleEntity, split_entity_id
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers.service import (call_from_config,
validate_service_call)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.script import Script
DOMAIN = "script"
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DEPENDENCIES = ["group"]
STATE_NOT_RUNNING = 'Not Running'
CONF_ALIAS = "alias"
CONF_SERVICE = "service"
CONF_SERVICE_DATA = "data"
CONF_SEQUENCE = "sequence"
CONF_EVENT = "event"
CONF_EVENT_DATA = "event_data"
CONF_DELAY = "delay"
ATTR_VARIABLES = 'variables'
ATTR_LAST_ACTION = 'last_action'
ATTR_CAN_CANCEL = 'can_cancel'
_LOGGER = logging.getLogger(__name__)
_ALIAS_VALIDATOR = vol.Schema(cv.string)
def _alias_stripper(validator):
"""Strip alias from object for validation."""
def validate(value):
"""Validate without alias value."""
value = value.copy()
alias = value.pop(CONF_ALIAS, None)
if alias is not None:
alias = _ALIAS_VALIDATOR(alias)
value = validator(value)
if alias is not None:
value[CONF_ALIAS] = alias
return value
return validate
_TIMESPEC = vol.Schema({
'days': cv.positive_int,
'hours': cv.positive_int,
'minutes': cv.positive_int,
'seconds': cv.positive_int,
'milliseconds': cv.positive_int,
})
_TIMESPEC_REQ = cv.has_at_least_one_key(
'days', 'hours', 'minutes', 'seconds', 'milliseconds',
)
_DELAY_SCHEMA = vol.Any(
vol.Schema({
vol.Required(CONF_DELAY): vol.All(_TIMESPEC.extend({
vol.Optional(CONF_ALIAS): cv.string
}), _TIMESPEC_REQ)
}),
# Alternative format in case people forgot to indent after 'delay:'
vol.All(_TIMESPEC.extend({
vol.Required(CONF_DELAY): None,
vol.Optional(CONF_ALIAS): cv.string,
}), _TIMESPEC_REQ)
)
_EVENT_SCHEMA = cv.EVENT_SCHEMA.extend({
CONF_ALIAS: cv.string,
})
_SCRIPT_ENTRY_SCHEMA = vol.Schema({
CONF_ALIAS: cv.string,
vol.Required(CONF_SEQUENCE): vol.All(vol.Length(min=1), [vol.Any(
_EVENT_SCHEMA,
_DELAY_SCHEMA,
# Can't extend SERVICE_SCHEMA because it is an vol.All
_alias_stripper(cv.SERVICE_SCHEMA),
)]),
vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA,
})
CONFIG_SCHEMA = vol.Schema({
vol.Required(DOMAIN): {cv.slug: _SCRIPT_ENTRY_SCHEMA}
}, extra=vol.ALLOW_EXTRA)
SCRIPT_SERVICE_SCHEMA = vol.Schema({})
SCRIPT_SERVICE_SCHEMA = vol.Schema(dict)
SCRIPT_TURN_ONOFF_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_VARIABLES): dict,
})
@ -120,11 +53,11 @@ def is_on(hass, entity_id):
return hass.states.is_state(entity_id, STATE_ON)
def turn_on(hass, entity_id):
def turn_on(hass, entity_id, variables=None):
"""Turn script on."""
_, object_id = split_entity_id(entity_id)
hass.services.call(DOMAIN, object_id)
hass.services.call(DOMAIN, object_id, variables)
def turn_off(hass, entity_id):
@ -148,11 +81,11 @@ def setup(hass, config):
if script.is_on:
_LOGGER.warning("Script %s already running.", entity_id)
return
script.turn_on()
script.turn_on(variables=service.data)
for object_id, cfg in config[DOMAIN].items():
alias = cfg.get(CONF_ALIAS, object_id)
script = Script(object_id, alias, cfg[CONF_SEQUENCE])
script = ScriptEntity(hass, object_id, alias, cfg[CONF_SEQUENCE])
component.add_entities((script,))
hass.services.register(DOMAIN, object_id, service_handler,
schema=SCRIPT_SERVICE_SCHEMA)
@ -160,9 +93,9 @@ def setup(hass, config):
def turn_on_service(service):
"""Call a service to turn script on."""
# We could turn on script directly here, but we only want to offer
# one way to do it. Otherwise no easy way to call invocations.
# one way to do it. Otherwise no easy way to detect invocations.
for script in component.extract_from_service(service):
turn_on(hass, script.entity_id)
turn_on(hass, script.entity_id, service.data.get(ATTR_VARIABLES))
def turn_off_service(service):
"""Cancel a script."""
@ -183,21 +116,14 @@ def setup(hass, config):
return True
class Script(ToggleEntity):
"""Representation of a script."""
class ScriptEntity(ToggleEntity):
"""Representation of a script entity."""
# pylint: disable=too-many-instance-attributes
def __init__(self, object_id, name, sequence):
def __init__(self, hass, object_id, name, sequence):
"""Initialize the script."""
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self._name = name
self.sequence = sequence
self._lock = threading.Lock()
self._cur = -1
self._last_action = None
self._listener = None
self._can_cancel = any(CONF_DELAY in action for action
in self.sequence)
self.script = Script(hass, sequence, name, self.update_ha_state)
@property
def should_poll(self):
@ -207,91 +133,27 @@ class Script(ToggleEntity):
@property
def name(self):
"""Return the name of the entity."""
return self._name
return self.script.name
@property
def state_attributes(self):
"""Return the state attributes."""
attrs = {}
if self._can_cancel:
attrs[ATTR_CAN_CANCEL] = self._can_cancel
if self._last_action:
attrs[ATTR_LAST_ACTION] = self._last_action
if self.script.can_cancel:
attrs[ATTR_CAN_CANCEL] = self.script.can_cancel
if self.script.last_action:
attrs[ATTR_LAST_ACTION] = self.script.last_action
return attrs
@property
def is_on(self):
"""Return true if script is on."""
return self._cur != -1
return self.script.is_running
def turn_on(self, **kwargs):
"""Turn the entity on."""
_LOGGER.info("Executing script %s", self._name)
with self._lock:
if self._cur == -1:
self._cur = 0
# Unregister callback if we were in a delay but turn on is called
# again. In that case we just continue execution.
self._remove_listener()
for cur, action in islice(enumerate(self.sequence), self._cur,
None):
if validate_service_call(action) is None:
self._call_service(action)
elif CONF_EVENT in action:
self._fire_event(action)
elif CONF_DELAY in action:
# Call ourselves in the future to continue work
def script_delay(now):
"""Called after delay is done."""
self._listener = None
self.turn_on()
timespec = action[CONF_DELAY] or action.copy()
timespec.pop(CONF_DELAY, None)
delay = timedelta(**timespec)
self._listener = track_point_in_utc_time(
self.hass, script_delay, date_util.utcnow() + delay)
self._cur = cur + 1
self.update_ha_state()
return
self._cur = -1
self._last_action = None
self.update_ha_state()
self.script.run(kwargs.get(ATTR_VARIABLES))
def turn_off(self, **kwargs):
"""Turn script off."""
_LOGGER.info("Cancelled script %s", self._name)
with self._lock:
if self._cur == -1:
return
self._cur = -1
self.update_ha_state()
self._remove_listener()
def _call_service(self, action):
"""Call the service specified in the action."""
self._last_action = action.get(CONF_ALIAS, 'call service')
_LOGGER.info("Executing script %s step %s", self._name,
self._last_action)
call_from_config(self.hass, action, True)
def _fire_event(self, action):
"""Fire an event."""
self._last_action = action.get(CONF_ALIAS, action[CONF_EVENT])
_LOGGER.info("Executing script %s step %s", self._name,
self._last_action)
self.hass.bus.fire(action[CONF_EVENT], action.get(CONF_EVENT_DATA))
def _remove_listener(self):
"""Remove point in time listener, if any."""
if self._listener:
self.hass.bus.remove_listener(EVENT_TIME_CHANGED,
self._listener)
self._listener = None
self.script.stop()

View File

@ -2,7 +2,7 @@
Allows to configure custom shell commands to turn a value for a sensor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.command_sensor/
https://home-assistant.io/components/sensor.command_line/
"""
import logging
import subprocess

View File

@ -10,7 +10,7 @@ from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['schiene==0.14']
REQUIREMENTS = ['schiene==0.15']
ICON = 'mdi:train'
# Return cached results if last scan was less then this time ago.
@ -69,11 +69,10 @@ class DeutscheBahnSensor(Entity):
"""Get the latest delay from bahn.de and updates the state."""
self.data.update()
self._state = self.data.connections[0].get('departure', 'Unknown')
delay = self.data.connections[0].get('delay',
{'delay_departure': 0,
'delay_arrival': 0})
if delay['delay_departure'] != 0:
self._state += " + {}".format(delay['delay_departure'])
if self.data.connections[0]['delay'] != 0:
self._state += " + {}".format(
self.data.connections[0]['delay']
)
# pylint: disable=too-few-public-methods
@ -95,6 +94,14 @@ class SchieneData(object):
self.goal,
datetime.now())
for con in self.connections:
# Details info are not useful.
# Details info is not useful.
# Having a more consistent interface simplifies
# usage of Template sensors later on
if 'details' in con:
con.pop('details')
delay = con.get('delay',
{'delay_departure': 0,
'delay_arrival': 0})
# IMHO only delay_departure is usefull
con['delay'] = delay['delay_departure']
con['ontime'] = con.get('ontime', False)

View File

@ -78,18 +78,10 @@ class EcobeeSensor(Entity):
data.update()
for sensor in data.ecobee.get_remote_sensors(self.index):
for item in sensor['capability']:
if (
item['type'] == self.type and
self.type == 'temperature' and
if (item['type'] == self.type and
self.sensor_name == sensor['name']):
self._state = float(item['value']) / 10
elif (
item['type'] == self.type and
self.type == 'humidity' and
self.sensor_name == sensor['name']):
self._state = item['value']
elif (
item['type'] == self.type and
self.type == 'occupancy' and
self.sensor_name == sensor['name']):
self._state = item['value']
if (self.type == 'temperature' and
item['value'] != 'unknown'):
self._state = float(item['value']) / 10
else:
self._state = item['value']

View File

@ -1,5 +1,5 @@
"""
Monitors home energy use for the eliq online service.
Monitors home energy use for the ELIQ Online service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.eliqonline/
@ -12,17 +12,21 @@ from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['eliqonline==1.0.11']
DEFAULT_NAME = "ELIQ Energy Usage"
REQUIREMENTS = ['eliqonline==1.0.12']
DEFAULT_NAME = "ELIQ Online"
UNIT_OF_MEASUREMENT = "W"
ICON = "mdi:speedometer"
CONF_CHANNEL_ID = "channel_id"
SCAN_INTERVAL = 60
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Eliq sensor."""
"""Setup the ELIQ Online sensor."""
import eliqonline
access_token = config.get(CONF_ACCESS_TOKEN)
name = config.get(CONF_NAME, DEFAULT_NAME)
channel_id = config.get("channel_id")
channel_id = config.get(CONF_CHANNEL_ID)
if access_token is None:
_LOGGER.error(
@ -32,20 +36,27 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False
api = eliqonline.API(access_token)
try:
_LOGGER.debug("Probing for access to ELIQ Online API")
api.get_data_now(channelid=channel_id)
except URLError:
_LOGGER.error("Could not access the ELIQ Online API. "
"Is the configuration valid?")
return False
add_devices([EliqSensor(api, channel_id, name)])
class EliqSensor(Entity):
"""Implementation of an Eliq sensor."""
"""Implementation of an ELIQ Online sensor."""
def __init__(self, api, channel_id, name):
"""Initialize the sensor."""
self._name = name
self._unit_of_measurement = "W"
self._state = STATE_UNKNOWN
self.api = api
self.channel_id = channel_id
self._api = api
self._channel_id = channel_id
self.update()
@property
@ -56,12 +67,12 @@ class EliqSensor(Entity):
@property
def icon(self):
"""Return icon."""
return "mdi:speedometer"
return ICON
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
return UNIT_OF_MEASUREMENT
@property
def state(self):
@ -71,7 +82,8 @@ class EliqSensor(Entity):
def update(self):
"""Get the latest data."""
try:
response = self.api.get_data_now(channelid=self.channel_id)
response = self._api.get_data_now(channelid=self._channel_id)
self._state = int(response.power)
except (TypeError, URLError):
_LOGGER.error("Could not connect to the eliqonline servers")
_LOGGER.debug("Updated power from server %d W", self._state)
except URLError:
_LOGGER.error("Could not connect to the ELIQ Online API")

View File

@ -0,0 +1,372 @@
"""
Support for the Fitbit API.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.fitbit/
"""
import os
import json
import logging
import datetime
import time
from homeassistant.const import HTTP_OK
from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ["fitbit==0.2.2"]
DEPENDENCIES = ["http"]
ICON = "mdi:walk"
_CONFIGURING = {}
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=30)
FITBIT_AUTH_START = "/auth/fitbit"
FITBIT_AUTH_CALLBACK_PATH = "/auth/fitbit/callback"
DEFAULT_CONFIG = {
"client_id": "CLIENT_ID_HERE",
"client_secret": "CLIENT_SECRET_HERE"
}
FITBIT_CONFIG_FILE = "fitbit.conf"
FITBIT_RESOURCES_LIST = {
"activities/activityCalories": "cal",
"activities/calories": "cal",
"activities/caloriesBMR": "cal",
"activities/distance": "",
"activities/elevation": "",
"activities/floors": "floors",
"activities/heart": "bpm",
"activities/minutesFairlyActive": "minutes",
"activities/minutesLightlyActive": "minutes",
"activities/minutesSedentary": "minutes",
"activities/minutesVeryActive": "minutes",
"activities/steps": "steps",
"activities/tracker/activityCalories": "cal",
"activities/tracker/calories": "cal",
"activities/tracker/distance": "",
"activities/tracker/elevation": "",
"activities/tracker/floors": "floors",
"activities/tracker/minutesFairlyActive": "minutes",
"activities/tracker/minutesLightlyActive": "minutes",
"activities/tracker/minutesSedentary": "minutes",
"activities/tracker/minutesVeryActive": "minutes",
"activities/tracker/steps": "steps",
"body/bmi": "BMI",
"body/fat": "%",
"sleep/awakeningsCount": "times awaken",
"sleep/efficiency": "%",
"sleep/minutesAfterWakeup": "minutes",
"sleep/minutesAsleep": "minutes",
"sleep/minutesAwake": "minutes",
"sleep/minutesToFallAsleep": "minutes",
"sleep/startTime": "start time",
"sleep/timeInBed": "time in bed",
"body/weight": ""
}
FITBIT_DEFAULT_RESOURCE_LIST = ["activities/steps"]
FITBIT_MEASUREMENTS = {
"en_US": {
"duration": "ms",
"distance": "mi",
"elevation": "ft",
"height": "in",
"weight": "lbs",
"body": "in",
"liquids": "fl. oz.",
"blood glucose": "mg/dL",
},
"en_UK": {
"duration": "milliseconds",
"distance": "kilometers",
"elevation": "meters",
"height": "centimeters",
"weight": "stone",
"body": "centimeters",
"liquids": "millileters",
"blood glucose": "mmol/l"
},
"metric": {
"duration": "milliseconds",
"distance": "kilometers",
"elevation": "meters",
"height": "centimeters",
"weight": "kilograms",
"body": "centimeters",
"liquids": "millileters",
"blood glucose": "mmol/l"
}
}
def config_from_file(filename, config=None):
"""Small configuration file management function."""
if config:
# We"re writing configuration
try:
with open(filename, "w") as fdesc:
fdesc.write(json.dumps(config))
except IOError as error:
_LOGGER.error("Saving config file failed: %s", error)
return False
return config
else:
# We"re reading config
if os.path.isfile(filename):
try:
with open(filename, "r") as fdesc:
return json.loads(fdesc.read())
except IOError as error:
_LOGGER.error("Reading config file failed: %s", error)
# This won"t work yet
return False
else:
return {}
def request_app_setup(hass, config, add_devices, config_path,
discovery_info=None):
"""Assist user with configuring the Fitbit dev application."""
configurator = get_component("configurator")
# pylint: disable=unused-argument
def fitbit_configuration_callback(callback_data):
"""The actions to do when our configuration callback is called."""
config_path = hass.config.path(FITBIT_CONFIG_FILE)
if os.path.isfile(config_path):
config_file = config_from_file(config_path)
if config_file == DEFAULT_CONFIG:
error_msg = ("You didn't correctly modify fitbit.conf",
" please try again")
configurator.notify_errors(_CONFIGURING["fitbit"], error_msg)
else:
setup_platform(hass, config, add_devices, discovery_info)
else:
setup_platform(hass, config, add_devices, discovery_info)
start_url = "{}{}".format(hass.config.api.base_url, FITBIT_AUTH_START)
description = """Please create a Fitbit developer app at
https://dev.fitbit.com/apps/new.
For the OAuth 2.0 Application Type choose Personal.
Set the Callback URL to {}.
They will provide you a Client ID and secret.
These need to be saved into the file located at: {}.
Then come back here and hit the below button.
""".format(start_url, config_path)
submit = "I have saved my Client ID and Client Secret into fitbit.conf."
_CONFIGURING["fitbit"] = configurator.request_config(
hass, "Fitbit", fitbit_configuration_callback,
description=description, submit_caption=submit,
description_image="/static/images/config_fitbit_app.png"
)
def request_oauth_completion(hass):
"""Request user complete Fitbit OAuth2 flow."""
configurator = get_component("configurator")
if "fitbit" in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING["fitbit"], "Failed to register, please try again.")
return
# pylint: disable=unused-argument
def fitbit_configuration_callback(callback_data):
"""The actions to do when our configuration callback is called."""
start_url = "{}{}".format(hass.config.api.base_url, FITBIT_AUTH_START)
description = "Please authorize Fitbit by visiting {}".format(start_url)
_CONFIGURING["fitbit"] = configurator.request_config(
hass, "Fitbit", fitbit_configuration_callback,
description=description,
submit_caption="I have authorized Fitbit."
)
# pylint: disable=too-many-locals
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Fitbit sensor."""
config_path = hass.config.path(FITBIT_CONFIG_FILE)
if os.path.isfile(config_path):
config_file = config_from_file(config_path)
if config_file == DEFAULT_CONFIG:
request_app_setup(hass, config, add_devices, config_path,
discovery_info=None)
return False
else:
config_file = config_from_file(config_path, DEFAULT_CONFIG)
request_app_setup(hass, config, add_devices, config_path,
discovery_info=None)
return False
if "fitbit" in _CONFIGURING:
get_component("configurator").request_done(_CONFIGURING.pop("fitbit"))
import fitbit
access_token = config_file.get("access_token")
refresh_token = config_file.get("refresh_token")
if None not in (access_token, refresh_token):
authd_client = fitbit.Fitbit(config.get("client_id"),
config.get("client_secret"),
access_token=access_token,
refresh_token=refresh_token)
if int(time.time()) - config_file.get("last_saved_at", 0) > 3600:
authd_client.client.refresh_token()
authd_client.system = authd_client.user_profile_get()["user"]["locale"]
dev = []
for resource in config.get("monitored_resources",
FITBIT_DEFAULT_RESOURCE_LIST):
dev.append(FitbitSensor(authd_client, config_path, resource))
add_devices(dev)
else:
oauth = fitbit.api.FitbitOauth2Client(config.get("client_id"),
config.get("client_secret"))
redirect_uri = "{}{}".format(hass.config.api.base_url,
FITBIT_AUTH_CALLBACK_PATH)
def _start_fitbit_auth(handler, path_match, data):
"""Start Fitbit OAuth2 flow."""
url, _ = oauth.authorize_token_url(redirect_uri=redirect_uri,
scope=["activity", "heartrate",
"nutrition", "profile",
"settings", "sleep",
"weight"])
handler.send_response(301)
handler.send_header("Location", url)
handler.end_headers()
def _finish_fitbit_auth(handler, path_match, data):
"""Finish Fitbit OAuth2 flow."""
response_message = """Fitbit has been successfully authorized!
You can close this window now!"""
from oauthlib.oauth2.rfc6749.errors import MismatchingStateError
from oauthlib.oauth2.rfc6749.errors import MissingTokenError
if data.get("code") is not None:
try:
oauth.fetch_access_token(data.get("code"), redirect_uri)
except MissingTokenError as error:
_LOGGER.error("Missing token: %s", error)
response_message = """Something went wrong when
attempting authenticating with Fitbit. The error
encountered was {}. Please try again!""".format(error)
except MismatchingStateError as error:
_LOGGER.error("Mismatched state, CSRF error: %s", error)
response_message = """Something went wrong when
attempting authenticating with Fitbit. The error
encountered was {}. Please try again!""".format(error)
else:
_LOGGER.error("Unknown error when authing")
response_message = """Something went wrong when
attempting authenticating with Fitbit.
An unknown error occurred. Please try again!
"""
html_response = """<html><head><title>Fitbit Auth</title></head>
<body><h1>{}</h1></body></html>""".format(response_message)
html_response = html_response.encode("utf-8")
handler.send_response(HTTP_OK)
handler.write_content(html_response, content_type="text/html")
config_contents = {
"access_token": oauth.token["access_token"],
"refresh_token": oauth.token["refresh_token"],
"client_id": oauth.client_id,
"client_secret": oauth.client_secret
}
if not config_from_file(config_path, config_contents):
_LOGGER.error("failed to save config file")
setup_platform(hass, config, add_devices, discovery_info=None)
hass.http.register_path("GET", FITBIT_AUTH_START, _start_fitbit_auth)
hass.http.register_path("GET", FITBIT_AUTH_CALLBACK_PATH,
_finish_fitbit_auth)
request_oauth_completion(hass)
# pylint: disable=too-few-public-methods
class FitbitSensor(Entity):
"""Implementation of a Fitbit sensor."""
def __init__(self, client, config_path, resource_type):
"""Initialize the Uber sensor."""
self.client = client
self.config_path = config_path
self.resource_type = resource_type
pretty_resource = self.resource_type.replace("activities/", "")
pretty_resource = pretty_resource.replace("/", " ")
pretty_resource = pretty_resource.title()
if pretty_resource == "Body Bmi":
pretty_resource = "BMI"
self._name = pretty_resource
unit_type = FITBIT_RESOURCES_LIST[self.resource_type]
if unit_type == "":
split_resource = self.resource_type.split("/")
measurement_system = FITBIT_MEASUREMENTS[self.client.system]
unit_type = measurement_system[split_resource[-1]]
self._unit_of_measurement = unit_type
self._state = 0
self.update()
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return ICON
# pylint: disable=too-many-branches
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from the Fitbit API and update the states."""
container = self.resource_type.replace("/", "-")
response = self.client.time_series(self.resource_type, period="7d")
self._state = response[container][-1].get("value")
if self.resource_type == "activities/heart":
self._state = response[container][-1].get("restingHeartRate")
config_contents = {
"access_token": self.client.client.token["access_token"],
"refresh_token": self.client.client.token["refresh_token"],
"client_id": self.client.client.client_id,
"client_secret": self.client.client.client_secret,
"last_saved_at": int(time.time())
}
if not config_from_file(self.config_path, config_contents):
_LOGGER.error("failed to save config file")

View File

@ -18,6 +18,9 @@ _LOGGER = logging.getLogger(__name__)
# Name, si unit, us unit, ca unit, uk unit, uk2 unit
SENSOR_TYPES = {
'summary': ['Summary', None, None, None, None, None],
'minutely_summary': ['Minutely Summary', None, None, None, None, None],
'hourly_summary': ['Hourly Summary', None, None, None, None, None],
'daily_summary': ['Daily Summary', None, None, None, None, None],
'icon': ['Icon', None, None, None, None, None],
'nearest_storm_distance': ['Nearest Storm Distance',
'km', 'm', 'km', 'km', 'm'],
@ -134,11 +137,20 @@ class ForeCastSensor(Entity):
import forecastio
self.forecast_client.update()
data = self.forecast_client.data
data = self.forecast_client.data.currently()
data_minutely = self.forecast_client.data.minutely()
data_hourly = self.forecast_client.data.hourly()
data_daily = self.forecast_client.data.daily()
try:
if self.type == 'summary':
self._state = data.summary
elif self.type == 'minutely_summary':
self._state = data_minutely.summary
elif self.type == 'hourly_summary':
self._state = data_hourly.summary
elif self.type == 'daily_summary':
self._state = data_daily.summary
elif self.type == 'icon':
self._state = data.icon
elif self.type == 'nearest_storm_distance':
@ -198,5 +210,5 @@ class ForeCastData(object):
self.latitude,
self.longitude,
units=self.units)
self.data = forecast.currently()
self.data = forecast
self.unit_system = forecast.json['flags']['units']

View File

@ -0,0 +1,117 @@
"""
Support for Google travel time sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.google_travel_time/
"""
from datetime import datetime
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['googlemaps==2.4.3']
# Return cached results if last update was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
CONF_ORIGIN = 'origin'
CONF_DESTINATION = 'destination'
CONF_TRAVEL_MODE = 'travel_mode'
PLATFORM_SCHEMA = vol.Schema({
vol.Required('platform'): 'google_travel_time',
vol.Required(CONF_API_KEY): vol.Coerce(str),
vol.Required(CONF_ORIGIN): vol.Coerce(str),
vol.Required(CONF_DESTINATION): vol.Coerce(str),
vol.Optional(CONF_TRAVEL_MODE, default='driving'):
vol.In(["driving", "walking", "bicycling", "transit"])
})
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the travel time platform."""
# pylint: disable=too-many-locals
is_metric = (hass.config.temperature_unit == TEMP_CELSIUS)
api_key = config.get(CONF_API_KEY)
origin = config.get(CONF_ORIGIN)
destination = config.get(CONF_DESTINATION)
travel_mode = config.get(CONF_TRAVEL_MODE)
sensor = GoogleTravelTimeSensor(api_key, origin, destination,
travel_mode, is_metric)
if sensor.valid_api_connection:
add_devices_callback([sensor])
class GoogleTravelTimeSensor(Entity):
"""Representation of a tavel time sensor."""
# pylint: disable=too-many-arguments
def __init__(self, api_key, origin, destination, travel_mode, is_metric):
"""Initialize the sensor."""
if is_metric:
self._unit = 'metric'
else:
self._unit = 'imperial'
self._origin = origin
self._destination = destination
self._travel_mode = travel_mode
self._matrix = None
self.valid_api_connection = True
import googlemaps
self._client = googlemaps.Client(api_key, timeout=10)
try:
self.update()
except googlemaps.exceptions.ApiError as exp:
_LOGGER .error(exp)
self.valid_api_connection = False
return
@property
def state(self):
"""Return the state of the sensor."""
return self._matrix['rows'][0]['elements'][0]['duration']['value']/60.0
@property
def name(self):
"""Get the name of the sensor."""
return "Google Travel time"
@property
def device_state_attributes(self):
"""Return the state attributes."""
res = self._matrix.copy()
del res['rows']
_data = self._matrix['rows'][0]['elements'][0]
if 'duration_in_traffic' in _data:
res['duration_in_traffic'] = _data['duration_in_traffic']['text']
if 'duration' in _data:
res['duration'] = _data['duration']['text']
if 'distance' in _data:
res['distance'] = _data['distance']['text']
return res
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return "min"
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from Google."""
now = datetime.now()
self._matrix = self._client.distance_matrix(self._origin,
self._destination,
mode=self._travel_mode,
units=self._unit,
departure_time=now,
traffic_model="optimistic")

View File

@ -6,10 +6,9 @@ https://home-assistant.io/components/sensor.mysensors/
"""
import logging
from homeassistant.const import (ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON,
TEMP_CELSIUS, TEMP_FAHRENHEIT)
from homeassistant.components import mysensors
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = []
@ -22,8 +21,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None:
return
mysensors = get_component('mysensors')
for gateway in mysensors.GATEWAYS.values():
# Define the S_TYPES and V_TYPES that the platform should handle as
# states. Map them in a dict of lists.
@ -74,58 +71,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
map_sv_types, devices, add_devices, MySensorsSensor))
class MySensorsSensor(Entity):
"""Represent the value of a MySensors child node."""
# pylint: disable=too-many-arguments,too-many-instance-attributes
def __init__(
self, gateway, node_id, child_id, name, value_type, child_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.
child_type (str): Child type of child.
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.
mysensors (module): Mysensors main component module.
"""
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 = {}
self.mysensors = get_component('mysensors')
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of this entity."""
return self._name
class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity):
"""Represent the value of a MySensors Sensor child node."""
@property
def state(self):
"""Return the state of the device."""
if not self._values:
return ''
return self._values[self.value_type]
return self._values.get(self.value_type)
@property
def unit_of_measurement(self):
@ -153,50 +105,3 @@ class MySensorsSensor(Entity):
set_req.V_UNIT_PREFIX]
unit_map.update({set_req.V_PERCENTAGE: '%'})
return unit_map.get(self.value_type)
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
address = getattr(self.gateway, 'server_address', None)
if address:
device = '{}:{}'.format(address[0], address[1])
else:
device = self.gateway.port
attr = {
self.mysensors.ATTR_DEVICE: device,
self.mysensors.ATTR_NODE_ID: self.node_id,
self.mysensors.ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level,
}
set_req = self.gateway.const.SetReq
for value_type, value in self._values.items():
if value_type != self.value_type:
try:
attr[set_req(value_type).name] = value
except ValueError:
_LOGGER.error('value_type %s is not valid for mysensors '
'version %s', value_type,
self.gateway.version)
return attr
@property
def available(self):
"""Return True if entity is available."""
return self.value_type in self._values
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.debug(
"%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

View File

@ -0,0 +1,116 @@
"""
Support for monitoring OctoPrint sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.octoprint/
"""
import logging
import requests
from homeassistant.const import TEMP_CELSIUS, CONF_NAME
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
DEPENDENCIES = ["octoprint"]
SENSOR_TYPES = {
# API Endpoint, Group, Key, unit
"Temperatures": ["printer", "temperature", "*", TEMP_CELSIUS],
"Current State": ["printer", "state", "text", None],
"Job Percentage": ["job", "progress", "completion", "%"],
}
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the available OctoPrint sensors."""
octoprint = get_component('octoprint')
name = config.get(CONF_NAME, "OctoPrint")
monitored_conditions = config.get("monitored_conditions",
SENSOR_TYPES.keys())
devices = []
types = ["actual", "target"]
for octo_type in monitored_conditions:
if octo_type == "Temperatures":
for tool in octoprint.OCTOPRINT.get_tools():
for temp_type in types:
new_sensor = OctoPrintSensor(octoprint.OCTOPRINT,
temp_type,
temp_type,
name,
SENSOR_TYPES[octo_type][3],
SENSOR_TYPES[octo_type][0],
SENSOR_TYPES[octo_type][1],
tool)
devices.append(new_sensor)
elif octo_type in SENSOR_TYPES:
new_sensor = OctoPrintSensor(octoprint.OCTOPRINT,
octo_type,
SENSOR_TYPES[octo_type][2],
name,
SENSOR_TYPES[octo_type][3],
SENSOR_TYPES[octo_type][0],
SENSOR_TYPES[octo_type][1])
devices.append(new_sensor)
else:
_LOGGER.error("Unknown OctoPrint sensor type: %s", octo_type)
add_devices(devices)
# pylint: disable=too-many-instance-attributes
class OctoPrintSensor(Entity):
"""Representation of an OctoPrint sensor."""
# pylint: disable=too-many-arguments
def __init__(self, api, condition, sensor_type, sensor_name,
unit, endpoint, group, tool=None):
"""Initialize a new OctoPrint sensor."""
self.sensor_name = sensor_name
if tool is None:
self._name = sensor_name + ' ' + condition
else:
self._name = sensor_name + ' ' + condition + ' ' + tool + ' temp'
self.sensor_type = sensor_type
self.api = api
self._state = None
self._unit_of_measurement = unit
self.api_endpoint = endpoint
self.api_group = group
self.api_tool = tool
# Set initial state
self.update()
_LOGGER.debug("Created OctoPrint sensor %r", self)
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Unit of measurement of this entity, if any."""
return self._unit_of_measurement
def update(self):
"""Update state of sensor."""
try:
self._state = self.api.update(self.sensor_type,
self.api_endpoint,
self.api_group,
self.api_tool)
except requests.exceptions.ConnectionError:
# Error calling the api, already logged in api.update()
return
if self._state is None:
_LOGGER.warning("Unable to locate value for %s", self.sensor_type)
return

View File

@ -5,74 +5,52 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.rfxtrx/
"""
import logging
from collections import OrderedDict
import voluptuous as vol
import homeassistant.components.rfxtrx as rfxtrx
from homeassistant.const import TEMP_CELSIUS
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
from homeassistant.components.rfxtrx import (
ATTR_AUTOMATIC_ADD, ATTR_PACKETID, ATTR_NAME,
CONF_DEVICES, ATTR_DATA_TYPE)
ATTR_AUTOMATIC_ADD, ATTR_NAME,
CONF_DEVICES, ATTR_DATA_TYPE, DATA_TYPES)
DEPENDENCIES = ['rfxtrx']
DATA_TYPES = OrderedDict([
('Temperature', TEMP_CELSIUS),
('Humidity', '%'),
('Barometer', ''),
('Wind direction', ''),
('Rain rate', ''),
('Energy usage', 'W'),
('Total usage', 'W')])
_LOGGER = logging.getLogger(__name__)
DEVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_NAME, default=None): cv.string,
vol.Required(ATTR_PACKETID): rfxtrx.validate_packetid,
vol.Optional(ATTR_DATA_TYPE, default=None):
vol.In(list(DATA_TYPES.keys())),
})
def _valid_device(value):
"""Validate a dictionary of devices definitions."""
config = OrderedDict()
for key, device in value.items():
try:
key = rfxtrx.VALID_SENSOR_DEVICE_ID(key)
config[key] = DEVICE_SCHEMA(device)
if not config[key][ATTR_NAME]:
config[key][ATTR_NAME] = key
except vol.MultipleInvalid as ex:
raise vol.Invalid('Rfxtrx sensor {} is invalid: {}'
.format(key, ex))
return config
PLATFORM_SCHEMA = vol.Schema({
vol.Required("platform"): rfxtrx.DOMAIN,
vol.Required(CONF_DEVICES): vol.All(dict, _valid_device),
vol.Optional(CONF_DEVICES, default={}): vol.All(dict, rfxtrx.valid_sensor),
vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean,
}, extra=vol.ALLOW_EXTRA)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the RFXtrx platform."""
# pylint: disable=too-many-locals
from RFXtrx import SensorEvent
sensors = []
for device_id, entity_info in config['devices'].items():
for packet_id, entity_info in config['devices'].items():
event = rfxtrx.get_rfx_object(packet_id)
device_id = "sensor_" + slugify(event.device.id_string.lower())
if device_id in rfxtrx.RFX_DEVICES:
continue
_LOGGER.info("Add %s rfxtrx.sensor", entity_info[ATTR_NAME])
event = rfxtrx.get_rfx_object(entity_info[ATTR_PACKETID])
new_sensor = RfxtrxSensor(event, entity_info[ATTR_NAME],
entity_info[ATTR_DATA_TYPE])
rfxtrx.RFX_DEVICES[slugify(device_id)] = new_sensor
sensors.append(new_sensor)
sub_sensors = {}
data_types = entity_info[ATTR_DATA_TYPE]
if len(data_types) == 0:
for data_type in DATA_TYPES:
if data_type in event.values:
data_types = [data_type]
break
for _data_type in data_types:
new_sensor = RfxtrxSensor(event, entity_info[ATTR_NAME],
_data_type)
sensors.append(new_sensor)
sub_sensors[_data_type] = new_sensor
rfxtrx.RFX_DEVICES[device_id] = sub_sensors
add_devices_callback(sensors)
@ -84,27 +62,29 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
device_id = "sensor_" + slugify(event.device.id_string.lower())
if device_id in rfxtrx.RFX_DEVICES:
rfxtrx.RFX_DEVICES[device_id].event = event
k = 2
_device_id = device_id + "_" + str(k)
while _device_id in rfxtrx.RFX_DEVICES:
rfxtrx.RFX_DEVICES[_device_id].event = event
k = k + 1
_device_id = device_id + "_" + str(k)
sensors = rfxtrx.RFX_DEVICES[device_id]
for key in sensors:
sensors[key].event = event
return
# Add entity if not exist and the automatic_add is True
if config[ATTR_AUTOMATIC_ADD]:
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
entity_name = "%s : %s" % (device_id, pkt_id)
_LOGGER.info(
"Automatic add rfxtrx.sensor: (%s : %s)",
device_id,
pkt_id)
if not config[ATTR_AUTOMATIC_ADD]:
return
new_sensor = RfxtrxSensor(event, entity_name)
rfxtrx.RFX_DEVICES[device_id] = new_sensor
add_devices_callback([new_sensor])
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
_LOGGER.info("Automatic add rfxtrx.sensor: %s",
device_id)
data_type = "Unknown"
for _data_type in DATA_TYPES:
if _data_type in event.values:
data_type = _data_type
break
new_sensor = RfxtrxSensor(event, pkt_id, data_type)
sub_sensors = {}
sub_sensors[new_sensor.data_type] = new_sensor
rfxtrx.RFX_DEVICES[device_id] = sub_sensors
add_devices_callback([new_sensor])
if sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(sensor_update)
@ -113,21 +93,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class RfxtrxSensor(Entity):
"""Representation of a RFXtrx sensor."""
def __init__(self, event, name, data_type=None):
def __init__(self, event, name, data_type):
"""Initialize the sensor."""
self.event = event
self._unit_of_measurement = None
self._data_type = None
self._name = name
if data_type:
self._data_type = data_type
self._unit_of_measurement = DATA_TYPES[data_type]
return
for data_type in DATA_TYPES:
if data_type in self.event.values:
self._unit_of_measurement = DATA_TYPES[data_type]
self._data_type = data_type
break
if data_type not in DATA_TYPES:
data_type = "Unknown"
self.data_type = data_type
self._unit_of_measurement = DATA_TYPES[data_type]
def __str__(self):
"""Return the name of the sensor."""
@ -136,8 +109,8 @@ class RfxtrxSensor(Entity):
@property
def state(self):
"""Return the state of the sensor."""
if self._data_type:
return self.event.values[self._data_type]
if self.data_type:
return self.event.values[self.data_type]
return None
@property

View File

@ -47,7 +47,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
(product_id not in wanted_product_ids):
continue
dev.append(UberSensor('time', timeandpriceest, product_id, product))
if 'price_details' in product:
is_metered = (product['price_details']['estimate'] == "Metered")
if 'price_details' in product and is_metered is False:
dev.append(UberSensor('price', timeandpriceest,
product_id, product))
add_devices(dev)

View File

@ -65,6 +65,11 @@ class VerisureThermometer(Entity):
# Remove ° character
return hub.climate_status[self._id].temperature[:-1]
@property
def available(self):
"""Return True if entity is available."""
return hub.available
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
@ -95,6 +100,11 @@ class VerisureHygrometer(Entity):
# remove % character
return hub.climate_status[self._id].humidity[:-1]
@property
def available(self):
"""Return True if entity is available."""
return hub.available
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this sensor."""
@ -124,6 +134,11 @@ class VerisureMouseDetection(Entity):
"""Return the state of the sensor."""
return hub.mouse_status[self._id].count
@property
def available(self):
"""Return True if entity is available."""
return hub.available
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this sensor."""

View File

@ -7,10 +7,11 @@ at https://home-assistant.io/components/sensor.wink/
import logging
from homeassistant.const import (CONF_ACCESS_TOKEN, STATE_CLOSED,
STATE_OPEN, TEMP_CELSIUS)
STATE_OPEN, TEMP_CELSIUS,
ATTR_BATTERY_LEVEL)
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['python-wink==0.7.4']
REQUIREMENTS = ['python-wink==0.7.6']
SENSOR_TYPES = ['temperature', 'humidity']
@ -44,6 +45,7 @@ class WinkSensorDevice(Entity):
"""Initialize the sensor."""
self.wink = wink
self.capability = self.wink.capability()
self._battery = self.wink.battery_level
if self.wink.UNIT == "°":
self._unit_of_measurement = TEMP_CELSIUS
else:
@ -88,6 +90,19 @@ class WinkSensorDevice(Entity):
"""Return true if door is open."""
return self.wink.state()
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100
class WinkEggMinder(Entity):
"""Representation of a Wink Egg Minder."""
@ -95,6 +110,7 @@ class WinkEggMinder(Entity):
def __init__(self, wink):
"""Initialize the sensor."""
self.wink = wink
self._battery = self.wink.battery_level
@property
def state(self):
@ -114,3 +130,16 @@ class WinkEggMinder(Entity):
def update(self):
"""Update state of the Egg Minder."""
self.wink.update_state()
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100

View File

@ -111,7 +111,7 @@ class YrSensor(Entity):
"""Weather symbol if type is symbol."""
if self.type != 'symbol':
return None
return "http://api.met.no/weatherapi/weathericon/1.1/" \
return "//api.met.no/weatherapi/weathericon/1.1/" \
"?symbol={0};content_type=image/png".format(self._state)
@property

View File

@ -0,0 +1,145 @@
"""
Use serial protocol of acer projector to obtain state of the projector.
This component allows to control almost all projectors from acer using
their RS232 serial communication protocol.
"""
import logging
import re
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN,
CONF_NAME, CONF_FILENAME)
LAMP_HOURS = 'Lamp Hours'
INPUT_SOURCE = 'Input Source'
ECO_MODE = 'ECO Mode'
MODEL = 'Model'
LAMP = 'Lamp'
# Commands known to the projector
CMD_DICT = {LAMP: '* 0 Lamp ?\r',
LAMP_HOURS: '* 0 Lamp\r',
INPUT_SOURCE: '* 0 Src ?\r',
ECO_MODE: '* 0 IR 052\r',
MODEL: '* 0 IR 035\r',
STATE_ON: '* 0 IR 001\r',
STATE_OFF: '* 0 IR 002\r'}
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyserial<=3.0']
ICON = 'mdi:projector'
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Connect with serial port and return Acer Projector."""
serial_port = config.get(CONF_FILENAME, None)
name = config.get(CONF_NAME, 'Projector')
timeout = config.get('timeout', 1)
write_timeout = config.get('write_timeout', 1)
if not serial_port:
_LOGGER.error('Missing path of serial device')
return
devices = []
devices.append(AcerSwitch(serial_port, name, timeout, write_timeout))
add_devices_callback(devices)
class AcerSwitch(SwitchDevice):
"""Represents an Acer Projector as an switch."""
def __init__(self, serial_port, name='Projector',
timeout=1, write_timeout=1, **kwargs):
"""Init of the Acer projector."""
import serial
self.ser = serial.Serial(port=serial_port, timeout=timeout,
write_timeout=write_timeout, **kwargs)
self._serial_port = serial_port
self._name = name
self._state = STATE_UNKNOWN
self._attributes = {
LAMP_HOURS: STATE_UNKNOWN,
INPUT_SOURCE: STATE_UNKNOWN,
ECO_MODE: STATE_UNKNOWN,
}
self.update()
def _write_read(self, msg):
"""Write to the projector and read the return."""
import serial
ret = ""
# Sometimes the projector won't answer for no reason,
# or the projector was disconnected during runtime.
# Thisway the projector can be reconnected and will still
# work
try:
if not self.ser.is_open:
self.ser.open()
msg = msg.encode('utf-8')
self.ser.write(msg)
# size is an experience value there is no real limit.
# AFAIK there is no limit and no end character so
# we will usually need to wait for timeout
ret = self.ser.read_until(size=20).decode('utf-8')
except serial.SerialException:
_LOGGER.error('Problem comunicating with %s', self._serial_port)
self.ser.close()
return ret
def _write_read_format(self, msg):
"""Write msg, obtain awnser and format output."""
# awnsers are formated as ***\rawnser\r***
awns = self._write_read(msg)
match = re.search(r'\r(.+)\r', awns)
if match:
return match.group(1)
return STATE_UNKNOWN
@property
def name(self):
"""Return name of the projector."""
return self._name
@property
def state(self):
"""Return the current state of the projector."""
return self._state
@property
def state_attributes(self):
"""Return state attributes."""
return self._attributes
def update(self):
"""Get the latest state from the projector."""
msg = CMD_DICT[LAMP]
awns = self._write_read_format(msg)
if awns == 'Lamp 1':
self._state = STATE_ON
elif awns == 'Lamp 0':
self._state = STATE_OFF
else:
self._state = STATE_UNKNOWN
for key in self._attributes.keys():
msg = CMD_DICT.get(key, None)
if msg:
awns = self._write_read_format(msg)
self._attributes[key] = awns
def turn_on(self):
"""Turn the projector on."""
msg = CMD_DICT[STATE_ON]
self._write_read(msg)
self._state = STATE_ON
def turn_off(self):
"""Turn the projector off."""
msg = CMD_DICT[STATE_OFF]
self._write_read(msg)
self._state = STATE_OFF

View File

@ -2,7 +2,7 @@
Support for custom shell commands to turn a switch on/off.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.command_switch/
https://home-assistant.io/components/switch.command_line/
"""
import logging
import subprocess

View File

@ -6,9 +6,9 @@ https://home-assistant.io/components/switch.mysensors/
"""
import logging
from homeassistant.components import mysensors
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON
from homeassistant.loader import get_component
from homeassistant.const import STATE_OFF, STATE_ON
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = []
@ -21,8 +21,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None:
return
mysensors = get_component('mysensors')
for gateway in mysensors.GATEWAYS.values():
# Define the S_TYPES and V_TYPES that the platform should handle as
# states. Map them in a dict of lists.
@ -51,77 +49,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
map_sv_types, devices, add_devices, MySensorsSwitch))
class MySensorsSwitch(SwitchDevice):
"""Representation of the value of a MySensors child node."""
# pylint: disable=too-many-arguments,too-many-instance-attributes
def __init__(
self, gateway, node_id, child_id, name, value_type, child_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.
child_type (str): Child type of child.
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.
mysensors (module): Mysensors main component module.
"""
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 = {}
self.mysensors = get_component('mysensors')
@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."""
address = getattr(self.gateway, 'server_address', None)
if address:
device = '{}:{}'.format(address[0], address[1])
else:
device = self.gateway.port
attr = {
self.mysensors.ATTR_DEVICE: device,
self.mysensors.ATTR_NODE_ID: self.node_id,
self.mysensors.ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level,
}
set_req = self.gateway.const.SetReq
for value_type, value in self._values.items():
if value_type != self.value_type:
try:
attr[set_req(value_type).name] = value
except ValueError:
_LOGGER.error('value_type %s is not valid for mysensors '
'version %s', value_type,
self.gateway.version)
return attr
class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice):
"""Representation of the value of a MySensors Switch child node."""
@property
def is_on(self):
@ -148,28 +77,7 @@ class MySensorsSwitch(SwitchDevice):
self._values[self.value_type] = STATE_OFF
self.update_ha_state()
@property
def available(self):
"""Return True if entity is available."""
return self.value_type in self._values
@property
def assumed_state(self):
"""Return True if unable to access real state of entity."""
return self.gateway.optimistic
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.debug(
"%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_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

View File

@ -7,21 +7,29 @@ https://home-assistant.io/components/switch.pulseaudio_loopback/
import logging
import re
import socket
from datetime import timedelta
import homeassistant.util as util
from homeassistant.components.switch import SwitchDevice
from homeassistant.util import convert
_LOGGER = logging.getLogger(__name__)
_PULSEAUDIO_SERVERS = {}
DEFAULT_NAME = "paloopback"
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 4712
DEFAULT_BUFFER_SIZE = 1024
DEFAULT_TCP_TIMEOUT = 3
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
LOAD_CMD = "load-module module-loopback sink={0} source={1}"
UNLOAD_CMD = "unload-module {0}"
MOD_REGEX = r"index: ([0-9]+)\s+name: <module-loopback>" \
r"\s+argument: <sink={0} source={1}>"
r"\s+argument: (?=<.*sink={0}.*>)(?=<.*source={1}.*>)"
IGNORED_SWITCH_WARN = "Switch is already in the desired state. Ignoring."
# pylint: disable=unused-argument
@ -35,45 +43,45 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
_LOGGER.error("Missing required variable: source_name")
return False
name = convert(config.get('name'), str, DEFAULT_NAME)
sink_name = config.get('sink_name')
source_name = config.get('source_name')
host = convert(config.get('host'), str, DEFAULT_HOST)
port = convert(config.get('port'), int, DEFAULT_PORT)
buffer_size = convert(config.get('buffer_size'), int, DEFAULT_BUFFER_SIZE)
tcp_timeout = convert(config.get('tcp_timeout'), int, DEFAULT_TCP_TIMEOUT)
server_id = str.format("{0}:{1}", host, port)
if server_id in _PULSEAUDIO_SERVERS:
server = _PULSEAUDIO_SERVERS[server_id]
else:
server = PAServer(host, port, buffer_size, tcp_timeout)
_PULSEAUDIO_SERVERS[server_id] = server
add_devices_callback([PALoopbackSwitch(
hass,
convert(config.get('name'), str, DEFAULT_NAME),
convert(config.get('host'), str, DEFAULT_HOST),
convert(config.get('port'), int, DEFAULT_PORT),
convert(config.get('buffer_size'), int, DEFAULT_BUFFER_SIZE),
convert(config.get('tcp_timeout'), int, DEFAULT_TCP_TIMEOUT),
config.get('sink_name'),
config.get('source_name')
name,
server,
sink_name,
source_name
)])
# pylint: disable=too-many-arguments, too-many-instance-attributes
class PALoopbackSwitch(SwitchDevice):
"""Represents the presence or absence of a pa loopback module."""
class PAServer():
"""Represents a pulseaudio server."""
def __init__(self, hass, name, pa_host, pa_port, buff_sz,
tcp_timeout, sink_name, source_name):
"""Initialize the switch."""
self._module_idx = -1
self._hass = hass
self._name = name
self._pa_host = pa_host
self._pa_port = int(pa_port)
self._sink_name = sink_name
self._source_name = source_name
_current_module_state = ""
def __init__(self, host, port, buff_sz, tcp_timeout):
"""Simple constructor for reading in our configuration."""
self._pa_host = host
self._pa_port = int(port)
self._buffer_size = int(buff_sz)
self._tcp_timeout = int(tcp_timeout)
@property
def name(self):
"""Return the name of the switch."""
return self._name
@property
def is_on(self):
"""Tell the core logic if device is on."""
return self._module_idx > 0
def _send_command(self, cmd, response_expected):
"""Send a command to the pa server using a socket."""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@ -103,29 +111,82 @@ class PALoopbackSwitch(SwitchDevice):
return result
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_module_state(self):
"""Refresh state in case an alternate process modified this data."""
self._current_module_state = self._send_command("list-modules", True)
def turn_on(self, sink_name, source_name):
"""Send a command to pulseaudio to turn on the loopback."""
self._send_command(str.format(LOAD_CMD,
sink_name,
source_name),
False)
def turn_off(self, module_idx):
"""Send a command to pulseaudio to turn off the loopback."""
self._send_command(str.format(UNLOAD_CMD, module_idx), False)
def get_module_idx(self, sink_name, source_name):
"""For a sink/source, return it's module id in our cache, if found."""
result = re.search(str.format(MOD_REGEX,
re.escape(sink_name),
re.escape(source_name)),
self._current_module_state)
if result and result.group(1).isdigit():
return int(result.group(1))
else:
return -1
# pylint: disable=too-many-arguments
class PALoopbackSwitch(SwitchDevice):
"""Represents the presence or absence of a pa loopback module."""
def __init__(self, hass, name, pa_server,
sink_name, source_name):
"""Initialize the switch."""
self._module_idx = -1
self._hass = hass
self._name = name
self._sink_name = sink_name
self._source_name = source_name
self._pa_svr = pa_server
@property
def name(self):
"""Return the name of the switch."""
return self._name
@property
def is_on(self):
"""Tell the core logic if device is on."""
return self._module_idx > 0
def turn_on(self, **kwargs):
"""Turn the device on."""
self._send_command(str.format(LOAD_CMD,
self._sink_name,
self._source_name),
False)
self.update()
self.update_ha_state()
if not self.is_on:
self._pa_svr.turn_on(self._sink_name, self._source_name)
self._pa_svr.update_module_state(no_throttle=True)
self._module_idx = self._pa_svr.get_module_idx(self._sink_name,
self._source_name)
self.update_ha_state()
else:
_LOGGER.warning(IGNORED_SWITCH_WARN)
def turn_off(self, **kwargs):
"""Turn the device off."""
self._send_command(str.format(UNLOAD_CMD, self._module_idx), False)
self.update()
self.update_ha_state()
if self.is_on:
self._pa_svr.turn_off(self._module_idx)
self._pa_svr.update_module_state(no_throttle=True)
self._module_idx = self._pa_svr.get_module_idx(self._sink_name,
self._source_name)
self.update_ha_state()
else:
_LOGGER.warning(IGNORED_SWITCH_WARN)
def update(self):
"""Refresh state in case an alternate process modified this data."""
return_data = self._send_command("list-modules", True)
result = re.search(str.format(MOD_REGEX,
re.escape(self._sink_name),
re.escape(self._source_name)),
return_data)
if result and result.group(1).isdigit():
self._module_idx = int(result.group(1))
else:
self._module_idx = -1
self._pa_svr.update_module_state()
self._module_idx = self._pa_svr.get_module_idx(self._sink_name,
self._source_name)

View File

@ -0,0 +1,103 @@
"""
Allows to configure a switch using a 433MHz module via GPIO on a Raspberry Pi.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.rpi_rf/
"""
import logging
from homeassistant.components.switch import SwitchDevice
REQUIREMENTS = ['rpi-rf==0.9.5']
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Find and return switches controlled by a generic RF device via GPIO."""
import rpi_rf
gpio = config.get('gpio')
if not gpio:
_LOGGER.error("No GPIO specified")
return False
rfdevice = rpi_rf.RFDevice(gpio)
switches = config.get('switches', {})
devices = []
for dev_name, properties in switches.items():
if not properties.get('code_on'):
_LOGGER.error("%s: code_on not specified", dev_name)
continue
if not properties.get('code_off'):
_LOGGER.error("%s: code_off not specified", dev_name)
continue
devices.append(
RPiRFSwitch(
hass,
properties.get('name', dev_name),
rfdevice,
properties.get('protocol', None),
properties.get('pulselength', None),
properties.get('code_on'),
properties.get('code_off')))
if devices:
rfdevice.enable_tx()
add_devices_callback(devices)
class RPiRFSwitch(SwitchDevice):
"""Representation of a GPIO RF switch."""
# pylint: disable=too-many-arguments, too-many-instance-attributes
def __init__(self, hass, name, rfdevice, protocol, pulselength,
code_on, code_off):
"""Initialize the switch."""
self._hass = hass
self._name = name
self._state = False
self._rfdevice = rfdevice
self._protocol = protocol
self._pulselength = pulselength
self._code_on = code_on
self._code_off = code_off
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the switch."""
return self._name
@property
def is_on(self):
"""Return true if device is on."""
return self._state
def _send_code(self, code, protocol, pulselength):
"""Send the code with a specified pulselength."""
_LOGGER.info('Sending code: %s', code)
res = self._rfdevice.tx_code(code, protocol, pulselength)
if not res:
_LOGGER.error('Sending code %s failed', code)
return res
def turn_on(self):
"""Turn the switch on."""
if self._send_code(self._code_on, self._protocol, self._pulselength):
self._state = True
self.update_ha_state()
def turn_off(self):
"""Turn the switch off."""
if self._send_code(self._code_off, self._protocol, self._pulselength):
self._state = False
self.update_ha_state()

View File

@ -4,11 +4,15 @@ Support for Tellstick switches.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.tellstick/
"""
import voluptuous as vol
from homeassistant.components import tellstick
from homeassistant.components.tellstick import (ATTR_DISCOVER_DEVICES,
ATTR_DISCOVER_CONFIG)
from homeassistant.helpers.entity import ToggleEntity
PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): tellstick.DOMAIN})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -20,7 +20,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hub.update_smartplugs()
switches = []
switches.extend([
VerisureSmartplug(value.id)
VerisureSmartplug(value.deviceLabel)
for value in hub.smartplug_status.values()])
add_devices(switches)
@ -42,6 +42,11 @@ class VerisureSmartplug(SwitchDevice):
"""Return true if on."""
return hub.smartplug_status[self._id].status == 'on'
@property
def available(self):
"""Return True if entity is available."""
return hub.available
def turn_on(self):
"""Set smartplug status on."""
hub.my_pages.smartplug.set(self._id, 'on')

View File

@ -9,7 +9,7 @@ import logging
from homeassistant.components.wink import WinkToggleDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.4']
REQUIREMENTS = ['python-wink==0.7.6']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/Tellstick/
"""
import logging
import threading
import voluptuous as vol
from homeassistant import bootstrap
from homeassistant.const import (
@ -39,6 +40,14 @@ TELLSTICK_LOCK = threading.Lock()
# Used from entities that register callback listeners
TELLCORE_REGISTRY = None
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(ATTR_SIGNAL_REPETITIONS,
default=DEFAULT_SIGNAL_REPETITIONS):
vol.Coerce(int),
}),
}, extra=vol.ALLOW_EXTRA)
def _discover(hass, config, found_devices, component_name):
"""Setup and send the discovery event."""
@ -52,8 +61,7 @@ def _discover(hass, config, found_devices, component_name):
bootstrap.setup_component(hass, component.DOMAIN,
config)
signal_repetitions = config[DOMAIN].get(
ATTR_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS)
signal_repetitions = config[DOMAIN].get(ATTR_SIGNAL_REPETITIONS)
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
{ATTR_SERVICE: DISCOVERY_TYPES[component_name],

View File

@ -5,7 +5,9 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/thermostat.heat_control/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.util as util
from homeassistant.components import switch
from homeassistant.components.thermostat import (
@ -28,21 +30,26 @@ CONF_TARGET_TEMP = 'target_temp'
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = vol.Schema({
vol.Required("platform"): "heat_control",
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HEATER): cv.entity_id,
vol.Required(CONF_SENSOR): cv.entity_id,
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the heat control thermostat."""
name = config.get(CONF_NAME, DEFAULT_NAME)
name = config.get(CONF_NAME)
heater_entity_id = config.get(CONF_HEATER)
sensor_entity_id = config.get(CONF_SENSOR)
min_temp = util.convert(config.get(CONF_MIN_TEMP), float, None)
max_temp = util.convert(config.get(CONF_MAX_TEMP), float, None)
target_temp = util.convert(config.get(CONF_TARGET_TEMP), float, None)
if None in (heater_entity_id, sensor_entity_id):
_LOGGER.error('Missing required key %s or %s', CONF_HEATER,
CONF_SENSOR)
return False
min_temp = config.get(CONF_MIN_TEMP)
max_temp = config.get(CONF_MAX_TEMP)
target_temp = config.get(CONF_TARGET_TEMP)
add_devices([HeatControl(hass, name, heater_entity_id, sensor_entity_id,
min_temp, max_temp, target_temp)])

View File

@ -11,7 +11,7 @@ from homeassistant.components.thermostat import ThermostatDevice
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT)
REQUIREMENTS = ['evohomeclient==0.2.4',
REQUIREMENTS = ['evohomeclient==0.2.5',
'somecomfort==0.2.1']
_LOGGER = logging.getLogger(__name__)

View File

@ -2,6 +2,7 @@
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import logging
from homeassistant.components.thermostat import DOMAIN
from homeassistant.components.thermostat import (
ThermostatDevice,
@ -9,19 +10,46 @@ from homeassistant.components.thermostat import (
from homeassistant.components import zwave
from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS
_LOGGER = logging.getLogger(__name__)
CONF_NAME = 'name'
DEFAULT_NAME = 'ZWave Thermostat'
REMOTEC = 0x5254
REMOTEC_ZXT_120 = 0x8377
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0)
WORKAROUND_IGNORE = 'ignore'
DEVICE_MAPPINGS = {
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_IGNORE
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the ZWave thermostats."""
if discovery_info is None or zwave.NETWORK is None:
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
discovery_info, zwave.NETWORK)
return
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
value.set_change_verified(False)
add_devices([ZWaveThermostat(value)])
# Make sure that we have values for the key before converting to int
if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16),
int(value.node.product_id, 16),
value.index)
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_IGNORE:
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat, ignoring")
return
else:
add_devices([ZWaveThermostat(value)])
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
discovery_info, zwave.NETWORK)
# pylint: disable=too-many-arguments

View File

@ -23,7 +23,7 @@ DISCOVER_SWITCHES = 'verisure.switches'
DISCOVER_ALARMS = 'verisure.alarm_control_panel'
DISCOVER_LOCKS = 'verisure.lock'
REQUIREMENTS = ['vsure==0.7.1']
REQUIREMENTS = ['vsure==0.8.1']
_LOGGER = logging.getLogger(__name__)
@ -77,7 +77,7 @@ class VerisureHub(object):
# "wrong password" message. We will continue to retry after maintenance
# regardless of that error.
self._disable_wrong_password_error = False
self._wrong_password_given = False
self._password_retries = 1
self._reconnect_timeout = time.time()
self.my_pages = verisure.MyPages(
@ -128,11 +128,13 @@ class VerisureHub(object):
self.my_pages.smartplug.get,
self.smartplug_status)
@property
def available(self):
"""Return True if hub is available."""
return self._password_retries >= 0
def update_component(self, get_function, status):
"""Update the status of Verisure components."""
if self._wrong_password_given:
_LOGGER.error('Wrong password for Verisure, update config')
return
try:
for overview in get_function():
try:
@ -145,25 +147,26 @@ class VerisureHub(object):
def reconnect(self):
"""Reconnect to Verisure MyPages."""
if self._reconnect_timeout > time.time():
return
if not self._lock.acquire(blocking=False):
if (self._reconnect_timeout > time.time() or
not self._lock.acquire(blocking=False) or
self._password_retries < 0):
return
try:
self.my_pages.login()
self._disable_wrong_password_error = False
self._password_retries = 1
except self._verisure.LoginError as ex:
_LOGGER.error("Wrong user name or password for Verisure MyPages")
if self._disable_wrong_password_error:
self._reconnect_timeout = time.time() + 60
self._reconnect_timeout = time.time() + 60*60
else:
self._wrong_password_given = True
self._password_retries = self._password_retries - 1
except self._verisure.MaintenanceError:
self._disable_wrong_password_error = True
self._reconnect_timeout = time.time() + 60
self._reconnect_timeout = time.time() + 60*60
_LOGGER.error("Verisure MyPages down for maintenance")
except self._verisure.Error as ex:
_LOGGER.error("Could not login to Verisure MyPages, %s", ex)
self._reconnect_timeout = time.time() + 5
self._reconnect_timeout = time.time() + 60
finally:
self._lock.release()

View File

@ -9,13 +9,13 @@ import logging
from homeassistant import bootstrap
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, CONF_ACCESS_TOKEN,
EVENT_PLATFORM_DISCOVERED)
EVENT_PLATFORM_DISCOVERED, ATTR_BATTERY_LEVEL)
from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.loader import get_component
DOMAIN = "wink"
REQUIREMENTS = ['python-wink==0.7.4']
REQUIREMENTS = ['python-wink==0.7.6']
DISCOVER_LIGHTS = "wink.lights"
DISCOVER_SWITCHES = "wink.switches"
@ -68,6 +68,7 @@ class WinkToggleDevice(ToggleEntity):
def __init__(self, wink):
"""Initialize the Wink device."""
self.wink = wink
self._battery = self.wink.battery_level
@property
def unique_id(self):
@ -100,3 +101,16 @@ class WinkToggleDevice(ToggleEntity):
def update(self):
"""Update state of the device."""
self.wink.update_state()
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100

View File

@ -5,14 +5,18 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zigbee/
"""
import logging
import pickle
from binascii import hexlify, unhexlify
from base64 import b64encode, b64decode
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import JobPriority
from homeassistant.helpers.entity import Entity
DOMAIN = "zigbee"
REQUIREMENTS = ("xbee-helper==0.0.6",)
REQUIREMENTS = ("xbee-helper==0.0.7",)
EVENT_ZIGBEE_FRAME_RECEIVED = "zigbee_frame_received"
CONF_DEVICE = "device"
CONF_BAUD = "baud"
@ -25,9 +29,14 @@ DEFAULT_ADC_MAX_VOLTS = 1.2
GPIO_DIGITAL_OUTPUT_LOW = None
GPIO_DIGITAL_OUTPUT_HIGH = None
ADC_PERCENTAGE = None
DIGITAL_PINS = None
ANALOG_PINS = None
CONVERT_ADC = None
ZIGBEE_EXCEPTION = None
ZIGBEE_TX_FAILURE = None
ATTR_FRAME = "frame"
DEVICE = None
_LOGGER = logging.getLogger(__name__)
@ -39,17 +48,24 @@ def setup(hass, config):
global GPIO_DIGITAL_OUTPUT_LOW
global GPIO_DIGITAL_OUTPUT_HIGH
global ADC_PERCENTAGE
global DIGITAL_PINS
global ANALOG_PINS
global CONVERT_ADC
global ZIGBEE_EXCEPTION
global ZIGBEE_TX_FAILURE
import xbee_helper.const as xb_const
from xbee_helper import ZigBee
from xbee_helper.device import convert_adc
from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure
from serial import Serial, SerialException
GPIO_DIGITAL_OUTPUT_LOW = xb_const.GPIO_DIGITAL_OUTPUT_LOW
GPIO_DIGITAL_OUTPUT_HIGH = xb_const.GPIO_DIGITAL_OUTPUT_HIGH
ADC_PERCENTAGE = xb_const.ADC_PERCENTAGE
DIGITAL_PINS = xb_const.DIGITAL_PINS
ANALOG_PINS = xb_const.ANALOG_PINS
CONVERT_ADC = convert_adc
ZIGBEE_EXCEPTION = ZigBeeException
ZIGBEE_TX_FAILURE = ZigBeeTxFailure
@ -62,6 +78,19 @@ def setup(hass, config):
return False
DEVICE = ZigBee(ser)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_serial_port)
def _frame_received(frame):
"""Called when a ZigBee frame is received.
Pickles the frame, then encodes it into base64 since it contains
non JSON serializable binary.
"""
hass.bus.fire(
EVENT_ZIGBEE_FRAME_RECEIVED,
{ATTR_FRAME: b64encode(pickle.dumps(frame)).decode("ascii")})
DEVICE.add_frame_rx_handler(_frame_received)
return True
@ -70,6 +99,25 @@ def close_serial_port(*args):
DEVICE.zb.serial.close()
def frame_is_relevant(entity, frame):
"""Test whether the frame is relevant to the entity."""
if frame.get("source_addr_long") != entity.config.address:
return False
if "samples" not in frame:
return False
return True
def subscribe(hass, callback):
"""Subscribe to incoming ZigBee frames."""
def zigbee_frame_subscriber(event):
"""Decode and unpickle the frame from the event bus, and call back."""
frame = pickle.loads(b64decode(event.data[ATTR_FRAME]))
callback(frame)
hass.bus.listen(EVENT_ZIGBEE_FRAME_RECEIVED, zigbee_frame_subscriber)
class ZigBeeConfig(object):
"""Handle the fetching of configuration from the config file."""
@ -110,14 +158,65 @@ class ZigBeePinConfig(ZigBeeConfig):
return self._config["pin"]
class ZigBeeDigitalPinConfig(ZigBeePinConfig):
"""Handle the fetching of configuration from the config file."""
class ZigBeeDigitalInConfig(ZigBeePinConfig):
"""A subclass of ZigBeePinConfig."""
def __init__(self, config):
"""Initialize the configuration."""
super(ZigBeeDigitalPinConfig, self).__init__(config)
"""Initialise the ZigBee Digital input config."""
super(ZigBeeDigitalInConfig, self).__init__(config)
self._bool2state, self._state2bool = self.boolean_maps
@property
def boolean_maps(self):
"""Create mapping dictionaries for potential inversion of booleans.
Create dicts to map the pin state (true/false) to potentially inverted
values depending on the on_state config value which should be set to
"low" or "high".
"""
if self._config.get("on_state", "").lower() == "low":
bool2state = {
True: False,
False: True
}
else:
bool2state = {
True: True,
False: False
}
state2bool = {v: k for k, v in bool2state.items()}
return bool2state, state2bool
@property
def bool2state(self):
"""A dictionary mapping the internal value to the ZigBee value.
For the translation of on/off as being pin high or low.
"""
return self._bool2state
@property
def state2bool(self):
"""A dictionary mapping the ZigBee value to the internal value.
For the translation of pin high/low as being on or off.
"""
return self._state2bool
class ZigBeeDigitalOutConfig(ZigBeePinConfig):
"""A subclass of ZigBeePinConfig.
Set _should_poll to default as False instead of True. The value will
still be overridden by the presence of a 'poll' config entry.
"""
def __init__(self, config):
"""Initialize the ZigBee Digital out."""
super(ZigBeeDigitalOutConfig, self).__init__(config)
self._bool2state, self._state2bool = self.boolean_maps
self._should_poll = config.get("poll", False)
@property
def boolean_maps(self):
"""Create dicts to map booleans to pin high/low and vice versa.
@ -154,22 +253,6 @@ class ZigBeeDigitalPinConfig(ZigBeePinConfig):
"""
return self._state2bool
# Create an alias so that ZigBeeDigitalOutConfig has a logical opposite.
ZigBeeDigitalInConfig = ZigBeeDigitalPinConfig
class ZigBeeDigitalOutConfig(ZigBeeDigitalPinConfig):
"""A subclass of ZigBeeDigitalPinConfig.
Set _should_poll to default as False instead of True. The value will
still be overridden by the presence of a 'poll' config entry.
"""
def __init__(self, config):
"""Initialize the ZigBee Digital out."""
super(ZigBeeDigitalOutConfig, self).__init__(config)
self._should_poll = config.get("poll", False)
class ZigBeeAnalogInConfig(ZigBeePinConfig):
"""Representation of a ZigBee GPIO pin set to analog in."""
@ -187,6 +270,25 @@ class ZigBeeDigitalIn(Entity):
"""Initialize the device."""
self._config = config
self._state = False
def handle_frame(frame):
"""Handle an incoming frame.
Handle an incoming frame and update our status if it contains
information relating to this device.
"""
if not frame_is_relevant(self, frame):
return
sample = frame["samples"].pop()
pin_name = DIGITAL_PINS[self._config.pin]
if pin_name not in sample:
# Doesn't contain information about our pin
return
self._state = self._config.state2bool[sample[pin_name]]
self.update_ha_state()
subscribe(hass, handle_frame)
# Get initial state
hass.pool.add_job(
JobPriority.EVENT_STATE, (self.update_ha_state, True))
@ -196,6 +298,11 @@ class ZigBeeDigitalIn(Entity):
"""Return the name of the input."""
return self._config.name
@property
def config(self):
"""The entity's configuration."""
return self._config
@property
def should_poll(self):
"""Return the state of the polling, if needed."""
@ -207,11 +314,9 @@ class ZigBeeDigitalIn(Entity):
return self._state
def update(self):
"""Ask the ZigBee device what its output is set to."""
"""Ask the ZigBee device what state its input pin is in."""
try:
pin_state = DEVICE.get_gpio_pin(
self._config.pin,
self._config.address)
sample = DEVICE.get_sample(self._config.address)
except ZIGBEE_TX_FAILURE:
_LOGGER.warning(
"Transmission failure when attempting to get sample from "
@ -221,7 +326,14 @@ class ZigBeeDigitalIn(Entity):
_LOGGER.exception(
"Unable to get sample from ZigBee device: %s", exc)
return
self._state = self._config.state2bool[pin_state]
pin_name = DIGITAL_PINS[self._config.pin]
if pin_name not in sample:
_LOGGER.warning(
"Pin %s (%s) was not in the sample provided by ZigBee device "
"%s.",
self._config.pin, pin_name, hexlify(self._config.address))
return
self._state = self._config.state2bool[sample[pin_name]]
class ZigBeeDigitalOut(ZigBeeDigitalIn):
@ -255,6 +367,24 @@ class ZigBeeDigitalOut(ZigBeeDigitalIn):
"""Set the digital output to its 'off' state."""
self._set_state(False)
def update(self):
"""Ask the ZigBee device what its output is set to."""
try:
pin_state = DEVICE.get_gpio_pin(
self._config.pin,
self._config.address)
except ZIGBEE_TX_FAILURE:
_LOGGER.warning(
"Transmission failure when attempting to get output pin status"
" from ZigBee device at address: %s",
hexlify(self._config.address))
return
except ZIGBEE_EXCEPTION as exc:
_LOGGER.exception(
"Unable to get output pin status from ZigBee device: %s", exc)
return
self._state = self._config.state2bool[pin_state]
class ZigBeeAnalogIn(Entity):
"""Representation of a GPIO pin configured as an analog input."""
@ -263,6 +393,29 @@ class ZigBeeAnalogIn(Entity):
"""Initialize the ZigBee analog in device."""
self._config = config
self._value = None
def handle_frame(frame):
"""Handle an incoming frame.
Handle an incoming frame and update our status if it contains
information relating to this device.
"""
if not frame_is_relevant(self, frame):
return
sample = frame["samples"].pop()
pin_name = ANALOG_PINS[self._config.pin]
if pin_name not in sample:
# Doesn't contain information about our pin
return
self._value = CONVERT_ADC(
sample[pin_name],
ADC_PERCENTAGE,
self._config.max_voltage
)
self.update_ha_state()
subscribe(hass, handle_frame)
# Get initial state
hass.pool.add_job(
JobPriority.EVENT_STATE, (self.update_ha_state, True))
@ -272,6 +425,11 @@ class ZigBeeAnalogIn(Entity):
"""The name of the input."""
return self._config.name
@property
def config(self):
"""The entity's configuration."""
return self._config
@property
def should_poll(self):
"""The state of the polling, if needed."""

Some files were not shown because too many files have changed in this diff Show More