mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
commit
6b1f9a32dd
12
.coveragerc
12
.coveragerc
@ -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
|
||||
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -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
1
.gitignore
vendored
@ -78,6 +78,7 @@ nosetests.xml
|
||||
pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
venv
|
||||
.venv
|
||||
|
||||
# vimmy stuff
|
||||
*.swp
|
||||
|
12
README.rst
12
README.rst
@ -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/
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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')
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
109
homeassistant/components/binary_sensor/octoprint.py
Normal file
109
homeassistant/components/binary_sensor/octoprint.py
Normal 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)
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
73
homeassistant/components/dweet.py
Normal file
73
homeassistant/components/dweet.py
Normal 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)
|
@ -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):
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
|
||||
VERSION = "af8a531f1c2e477c07c4b3394bd1ce13"
|
||||
VERSION = "1baebe8155deb447230866d7ae854bd9"
|
||||
|
@ -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
@ -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
@ -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
|
||||
|
@ -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':
|
||||
|
491
homeassistant/components/hvac/__init__.py
Normal file
491
homeassistant/components/hvac/__init__.py
Normal 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)
|
164
homeassistant/components/hvac/demo.py
Normal file
164
homeassistant/components/hvac/demo.py
Normal 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()
|
84
homeassistant/components/hvac/services.yaml
Normal file
84
homeassistant/components/hvac/services.yaml
Normal 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
|
228
homeassistant/components/hvac/zwave.py
Normal file
228
homeassistant/components/hvac/zwave.py
Normal 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)
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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({
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
64
homeassistant/components/lock/zwave.py
Normal file
64
homeassistant/components/lock/zwave.py
Normal 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
|
@ -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."""
|
||||
|
@ -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()
|
||||
|
199
homeassistant/components/media_player/pioneer.py
Normal file
199
homeassistant/components/media_player/pioneer.py
Normal 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")
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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']
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
|
@ -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.')
|
||||
|
120
homeassistant/components/octoprint.py
Normal file
120
homeassistant/components/octoprint.py
Normal 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]
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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']
|
||||
|
@ -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")
|
||||
|
372
homeassistant/components/sensor/fitbit.py
Normal file
372
homeassistant/components/sensor/fitbit.py
Normal 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")
|
@ -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']
|
||||
|
117
homeassistant/components/sensor/google_travel_time.py
Normal file
117
homeassistant/components/sensor/google_travel_time.py
Normal 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")
|
@ -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
|
||||
|
116
homeassistant/components/sensor/octoprint.py
Normal file
116
homeassistant/components/sensor/octoprint.py
Normal 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
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
145
homeassistant/components/switch/acer_projector.py
Normal file
145
homeassistant/components/switch/acer_projector.py
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
103
homeassistant/components/switch/rpi_rf.py
Normal file
103
homeassistant/components/switch/rpi_rf.py
Normal 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()
|
@ -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):
|
||||
|
@ -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')
|
||||
|
@ -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):
|
||||
|
@ -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],
|
||||
|
@ -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)])
|
||||
|
@ -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__)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user