mirror of
https://github.com/home-assistant/core.git
synced 2025-07-29 08:07:45 +00:00
commit
6b1f9a32dd
12
.coveragerc
12
.coveragerc
@ -14,6 +14,9 @@ omit =
|
|||||||
homeassistant/components/bloomsky.py
|
homeassistant/components/bloomsky.py
|
||||||
homeassistant/components/*/bloomsky.py
|
homeassistant/components/*/bloomsky.py
|
||||||
|
|
||||||
|
homeassistant/components/dweet.py
|
||||||
|
homeassistant/components/*/dweet.py
|
||||||
|
|
||||||
homeassistant/components/ecobee.py
|
homeassistant/components/ecobee.py
|
||||||
homeassistant/components/*/ecobee.py
|
homeassistant/components/*/ecobee.py
|
||||||
|
|
||||||
@ -32,6 +35,9 @@ omit =
|
|||||||
homeassistant/components/nest.py
|
homeassistant/components/nest.py
|
||||||
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
|
||||||
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/mpd.py
|
||||||
homeassistant/components/media_player/onkyo.py
|
homeassistant/components/media_player/onkyo.py
|
||||||
homeassistant/components/media_player/panasonic_viera.py
|
homeassistant/components/media_player/panasonic_viera.py
|
||||||
|
homeassistant/components/media_player/pioneer.py
|
||||||
homeassistant/components/media_player/plex.py
|
homeassistant/components/media_player/plex.py
|
||||||
homeassistant/components/media_player/samsungtv.py
|
homeassistant/components/media_player/samsungtv.py
|
||||||
homeassistant/components/media_player/snapcast.py
|
homeassistant/components/media_player/snapcast.py
|
||||||
@ -139,11 +146,12 @@ omit =
|
|||||||
homeassistant/components/sensor/cpuspeed.py
|
homeassistant/components/sensor/cpuspeed.py
|
||||||
homeassistant/components/sensor/deutsche_bahn.py
|
homeassistant/components/sensor/deutsche_bahn.py
|
||||||
homeassistant/components/sensor/dht.py
|
homeassistant/components/sensor/dht.py
|
||||||
homeassistant/components/sensor/dweet.py
|
|
||||||
homeassistant/components/sensor/efergy.py
|
homeassistant/components/sensor/efergy.py
|
||||||
homeassistant/components/sensor/eliqonline.py
|
homeassistant/components/sensor/eliqonline.py
|
||||||
|
homeassistant/components/sensor/fitbit.py
|
||||||
homeassistant/components/sensor/forecast.py
|
homeassistant/components/sensor/forecast.py
|
||||||
homeassistant/components/sensor/glances.py
|
homeassistant/components/sensor/glances.py
|
||||||
|
homeassistant/components/sensor/google_travel_time.py
|
||||||
homeassistant/components/sensor/gtfs.py
|
homeassistant/components/sensor/gtfs.py
|
||||||
homeassistant/components/sensor/loopenergy.py
|
homeassistant/components/sensor/loopenergy.py
|
||||||
homeassistant/components/sensor/netatmo.py
|
homeassistant/components/sensor/netatmo.py
|
||||||
@ -164,6 +172,7 @@ omit =
|
|||||||
homeassistant/components/sensor/twitch.py
|
homeassistant/components/sensor/twitch.py
|
||||||
homeassistant/components/sensor/uber.py
|
homeassistant/components/sensor/uber.py
|
||||||
homeassistant/components/sensor/worldclock.py
|
homeassistant/components/sensor/worldclock.py
|
||||||
|
homeassistant/components/switch/acer_projector.py
|
||||||
homeassistant/components/switch/arest.py
|
homeassistant/components/switch/arest.py
|
||||||
homeassistant/components/switch/dlink.py
|
homeassistant/components/switch/dlink.py
|
||||||
homeassistant/components/switch/edimax.py
|
homeassistant/components/switch/edimax.py
|
||||||
@ -172,6 +181,7 @@ omit =
|
|||||||
homeassistant/components/switch/orvibo.py
|
homeassistant/components/switch/orvibo.py
|
||||||
homeassistant/components/switch/pulseaudio_loopback.py
|
homeassistant/components/switch/pulseaudio_loopback.py
|
||||||
homeassistant/components/switch/rest.py
|
homeassistant/components/switch/rest.py
|
||||||
|
homeassistant/components/switch/rpi_rf.py
|
||||||
homeassistant/components/switch/transmission.py
|
homeassistant/components/switch/transmission.py
|
||||||
homeassistant/components/switch/wake_on_lan.py
|
homeassistant/components/switch/wake_on_lan.py
|
||||||
homeassistant/components/thermostat/eq3btsmart.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`):**
|
**Home Assistant release (`hass --version`):**
|
||||||
|
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -78,6 +78,7 @@ nosetests.xml
|
|||||||
pyvenv.cfg
|
pyvenv.cfg
|
||||||
pip-selfcheck.json
|
pip-selfcheck.json
|
||||||
venv
|
venv
|
||||||
|
.venv
|
||||||
|
|
||||||
# vimmy stuff
|
# vimmy stuff
|
||||||
*.swp
|
*.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
|
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
|
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
|
The system is built modular so support for other devices or actions can
|
||||||
be implemented easily. See also the `section on
|
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
|
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
|
If you run into issues while using Home Assistant or during development
|
||||||
of a component, check the `Home Assistant help
|
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
|
:target: https://travis-ci.org/home-assistant/home-assistant
|
||||||
.. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg
|
.. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg
|
||||||
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
|
: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
|
: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
|
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
||||||
:target: https://home-assistant.io/demo/
|
:target: https://home-assistant.io/demo/
|
||||||
|
@ -14,6 +14,7 @@ import homeassistant.components as core_components
|
|||||||
import homeassistant.components.group as group
|
import homeassistant.components.group as group
|
||||||
import homeassistant.config as config_util
|
import homeassistant.config as config_util
|
||||||
import homeassistant.core as core
|
import homeassistant.core as core
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.loader as loader
|
import homeassistant.loader as loader
|
||||||
import homeassistant.util.dt as date_util
|
import homeassistant.util.dt as date_util
|
||||||
import homeassistant.util.location as loc_util
|
import homeassistant.util.location as loc_util
|
||||||
@ -103,7 +104,7 @@ def _setup_component(hass, domain, config):
|
|||||||
try:
|
try:
|
||||||
config = component.CONFIG_SCHEMA(config)
|
config = component.CONFIG_SCHEMA(config)
|
||||||
except vol.MultipleInvalid as ex:
|
except vol.MultipleInvalid as ex:
|
||||||
_LOGGER.error('Invalid config for [%s]: %s', domain, ex)
|
cv.log_exception(_LOGGER, ex, domain)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
||||||
@ -113,8 +114,7 @@ def _setup_component(hass, domain, config):
|
|||||||
try:
|
try:
|
||||||
p_validated = component.PLATFORM_SCHEMA(p_config)
|
p_validated = component.PLATFORM_SCHEMA(p_config)
|
||||||
except vol.MultipleInvalid as ex:
|
except vol.MultipleInvalid as ex:
|
||||||
_LOGGER.error('Invalid platform config for [%s]: %s. %s',
|
cv.log_exception(_LOGGER, ex, domain)
|
||||||
domain, ex, p_config)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Not all platform components follow same pattern for platforms
|
# Not all platform components follow same pattern for platforms
|
||||||
@ -135,9 +135,8 @@ def _setup_component(hass, domain, config):
|
|||||||
try:
|
try:
|
||||||
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
||||||
except vol.MultipleInvalid as ex:
|
except vol.MultipleInvalid as ex:
|
||||||
_LOGGER.error(
|
cv.log_exception(_LOGGER, ex, '{}.{}'
|
||||||
'Invalid platform config for [%s.%s]: %s. %s',
|
.format(domain, p_name))
|
||||||
domain, p_name, ex, p_config)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
platforms.append(p_validated)
|
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(
|
process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA(
|
||||||
config.get(core.DOMAIN, {})))
|
config.get(core.DOMAIN, {})))
|
||||||
except vol.MultipleInvalid as ex:
|
except vol.MultipleInvalid as ex:
|
||||||
_LOGGER.error('Invalid config for [homeassistant]: %s', ex)
|
cv.log_exception(_LOGGER, ex, 'homeassistant')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
process_ha_config_upgrade(hass)
|
process_ha_config_upgrade(hass)
|
||||||
|
@ -48,6 +48,11 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return hub.available
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def code_format(self):
|
def code_format(self):
|
||||||
"""The code format as regex."""
|
"""The code format as regex."""
|
||||||
|
@ -8,8 +8,7 @@ import enum
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
|
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
|
||||||
from homeassistant.helpers.service import call_from_config
|
from homeassistant.helpers import template, script
|
||||||
from homeassistant.helpers import template
|
|
||||||
|
|
||||||
DOMAIN = 'alexa'
|
DOMAIN = 'alexa'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
@ -27,7 +26,14 @@ CONF_ACTION = 'action'
|
|||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Activate Alexa component."""
|
"""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)
|
hass.http.register_path('POST', API_ENDPOINT, _handle_alexa, True)
|
||||||
|
|
||||||
@ -91,7 +97,7 @@ def _handle_alexa(handler, path_match, data):
|
|||||||
card['content'])
|
card['content'])
|
||||||
|
|
||||||
if action is not None:
|
if action is not None:
|
||||||
call_from_config(handler.server.hass, action, True)
|
action.run(response.variables)
|
||||||
|
|
||||||
handler.write_json(response.as_dict())
|
handler.write_json(response.as_dict())
|
||||||
|
|
||||||
|
@ -16,9 +16,10 @@ from homeassistant.const import (
|
|||||||
CONTENT_TYPE_TEXT_PLAIN, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
CONTENT_TYPE_TEXT_PLAIN, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
||||||
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_HEADER_CONTENT_TYPE, HTTP_NOT_FOUND,
|
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_HEADER_CONTENT_TYPE, HTTP_NOT_FOUND,
|
||||||
HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
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_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
|
||||||
URL_API_LOG_OUT, URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY,
|
URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_LOG_OUT, URL_API_SERVICES,
|
||||||
URL_API_STREAM, URL_API_TEMPLATE)
|
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
|
||||||
|
__version__)
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers.state import TrackStates
|
from homeassistant.helpers.state import TrackStates
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template
|
||||||
@ -37,13 +38,18 @@ def setup(hass, config):
|
|||||||
# /api - for validation purposes
|
# /api - for validation purposes
|
||||||
hass.http.register_path('GET', URL_API, _handle_get_api)
|
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
|
# /api/config
|
||||||
hass.http.register_path('GET', URL_API_CONFIG, _handle_get_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', URL_API_STATES, _handle_get_api_states)
|
||||||
hass.http.register_path(
|
hass.http.register_path(
|
||||||
'GET', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
'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]+)'),
|
'DELETE', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||||
_handle_delete_state_entity)
|
_handle_delete_state_entity)
|
||||||
|
|
||||||
# /events
|
# /api/events
|
||||||
hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events)
|
hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events)
|
||||||
hass.http.register_path(
|
hass.http.register_path(
|
||||||
'POST', re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
|
'POST', re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
|
||||||
_handle_api_post_events_event)
|
_handle_api_post_events_event)
|
||||||
|
|
||||||
# /services
|
# /api/services
|
||||||
hass.http.register_path('GET', URL_API_SERVICES, _handle_get_api_services)
|
hass.http.register_path('GET', URL_API_SERVICES, _handle_get_api_services)
|
||||||
hass.http.register_path(
|
hass.http.register_path(
|
||||||
'POST',
|
'POST',
|
||||||
@ -73,23 +79,23 @@ def setup(hass, config):
|
|||||||
r'(?P<service>[a-zA-Z\._0-9]+)')),
|
r'(?P<service>[a-zA-Z\._0-9]+)')),
|
||||||
_handle_post_api_services_domain_service)
|
_handle_post_api_services_domain_service)
|
||||||
|
|
||||||
# /event_forwarding
|
# /api/event_forwarding
|
||||||
hass.http.register_path(
|
hass.http.register_path(
|
||||||
'POST', URL_API_EVENT_FORWARD, _handle_post_api_event_forward)
|
'POST', URL_API_EVENT_FORWARD, _handle_post_api_event_forward)
|
||||||
hass.http.register_path(
|
hass.http.register_path(
|
||||||
'DELETE', URL_API_EVENT_FORWARD, _handle_delete_api_event_forward)
|
'DELETE', URL_API_EVENT_FORWARD, _handle_delete_api_event_forward)
|
||||||
|
|
||||||
# /components
|
# /api/components
|
||||||
hass.http.register_path(
|
hass.http.register_path(
|
||||||
'GET', URL_API_COMPONENTS, _handle_get_api_components)
|
'GET', URL_API_COMPONENTS, _handle_get_api_components)
|
||||||
|
|
||||||
# /error_log
|
# /api/error_log
|
||||||
hass.http.register_path('GET', URL_API_ERROR_LOG,
|
hass.http.register_path('GET', URL_API_ERROR_LOG,
|
||||||
_handle_get_api_error_log)
|
_handle_get_api_error_log)
|
||||||
|
|
||||||
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
|
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,
|
hass.http.register_path('POST', URL_API_TEMPLATE,
|
||||||
_handle_post_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())
|
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):
|
def _handle_get_api_states(handler, path_match, data):
|
||||||
"""Return a dict containing all entity ids and their state."""
|
"""Return a dict containing all entity ids and their state."""
|
||||||
handler.write_json(handler.server.hass.states.all())
|
handler.write_json(handler.server.hass.states.all())
|
||||||
|
@ -11,7 +11,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
|
|
||||||
DOMAIN = "arduino"
|
DOMAIN = "arduino"
|
||||||
REQUIREMENTS = ['PyMata==2.07a']
|
REQUIREMENTS = ['PyMata==2.12']
|
||||||
BOARD = None
|
BOARD = None
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ class ArduinoBoard(object):
|
|||||||
self._board.ANALOG)
|
self._board.ANALOG)
|
||||||
elif mode == 'digital' and direction == 'in':
|
elif mode == 'digital' and direction == 'in':
|
||||||
self._board.set_pin_mode(pin,
|
self._board.set_pin_mode(pin,
|
||||||
self._board.OUTPUT,
|
self._board.INPUT,
|
||||||
self._board.DIGITAL)
|
self._board.DIGITAL)
|
||||||
elif mode == 'digital' and direction == 'out':
|
elif mode == 'digital' and direction == 'out':
|
||||||
self._board.set_pin_mode(pin,
|
self._board.set_pin_mode(pin,
|
||||||
|
@ -9,10 +9,10 @@ import logging
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.bootstrap import prepare_setup_platform
|
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.components import logbook
|
||||||
from homeassistant.helpers import extract_domain_configs
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.service import call_from_config
|
from homeassistant.helpers import extract_domain_configs, script, condition
|
||||||
from homeassistant.loader import get_platform
|
from homeassistant.loader import get_platform
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
@ -74,10 +74,11 @@ _CONDITION_SCHEMA = vol.Any(
|
|||||||
[
|
[
|
||||||
vol.All(
|
vol.All(
|
||||||
vol.Schema({
|
vol.Schema({
|
||||||
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN),
|
CONF_PLATFORM: str,
|
||||||
|
CONF_CONDITION: str,
|
||||||
}, extra=vol.ALLOW_EXTRA),
|
}, 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.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE):
|
||||||
vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)),
|
vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)),
|
||||||
CONF_CONDITION: _CONDITION_SCHEMA,
|
CONF_CONDITION: _CONDITION_SCHEMA,
|
||||||
vol.Required(CONF_ACTION): cv.SERVICE_SCHEMA,
|
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup the automation."""
|
"""Setup the automation."""
|
||||||
|
success = False
|
||||||
for config_key in extract_domain_configs(config, DOMAIN):
|
for config_key in extract_domain_configs(config, DOMAIN):
|
||||||
conf = config[config_key]
|
conf = config[config_key]
|
||||||
|
|
||||||
for list_no, config_block in enumerate(conf):
|
for list_no, config_block in enumerate(conf):
|
||||||
name = config_block.get(CONF_ALIAS, "{}, {}".format(config_key,
|
name = config_block.get(CONF_ALIAS, "{}, {}".format(config_key,
|
||||||
list_no))
|
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):
|
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):
|
def _get_action(hass, config, name):
|
||||||
"""Return an action based on a configuration."""
|
"""Return an action based on a configuration."""
|
||||||
def action():
|
script_obj = script.Script(hass, config, name)
|
||||||
|
|
||||||
|
def action(variables=None):
|
||||||
"""Action to be executed."""
|
"""Action to be executed."""
|
||||||
_LOGGER.info('Executing %s', name)
|
_LOGGER.info('Executing %s', name)
|
||||||
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||||
|
script_obj.run(variables)
|
||||||
call_from_config(hass, config)
|
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|
||||||
@ -137,6 +141,11 @@ def _process_if(hass, config, p_config, action):
|
|||||||
cond_type = p_config.get(CONF_CONDITION_TYPE,
|
cond_type = p_config.get(CONF_CONDITION_TYPE,
|
||||||
DEFAULT_CONDITION_TYPE).lower()
|
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)
|
if_configs = p_config.get(CONF_CONDITION)
|
||||||
use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES
|
use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES
|
||||||
|
|
||||||
@ -145,38 +154,47 @@ def _process_if(hass, config, p_config, action):
|
|||||||
|
|
||||||
checks = []
|
checks = []
|
||||||
for if_config in if_configs:
|
for if_config in if_configs:
|
||||||
platform = _resolve_platform(METHOD_IF_ACTION, hass, config,
|
# Deprecated except for used by use_trigger_values
|
||||||
if_config.get(CONF_PLATFORM))
|
# since 0.19 - 5/5/2016
|
||||||
if platform is None:
|
if CONF_PLATFORM in if_config:
|
||||||
continue
|
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]
|
||||||
|
|
||||||
|
try:
|
||||||
|
checks.append(condition.from_config(if_config))
|
||||||
|
except HomeAssistantError as ex:
|
||||||
# Invalid conditions are allowed if we base it on trigger
|
# Invalid conditions are allowed if we base it on trigger
|
||||||
if check is None and not use_trigger:
|
if use_trigger:
|
||||||
|
_LOGGER.warning('Ignoring invalid condition: %s', ex)
|
||||||
|
else:
|
||||||
|
_LOGGER.warning('Invalid condition: %s', ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
checks.append(check)
|
|
||||||
|
|
||||||
if cond_type == CONDITION_TYPE_AND:
|
if cond_type == CONDITION_TYPE_AND:
|
||||||
def if_action():
|
def if_action(variables=None):
|
||||||
"""AND all conditions."""
|
"""AND all conditions."""
|
||||||
if all(check() for check in checks):
|
if all(check(hass, variables) for check in checks):
|
||||||
action()
|
action(variables)
|
||||||
else:
|
else:
|
||||||
def if_action():
|
def if_action(variables=None):
|
||||||
"""OR all conditions."""
|
"""OR all conditions."""
|
||||||
if any(check() for check in checks):
|
if any(check(hass, variables) for check in checks):
|
||||||
action()
|
action(variables)
|
||||||
|
|
||||||
return if_action
|
return if_action
|
||||||
|
|
||||||
|
|
||||||
def _process_trigger(hass, config, trigger_configs, name, action):
|
def _process_trigger(hass, config, trigger_configs, name, action):
|
||||||
"""Setup the triggers."""
|
"""Setup the triggers."""
|
||||||
if isinstance(trigger_configs, dict):
|
|
||||||
trigger_configs = [trigger_configs]
|
|
||||||
|
|
||||||
for conf in trigger_configs:
|
for conf in trigger_configs:
|
||||||
platform = _resolve_platform(METHOD_TRIGGER, hass, config,
|
platform = _resolve_platform(METHOD_TRIGGER, hass, config,
|
||||||
conf.get(CONF_PLATFORM))
|
conf.get(CONF_PLATFORM))
|
||||||
|
@ -6,27 +6,38 @@ at https://home-assistant.io/components/automation/#event-trigger
|
|||||||
"""
|
"""
|
||||||
import logging
|
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_TYPE = "event_type"
|
||||||
CONF_EVENT_DATA = "event_data"
|
CONF_EVENT_DATA = "event_data"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
def trigger(hass, config, action):
|
||||||
"""Listen for events based on configuration."""
|
"""Listen for events based on configuration."""
|
||||||
event_type = config.get(CONF_EVENT_TYPE)
|
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)
|
event_data = config.get(CONF_EVENT_DATA)
|
||||||
|
|
||||||
def handle_event(event):
|
def handle_event(event):
|
||||||
"""Listen for events and calls the action when data matches."""
|
"""Listen for events and calls the action when data matches."""
|
||||||
if not event_data or all(val == event.data.get(key) for key, val
|
if not event_data or all(val == event.data.get(key) for key, val
|
||||||
in event_data.items()):
|
in event_data.items()):
|
||||||
action()
|
action({
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'event',
|
||||||
|
'event': event,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
hass.bus.listen(event_type, handle_event)
|
hass.bus.listen(event_type, handle_event)
|
||||||
return True
|
return True
|
||||||
|
@ -30,7 +30,14 @@ def trigger(hass, config, action):
|
|||||||
def mqtt_automation_listener(msg_topic, msg_payload, qos):
|
def mqtt_automation_listener(msg_topic, msg_payload, qos):
|
||||||
"""Listen for MQTT messages."""
|
"""Listen for MQTT messages."""
|
||||||
if payload is None or payload == msg_payload:
|
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)
|
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
|
at https://home-assistant.io/components/automation/#numeric-state-trigger
|
||||||
"""
|
"""
|
||||||
import logging
|
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.event import track_state_change
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import condition, config_validation as cv
|
||||||
|
|
||||||
CONF_ENTITY_ID = "entity_id"
|
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||||
CONF_BELOW = "below"
|
vol.Required(CONF_PLATFORM): 'numeric_state',
|
||||||
CONF_ABOVE = "above"
|
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__)
|
_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):
|
def trigger(hass, config, action):
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
entity_id = config.get(CONF_ENTITY_ID)
|
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)
|
below = config.get(CONF_BELOW)
|
||||||
above = config.get(CONF_ABOVE)
|
above = config.get(CONF_ABOVE)
|
||||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
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
|
# pylint: disable=unused-argument
|
||||||
def state_automation_listener(entity, from_s, to_s):
|
def state_automation_listener(entity, from_s, to_s):
|
||||||
"""Listen for state changes and calls action."""
|
"""Listen for state changes and calls action."""
|
||||||
# Fire action if we go from outside range into range
|
if to_s is None:
|
||||||
if _in_range(above, below, renderer(to_s)) and \
|
return
|
||||||
(from_s is None or not _in_range(above, below, renderer(from_s))):
|
|
||||||
action()
|
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(
|
track_state_change(
|
||||||
hass, entity_id, state_automation_listener)
|
hass, entity_id, state_automation_listener)
|
||||||
|
|
||||||
return True
|
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
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/components/automation/#state-trigger
|
at https://home-assistant.io/components/automation/#state-trigger
|
||||||
"""
|
"""
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, CONF_PLATFORM)
|
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
|
from homeassistant.helpers.event import track_state_change, track_point_in_time
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
@ -22,46 +18,19 @@ CONF_TO = "to"
|
|||||||
CONF_STATE = "state"
|
CONF_STATE = "state"
|
||||||
CONF_FOR = "for"
|
CONF_FOR = "for"
|
||||||
|
|
||||||
BASE_SCHEMA = vol.Schema({
|
TRIGGER_SCHEMA = vol.All(
|
||||||
|
vol.Schema({
|
||||||
vol.Required(CONF_PLATFORM): 'state',
|
vol.Required(CONF_PLATFORM): 'state',
|
||||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||||
# 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({
|
|
||||||
# These are str on purpose. Want to catch YAML conversions
|
# These are str on purpose. Want to catch YAML conversions
|
||||||
CONF_FROM: str,
|
CONF_FROM: str,
|
||||||
CONF_TO: 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),
|
vol.Any(cv.key_dependency(CONF_FOR, CONF_TO),
|
||||||
cv.key_dependency(CONF_FOR, CONF_STATE))
|
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):
|
def trigger(hass, config, action):
|
||||||
@ -69,52 +38,48 @@ def trigger(hass, config, action):
|
|||||||
entity_id = config.get(CONF_ENTITY_ID)
|
entity_id = config.get(CONF_ENTITY_ID)
|
||||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||||
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or 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):
|
def state_automation_listener(entity, from_s, to_s):
|
||||||
"""Listen for state changes and calls action."""
|
"""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):
|
def state_for_listener(now):
|
||||||
"""Fire on state changes after a delay and calls action."""
|
"""Fire on state changes after a delay and calls action."""
|
||||||
hass.bus.remove_listener(
|
hass.bus.remove_listener(
|
||||||
EVENT_STATE_CHANGED, for_state_listener)
|
EVENT_STATE_CHANGED, attached_state_for_cancel)
|
||||||
action()
|
call_action()
|
||||||
|
|
||||||
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
|
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
|
||||||
"""Fire on changes and cancel for listener if changed."""
|
"""Fire on changes and cancel for listener if changed."""
|
||||||
if inner_to_s == to_s:
|
if inner_to_s.state == to_s.state:
|
||||||
return
|
return
|
||||||
hass.bus.remove_listener(EVENT_TIME_CHANGED, for_time_listener)
|
hass.bus.remove_listener(EVENT_TIME_CHANGED,
|
||||||
hass.bus.remove_listener(
|
attached_state_for_listener)
|
||||||
EVENT_STATE_CHANGED, for_state_listener)
|
hass.bus.remove_listener(EVENT_STATE_CHANGED,
|
||||||
|
attached_state_for_cancel)
|
||||||
|
|
||||||
if time_delta is not None:
|
attached_state_for_listener = track_point_in_time(
|
||||||
target_tm = dt_util.utcnow() + time_delta
|
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
||||||
for_time_listener = track_point_in_time(
|
|
||||||
hass, state_for_listener, target_tm)
|
attached_state_for_cancel = track_state_change(
|
||||||
for_state_listener = track_state_change(
|
hass, entity, state_for_cancel_listener)
|
||||||
hass, entity_id, state_for_cancel_listener,
|
|
||||||
MATCH_ALL, MATCH_ALL)
|
|
||||||
else:
|
|
||||||
action()
|
|
||||||
|
|
||||||
track_state_change(
|
track_state_change(
|
||||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||||
|
|
||||||
return True
|
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
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_PLATFORM
|
from homeassistant.const import (
|
||||||
import homeassistant.util.dt as dt_util
|
CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE)
|
||||||
from homeassistant.components import sun
|
|
||||||
from homeassistant.helpers.event import track_sunrise, track_sunset
|
from homeassistant.helpers.event import track_sunrise, track_sunset
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DEPENDENCIES = ['sun']
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
_SUN_EVENT = vol.All(vol.Lower, vol.Any(EVENT_SUNRISE, EVENT_SUNSET))
|
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.Schema({
|
TRIGGER_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_PLATFORM): 'sun',
|
vol.Required(CONF_PLATFORM): 'sun',
|
||||||
vol.Required(CONF_EVENT): _SUN_EVENT,
|
vol.Required(CONF_EVENT): cv.sun_event,
|
||||||
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_offset,
|
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):
|
def trigger(hass, config, action):
|
||||||
"""Listen for events based on configuration."""
|
"""Listen for events based on configuration."""
|
||||||
event = config.get(CONF_EVENT)
|
event = config.get(CONF_EVENT)
|
||||||
offset = config.get(CONF_OFFSET)
|
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
|
# Do something to call action
|
||||||
if event == EVENT_SUNRISE:
|
if event == SUN_EVENT_SUNRISE:
|
||||||
track_sunrise(hass, action, offset)
|
track_sunrise(hass, call_action, offset)
|
||||||
else:
|
else:
|
||||||
track_sunset(hass, action, offset)
|
track_sunset(hass, call_action, offset)
|
||||||
|
|
||||||
return True
|
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
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED, CONF_PLATFORM)
|
CONF_VALUE_TEMPLATE, CONF_PLATFORM, MATCH_ALL)
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.helpers import condition
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers.event import track_state_change
|
||||||
import homeassistant.helpers.config_validation as cv
|
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
|
# Local variable to keep track of if the action has already been triggered
|
||||||
already_triggered = False
|
already_triggered = False
|
||||||
|
|
||||||
def event_listener(event):
|
def state_changed_listener(entity_id, from_s, to_s):
|
||||||
"""Listen for state changes and calls action."""
|
"""Listen for state changes and calls action."""
|
||||||
nonlocal already_triggered
|
nonlocal already_triggered
|
||||||
template_result = _check_template(hass, value_template)
|
template_result = condition.template(hass, value_template)
|
||||||
|
|
||||||
# Check to see if template returns true
|
# Check to see if template returns true
|
||||||
if template_result and not already_triggered:
|
if template_result and not already_triggered:
|
||||||
already_triggered = True
|
already_triggered = True
|
||||||
action()
|
action({
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'entity_id': entity_id,
|
||||||
|
'from_state': from_s,
|
||||||
|
'to_state': to_s,
|
||||||
|
},
|
||||||
|
})
|
||||||
elif not template_result:
|
elif not template_result:
|
||||||
already_triggered = False
|
already_triggered = False
|
||||||
|
|
||||||
hass.bus.listen(EVENT_STATE_CHANGED, event_listener)
|
track_state_change(hass, MATCH_ALL, state_changed_listener)
|
||||||
return True
|
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 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
|
from homeassistant.helpers.event import track_time_change
|
||||||
|
|
||||||
CONF_HOURS = "hours"
|
CONF_HOURS = "hours"
|
||||||
CONF_MINUTES = "minutes"
|
CONF_MINUTES = "minutes"
|
||||||
CONF_SECONDS = "seconds"
|
CONF_SECONDS = "seconds"
|
||||||
CONF_BEFORE = "before"
|
|
||||||
CONF_AFTER = "after"
|
|
||||||
CONF_WEEKDAY = "weekday"
|
|
||||||
|
|
||||||
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
def trigger(hass, config, action):
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
if CONF_AFTER in config:
|
if CONF_AFTER in config:
|
||||||
after = dt_util.parse_time(config[CONF_AFTER])
|
after = config.get(CONF_AFTER)
|
||||||
if after is None:
|
|
||||||
_error_time(config[CONF_AFTER], CONF_AFTER)
|
|
||||||
return False
|
|
||||||
hours, minutes, seconds = after.hour, after.minute, after.second
|
hours, minutes, seconds = after.hour, after.minute, after.second
|
||||||
elif (CONF_HOURS in config or CONF_MINUTES in config or
|
else:
|
||||||
CONF_SECONDS in config):
|
|
||||||
hours = config.get(CONF_HOURS)
|
hours = config.get(CONF_HOURS)
|
||||||
minutes = config.get(CONF_MINUTES)
|
minutes = config.get(CONF_MINUTES)
|
||||||
seconds = config.get(CONF_SECONDS)
|
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):
|
def time_automation_listener(now):
|
||||||
"""Listen for time changes and calls action."""
|
"""Listen for time changes and calls action."""
|
||||||
action()
|
action({
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'time',
|
||||||
|
'now': now,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
track_time_change(hass, time_automation_listener,
|
track_time_change(hass, time_automation_listener,
|
||||||
hour=hours, minute=minutes, second=seconds)
|
hour=hours, minute=minutes, second=seconds)
|
||||||
|
|
||||||
return True
|
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
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import zone
|
|
||||||
from homeassistant.const import (
|
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
|
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_ENTER = "enter"
|
||||||
EVENT_LEAVE = "leave"
|
EVENT_LEAVE = "leave"
|
||||||
DEFAULT_EVENT = EVENT_ENTER
|
DEFAULT_EVENT = EVENT_ENTER
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.Schema({
|
TRIGGER_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_PLATFORM): 'zone',
|
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_ZONE): cv.entity_id,
|
||||||
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
|
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
|
||||||
vol.Any(EVENT_ENTER, EVENT_LEAVE),
|
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):
|
def trigger(hass, config, action):
|
||||||
"""Listen for state changes based on configuration."""
|
"""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):
|
def zone_automation_listener(entity, from_s, to_s):
|
||||||
"""Listen for state changes and calls action."""
|
"""Listen for state changes and calls action."""
|
||||||
if from_s and None in (from_s.attributes.get(ATTR_LATITUDE),
|
if from_s and not location.has_location(from_s) or \
|
||||||
from_s.attributes.get(ATTR_LONGITUDE)) or \
|
not location.has_location(to_s):
|
||||||
None in (to_s.attributes.get(ATTR_LATITUDE),
|
|
||||||
to_s.attributes.get(ATTR_LONGITUDE)):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
from_match = _in_zone(hass, zone_entity_id, from_s) if from_s else None
|
zone_state = hass.states.get(zone_entity_id)
|
||||||
to_match = _in_zone(hass, zone_entity_id, to_s)
|
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
|
# pylint: disable=too-many-boolean-expressions
|
||||||
if event == EVENT_ENTER and not from_match and to_match or \
|
if event == EVENT_ENTER and not from_match and to_match or \
|
||||||
event == EVENT_LEAVE and from_match and not to_match:
|
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(
|
track_state_change(
|
||||||
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL)
|
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL)
|
||||||
|
|
||||||
return True
|
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
|
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
|
import logging
|
||||||
from datetime import timedelta
|
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.components.sensor.command_line import CommandSensorData
|
||||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template
|
||||||
@ -15,6 +16,7 @@ from homeassistant.helpers import template
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = "Binary Command Sensor"
|
DEFAULT_NAME = "Binary Command Sensor"
|
||||||
|
DEFAULT_SENSOR_CLASS = None
|
||||||
DEFAULT_PAYLOAD_ON = 'ON'
|
DEFAULT_PAYLOAD_ON = 'ON'
|
||||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||||
|
|
||||||
@ -29,28 +31,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
_LOGGER.error('Missing required variable: "command"')
|
_LOGGER.error('Missing required variable: "command"')
|
||||||
return False
|
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'))
|
data = CommandSensorData(config.get('command'))
|
||||||
|
|
||||||
add_devices([CommandBinarySensor(
|
add_devices([CommandBinarySensor(
|
||||||
hass,
|
hass,
|
||||||
data,
|
data,
|
||||||
config.get('name', DEFAULT_NAME),
|
config.get('name', DEFAULT_NAME),
|
||||||
|
sensor_class,
|
||||||
config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
||||||
config.get('payload_off', DEFAULT_PAYLOAD_OFF),
|
config.get('payload_off', DEFAULT_PAYLOAD_OFF),
|
||||||
config.get(CONF_VALUE_TEMPLATE)
|
config.get(CONF_VALUE_TEMPLATE)
|
||||||
)])
|
)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||||
class CommandBinarySensor(BinarySensorDevice):
|
class CommandBinarySensor(BinarySensorDevice):
|
||||||
"""Represent a command line binary sensor."""
|
"""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):
|
payload_off, value_template):
|
||||||
"""Initialize the Command line binary sensor."""
|
"""Initialize the Command line binary sensor."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self.data = data
|
self.data = data
|
||||||
self._name = name
|
self._name = name
|
||||||
|
self._sensor_class = sensor_class
|
||||||
self._state = False
|
self._state = False
|
||||||
self._payload_on = payload_on
|
self._payload_on = payload_on
|
||||||
self._payload_off = payload_off
|
self._payload_off = payload_off
|
||||||
@ -67,6 +76,11 @@ class CommandBinarySensor(BinarySensorDevice):
|
|||||||
"""Return true if the binary sensor is on."""
|
"""Return true if the binary sensor is on."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
@ property
|
||||||
|
def sensor_class(self):
|
||||||
|
"""Return the class of the binary sensor."""
|
||||||
|
return self._sensor_class
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest data and updates the state."""
|
"""Get the latest data and updates the state."""
|
||||||
self.data.update()
|
self.data.update()
|
||||||
|
@ -6,10 +6,10 @@ https://home-assistant.io/components/binary_sensor.mysensors/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components import mysensors
|
||||||
from homeassistant.components.binary_sensor import (SENSOR_CLASSES,
|
from homeassistant.components.binary_sensor import (SENSOR_CLASSES,
|
||||||
BinarySensorDevice)
|
BinarySensorDevice)
|
||||||
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON
|
from homeassistant.const import STATE_ON
|
||||||
from homeassistant.loader import get_component
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
@ -22,8 +22,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
mysensors = get_component('mysensors')
|
|
||||||
|
|
||||||
for gateway in mysensors.GATEWAYS.values():
|
for gateway in mysensors.GATEWAYS.values():
|
||||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||||
# states. Map them in a dict of lists.
|
# 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))
|
map_sv_types, devices, add_devices, MySensorsBinarySensor))
|
||||||
|
|
||||||
|
|
||||||
class MySensorsBinarySensor(BinarySensorDevice):
|
class MySensorsBinarySensor(
|
||||||
"""Represent the value of a MySensors child node."""
|
mysensors.MySensorsDeviceEntity, BinarySensorDevice):
|
||||||
|
"""Represent the value of a MySensors Binary Sensor 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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
@ -150,23 +76,3 @@ class MySensorsBinarySensor(BinarySensorDevice):
|
|||||||
})
|
})
|
||||||
if class_map.get(self.child_type) in SENSOR_CLASSES:
|
if class_map.get(self.child_type) in SENSOR_CLASSES:
|
||||||
return class_map.get(self.child_type)
|
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
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
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
|
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
|
# These are the available sensors mapped to binary_sensor class
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
@ -48,6 +48,7 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity):
|
|||||||
"""Initialize the Wink binary sensor."""
|
"""Initialize the Wink binary sensor."""
|
||||||
self.wink = wink
|
self.wink = wink
|
||||||
self._unit_of_measurement = self.wink.UNIT
|
self._unit_of_measurement = self.wink.UNIT
|
||||||
|
self._battery = self.wink.battery_level
|
||||||
self.capability = self.wink.capability()
|
self.capability = self.wink.capability()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -85,3 +86,16 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity):
|
|||||||
def update(self):
|
def update(self):
|
||||||
"""Update state of the sensor."""
|
"""Update state of the sensor."""
|
||||||
self.wink.update_state()
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['pyicloud==0.7.2']
|
REQUIREMENTS = ['pyicloud==0.8.3']
|
||||||
|
|
||||||
CONF_INTERVAL = 'interval'
|
CONF_INTERVAL = 'interval'
|
||||||
DEFAULT_INTERVAL = 8
|
DEFAULT_INTERVAL = 8
|
||||||
|
@ -101,7 +101,7 @@ def setup_scanner(hass, config, see):
|
|||||||
"""Execute enter event."""
|
"""Execute enter event."""
|
||||||
zone = hass.states.get("zone.{}".format(location))
|
zone = hass.states.get("zone.{}".format(location))
|
||||||
with LOCK:
|
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
|
# Not a HA zone, and a beacon so assume mobile
|
||||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
||||||
if location not in beacons:
|
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
|
from homeassistant.helpers import validate_config
|
||||||
|
|
||||||
# Unifi package doesn't list urllib3 as a requirement
|
# 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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
CONF_PORT = 'port'
|
CONF_PORT = 'port'
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||||||
EVENT_PLATFORM_DISCOVERED)
|
EVENT_PLATFORM_DISCOVERED)
|
||||||
|
|
||||||
DOMAIN = "discovery"
|
DOMAIN = "discovery"
|
||||||
REQUIREMENTS = ['netdisco==0.6.4']
|
REQUIREMENTS = ['netdisco==0.6.6']
|
||||||
|
|
||||||
SCAN_INTERVAL = 300 # seconds
|
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 datetime import datetime
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||||
from homeassistant.helpers.event import track_utc_time_change
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
|
|
||||||
REQUIREMENTS = ['feedparser==5.2.1']
|
REQUIREMENTS = ['feedparser==5.2.1']
|
||||||
@ -14,6 +20,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
'urls': [vol.Url()],
|
'urls': [vol.Url()],
|
||||||
}
|
}
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
MAX_ENTRIES = 20
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
@ -25,52 +32,75 @@ class FeedManager(object):
|
|||||||
self._url = url
|
self._url = url
|
||||||
self._feed = None
|
self._feed = None
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
|
self._firstrun = True
|
||||||
# Initialize last entry timestamp as epoch time
|
# Initialize last entry timestamp as epoch time
|
||||||
self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple()
|
self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple()
|
||||||
_LOGGER.debug('Loading feed %s', self._url)
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
|
||||||
self._update()
|
lambda _: self._update())
|
||||||
track_utc_time_change(hass, lambda now: self._update(),
|
track_utc_time_change(hass, lambda now: self._update(),
|
||||||
minute=0, second=0)
|
minute=0, second=0)
|
||||||
|
|
||||||
def _log_no_entries(self):
|
def _log_no_entries(self):
|
||||||
"""Send no entries log at debug level."""
|
"""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):
|
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
|
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,
|
self._feed = feedparser.parse(self._url,
|
||||||
etag=None if not self._feed
|
etag=None if not self._feed
|
||||||
else self._feed.get('etag'),
|
else self._feed.get('etag'),
|
||||||
modified=None if not self._feed
|
modified=None if not self._feed
|
||||||
else self._feed.get('modified'))
|
else self._feed.get('modified'))
|
||||||
if not self._feed:
|
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:
|
else:
|
||||||
if self._feed.bozo != 0:
|
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,
|
# Using etag and modified, if there's no new data available,
|
||||||
# the entries list will be empty
|
# the entries list will be empty
|
||||||
elif len(self._feed.entries) > 0:
|
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._publish_new_entries()
|
||||||
self._last_entry_timestamp = \
|
|
||||||
self._feed.entries[0].published_parsed
|
|
||||||
else:
|
else:
|
||||||
self._log_no_entries()
|
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):
|
def _publish_new_entries(self):
|
||||||
"""Publish new entries to the event bus."""
|
"""Publish new entries to the event bus."""
|
||||||
new_entries = False
|
new_entries = False
|
||||||
for entry in self._feed.entries:
|
for entry in self._feed.entries:
|
||||||
# Consider only entries newer then the latest parsed one
|
if self._firstrun or (
|
||||||
if entry.published_parsed > self._last_entry_timestamp:
|
'published_parsed' in entry.keys() and
|
||||||
|
entry.published_parsed > self._last_entry_timestamp):
|
||||||
|
self._update_and_fire_entry(entry)
|
||||||
new_entries = True
|
new_entries = True
|
||||||
entry.update({'feed_url': self._url})
|
else:
|
||||||
self._hass.bus.fire(EVENT_FEEDREADER, entry)
|
_LOGGER.debug('Entry "%s" already processed', entry.title)
|
||||||
if not new_entries:
|
if not new_entries:
|
||||||
self._log_no_entries()
|
self._log_no_entries()
|
||||||
|
self._firstrun = False
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
|
@ -28,20 +28,55 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 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>
|
</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>
|
</head>
|
||||||
<body fullbleed>
|
<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>
|
<script>
|
||||||
var webComponentsSupported = ('registerElement' in document &&
|
var webComponentsSupported = (
|
||||||
|
'registerElement' in document &&
|
||||||
'import' in document.createElement('link') &&
|
'import' in document.createElement('link') &&
|
||||||
'content' in document.createElement('template'))
|
'content' in document.createElement('template'));
|
||||||
if (!webComponentsSupported) {
|
if (!webComponentsSupported) {
|
||||||
var script = document.createElement('script')
|
var script = document.createElement('script')
|
||||||
script.async = true
|
script.async = true
|
||||||
|
script.onerror = initError;
|
||||||
script.src = '/static/webcomponents-lite.min.js'
|
script.src = '/static/webcomponents-lite.min.js'
|
||||||
document.head.appendChild(script)
|
document.head.appendChild(script)
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
|
"""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."""
|
"""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)}([/*!*************************************!*\
|
!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})})}))})}});
|
||||||
!*** ./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
|
|
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
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.garage_door import GarageDoorDevice
|
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):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
@ -37,6 +37,7 @@ class WinkGarageDoorDevice(GarageDoorDevice):
|
|||||||
def __init__(self, wink):
|
def __init__(self, wink):
|
||||||
"""Initialize the garage door."""
|
"""Initialize the garage door."""
|
||||||
self.wink = wink
|
self.wink = wink
|
||||||
|
self._battery = self.wink.battery_level
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
@ -69,3 +70,16 @@ class WinkGarageDoorDevice(GarageDoorDevice):
|
|||||||
def open_door(self):
|
def open_door(self):
|
||||||
"""Open the door."""
|
"""Open the door."""
|
||||||
self.wink.set_state(1)
|
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 http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||||
from socketserver import ThreadingMixIn
|
from socketserver import ThreadingMixIn
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.bootstrap as bootstrap
|
import homeassistant.bootstrap as bootstrap
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
import homeassistant.remote as rem
|
import homeassistant.remote as rem
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
import homeassistant.util.dt as date_util
|
import homeassistant.util.dt as date_util
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, HTTP_HEADER_ACCEPT_ENCODING,
|
CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, HTTP_HEADER_ACCEPT_ENCODING,
|
||||||
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_CONTENT_ENCODING,
|
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_CONTENT_ENCODING,
|
||||||
HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_EXPIRES,
|
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,
|
HTTP_NOT_FOUND, HTTP_OK, HTTP_UNAUTHORIZED, HTTP_UNPROCESSABLE_ENTITY,
|
||||||
|
ALLOWED_CORS_HEADERS,
|
||||||
SERVER_PORT, URL_ROOT, URL_API_EVENT_FORWARD)
|
SERVER_PORT, URL_ROOT, URL_API_EVENT_FORWARD)
|
||||||
|
|
||||||
DOMAIN = "http"
|
DOMAIN = "http"
|
||||||
@ -38,6 +43,7 @@ CONF_SERVER_PORT = "server_port"
|
|||||||
CONF_DEVELOPMENT = "development"
|
CONF_DEVELOPMENT = "development"
|
||||||
CONF_SSL_CERTIFICATE = 'ssl_certificate'
|
CONF_SSL_CERTIFICATE = 'ssl_certificate'
|
||||||
CONF_SSL_KEY = 'ssl_key'
|
CONF_SSL_KEY = 'ssl_key'
|
||||||
|
CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
||||||
|
|
||||||
DATA_API_PASSWORD = 'api_password'
|
DATA_API_PASSWORD = 'api_password'
|
||||||
|
|
||||||
@ -48,6 +54,19 @@ SESSION_KEY = 'sessionId'
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
def setup(hass, config):
|
||||||
"""Set up the HTTP API and debug interface."""
|
"""Set up the HTTP API and debug interface."""
|
||||||
@ -61,11 +80,12 @@ def setup(hass, config):
|
|||||||
development = str(conf.get(CONF_DEVELOPMENT, "")) == "1"
|
development = str(conf.get(CONF_DEVELOPMENT, "")) == "1"
|
||||||
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
|
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
|
||||||
ssl_key = conf.get(CONF_SSL_KEY)
|
ssl_key = conf.get(CONF_SSL_KEY)
|
||||||
|
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server = HomeAssistantHTTPServer(
|
server = HomeAssistantHTTPServer(
|
||||||
(server_host, server_port), RequestHandler, hass, api_password,
|
(server_host, server_port), RequestHandler, hass, api_password,
|
||||||
development, ssl_certificate, ssl_key)
|
development, ssl_certificate, ssl_key, cors_origins)
|
||||||
except OSError:
|
except OSError:
|
||||||
# If address already in use
|
# If address already in use
|
||||||
_LOGGER.exception("Error setting up HTTP server")
|
_LOGGER.exception("Error setting up HTTP server")
|
||||||
@ -96,7 +116,8 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
|||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def __init__(self, server_address, request_handler_class,
|
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."""
|
"""Initialize the server."""
|
||||||
super().__init__(server_address, request_handler_class)
|
super().__init__(server_address, request_handler_class)
|
||||||
|
|
||||||
@ -107,6 +128,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
|||||||
self.paths = []
|
self.paths = []
|
||||||
self.sessions = SessionStore()
|
self.sessions = SessionStore()
|
||||||
self.use_ssl = ssl_certificate is not None
|
self.use_ssl = ssl_certificate is not None
|
||||||
|
self.cors_origins = cors_origins
|
||||||
|
|
||||||
# We will lazy init this one if needed
|
# We will lazy init this one if needed
|
||||||
self.event_forwarder = None
|
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_VARY, HTTP_HEADER_ACCEPT_ENCODING)
|
||||||
|
|
||||||
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(content)))
|
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()
|
self.end_headers()
|
||||||
|
|
||||||
if self.command == 'HEAD':
|
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
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components import mysensors
|
||||||
from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
|
from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
|
||||||
Light)
|
Light)
|
||||||
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
from homeassistant.loader import get_component
|
|
||||||
from homeassistant.util.color import rgb_hex_to_rgb_list
|
from homeassistant.util.color import rgb_hex_to_rgb_list
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -25,8 +25,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
mysensors = get_component('mysensors')
|
|
||||||
|
|
||||||
for gateway in mysensors.GATEWAYS.values():
|
for gateway in mysensors.GATEWAYS.values():
|
||||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||||
# states. Map them in a dict of lists.
|
# 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))
|
map_sv_types, devices, add_devices, device_class_map))
|
||||||
|
|
||||||
|
|
||||||
class MySensorsLight(Light):
|
class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
|
||||||
"""Represent the value of a MySensors child node."""
|
"""Represent the value of a MySensors Light child node."""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments,too-many-instance-attributes
|
def __init__(self, *args):
|
||||||
def __init__(
|
|
||||||
self, gateway, node_id, child_id, name, value_type, child_type):
|
|
||||||
"""Setup instance attributes."""
|
"""Setup instance attributes."""
|
||||||
self.gateway = gateway
|
mysensors.MySensorsDeviceEntity.__init__(self, *args)
|
||||||
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._state = None
|
self._state = None
|
||||||
self._brightness = None
|
self._brightness = None
|
||||||
self._rgb = None
|
self._rgb = None
|
||||||
self._white = 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
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
@ -97,29 +76,6 @@ class MySensorsLight(Light):
|
|||||||
"""Return the white value in RGBW, value between 0..255."""
|
"""Return the white value in RGBW, value between 0..255."""
|
||||||
return self._white
|
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
|
@property
|
||||||
def assumed_state(self):
|
def assumed_state(self):
|
||||||
"""Return true if unable to access real state of entity."""
|
"""Return true if unable to access real state of entity."""
|
||||||
@ -319,28 +275,11 @@ class MySensorsLightRGB(MySensorsLight):
|
|||||||
self._update_rgb_or_w()
|
self._update_rgb_or_w()
|
||||||
|
|
||||||
|
|
||||||
class MySensorsLightRGBW(MySensorsLight):
|
class MySensorsLightRGBW(MySensorsLightRGB):
|
||||||
"""RGBW child class to MySensorsLight."""
|
"""RGBW child class to MySensorsLightRGB."""
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
self._turn_on_light()
|
self._turn_on_light()
|
||||||
self._turn_on_dimmer(**kwargs)
|
self._turn_on_dimmer(**kwargs)
|
||||||
self._turn_on_rgb_and_w('%02x%02x%02x%02x', **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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/light.tellstick/
|
https://home-assistant.io/components/light.tellstick/
|
||||||
"""
|
"""
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import tellstick
|
from homeassistant.components import tellstick
|
||||||
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
|
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
|
||||||
from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS,
|
from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS,
|
||||||
ATTR_DISCOVER_DEVICES,
|
ATTR_DISCOVER_DEVICES,
|
||||||
ATTR_DISCOVER_CONFIG)
|
ATTR_DISCOVER_CONFIG)
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): tellstick.DOMAIN})
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
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 \
|
from homeassistant.util.color import \
|
||||||
color_temperature_mired_to_kelvin as mired_to_kelvin
|
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):
|
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 (
|
from homeassistant.const import (
|
||||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED,
|
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED,
|
||||||
STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK)
|
STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK)
|
||||||
from homeassistant.components import (group, verisure, wink)
|
from homeassistant.components import (group, verisure, wink, zwave)
|
||||||
|
|
||||||
DOMAIN = 'lock'
|
DOMAIN = 'lock'
|
||||||
SCAN_INTERVAL = 30
|
SCAN_INTERVAL = 30
|
||||||
@ -33,7 +33,8 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
|||||||
# Maps discovered services to their platforms
|
# Maps discovered services to their platforms
|
||||||
DISCOVERY_PLATFORMS = {
|
DISCOVERY_PLATFORMS = {
|
||||||
wink.DISCOVER_LOCKS: 'wink',
|
wink.DISCOVER_LOCKS: 'wink',
|
||||||
verisure.DISCOVER_LOCKS: 'verisure'
|
verisure.DISCOVER_LOCKS: 'verisure',
|
||||||
|
zwave.DISCOVER_LOCKS: 'zwave',
|
||||||
}
|
}
|
||||||
|
|
||||||
LOCK_SERVICE_SCHEMA = vol.Schema({
|
LOCK_SERVICE_SCHEMA = vol.Schema({
|
||||||
|
@ -46,6 +46,11 @@ class VerisureDoorlock(LockDevice):
|
|||||||
"""Return the state of the lock."""
|
"""Return the state of the lock."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return hub.available
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def code_format(self):
|
def code_format(self):
|
||||||
"""Return the required six digit code."""
|
"""Return the required six digit code."""
|
||||||
|
@ -7,9 +7,9 @@ https://home-assistant.io/components/lock.wink/
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.lock import LockDevice
|
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):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
@ -36,6 +36,7 @@ class WinkLockDevice(LockDevice):
|
|||||||
def __init__(self, wink):
|
def __init__(self, wink):
|
||||||
"""Initialize the lock."""
|
"""Initialize the lock."""
|
||||||
self.wink = wink
|
self.wink = wink
|
||||||
|
self._battery = self.wink.battery_level
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
@ -68,3 +69,16 @@ class WinkLockDevice(LockDevice):
|
|||||||
def unlock(self, **kwargs):
|
def unlock(self, **kwargs):
|
||||||
"""Unlock the device."""
|
"""Unlock the device."""
|
||||||
self.wink.set_state(False)
|
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,
|
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE,
|
||||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||||
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
|
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_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
|
||||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
|
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
|
||||||
|
|
||||||
@ -82,6 +82,7 @@ SUPPORT_TURN_OFF = 256
|
|||||||
SUPPORT_PLAY_MEDIA = 512
|
SUPPORT_PLAY_MEDIA = 512
|
||||||
SUPPORT_VOLUME_STEP = 1024
|
SUPPORT_VOLUME_STEP = 1024
|
||||||
SUPPORT_SELECT_SOURCE = 2048
|
SUPPORT_SELECT_SOURCE = 2048
|
||||||
|
SUPPORT_STOP = 4096
|
||||||
|
|
||||||
# simple services that only take entity_id(s) as optional argument
|
# simple services that only take entity_id(s) as optional argument
|
||||||
SERVICE_TO_METHOD = {
|
SERVICE_TO_METHOD = {
|
||||||
@ -93,6 +94,7 @@ SERVICE_TO_METHOD = {
|
|||||||
SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause',
|
SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause',
|
||||||
SERVICE_MEDIA_PLAY: 'media_play',
|
SERVICE_MEDIA_PLAY: 'media_play',
|
||||||
SERVICE_MEDIA_PAUSE: 'media_pause',
|
SERVICE_MEDIA_PAUSE: 'media_pause',
|
||||||
|
SERVICE_MEDIA_STOP: 'media_stop',
|
||||||
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
|
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
|
||||||
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
|
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
|
||||||
SERVICE_SELECT_SOURCE: 'select_source'
|
SERVICE_SELECT_SOURCE: 'select_source'
|
||||||
@ -228,6 +230,12 @@ def media_pause(hass, entity_id=None):
|
|||||||
hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data)
|
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):
|
def media_next_track(hass, entity_id=None):
|
||||||
"""Send the media player the command for next track."""
|
"""Send the media player the command for next track."""
|
||||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||||
@ -510,6 +518,10 @@ class MediaPlayerDevice(Entity):
|
|||||||
"""Send pause command."""
|
"""Send pause command."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def media_stop(self):
|
||||||
|
"""Send stop command."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def media_previous_track(self):
|
def media_previous_track(self):
|
||||||
"""Send previous track command."""
|
"""Send previous track command."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@ -536,6 +548,11 @@ class MediaPlayerDevice(Entity):
|
|||||||
"""Boolean if pause is supported."""
|
"""Boolean if pause is supported."""
|
||||||
return bool(self.supported_media_commands & SUPPORT_PAUSE)
|
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
|
@property
|
||||||
def support_seek(self):
|
def support_seek(self):
|
||||||
"""Boolean if seek is supported."""
|
"""Boolean if seek is supported."""
|
||||||
|
@ -9,17 +9,17 @@ import urllib
|
|||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
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)
|
MediaPlayerDevice)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
|
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
|
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):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
@ -229,6 +229,13 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
"""Pause the media player."""
|
"""Pause the media player."""
|
||||||
self._set_play_state(False)
|
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):
|
def _goto(self, direction):
|
||||||
"""Helper method used for previous/next track."""
|
"""Helper method used for previous/next track."""
|
||||||
players = self._get_players()
|
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
|
description: Name(s) of entities to pause on
|
||||||
example: 'media_player.living_room_sonos'
|
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:
|
media_next_track:
|
||||||
description: Send the media player the command for next track.
|
description: Send the media player the command for next track.
|
||||||
|
|
||||||
|
@ -9,12 +9,16 @@ import logging
|
|||||||
import socket
|
import socket
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
|
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE,
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
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'
|
DOMAIN = 'snapcast'
|
||||||
REQUIREMENTS = ['snapcast==1.1.1']
|
REQUIREMENTS = ['snapcast==1.2.1']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -67,9 +71,23 @@ class SnapcastDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the player."""
|
"""Return the state of the player."""
|
||||||
if self._client.connected:
|
if not self._client.connected:
|
||||||
return STATE_ON
|
|
||||||
return STATE_OFF
|
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):
|
def mute_volume(self, mute):
|
||||||
"""Send the mute command."""
|
"""Send the mute command."""
|
||||||
@ -78,3 +96,7 @@ class SnapcastDevice(MediaPlayerDevice):
|
|||||||
def set_volume_level(self, volume):
|
def set_volume_level(self, volume):
|
||||||
"""Set the volume level."""
|
"""Set the volume level."""
|
||||||
self._client.volume = round(volume * 100)
|
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_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE,
|
||||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF,
|
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF,
|
||||||
SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
|
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.event import track_state_change
|
||||||
from homeassistant.helpers.service import call_from_config
|
from homeassistant.helpers.service import call_from_config
|
||||||
|
|
||||||
@ -384,6 +385,10 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
|||||||
"""Send pause command."""
|
"""Send pause command."""
|
||||||
self._call_service(SERVICE_MEDIA_PAUSE)
|
self._call_service(SERVICE_MEDIA_PAUSE)
|
||||||
|
|
||||||
|
def media_stop(self):
|
||||||
|
"""Send stop command."""
|
||||||
|
self._call_service(SERVICE_MEDIA_STOP)
|
||||||
|
|
||||||
def media_previous_track(self):
|
def media_previous_track(self):
|
||||||
"""Send previous track command."""
|
"""Send previous track command."""
|
||||||
self._call_service(SERVICE_MEDIA_PREVIOUS_TRACK)
|
self._call_service(SERVICE_MEDIA_PREVIOUS_TRACK)
|
||||||
|
@ -69,7 +69,7 @@ def setup_tv(host, hass, add_devices):
|
|||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
'Connected to LG WebOS TV at %s but not paired.', host)
|
'Connected to LG WebOS TV at %s but not paired.', host)
|
||||||
return
|
return
|
||||||
except ConnectionRefusedError:
|
except OSError:
|
||||||
_LOGGER.error('Unable to connect to host %s.', host)
|
_LOGGER.error('Unable to connect to host %s.', host)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
@ -158,7 +158,7 @@ class LgWebOSDevice(MediaPlayerDevice):
|
|||||||
if source['appId'] == self._current_source_id:
|
if source['appId'] == self._current_source_id:
|
||||||
self._current_source = source['label']
|
self._current_source = source['label']
|
||||||
|
|
||||||
except ConnectionRefusedError:
|
except OSError:
|
||||||
self._state = STATE_OFF
|
self._state = STATE_OFF
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -208,6 +208,7 @@ class LgWebOSDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
"""Turn off media player."""
|
"""Turn off media player."""
|
||||||
|
self._state = STATE_OFF
|
||||||
self._client.power_off()
|
self._client.power_off()
|
||||||
|
|
||||||
def volume_up(self):
|
def volume_up(self):
|
||||||
|
@ -18,11 +18,19 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
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):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Yamaha platform."""
|
"""Setup the Yamaha platform."""
|
||||||
import rxv
|
import rxv
|
||||||
add_devices(YamahaDevice(config.get("name"), receiver)
|
|
||||||
|
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())
|
for receiver in rxv.find())
|
||||||
|
|
||||||
|
|
||||||
@ -30,7 +38,8 @@ class YamahaDevice(MediaPlayerDevice):
|
|||||||
"""Representation of a Yamaha device."""
|
"""Representation of a Yamaha device."""
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods, abstract-method
|
# 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."""
|
"""Initialize the Yamaha Receiver."""
|
||||||
self._receiver = receiver
|
self._receiver = receiver
|
||||||
self._muted = False
|
self._muted = False
|
||||||
@ -38,6 +47,9 @@ class YamahaDevice(MediaPlayerDevice):
|
|||||||
self._pwstate = STATE_OFF
|
self._pwstate = STATE_OFF
|
||||||
self._current_source = None
|
self._current_source = None
|
||||||
self._source_list = None
|
self._source_list = None
|
||||||
|
self._source_ignore = source_ignore
|
||||||
|
self._source_names = source_names
|
||||||
|
self._reverse_mapping = None
|
||||||
self.update()
|
self.update()
|
||||||
self._name = name
|
self._name = name
|
||||||
|
|
||||||
@ -49,8 +61,23 @@ class YamahaDevice(MediaPlayerDevice):
|
|||||||
self._pwstate = STATE_OFF
|
self._pwstate = STATE_OFF
|
||||||
self._muted = self._receiver.mute
|
self._muted = self._receiver.mute
|
||||||
self._volume = (self._receiver.volume / 100) + 1
|
self._volume = (self._receiver.volume / 100) + 1
|
||||||
self._current_source = self._receiver.input
|
|
||||||
self._source_list = list(self._receiver.inputs().keys())
|
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
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -108,4 +135,5 @@ class YamahaDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
def select_source(self, source):
|
def select_source(self, source):
|
||||||
"""Select input 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_USERNAME = 'username'
|
||||||
CONF_PASSWORD = 'password'
|
CONF_PASSWORD = 'password'
|
||||||
CONF_CERTIFICATE = 'certificate'
|
CONF_CERTIFICATE = 'certificate'
|
||||||
|
CONF_CLIENT_KEY = 'client_key'
|
||||||
|
CONF_CLIENT_CERT = 'client_cert'
|
||||||
|
CONF_TLS_INSECURE = 'tls_insecure'
|
||||||
CONF_PROTOCOL = 'protocol'
|
CONF_PROTOCOL = 'protocol'
|
||||||
|
|
||||||
CONF_STATE_TOPIC = 'state_topic'
|
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]))
|
_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2]))
|
||||||
_HBMQTT_CONFIG_SCHEMA = vol.Schema(dict)
|
_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({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
vol.Optional(CONF_CLIENT_ID): cv.string,
|
vol.Optional(CONF_CLIENT_ID): cv.string,
|
||||||
@ -89,6 +95,11 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
vol.Optional(CONF_USERNAME): cv.string,
|
vol.Optional(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_PASSWORD): cv.string,
|
vol.Optional(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_CERTIFICATE): cv.isfile,
|
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.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL):
|
||||||
vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])),
|
vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])),
|
||||||
vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA,
|
vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA,
|
||||||
@ -192,20 +203,26 @@ def setup(hass, config):
|
|||||||
|
|
||||||
broker_config = _setup_server(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
|
# Only auto config if no server config was passed in
|
||||||
if broker_config and CONF_EMBEDDED not in conf:
|
if broker_config and CONF_EMBEDDED not in conf:
|
||||||
broker, port, username, password, certificate, protocol = broker_config
|
broker, port, username, password, certificate, protocol = broker_config
|
||||||
elif not broker_config and (CONF_EMBEDDED in conf or
|
# Embedded broker doesn't have some ssl variables
|
||||||
CONF_BROKER not in conf):
|
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.')
|
_LOGGER.error('Unable to start broker and auto-configure MQTT.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if CONF_BROKER in conf:
|
if broker_in_conf:
|
||||||
broker = conf[CONF_BROKER]
|
broker = conf[CONF_BROKER]
|
||||||
port = conf[CONF_PORT]
|
port = conf[CONF_PORT]
|
||||||
username = conf.get(CONF_USERNAME)
|
username = conf.get(CONF_USERNAME)
|
||||||
password = conf.get(CONF_PASSWORD)
|
password = conf.get(CONF_PASSWORD)
|
||||||
certificate = conf.get(CONF_CERTIFICATE)
|
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]
|
protocol = conf[CONF_PROTOCOL]
|
||||||
|
|
||||||
# For cloudmqtt.com, secured connection, auto fill in certificate
|
# For cloudmqtt.com, secured connection, auto fill in certificate
|
||||||
@ -216,8 +233,9 @@ def setup(hass, config):
|
|||||||
|
|
||||||
global MQTT_CLIENT
|
global MQTT_CLIENT
|
||||||
try:
|
try:
|
||||||
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username,
|
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive,
|
||||||
password, certificate, protocol)
|
username, password, certificate, client_key,
|
||||||
|
client_cert, tls_insecure, protocol)
|
||||||
except socket.error:
|
except socket.error:
|
||||||
_LOGGER.exception("Can't connect to the broker. "
|
_LOGGER.exception("Can't connect to the broker. "
|
||||||
"Please check your settings and the broker "
|
"Please check your settings and the broker "
|
||||||
@ -268,7 +286,8 @@ class MQTT(object):
|
|||||||
"""Home Assistant MQTT client."""
|
"""Home Assistant MQTT client."""
|
||||||
|
|
||||||
def __init__(self, hass, broker, port, client_id, keepalive, username,
|
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."""
|
"""Initialize Home Assistant MQTT client."""
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
@ -288,8 +307,13 @@ class MQTT(object):
|
|||||||
|
|
||||||
if username is not None:
|
if username is not None:
|
||||||
self._mqttc.username_pw_set(username, password)
|
self._mqttc.username_pw_set(username, password)
|
||||||
|
|
||||||
if certificate is not None:
|
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_subscribe = self._mqtt_on_subscribe
|
||||||
self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe
|
self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe
|
||||||
|
@ -12,7 +12,7 @@ import threading
|
|||||||
from homeassistant.components.mqtt import PROTOCOL_311
|
from homeassistant.components.mqtt import PROTOCOL_311
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
|
|
||||||
REQUIREMENTS = ['hbmqtt==0.6.3']
|
REQUIREMENTS = ['hbmqtt==0.7.1']
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,10 +8,12 @@ import logging
|
|||||||
import socket
|
import socket
|
||||||
|
|
||||||
import homeassistant.bootstrap as bootstrap
|
import homeassistant.bootstrap as bootstrap
|
||||||
from homeassistant.const import (ATTR_DISCOVERED, ATTR_SERVICE,
|
from homeassistant.const import (ATTR_BATTERY_LEVEL, ATTR_DISCOVERED,
|
||||||
CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START,
|
ATTR_SERVICE, CONF_OPTIMISTIC,
|
||||||
|
EVENT_HOMEASSISTANT_START,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
EVENT_PLATFORM_DISCOVERED, TEMP_CELSIUS)
|
EVENT_PLATFORM_DISCOVERED, STATE_OFF,
|
||||||
|
STATE_ON, TEMP_CELSIUS)
|
||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
|
|
||||||
CONF_GATEWAYS = 'gateways'
|
CONF_GATEWAYS = 'gateways'
|
||||||
@ -170,6 +172,8 @@ def pf_callback_factory(map_sv_types, devices, add_devices, entity_class):
|
|||||||
class GatewayWrapper(object):
|
class GatewayWrapper(object):
|
||||||
"""Gateway wrapper class."""
|
"""Gateway wrapper class."""
|
||||||
|
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
def __init__(self, gateway, version, optimistic):
|
def __init__(self, gateway, version, optimistic):
|
||||||
"""Setup class attributes on instantiation.
|
"""Setup class attributes on instantiation.
|
||||||
|
|
||||||
@ -182,14 +186,12 @@ class GatewayWrapper(object):
|
|||||||
_wrapped_gateway (mysensors.SerialGateway): Wrapped gateway.
|
_wrapped_gateway (mysensors.SerialGateway): Wrapped gateway.
|
||||||
version (str): Version of mysensors API.
|
version (str): Version of mysensors API.
|
||||||
platform_callbacks (list): Callback functions, one per platform.
|
platform_callbacks (list): Callback functions, one per platform.
|
||||||
const (module): Mysensors API constants.
|
|
||||||
optimistic (bool): Send values to actuators without feedback state.
|
optimistic (bool): Send values to actuators without feedback state.
|
||||||
__initialised (bool): True if GatewayWrapper is initialised.
|
__initialised (bool): True if GatewayWrapper is initialised.
|
||||||
"""
|
"""
|
||||||
self._wrapped_gateway = gateway
|
self._wrapped_gateway = gateway
|
||||||
self.version = version
|
self.version = version
|
||||||
self.platform_callbacks = []
|
self.platform_callbacks = []
|
||||||
self.const = self.get_const()
|
|
||||||
self.optimistic = optimistic
|
self.optimistic = optimistic
|
||||||
self.__initialised = True
|
self.__initialised = True
|
||||||
|
|
||||||
@ -197,9 +199,9 @@ class GatewayWrapper(object):
|
|||||||
"""See if this object has attribute name."""
|
"""See if this object has attribute name."""
|
||||||
# Do not use hasattr, it goes into infinite recurrsion
|
# Do not use hasattr, it goes into infinite recurrsion
|
||||||
if name in self.__dict__:
|
if name in self.__dict__:
|
||||||
# this object has it
|
# This object has the attribute.
|
||||||
return getattr(self, name)
|
return getattr(self, name)
|
||||||
# proxy to the wrapped object
|
# The wrapped object has the attribute.
|
||||||
return getattr(self._wrapped_gateway, name)
|
return getattr(self._wrapped_gateway, name)
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
def __setattr__(self, name, value):
|
||||||
@ -211,14 +213,6 @@ class GatewayWrapper(object):
|
|||||||
else:
|
else:
|
||||||
object.__setattr__(self._wrapped_gateway, name, value)
|
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):
|
def callback_factory(self):
|
||||||
"""Return a new callback function."""
|
"""Return a new callback function."""
|
||||||
def node_update(update_type, node_id):
|
def node_update(update_type, node_id):
|
||||||
@ -228,3 +222,99 @@ class GatewayWrapper(object):
|
|||||||
callback(self, node_id)
|
callback(self, node_id)
|
||||||
|
|
||||||
return node_update
|
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'
|
CONF_SENDER = 'sender'
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = ['messagebird==1.1.1']
|
REQUIREMENTS = ['messagebird==1.2.0']
|
||||||
|
|
||||||
|
|
||||||
def is_valid_sender(sender):
|
def is_valid_sender(sender):
|
||||||
|
@ -11,7 +11,7 @@ from homeassistant.components.notify import (
|
|||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = ['pushbullet.py==0.9.0']
|
REQUIREMENTS = ['pushbullet.py==0.10.0']
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
|
@ -10,7 +10,7 @@ from homeassistant.components.notify import DOMAIN, BaseNotificationService
|
|||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
|
|
||||||
REQUIREMENTS = ['slacker==0.6.8']
|
REQUIREMENTS = ['slacker==0.9.10']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ class SlackNotificationService(BaseNotificationService):
|
|||||||
"""Send a message to a user."""
|
"""Send a message to a user."""
|
||||||
import slacker
|
import slacker
|
||||||
|
|
||||||
channel = kwargs.get('channel', self._default_channel)
|
channel = kwargs.get('target', self._default_channel)
|
||||||
try:
|
try:
|
||||||
self.slack.chat.post_message(channel, message)
|
self.slack.chat.post_message(channel, message)
|
||||||
except slacker.Error:
|
except slacker.Error:
|
||||||
|
@ -14,7 +14,7 @@ from homeassistant.helpers import validate_config
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['python-telegram-bot==3.4']
|
REQUIREMENTS = ['python-telegram-bot==4.0.1']
|
||||||
|
|
||||||
|
|
||||||
def get_service(hass, config):
|
def get_service(hass, config):
|
||||||
|
@ -11,7 +11,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
|||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = ['TwitterAPI==2.3.6']
|
REQUIREMENTS = ['TwitterAPI==2.4.1']
|
||||||
|
|
||||||
CONF_CONSUMER_KEY = "consumer_key"
|
CONF_CONSUMER_KEY = "consumer_key"
|
||||||
CONF_CONSUMER_SECRET = "consumer_secret"
|
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.const import (CONF_HOST, CONF_NAME)
|
||||||
from homeassistant.helpers import validate_config
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -35,7 +39,7 @@ def get_service(hass, config):
|
|||||||
except PyLGTVPairException:
|
except PyLGTVPairException:
|
||||||
_LOGGER.error('Pairing failed.')
|
_LOGGER.error('Pairing failed.')
|
||||||
return None
|
return None
|
||||||
except ConnectionRefusedError:
|
except OSError:
|
||||||
_LOGGER.error('Host unreachable.')
|
_LOGGER.error('Host unreachable.')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -58,5 +62,5 @@ class LgWebOSNotificationService(BaseNotificationService):
|
|||||||
self._client.send_message(message)
|
self._client.send_message(message)
|
||||||
except PyLGTVPairException:
|
except PyLGTVPairException:
|
||||||
_LOGGER.error('Pairing failed.')
|
_LOGGER.error('Pairing failed.')
|
||||||
except ConnectionRefusedError:
|
except OSError:
|
||||||
_LOGGER.error('Host unreachable.')
|
_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/
|
https://home-assistant.io/components/rfxtrx/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.helpers.entity import Entity
|
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']
|
REQUIREMENTS = ['pyRFXtrx==0.6.5']
|
||||||
|
|
||||||
DOMAIN = "rfxtrx"
|
DOMAIN = "rfxtrx"
|
||||||
|
|
||||||
|
DEFAULT_SIGNAL_REPETITIONS = 1
|
||||||
|
|
||||||
ATTR_AUTOMATIC_ADD = 'automatic_add'
|
ATTR_AUTOMATIC_ADD = 'automatic_add'
|
||||||
ATTR_DEVICE = 'device'
|
ATTR_DEVICE = 'device'
|
||||||
ATTR_DEBUG = 'debug'
|
ATTR_DEBUG = 'debug'
|
||||||
ATTR_STATE = 'state'
|
ATTR_STATE = 'state'
|
||||||
ATTR_NAME = 'name'
|
ATTR_NAME = 'name'
|
||||||
ATTR_PACKETID = 'packetid'
|
|
||||||
ATTR_FIREEVENT = 'fire_event'
|
ATTR_FIREEVENT = 'fire_event'
|
||||||
ATTR_DATA_TYPE = 'data_type'
|
ATTR_DATA_TYPE = 'data_type'
|
||||||
ATTR_DUMMY = 'dummy'
|
ATTR_DUMMY = 'dummy'
|
||||||
CONF_SIGNAL_REPETITIONS = 'signal_repetitions'
|
CONF_SIGNAL_REPETITIONS = 'signal_repetitions'
|
||||||
CONF_DEVICES = 'devices'
|
CONF_DEVICES = 'devices'
|
||||||
DEFAULT_SIGNAL_REPETITIONS = 1
|
|
||||||
|
|
||||||
EVENT_BUTTON_PRESSED = 'button_pressed'
|
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 = []
|
RECEIVED_EVT_SUBSCRIBERS = []
|
||||||
RFX_DEVICES = {}
|
RFX_DEVICES = {}
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
RFXOBJECT = None
|
RFXOBJECT = None
|
||||||
|
|
||||||
|
|
||||||
def validate_packetid(value):
|
def _valid_device(value, device_type):
|
||||||
"""Validate that value is a valid packet id for rfxtrx."""
|
"""Validate a dictionary of devices definitions."""
|
||||||
if get_rfx_object(value):
|
config = OrderedDict()
|
||||||
return value
|
for key, device in value.items():
|
||||||
else:
|
|
||||||
raise vol.Invalid('invalid packet id for {}'.format(value))
|
|
||||||
|
|
||||||
# Share between rfxtrx platforms
|
# Still accept old configuration
|
||||||
VALID_DEVICE_ID = vol.All(cv.string, vol.Lower)
|
if 'packetid' in device.keys():
|
||||||
VALID_SENSOR_DEVICE_ID = vol.All(VALID_DEVICE_ID,
|
msg = 'You are using an outdated configuration of the rfxtrx ' +\
|
||||||
vol.truth(lambda val:
|
'device, {}.'.format(key) +\
|
||||||
val.startswith('sensor_')))
|
' 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({
|
DEVICE_SCHEMA = vol.Schema({
|
||||||
vol.Required(ATTR_NAME): cv.string,
|
vol.Required(ATTR_NAME): cv.string,
|
||||||
vol.Required(ATTR_PACKETID): validate_packetid,
|
|
||||||
vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean,
|
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({
|
DEFAULT_SCHEMA = vol.Schema({
|
||||||
vol.Required("platform"): DOMAIN,
|
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(ATTR_AUTOMATIC_ADD, default=False): cv.boolean,
|
||||||
vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS):
|
vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS):
|
||||||
vol.Coerce(int),
|
vol.Coerce(int),
|
||||||
@ -82,11 +125,7 @@ def setup(hass, config):
|
|||||||
# Log RFXCOM event
|
# Log RFXCOM event
|
||||||
if not event.device.id_string:
|
if not event.device.id_string:
|
||||||
return
|
return
|
||||||
entity_id = slugify(event.device.id_string.lower())
|
_LOGGER.info("Receive RFXCOM event from %s", event.device)
|
||||||
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)
|
|
||||||
|
|
||||||
# Callback to HA registered components.
|
# Callback to HA registered components.
|
||||||
for subscriber in RECEIVED_EVT_SUBSCRIBERS:
|
for subscriber in RECEIVED_EVT_SUBSCRIBERS:
|
||||||
@ -121,27 +160,26 @@ def get_rfx_object(packetid):
|
|||||||
import RFXtrx as rfxtrxmod
|
import RFXtrx as rfxtrxmod
|
||||||
|
|
||||||
binarypacket = bytearray.fromhex(packetid)
|
binarypacket = bytearray.fromhex(packetid)
|
||||||
|
|
||||||
pkt = rfxtrxmod.lowlevel.parse(binarypacket)
|
pkt = rfxtrxmod.lowlevel.parse(binarypacket)
|
||||||
if pkt is not None:
|
if pkt is None:
|
||||||
|
return None
|
||||||
if isinstance(pkt, rfxtrxmod.lowlevel.SensorPacket):
|
if isinstance(pkt, rfxtrxmod.lowlevel.SensorPacket):
|
||||||
obj = rfxtrxmod.SensorEvent(pkt)
|
obj = rfxtrxmod.SensorEvent(pkt)
|
||||||
elif isinstance(pkt, rfxtrxmod.lowlevel.Status):
|
elif isinstance(pkt, rfxtrxmod.lowlevel.Status):
|
||||||
obj = rfxtrxmod.StatusEvent(pkt)
|
obj = rfxtrxmod.StatusEvent(pkt)
|
||||||
else:
|
else:
|
||||||
obj = rfxtrxmod.ControlEvent(pkt)
|
obj = rfxtrxmod.ControlEvent(pkt)
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_devices_from_config(config, device):
|
def get_devices_from_config(config, device):
|
||||||
"""Read rfxtrx configuration."""
|
"""Read rfxtrx configuration."""
|
||||||
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
|
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
|
||||||
|
|
||||||
devices = []
|
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:
|
if device_id in RFX_DEVICES:
|
||||||
continue
|
continue
|
||||||
_LOGGER.info("Add %s rfxtrx", entity_info[ATTR_NAME])
|
_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]
|
fire_event = entity_info[ATTR_FIREEVENT]
|
||||||
datas = {ATTR_STATE: False, ATTR_FIREEVENT: fire_event}
|
datas = {ATTR_STATE: False, ATTR_FIREEVENT: fire_event}
|
||||||
|
|
||||||
rfxobject = get_rfx_object(entity_info[ATTR_PACKETID])
|
new_device = device(entity_info[ATTR_NAME], event, datas,
|
||||||
new_device = device(entity_info[ATTR_NAME], rfxobject, datas,
|
|
||||||
signal_repetitions)
|
signal_repetitions)
|
||||||
RFX_DEVICES[device_id] = new_device
|
RFX_DEVICES[device_id] = new_device
|
||||||
devices.append(new_device)
|
devices.append(new_device)
|
||||||
@ -161,9 +198,10 @@ def get_devices_from_config(config, device):
|
|||||||
def get_new_device(event, config, device):
|
def get_new_device(event, config, device):
|
||||||
"""Add entity if not exist and the automatic_add is True."""
|
"""Add entity if not exist and the automatic_add is True."""
|
||||||
device_id = slugify(event.device.id_string.lower())
|
device_id = slugify(event.device.id_string.lower())
|
||||||
if device_id not in RFX_DEVICES:
|
if device_id in RFX_DEVICES:
|
||||||
automatic_add = config[ATTR_AUTOMATIC_ADD]
|
return
|
||||||
if not automatic_add:
|
|
||||||
|
if not config[ATTR_AUTOMATIC_ADD]:
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
@ -173,10 +211,9 @@ def get_new_device(event, config, device):
|
|||||||
event.device.subtype
|
event.device.subtype
|
||||||
)
|
)
|
||||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
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}
|
datas = {ATTR_STATE: False, ATTR_FIREEVENT: False}
|
||||||
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
|
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
|
||||||
new_device = device(entity_name, event, datas,
|
new_device = device(pkt_id, event, datas,
|
||||||
signal_repetitions)
|
signal_repetitions)
|
||||||
RFX_DEVICES[device_id] = new_device
|
RFX_DEVICES[device_id] = new_device
|
||||||
return new_device
|
return new_device
|
||||||
@ -186,7 +223,9 @@ def apply_received_command(event):
|
|||||||
"""Apply command from rfxtrx."""
|
"""Apply command from rfxtrx."""
|
||||||
device_id = slugify(event.device.id_string.lower())
|
device_id = slugify(event.device.id_string.lower())
|
||||||
# Check if entity exists or previously added automatically
|
# Check if entity exists or previously added automatically
|
||||||
if device_id in RFX_DEVICES:
|
if device_id not in RFX_DEVICES:
|
||||||
|
return
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"EntityID: %s device_update. Command: %s",
|
"EntityID: %s device_update. Command: %s",
|
||||||
device_id,
|
device_id,
|
||||||
@ -198,20 +237,15 @@ def apply_received_command(event):
|
|||||||
|
|
||||||
# Update the rfxtrx device state
|
# Update the rfxtrx device state
|
||||||
is_on = event.values['Command'] == 'On'
|
is_on = event.values['Command'] == 'On'
|
||||||
# pylint: disable=protected-access
|
RFX_DEVICES[device_id].update_state(is_on)
|
||||||
RFX_DEVICES[device_id]._state = is_on
|
|
||||||
RFX_DEVICES[device_id].update_ha_state()
|
|
||||||
|
|
||||||
elif hasattr(RFX_DEVICES[device_id], 'brightness')\
|
elif hasattr(RFX_DEVICES[device_id], 'brightness')\
|
||||||
and event.values['Command'] == 'Set level':
|
and event.values['Command'] == 'Set level':
|
||||||
# pylint: disable=protected-access
|
_brightness = (event.values['Dim level'] * 255 // 100)
|
||||||
RFX_DEVICES[device_id]._brightness = \
|
|
||||||
(event.values['Dim level'] * 255 // 100)
|
|
||||||
|
|
||||||
# Update the rfxtrx device state
|
# Update the rfxtrx device state
|
||||||
is_on = RFX_DEVICES[device_id]._brightness > 0
|
is_on = _brightness > 0
|
||||||
RFX_DEVICES[device_id]._state = is_on
|
RFX_DEVICES[device_id].update_state(is_on, _brightness)
|
||||||
RFX_DEVICES[device_id].update_ha_state()
|
|
||||||
|
|
||||||
# Fire event
|
# Fire event
|
||||||
if RFX_DEVICES[device_id].should_fire_event:
|
if RFX_DEVICES[device_id].should_fire_event:
|
||||||
@ -227,17 +261,17 @@ def apply_received_command(event):
|
|||||||
class RfxtrxDevice(Entity):
|
class RfxtrxDevice(Entity):
|
||||||
"""Represents a Rfxtrx device.
|
"""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):
|
def __init__(self, name, event, datas, signal_repetitions):
|
||||||
"""Initialize the device."""
|
"""Initialize the device."""
|
||||||
|
self.signal_repetitions = signal_repetitions
|
||||||
self._name = name
|
self._name = name
|
||||||
self._event = event
|
self._event = event
|
||||||
self._state = datas[ATTR_STATE]
|
self._state = datas[ATTR_STATE]
|
||||||
self._should_fire_event = datas[ATTR_FIREEVENT]
|
self._should_fire_event = datas[ATTR_FIREEVENT]
|
||||||
self.signal_repetitions = signal_repetitions
|
|
||||||
self._brightness = 0
|
self._brightness = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -269,6 +303,12 @@ class RfxtrxDevice(Entity):
|
|||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
self._send_command("turn_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):
|
def _send_command(self, command, brightness=0):
|
||||||
if not self._event:
|
if not self._event:
|
||||||
return
|
return
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Support for command roller shutters.
|
Support for command roller shutters.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
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 logging
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -8,110 +8,43 @@ For more details about this component, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/script/
|
https://home-assistant.io/components/script/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
from datetime import timedelta
|
|
||||||
from itertools import islice
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.util.dt as date_util
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, EVENT_TIME_CHANGED, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||||
SERVICE_TOGGLE, STATE_ON)
|
SERVICE_TOGGLE, STATE_ON, CONF_ALIAS)
|
||||||
from homeassistant.helpers.entity import ToggleEntity, split_entity_id
|
from homeassistant.helpers.entity import ToggleEntity, split_entity_id
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
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
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
from homeassistant.helpers.script import Script
|
||||||
|
|
||||||
DOMAIN = "script"
|
DOMAIN = "script"
|
||||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
DEPENDENCIES = ["group"]
|
DEPENDENCIES = ["group"]
|
||||||
|
|
||||||
STATE_NOT_RUNNING = 'Not Running'
|
|
||||||
|
|
||||||
CONF_ALIAS = "alias"
|
|
||||||
CONF_SERVICE = "service"
|
|
||||||
CONF_SERVICE_DATA = "data"
|
|
||||||
CONF_SEQUENCE = "sequence"
|
CONF_SEQUENCE = "sequence"
|
||||||
CONF_EVENT = "event"
|
|
||||||
CONF_EVENT_DATA = "event_data"
|
|
||||||
CONF_DELAY = "delay"
|
|
||||||
|
|
||||||
|
ATTR_VARIABLES = 'variables'
|
||||||
ATTR_LAST_ACTION = 'last_action'
|
ATTR_LAST_ACTION = 'last_action'
|
||||||
ATTR_CAN_CANCEL = 'can_cancel'
|
ATTR_CAN_CANCEL = 'can_cancel'
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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({
|
_SCRIPT_ENTRY_SCHEMA = vol.Schema({
|
||||||
CONF_ALIAS: cv.string,
|
CONF_ALIAS: cv.string,
|
||||||
vol.Required(CONF_SEQUENCE): vol.All(vol.Length(min=1), [vol.Any(
|
vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA,
|
||||||
_EVENT_SCHEMA,
|
|
||||||
_DELAY_SCHEMA,
|
|
||||||
# Can't extend SERVICE_SCHEMA because it is an vol.All
|
|
||||||
_alias_stripper(cv.SERVICE_SCHEMA),
|
|
||||||
)]),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
vol.Required(DOMAIN): {cv.slug: _SCRIPT_ENTRY_SCHEMA}
|
vol.Required(DOMAIN): {cv.slug: _SCRIPT_ENTRY_SCHEMA}
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
SCRIPT_SERVICE_SCHEMA = vol.Schema({})
|
SCRIPT_SERVICE_SCHEMA = vol.Schema(dict)
|
||||||
SCRIPT_TURN_ONOFF_SCHEMA = vol.Schema({
|
SCRIPT_TURN_ONOFF_SCHEMA = vol.Schema({
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
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)
|
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."""
|
"""Turn script on."""
|
||||||
_, object_id = split_entity_id(entity_id)
|
_, 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):
|
def turn_off(hass, entity_id):
|
||||||
@ -148,11 +81,11 @@ def setup(hass, config):
|
|||||||
if script.is_on:
|
if script.is_on:
|
||||||
_LOGGER.warning("Script %s already running.", entity_id)
|
_LOGGER.warning("Script %s already running.", entity_id)
|
||||||
return
|
return
|
||||||
script.turn_on()
|
script.turn_on(variables=service.data)
|
||||||
|
|
||||||
for object_id, cfg in config[DOMAIN].items():
|
for object_id, cfg in config[DOMAIN].items():
|
||||||
alias = cfg.get(CONF_ALIAS, object_id)
|
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,))
|
component.add_entities((script,))
|
||||||
hass.services.register(DOMAIN, object_id, service_handler,
|
hass.services.register(DOMAIN, object_id, service_handler,
|
||||||
schema=SCRIPT_SERVICE_SCHEMA)
|
schema=SCRIPT_SERVICE_SCHEMA)
|
||||||
@ -160,9 +93,9 @@ def setup(hass, config):
|
|||||||
def turn_on_service(service):
|
def turn_on_service(service):
|
||||||
"""Call a service to turn script on."""
|
"""Call a service to turn script on."""
|
||||||
# We could turn on script directly here, but we only want to offer
|
# 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):
|
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):
|
def turn_off_service(service):
|
||||||
"""Cancel a script."""
|
"""Cancel a script."""
|
||||||
@ -183,21 +116,14 @@ def setup(hass, config):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Script(ToggleEntity):
|
class ScriptEntity(ToggleEntity):
|
||||||
"""Representation of a script."""
|
"""Representation of a script entity."""
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
# pylint: disable=too-many-instance-attributes
|
||||||
def __init__(self, object_id, name, sequence):
|
def __init__(self, hass, object_id, name, sequence):
|
||||||
"""Initialize the script."""
|
"""Initialize the script."""
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||||
self._name = name
|
self.script = Script(hass, sequence, name, self.update_ha_state)
|
||||||
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)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
@ -207,91 +133,27 @@ class Script(ToggleEntity):
|
|||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the entity."""
|
"""Return the name of the entity."""
|
||||||
return self._name
|
return self.script.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
attrs = {}
|
attrs = {}
|
||||||
if self._can_cancel:
|
if self.script.can_cancel:
|
||||||
attrs[ATTR_CAN_CANCEL] = self._can_cancel
|
attrs[ATTR_CAN_CANCEL] = self.script.can_cancel
|
||||||
if self._last_action:
|
if self.script.last_action:
|
||||||
attrs[ATTR_LAST_ACTION] = self._last_action
|
attrs[ATTR_LAST_ACTION] = self.script.last_action
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if script is on."""
|
"""Return true if script is on."""
|
||||||
return self._cur != -1
|
return self.script.is_running
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn the entity on."""
|
"""Turn the entity on."""
|
||||||
_LOGGER.info("Executing script %s", self._name)
|
self.script.run(kwargs.get(ATTR_VARIABLES))
|
||||||
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()
|
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn script off."""
|
"""Turn script off."""
|
||||||
_LOGGER.info("Cancelled script %s", self._name)
|
self.script.stop()
|
||||||
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
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Allows to configure custom shell commands to turn a value for a sensor.
|
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
|
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 logging
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -10,7 +10,7 @@ from homeassistant.util import Throttle
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = ['schiene==0.14']
|
REQUIREMENTS = ['schiene==0.15']
|
||||||
ICON = 'mdi:train'
|
ICON = 'mdi:train'
|
||||||
|
|
||||||
# Return cached results if last scan was less then this time ago.
|
# 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."""
|
"""Get the latest delay from bahn.de and updates the state."""
|
||||||
self.data.update()
|
self.data.update()
|
||||||
self._state = self.data.connections[0].get('departure', 'Unknown')
|
self._state = self.data.connections[0].get('departure', 'Unknown')
|
||||||
delay = self.data.connections[0].get('delay',
|
if self.data.connections[0]['delay'] != 0:
|
||||||
{'delay_departure': 0,
|
self._state += " + {}".format(
|
||||||
'delay_arrival': 0})
|
self.data.connections[0]['delay']
|
||||||
if delay['delay_departure'] != 0:
|
)
|
||||||
self._state += " + {}".format(delay['delay_departure'])
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
@ -95,6 +94,14 @@ class SchieneData(object):
|
|||||||
self.goal,
|
self.goal,
|
||||||
datetime.now())
|
datetime.now())
|
||||||
for con in self.connections:
|
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:
|
if 'details' in con:
|
||||||
con.pop('details')
|
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()
|
data.update()
|
||||||
for sensor in data.ecobee.get_remote_sensors(self.index):
|
for sensor in data.ecobee.get_remote_sensors(self.index):
|
||||||
for item in sensor['capability']:
|
for item in sensor['capability']:
|
||||||
if (
|
if (item['type'] == self.type and
|
||||||
item['type'] == self.type and
|
|
||||||
self.type == 'temperature' and
|
|
||||||
self.sensor_name == sensor['name']):
|
self.sensor_name == sensor['name']):
|
||||||
|
if (self.type == 'temperature' and
|
||||||
|
item['value'] != 'unknown'):
|
||||||
self._state = float(item['value']) / 10
|
self._state = float(item['value']) / 10
|
||||||
elif (
|
else:
|
||||||
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']
|
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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/sensor.eliqonline/
|
https://home-assistant.io/components/sensor.eliqonline/
|
||||||
@ -12,17 +12,21 @@ from homeassistant.helpers.entity import Entity
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['eliqonline==1.0.11']
|
REQUIREMENTS = ['eliqonline==1.0.12']
|
||||||
DEFAULT_NAME = "ELIQ Energy Usage"
|
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):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Eliq sensor."""
|
"""Setup the ELIQ Online sensor."""
|
||||||
import eliqonline
|
import eliqonline
|
||||||
|
|
||||||
access_token = config.get(CONF_ACCESS_TOKEN)
|
access_token = config.get(CONF_ACCESS_TOKEN)
|
||||||
name = config.get(CONF_NAME, DEFAULT_NAME)
|
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:
|
if access_token is None:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
@ -32,20 +36,27 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
api = eliqonline.API(access_token)
|
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)])
|
add_devices([EliqSensor(api, channel_id, name)])
|
||||||
|
|
||||||
|
|
||||||
class EliqSensor(Entity):
|
class EliqSensor(Entity):
|
||||||
"""Implementation of an Eliq sensor."""
|
"""Implementation of an ELIQ Online sensor."""
|
||||||
|
|
||||||
def __init__(self, api, channel_id, name):
|
def __init__(self, api, channel_id, name):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._name = name
|
self._name = name
|
||||||
self._unit_of_measurement = "W"
|
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
|
self._api = api
|
||||||
self.api = api
|
self._channel_id = channel_id
|
||||||
self.channel_id = channel_id
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -56,12 +67,12 @@ class EliqSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
"""Return icon."""
|
"""Return icon."""
|
||||||
return "mdi:speedometer"
|
return ICON
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
"""Return the unit of measurement of this entity, if any."""
|
"""Return the unit of measurement of this entity, if any."""
|
||||||
return self._unit_of_measurement
|
return UNIT_OF_MEASUREMENT
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
@ -71,7 +82,8 @@ class EliqSensor(Entity):
|
|||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest data."""
|
"""Get the latest data."""
|
||||||
try:
|
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)
|
self._state = int(response.power)
|
||||||
except (TypeError, URLError):
|
_LOGGER.debug("Updated power from server %d W", self._state)
|
||||||
_LOGGER.error("Could not connect to the eliqonline servers")
|
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
|
# Name, si unit, us unit, ca unit, uk unit, uk2 unit
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
'summary': ['Summary', None, None, None, None, None],
|
'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],
|
'icon': ['Icon', None, None, None, None, None],
|
||||||
'nearest_storm_distance': ['Nearest Storm Distance',
|
'nearest_storm_distance': ['Nearest Storm Distance',
|
||||||
'km', 'm', 'km', 'km', 'm'],
|
'km', 'm', 'km', 'km', 'm'],
|
||||||
@ -134,11 +137,20 @@ class ForeCastSensor(Entity):
|
|||||||
import forecastio
|
import forecastio
|
||||||
|
|
||||||
self.forecast_client.update()
|
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:
|
try:
|
||||||
if self.type == 'summary':
|
if self.type == 'summary':
|
||||||
self._state = data.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':
|
elif self.type == 'icon':
|
||||||
self._state = data.icon
|
self._state = data.icon
|
||||||
elif self.type == 'nearest_storm_distance':
|
elif self.type == 'nearest_storm_distance':
|
||||||
@ -198,5 +210,5 @@ class ForeCastData(object):
|
|||||||
self.latitude,
|
self.latitude,
|
||||||
self.longitude,
|
self.longitude,
|
||||||
units=self.units)
|
units=self.units)
|
||||||
self.data = forecast.currently()
|
self.data = forecast
|
||||||
self.unit_system = forecast.json['flags']['units']
|
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
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import (ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON,
|
from homeassistant.components import mysensors
|
||||||
TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.loader import get_component
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
@ -22,8 +21,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
mysensors = get_component('mysensors')
|
|
||||||
|
|
||||||
for gateway in mysensors.GATEWAYS.values():
|
for gateway in mysensors.GATEWAYS.values():
|
||||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||||
# states. Map them in a dict of lists.
|
# 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))
|
map_sv_types, devices, add_devices, MySensorsSensor))
|
||||||
|
|
||||||
|
|
||||||
class MySensorsSensor(Entity):
|
class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity):
|
||||||
"""Represent the value of a MySensors child node."""
|
"""Represent the value of a MySensors Sensor 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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
if not self._values:
|
return self._values.get(self.value_type)
|
||||||
return ''
|
|
||||||
return self._values[self.value_type]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
@ -153,50 +105,3 @@ class MySensorsSensor(Entity):
|
|||||||
set_req.V_UNIT_PREFIX]
|
set_req.V_UNIT_PREFIX]
|
||||||
unit_map.update({set_req.V_PERCENTAGE: '%'})
|
unit_map.update({set_req.V_PERCENTAGE: '%'})
|
||||||
return unit_map.get(self.value_type)
|
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/
|
https://home-assistant.io/components/sensor.rfxtrx/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from collections import OrderedDict
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.components.rfxtrx as rfxtrx
|
import homeassistant.components.rfxtrx as rfxtrx
|
||||||
from homeassistant.const import TEMP_CELSIUS
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
from homeassistant.components.rfxtrx import (
|
from homeassistant.components.rfxtrx import (
|
||||||
ATTR_AUTOMATIC_ADD, ATTR_PACKETID, ATTR_NAME,
|
ATTR_AUTOMATIC_ADD, ATTR_NAME,
|
||||||
CONF_DEVICES, ATTR_DATA_TYPE)
|
CONF_DEVICES, ATTR_DATA_TYPE, DATA_TYPES)
|
||||||
|
|
||||||
DEPENDENCIES = ['rfxtrx']
|
DEPENDENCIES = ['rfxtrx']
|
||||||
|
|
||||||
DATA_TYPES = OrderedDict([
|
|
||||||
('Temperature', TEMP_CELSIUS),
|
|
||||||
('Humidity', '%'),
|
|
||||||
('Barometer', ''),
|
|
||||||
('Wind direction', ''),
|
|
||||||
('Rain rate', ''),
|
|
||||||
('Energy usage', 'W'),
|
|
||||||
('Total usage', 'W')])
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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({
|
PLATFORM_SCHEMA = vol.Schema({
|
||||||
vol.Required("platform"): rfxtrx.DOMAIN,
|
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,
|
vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean,
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
"""Setup the RFXtrx platform."""
|
"""Setup the RFXtrx platform."""
|
||||||
|
# pylint: disable=too-many-locals
|
||||||
from RFXtrx import SensorEvent
|
from RFXtrx import SensorEvent
|
||||||
|
|
||||||
sensors = []
|
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:
|
if device_id in rfxtrx.RFX_DEVICES:
|
||||||
continue
|
continue
|
||||||
_LOGGER.info("Add %s rfxtrx.sensor", entity_info[ATTR_NAME])
|
_LOGGER.info("Add %s rfxtrx.sensor", entity_info[ATTR_NAME])
|
||||||
event = rfxtrx.get_rfx_object(entity_info[ATTR_PACKETID])
|
|
||||||
|
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],
|
new_sensor = RfxtrxSensor(event, entity_info[ATTR_NAME],
|
||||||
entity_info[ATTR_DATA_TYPE])
|
_data_type)
|
||||||
rfxtrx.RFX_DEVICES[slugify(device_id)] = new_sensor
|
|
||||||
sensors.append(new_sensor)
|
sensors.append(new_sensor)
|
||||||
|
sub_sensors[_data_type] = new_sensor
|
||||||
|
rfxtrx.RFX_DEVICES[device_id] = sub_sensors
|
||||||
|
|
||||||
add_devices_callback(sensors)
|
add_devices_callback(sensors)
|
||||||
|
|
||||||
@ -84,26 +62,28 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
device_id = "sensor_" + slugify(event.device.id_string.lower())
|
device_id = "sensor_" + slugify(event.device.id_string.lower())
|
||||||
|
|
||||||
if device_id in rfxtrx.RFX_DEVICES:
|
if device_id in rfxtrx.RFX_DEVICES:
|
||||||
rfxtrx.RFX_DEVICES[device_id].event = event
|
sensors = rfxtrx.RFX_DEVICES[device_id]
|
||||||
k = 2
|
for key in sensors:
|
||||||
_device_id = device_id + "_" + str(k)
|
sensors[key].event = event
|
||||||
while _device_id in rfxtrx.RFX_DEVICES:
|
|
||||||
rfxtrx.RFX_DEVICES[_device_id].event = event
|
|
||||||
k = k + 1
|
|
||||||
_device_id = device_id + "_" + str(k)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Add entity if not exist and the automatic_add is True
|
# Add entity if not exist and the automatic_add is True
|
||||||
if config[ATTR_AUTOMATIC_ADD]:
|
if not config[ATTR_AUTOMATIC_ADD]:
|
||||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
return
|
||||||
entity_name = "%s : %s" % (device_id, pkt_id)
|
|
||||||
_LOGGER.info(
|
|
||||||
"Automatic add rfxtrx.sensor: (%s : %s)",
|
|
||||||
device_id,
|
|
||||||
pkt_id)
|
|
||||||
|
|
||||||
new_sensor = RfxtrxSensor(event, entity_name)
|
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||||
rfxtrx.RFX_DEVICES[device_id] = new_sensor
|
_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])
|
add_devices_callback([new_sensor])
|
||||||
|
|
||||||
if sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
if sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||||
@ -113,21 +93,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
class RfxtrxSensor(Entity):
|
class RfxtrxSensor(Entity):
|
||||||
"""Representation of a RFXtrx sensor."""
|
"""Representation of a RFXtrx sensor."""
|
||||||
|
|
||||||
def __init__(self, event, name, data_type=None):
|
def __init__(self, event, name, data_type):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self.event = event
|
self.event = event
|
||||||
self._unit_of_measurement = None
|
|
||||||
self._data_type = None
|
|
||||||
self._name = name
|
self._name = name
|
||||||
if data_type:
|
if data_type not in DATA_TYPES:
|
||||||
self._data_type = data_type
|
data_type = "Unknown"
|
||||||
|
self.data_type = data_type
|
||||||
self._unit_of_measurement = DATA_TYPES[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
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
@ -136,8 +109,8 @@ class RfxtrxSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
if self._data_type:
|
if self.data_type:
|
||||||
return self.event.values[self._data_type]
|
return self.event.values[self.data_type]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -47,7 +47,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
(product_id not in wanted_product_ids):
|
(product_id not in wanted_product_ids):
|
||||||
continue
|
continue
|
||||||
dev.append(UberSensor('time', timeandpriceest, product_id, product))
|
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,
|
dev.append(UberSensor('price', timeandpriceest,
|
||||||
product_id, product))
|
product_id, product))
|
||||||
add_devices(dev)
|
add_devices(dev)
|
||||||
|
@ -65,6 +65,11 @@ class VerisureThermometer(Entity):
|
|||||||
# Remove ° character
|
# Remove ° character
|
||||||
return hub.climate_status[self._id].temperature[:-1]
|
return hub.climate_status[self._id].temperature[:-1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return hub.available
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
"""Return the unit of measurement of this entity."""
|
"""Return the unit of measurement of this entity."""
|
||||||
@ -95,6 +100,11 @@ class VerisureHygrometer(Entity):
|
|||||||
# remove % character
|
# remove % character
|
||||||
return hub.climate_status[self._id].humidity[:-1]
|
return hub.climate_status[self._id].humidity[:-1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return hub.available
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
"""Return the unit of measurement of this sensor."""
|
"""Return the unit of measurement of this sensor."""
|
||||||
@ -124,6 +134,11 @@ class VerisureMouseDetection(Entity):
|
|||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return hub.mouse_status[self._id].count
|
return hub.mouse_status[self._id].count
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return hub.available
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
"""Return the unit of measurement of this sensor."""
|
"""Return the unit of measurement of this sensor."""
|
||||||
|
@ -7,10 +7,11 @@ at https://home-assistant.io/components/sensor.wink/
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import (CONF_ACCESS_TOKEN, STATE_CLOSED,
|
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
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
REQUIREMENTS = ['python-wink==0.7.4']
|
REQUIREMENTS = ['python-wink==0.7.6']
|
||||||
|
|
||||||
SENSOR_TYPES = ['temperature', 'humidity']
|
SENSOR_TYPES = ['temperature', 'humidity']
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ class WinkSensorDevice(Entity):
|
|||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self.wink = wink
|
self.wink = wink
|
||||||
self.capability = self.wink.capability()
|
self.capability = self.wink.capability()
|
||||||
|
self._battery = self.wink.battery_level
|
||||||
if self.wink.UNIT == "°":
|
if self.wink.UNIT == "°":
|
||||||
self._unit_of_measurement = TEMP_CELSIUS
|
self._unit_of_measurement = TEMP_CELSIUS
|
||||||
else:
|
else:
|
||||||
@ -88,6 +90,19 @@ class WinkSensorDevice(Entity):
|
|||||||
"""Return true if door is open."""
|
"""Return true if door is open."""
|
||||||
return self.wink.state()
|
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):
|
class WinkEggMinder(Entity):
|
||||||
"""Representation of a Wink Egg Minder."""
|
"""Representation of a Wink Egg Minder."""
|
||||||
@ -95,6 +110,7 @@ class WinkEggMinder(Entity):
|
|||||||
def __init__(self, wink):
|
def __init__(self, wink):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self.wink = wink
|
self.wink = wink
|
||||||
|
self._battery = self.wink.battery_level
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
@ -114,3 +130,16 @@ class WinkEggMinder(Entity):
|
|||||||
def update(self):
|
def update(self):
|
||||||
"""Update state of the Egg Minder."""
|
"""Update state of the Egg Minder."""
|
||||||
self.wink.update_state()
|
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."""
|
"""Weather symbol if type is symbol."""
|
||||||
if self.type != 'symbol':
|
if self.type != 'symbol':
|
||||||
return None
|
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)
|
"?symbol={0};content_type=image/png".format(self._state)
|
||||||
|
|
||||||
@property
|
@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.
|
Support for custom shell commands to turn a switch on/off.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
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 logging
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -6,9 +6,9 @@ https://home-assistant.io/components/switch.mysensors/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components import mysensors
|
||||||
from homeassistant.components.switch import SwitchDevice
|
from homeassistant.components.switch import SwitchDevice
|
||||||
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
from homeassistant.loader import get_component
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
@ -21,8 +21,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
mysensors = get_component('mysensors')
|
|
||||||
|
|
||||||
for gateway in mysensors.GATEWAYS.values():
|
for gateway in mysensors.GATEWAYS.values():
|
||||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||||
# states. Map them in a dict of lists.
|
# 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))
|
map_sv_types, devices, add_devices, MySensorsSwitch))
|
||||||
|
|
||||||
|
|
||||||
class MySensorsSwitch(SwitchDevice):
|
class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice):
|
||||||
"""Representation of the value of a MySensors child node."""
|
"""Representation of the value of a MySensors Switch 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
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
@ -148,28 +77,7 @@ class MySensorsSwitch(SwitchDevice):
|
|||||||
self._values[self.value_type] = STATE_OFF
|
self._values[self.value_type] = STATE_OFF
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self):
|
|
||||||
"""Return True if entity is available."""
|
|
||||||
return self.value_type in self._values
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def assumed_state(self):
|
def assumed_state(self):
|
||||||
"""Return True if unable to access real state of entity."""
|
"""Return True if unable to access real state of entity."""
|
||||||
return self.gateway.optimistic
|
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 logging
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import homeassistant.util as util
|
||||||
from homeassistant.components.switch import SwitchDevice
|
from homeassistant.components.switch import SwitchDevice
|
||||||
from homeassistant.util import convert
|
from homeassistant.util import convert
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
_PULSEAUDIO_SERVERS = {}
|
||||||
|
|
||||||
DEFAULT_NAME = "paloopback"
|
DEFAULT_NAME = "paloopback"
|
||||||
DEFAULT_HOST = "localhost"
|
DEFAULT_HOST = "localhost"
|
||||||
DEFAULT_PORT = 4712
|
DEFAULT_PORT = 4712
|
||||||
DEFAULT_BUFFER_SIZE = 1024
|
DEFAULT_BUFFER_SIZE = 1024
|
||||||
DEFAULT_TCP_TIMEOUT = 3
|
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}"
|
LOAD_CMD = "load-module module-loopback sink={0} source={1}"
|
||||||
UNLOAD_CMD = "unload-module {0}"
|
UNLOAD_CMD = "unload-module {0}"
|
||||||
MOD_REGEX = r"index: ([0-9]+)\s+name: <module-loopback>" \
|
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
|
# 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")
|
_LOGGER.error("Missing required variable: source_name")
|
||||||
return False
|
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(
|
add_devices_callback([PALoopbackSwitch(
|
||||||
hass,
|
hass,
|
||||||
convert(config.get('name'), str, DEFAULT_NAME),
|
name,
|
||||||
convert(config.get('host'), str, DEFAULT_HOST),
|
server,
|
||||||
convert(config.get('port'), int, DEFAULT_PORT),
|
sink_name,
|
||||||
convert(config.get('buffer_size'), int, DEFAULT_BUFFER_SIZE),
|
source_name
|
||||||
convert(config.get('tcp_timeout'), int, DEFAULT_TCP_TIMEOUT),
|
|
||||||
config.get('sink_name'),
|
|
||||||
config.get('source_name')
|
|
||||||
)])
|
)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
class PAServer():
|
||||||
class PALoopbackSwitch(SwitchDevice):
|
"""Represents a pulseaudio server."""
|
||||||
"""Represents the presence or absence of a pa loopback module."""
|
|
||||||
|
|
||||||
def __init__(self, hass, name, pa_host, pa_port, buff_sz,
|
_current_module_state = ""
|
||||||
tcp_timeout, sink_name, source_name):
|
|
||||||
"""Initialize the switch."""
|
def __init__(self, host, port, buff_sz, tcp_timeout):
|
||||||
self._module_idx = -1
|
"""Simple constructor for reading in our configuration."""
|
||||||
self._hass = hass
|
self._pa_host = host
|
||||||
self._name = name
|
self._pa_port = int(port)
|
||||||
self._pa_host = pa_host
|
|
||||||
self._pa_port = int(pa_port)
|
|
||||||
self._sink_name = sink_name
|
|
||||||
self._source_name = source_name
|
|
||||||
self._buffer_size = int(buff_sz)
|
self._buffer_size = int(buff_sz)
|
||||||
self._tcp_timeout = int(tcp_timeout)
|
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):
|
def _send_command(self, cmd, response_expected):
|
||||||
"""Send a command to the pa server using a socket."""
|
"""Send a command to the pa server using a socket."""
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
@ -103,29 +111,82 @@ class PALoopbackSwitch(SwitchDevice):
|
|||||||
|
|
||||||
return result
|
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):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
self._send_command(str.format(LOAD_CMD,
|
if not self.is_on:
|
||||||
self._sink_name,
|
self._pa_svr.turn_on(self._sink_name, self._source_name)
|
||||||
self._source_name),
|
self._pa_svr.update_module_state(no_throttle=True)
|
||||||
False)
|
self._module_idx = self._pa_svr.get_module_idx(self._sink_name,
|
||||||
self.update()
|
self._source_name)
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
else:
|
||||||
|
_LOGGER.warning(IGNORED_SWITCH_WARN)
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
self._send_command(str.format(UNLOAD_CMD, self._module_idx), False)
|
if self.is_on:
|
||||||
self.update()
|
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()
|
self.update_ha_state()
|
||||||
|
else:
|
||||||
|
_LOGGER.warning(IGNORED_SWITCH_WARN)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Refresh state in case an alternate process modified this data."""
|
"""Refresh state in case an alternate process modified this data."""
|
||||||
return_data = self._send_command("list-modules", True)
|
self._pa_svr.update_module_state()
|
||||||
result = re.search(str.format(MOD_REGEX,
|
self._module_idx = self._pa_svr.get_module_idx(self._sink_name,
|
||||||
re.escape(self._sink_name),
|
self._source_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
|
|
||||||
|
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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/switch.tellstick/
|
https://home-assistant.io/components/switch.tellstick/
|
||||||
"""
|
"""
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import tellstick
|
from homeassistant.components import tellstick
|
||||||
from homeassistant.components.tellstick import (ATTR_DISCOVER_DEVICES,
|
from homeassistant.components.tellstick import (ATTR_DISCOVER_DEVICES,
|
||||||
ATTR_DISCOVER_CONFIG)
|
ATTR_DISCOVER_CONFIG)
|
||||||
from homeassistant.helpers.entity import ToggleEntity
|
from homeassistant.helpers.entity import ToggleEntity
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): tellstick.DOMAIN})
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
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()
|
hub.update_smartplugs()
|
||||||
switches = []
|
switches = []
|
||||||
switches.extend([
|
switches.extend([
|
||||||
VerisureSmartplug(value.id)
|
VerisureSmartplug(value.deviceLabel)
|
||||||
for value in hub.smartplug_status.values()])
|
for value in hub.smartplug_status.values()])
|
||||||
add_devices(switches)
|
add_devices(switches)
|
||||||
|
|
||||||
@ -42,6 +42,11 @@ class VerisureSmartplug(SwitchDevice):
|
|||||||
"""Return true if on."""
|
"""Return true if on."""
|
||||||
return hub.smartplug_status[self._id].status == '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):
|
def turn_on(self):
|
||||||
"""Set smartplug status on."""
|
"""Set smartplug status on."""
|
||||||
hub.my_pages.smartplug.set(self._id, 'on')
|
hub.my_pages.smartplug.set(self._id, 'on')
|
||||||
|
@ -9,7 +9,7 @@ import logging
|
|||||||
from homeassistant.components.wink import WinkToggleDevice
|
from homeassistant.components.wink import WinkToggleDevice
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
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):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
@ -6,6 +6,7 @@ https://home-assistant.io/components/Tellstick/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import bootstrap
|
from homeassistant import bootstrap
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -39,6 +40,14 @@ TELLSTICK_LOCK = threading.Lock()
|
|||||||
# Used from entities that register callback listeners
|
# Used from entities that register callback listeners
|
||||||
TELLCORE_REGISTRY = None
|
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):
|
def _discover(hass, config, found_devices, component_name):
|
||||||
"""Setup and send the discovery event."""
|
"""Setup and send the discovery event."""
|
||||||
@ -52,8 +61,7 @@ def _discover(hass, config, found_devices, component_name):
|
|||||||
bootstrap.setup_component(hass, component.DOMAIN,
|
bootstrap.setup_component(hass, component.DOMAIN,
|
||||||
config)
|
config)
|
||||||
|
|
||||||
signal_repetitions = config[DOMAIN].get(
|
signal_repetitions = config[DOMAIN].get(ATTR_SIGNAL_REPETITIONS)
|
||||||
ATTR_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS)
|
|
||||||
|
|
||||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
|
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
|
||||||
{ATTR_SERVICE: DISCOVERY_TYPES[component_name],
|
{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/
|
https://home-assistant.io/components/thermostat.heat_control/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.components import switch
|
from homeassistant.components import switch
|
||||||
from homeassistant.components.thermostat import (
|
from homeassistant.components.thermostat import (
|
||||||
@ -28,21 +30,26 @@ CONF_TARGET_TEMP = 'target_temp'
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the heat control thermostat."""
|
"""Setup the heat control thermostat."""
|
||||||
name = config.get(CONF_NAME, DEFAULT_NAME)
|
name = config.get(CONF_NAME)
|
||||||
heater_entity_id = config.get(CONF_HEATER)
|
heater_entity_id = config.get(CONF_HEATER)
|
||||||
sensor_entity_id = config.get(CONF_SENSOR)
|
sensor_entity_id = config.get(CONF_SENSOR)
|
||||||
min_temp = util.convert(config.get(CONF_MIN_TEMP), float, None)
|
min_temp = config.get(CONF_MIN_TEMP)
|
||||||
max_temp = util.convert(config.get(CONF_MAX_TEMP), float, None)
|
max_temp = config.get(CONF_MAX_TEMP)
|
||||||
target_temp = util.convert(config.get(CONF_TARGET_TEMP), float, None)
|
target_temp = config.get(CONF_TARGET_TEMP)
|
||||||
|
|
||||||
if None in (heater_entity_id, sensor_entity_id):
|
|
||||||
_LOGGER.error('Missing required key %s or %s', CONF_HEATER,
|
|
||||||
CONF_SENSOR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
add_devices([HeatControl(hass, name, heater_entity_id, sensor_entity_id,
|
add_devices([HeatControl(hass, name, heater_entity_id, sensor_entity_id,
|
||||||
min_temp, max_temp, target_temp)])
|
min_temp, max_temp, target_temp)])
|
||||||
|
@ -11,7 +11,7 @@ from homeassistant.components.thermostat import ThermostatDevice
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||||
|
|
||||||
REQUIREMENTS = ['evohomeclient==0.2.4',
|
REQUIREMENTS = ['evohomeclient==0.2.5',
|
||||||
'somecomfort==0.2.1']
|
'somecomfort==0.2.1']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
# Because we do not compile openzwave on CI
|
# Because we do not compile openzwave on CI
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
|
import logging
|
||||||
from homeassistant.components.thermostat import DOMAIN
|
from homeassistant.components.thermostat import DOMAIN
|
||||||
from homeassistant.components.thermostat import (
|
from homeassistant.components.thermostat import (
|
||||||
ThermostatDevice,
|
ThermostatDevice,
|
||||||
@ -9,19 +10,46 @@ from homeassistant.components.thermostat import (
|
|||||||
from homeassistant.components import zwave
|
from homeassistant.components import zwave
|
||||||
from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS
|
from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_NAME = 'name'
|
CONF_NAME = 'name'
|
||||||
DEFAULT_NAME = 'ZWave Thermostat'
|
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):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the ZWave thermostats."""
|
"""Setup the ZWave thermostats."""
|
||||||
if discovery_info is None or zwave.NETWORK is None:
|
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
|
return
|
||||||
|
|
||||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||||
value.set_change_verified(False)
|
value.set_change_verified(False)
|
||||||
|
# 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)])
|
add_devices([ZWaveThermostat(value)])
|
||||||
|
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
||||||
|
discovery_info, zwave.NETWORK)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
|
@ -23,7 +23,7 @@ DISCOVER_SWITCHES = 'verisure.switches'
|
|||||||
DISCOVER_ALARMS = 'verisure.alarm_control_panel'
|
DISCOVER_ALARMS = 'verisure.alarm_control_panel'
|
||||||
DISCOVER_LOCKS = 'verisure.lock'
|
DISCOVER_LOCKS = 'verisure.lock'
|
||||||
|
|
||||||
REQUIREMENTS = ['vsure==0.7.1']
|
REQUIREMENTS = ['vsure==0.8.1']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ class VerisureHub(object):
|
|||||||
# "wrong password" message. We will continue to retry after maintenance
|
# "wrong password" message. We will continue to retry after maintenance
|
||||||
# regardless of that error.
|
# regardless of that error.
|
||||||
self._disable_wrong_password_error = False
|
self._disable_wrong_password_error = False
|
||||||
self._wrong_password_given = False
|
self._password_retries = 1
|
||||||
self._reconnect_timeout = time.time()
|
self._reconnect_timeout = time.time()
|
||||||
|
|
||||||
self.my_pages = verisure.MyPages(
|
self.my_pages = verisure.MyPages(
|
||||||
@ -128,11 +128,13 @@ class VerisureHub(object):
|
|||||||
self.my_pages.smartplug.get,
|
self.my_pages.smartplug.get,
|
||||||
self.smartplug_status)
|
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):
|
def update_component(self, get_function, status):
|
||||||
"""Update the status of Verisure components."""
|
"""Update the status of Verisure components."""
|
||||||
if self._wrong_password_given:
|
|
||||||
_LOGGER.error('Wrong password for Verisure, update config')
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
for overview in get_function():
|
for overview in get_function():
|
||||||
try:
|
try:
|
||||||
@ -145,25 +147,26 @@ class VerisureHub(object):
|
|||||||
|
|
||||||
def reconnect(self):
|
def reconnect(self):
|
||||||
"""Reconnect to Verisure MyPages."""
|
"""Reconnect to Verisure MyPages."""
|
||||||
if self._reconnect_timeout > time.time():
|
if (self._reconnect_timeout > time.time() or
|
||||||
return
|
not self._lock.acquire(blocking=False) or
|
||||||
if not self._lock.acquire(blocking=False):
|
self._password_retries < 0):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self.my_pages.login()
|
self.my_pages.login()
|
||||||
self._disable_wrong_password_error = False
|
self._disable_wrong_password_error = False
|
||||||
|
self._password_retries = 1
|
||||||
except self._verisure.LoginError as ex:
|
except self._verisure.LoginError as ex:
|
||||||
_LOGGER.error("Wrong user name or password for Verisure MyPages")
|
_LOGGER.error("Wrong user name or password for Verisure MyPages")
|
||||||
if self._disable_wrong_password_error:
|
if self._disable_wrong_password_error:
|
||||||
self._reconnect_timeout = time.time() + 60
|
self._reconnect_timeout = time.time() + 60*60
|
||||||
else:
|
else:
|
||||||
self._wrong_password_given = True
|
self._password_retries = self._password_retries - 1
|
||||||
except self._verisure.MaintenanceError:
|
except self._verisure.MaintenanceError:
|
||||||
self._disable_wrong_password_error = True
|
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")
|
_LOGGER.error("Verisure MyPages down for maintenance")
|
||||||
except self._verisure.Error as ex:
|
except self._verisure.Error as ex:
|
||||||
_LOGGER.error("Could not login to Verisure MyPages, %s", ex)
|
_LOGGER.error("Could not login to Verisure MyPages, %s", ex)
|
||||||
self._reconnect_timeout = time.time() + 5
|
self._reconnect_timeout = time.time() + 60
|
||||||
finally:
|
finally:
|
||||||
self._lock.release()
|
self._lock.release()
|
||||||
|
@ -9,13 +9,13 @@ import logging
|
|||||||
from homeassistant import bootstrap
|
from homeassistant import bootstrap
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_DISCOVERED, ATTR_SERVICE, CONF_ACCESS_TOKEN,
|
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 import validate_config
|
||||||
from homeassistant.helpers.entity import ToggleEntity
|
from homeassistant.helpers.entity import ToggleEntity
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
|
|
||||||
DOMAIN = "wink"
|
DOMAIN = "wink"
|
||||||
REQUIREMENTS = ['python-wink==0.7.4']
|
REQUIREMENTS = ['python-wink==0.7.6']
|
||||||
|
|
||||||
DISCOVER_LIGHTS = "wink.lights"
|
DISCOVER_LIGHTS = "wink.lights"
|
||||||
DISCOVER_SWITCHES = "wink.switches"
|
DISCOVER_SWITCHES = "wink.switches"
|
||||||
@ -68,6 +68,7 @@ class WinkToggleDevice(ToggleEntity):
|
|||||||
def __init__(self, wink):
|
def __init__(self, wink):
|
||||||
"""Initialize the Wink device."""
|
"""Initialize the Wink device."""
|
||||||
self.wink = wink
|
self.wink = wink
|
||||||
|
self._battery = self.wink.battery_level
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
@ -100,3 +101,16 @@ class WinkToggleDevice(ToggleEntity):
|
|||||||
def update(self):
|
def update(self):
|
||||||
"""Update state of the device."""
|
"""Update state of the device."""
|
||||||
self.wink.update_state()
|
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/
|
https://home-assistant.io/components/zigbee/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import pickle
|
||||||
from binascii import hexlify, unhexlify
|
from binascii import hexlify, unhexlify
|
||||||
|
from base64 import b64encode, b64decode
|
||||||
|
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import JobPriority
|
from homeassistant.core import JobPriority
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
DOMAIN = "zigbee"
|
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_DEVICE = "device"
|
||||||
CONF_BAUD = "baud"
|
CONF_BAUD = "baud"
|
||||||
@ -25,9 +29,14 @@ DEFAULT_ADC_MAX_VOLTS = 1.2
|
|||||||
GPIO_DIGITAL_OUTPUT_LOW = None
|
GPIO_DIGITAL_OUTPUT_LOW = None
|
||||||
GPIO_DIGITAL_OUTPUT_HIGH = None
|
GPIO_DIGITAL_OUTPUT_HIGH = None
|
||||||
ADC_PERCENTAGE = None
|
ADC_PERCENTAGE = None
|
||||||
|
DIGITAL_PINS = None
|
||||||
|
ANALOG_PINS = None
|
||||||
|
CONVERT_ADC = None
|
||||||
ZIGBEE_EXCEPTION = None
|
ZIGBEE_EXCEPTION = None
|
||||||
ZIGBEE_TX_FAILURE = None
|
ZIGBEE_TX_FAILURE = None
|
||||||
|
|
||||||
|
ATTR_FRAME = "frame"
|
||||||
|
|
||||||
DEVICE = None
|
DEVICE = None
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -39,17 +48,24 @@ def setup(hass, config):
|
|||||||
global GPIO_DIGITAL_OUTPUT_LOW
|
global GPIO_DIGITAL_OUTPUT_LOW
|
||||||
global GPIO_DIGITAL_OUTPUT_HIGH
|
global GPIO_DIGITAL_OUTPUT_HIGH
|
||||||
global ADC_PERCENTAGE
|
global ADC_PERCENTAGE
|
||||||
|
global DIGITAL_PINS
|
||||||
|
global ANALOG_PINS
|
||||||
|
global CONVERT_ADC
|
||||||
global ZIGBEE_EXCEPTION
|
global ZIGBEE_EXCEPTION
|
||||||
global ZIGBEE_TX_FAILURE
|
global ZIGBEE_TX_FAILURE
|
||||||
|
|
||||||
import xbee_helper.const as xb_const
|
import xbee_helper.const as xb_const
|
||||||
from xbee_helper import ZigBee
|
from xbee_helper import ZigBee
|
||||||
|
from xbee_helper.device import convert_adc
|
||||||
from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure
|
from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure
|
||||||
from serial import Serial, SerialException
|
from serial import Serial, SerialException
|
||||||
|
|
||||||
GPIO_DIGITAL_OUTPUT_LOW = xb_const.GPIO_DIGITAL_OUTPUT_LOW
|
GPIO_DIGITAL_OUTPUT_LOW = xb_const.GPIO_DIGITAL_OUTPUT_LOW
|
||||||
GPIO_DIGITAL_OUTPUT_HIGH = xb_const.GPIO_DIGITAL_OUTPUT_HIGH
|
GPIO_DIGITAL_OUTPUT_HIGH = xb_const.GPIO_DIGITAL_OUTPUT_HIGH
|
||||||
ADC_PERCENTAGE = xb_const.ADC_PERCENTAGE
|
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_EXCEPTION = ZigBeeException
|
||||||
ZIGBEE_TX_FAILURE = ZigBeeTxFailure
|
ZIGBEE_TX_FAILURE = ZigBeeTxFailure
|
||||||
|
|
||||||
@ -62,6 +78,19 @@ def setup(hass, config):
|
|||||||
return False
|
return False
|
||||||
DEVICE = ZigBee(ser)
|
DEVICE = ZigBee(ser)
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_serial_port)
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -70,6 +99,25 @@ def close_serial_port(*args):
|
|||||||
DEVICE.zb.serial.close()
|
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):
|
class ZigBeeConfig(object):
|
||||||
"""Handle the fetching of configuration from the config file."""
|
"""Handle the fetching of configuration from the config file."""
|
||||||
|
|
||||||
@ -110,14 +158,65 @@ class ZigBeePinConfig(ZigBeeConfig):
|
|||||||
return self._config["pin"]
|
return self._config["pin"]
|
||||||
|
|
||||||
|
|
||||||
class ZigBeeDigitalPinConfig(ZigBeePinConfig):
|
class ZigBeeDigitalInConfig(ZigBeePinConfig):
|
||||||
"""Handle the fetching of configuration from the config file."""
|
"""A subclass of ZigBeePinConfig."""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
"""Initialize the configuration."""
|
"""Initialise the ZigBee Digital input config."""
|
||||||
super(ZigBeeDigitalPinConfig, self).__init__(config)
|
super(ZigBeeDigitalInConfig, self).__init__(config)
|
||||||
self._bool2state, self._state2bool = self.boolean_maps
|
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
|
@property
|
||||||
def boolean_maps(self):
|
def boolean_maps(self):
|
||||||
"""Create dicts to map booleans to pin high/low and vice versa.
|
"""Create dicts to map booleans to pin high/low and vice versa.
|
||||||
@ -154,22 +253,6 @@ class ZigBeeDigitalPinConfig(ZigBeePinConfig):
|
|||||||
"""
|
"""
|
||||||
return self._state2bool
|
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):
|
class ZigBeeAnalogInConfig(ZigBeePinConfig):
|
||||||
"""Representation of a ZigBee GPIO pin set to analog in."""
|
"""Representation of a ZigBee GPIO pin set to analog in."""
|
||||||
@ -187,6 +270,25 @@ class ZigBeeDigitalIn(Entity):
|
|||||||
"""Initialize the device."""
|
"""Initialize the device."""
|
||||||
self._config = config
|
self._config = config
|
||||||
self._state = False
|
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
|
# Get initial state
|
||||||
hass.pool.add_job(
|
hass.pool.add_job(
|
||||||
JobPriority.EVENT_STATE, (self.update_ha_state, True))
|
JobPriority.EVENT_STATE, (self.update_ha_state, True))
|
||||||
@ -196,6 +298,11 @@ class ZigBeeDigitalIn(Entity):
|
|||||||
"""Return the name of the input."""
|
"""Return the name of the input."""
|
||||||
return self._config.name
|
return self._config.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self):
|
||||||
|
"""The entity's configuration."""
|
||||||
|
return self._config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
"""Return the state of the polling, if needed."""
|
"""Return the state of the polling, if needed."""
|
||||||
@ -207,11 +314,9 @@ class ZigBeeDigitalIn(Entity):
|
|||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
def update(self):
|
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:
|
try:
|
||||||
pin_state = DEVICE.get_gpio_pin(
|
sample = DEVICE.get_sample(self._config.address)
|
||||||
self._config.pin,
|
|
||||||
self._config.address)
|
|
||||||
except ZIGBEE_TX_FAILURE:
|
except ZIGBEE_TX_FAILURE:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Transmission failure when attempting to get sample from "
|
"Transmission failure when attempting to get sample from "
|
||||||
@ -221,7 +326,14 @@ class ZigBeeDigitalIn(Entity):
|
|||||||
_LOGGER.exception(
|
_LOGGER.exception(
|
||||||
"Unable to get sample from ZigBee device: %s", exc)
|
"Unable to get sample from ZigBee device: %s", exc)
|
||||||
return
|
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):
|
class ZigBeeDigitalOut(ZigBeeDigitalIn):
|
||||||
@ -255,6 +367,24 @@ class ZigBeeDigitalOut(ZigBeeDigitalIn):
|
|||||||
"""Set the digital output to its 'off' state."""
|
"""Set the digital output to its 'off' state."""
|
||||||
self._set_state(False)
|
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):
|
class ZigBeeAnalogIn(Entity):
|
||||||
"""Representation of a GPIO pin configured as an analog input."""
|
"""Representation of a GPIO pin configured as an analog input."""
|
||||||
@ -263,6 +393,29 @@ class ZigBeeAnalogIn(Entity):
|
|||||||
"""Initialize the ZigBee analog in device."""
|
"""Initialize the ZigBee analog in device."""
|
||||||
self._config = config
|
self._config = config
|
||||||
self._value = None
|
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
|
# Get initial state
|
||||||
hass.pool.add_job(
|
hass.pool.add_job(
|
||||||
JobPriority.EVENT_STATE, (self.update_ha_state, True))
|
JobPriority.EVENT_STATE, (self.update_ha_state, True))
|
||||||
@ -272,6 +425,11 @@ class ZigBeeAnalogIn(Entity):
|
|||||||
"""The name of the input."""
|
"""The name of the input."""
|
||||||
return self._config.name
|
return self._config.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self):
|
||||||
|
"""The entity's configuration."""
|
||||||
|
return self._config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
"""The state of the polling, if needed."""
|
"""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