Merge pull request #3715 from home-assistant/dev

0.30
This commit is contained in:
Paulus Schoutsen 2016-10-08 09:58:08 -07:00 committed by GitHub
commit 4239a2b844
180 changed files with 4528 additions and 1877 deletions

View File

@ -16,6 +16,9 @@ omit =
homeassistant/components/bloomsky.py homeassistant/components/bloomsky.py
homeassistant/components/*/bloomsky.py homeassistant/components/*/bloomsky.py
homeassistant/components/digital_ocean.py
homeassistant/components/*/digital_ocean.py
homeassistant/components/dweet.py homeassistant/components/dweet.py
homeassistant/components/*/dweet.py homeassistant/components/*/dweet.py
@ -46,6 +49,9 @@ omit =
homeassistant/components/qwikswitch.py homeassistant/components/qwikswitch.py
homeassistant/components/*/qwikswitch.py homeassistant/components/*/qwikswitch.py
homeassistant/components/rfxtrx.py
homeassistant/components/*/rfxtrx.py
homeassistant/components/rpi_gpio.py homeassistant/components/rpi_gpio.py
homeassistant/components/*/rpi_gpio.py homeassistant/components/*/rpi_gpio.py
@ -77,7 +83,7 @@ omit =
homeassistant/components/zigbee.py homeassistant/components/zigbee.py
homeassistant/components/*/zigbee.py homeassistant/components/*/zigbee.py
homeassistant/components/zwave.py homeassistant/components/zwave/*
homeassistant/components/*/zwave.py homeassistant/components/*/zwave.py
homeassistant/components/enocean.py homeassistant/components/enocean.py
@ -135,6 +141,7 @@ omit =
homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/tplink.py homeassistant/components/device_tracker/tplink.py
homeassistant/components/device_tracker/ubus.py homeassistant/components/device_tracker/ubus.py
homeassistant/components/device_tracker/volvooncall.py
homeassistant/components/discovery.py homeassistant/components/discovery.py
homeassistant/components/downloader.py homeassistant/components/downloader.py
homeassistant/components/fan/mqtt.py homeassistant/components/fan/mqtt.py
@ -213,6 +220,7 @@ omit =
homeassistant/components/sensor/bom.py homeassistant/components/sensor/bom.py
homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/coinmarketcap.py
homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/cpuspeed.py
homeassistant/components/sensor/darksky.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/dte_energy_bridge.py homeassistant/components/sensor/dte_energy_bridge.py
@ -222,7 +230,6 @@ omit =
homeassistant/components/sensor/fastdotcom.py homeassistant/components/sensor/fastdotcom.py
homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/fritzbox_callmonitor.py homeassistant/components/sensor/fritzbox_callmonitor.py
homeassistant/components/sensor/glances.py homeassistant/components/sensor/glances.py
homeassistant/components/sensor/google_travel_time.py homeassistant/components/sensor/google_travel_time.py
@ -255,17 +262,20 @@ omit =
homeassistant/components/sensor/swiss_hydrological_data.py homeassistant/components/sensor/swiss_hydrological_data.py
homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/ted5000.py
homeassistant/components/sensor/temper.py homeassistant/components/sensor/temper.py
homeassistant/components/sensor/time_date.py homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/torque.py homeassistant/components/sensor/torque.py
homeassistant/components/sensor/transmission.py homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/twitch.py homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py homeassistant/components/sensor/uber.py
homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/worldclock.py homeassistant/components/sensor/worldclock.py
homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/yahoo_finance.py homeassistant/components/sensor/yahoo_finance.py
homeassistant/components/sensor/yweather.py homeassistant/components/sensor/yweather.py
homeassistant/components/switch/acer_projector.py homeassistant/components/switch/acer_projector.py
homeassistant/components/switch/anel_pwrctrl.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

View File

@ -15,7 +15,7 @@
If user exposed functionality or configuration variables are added/changed: If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) - [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
If code communicates with devices, web services, or a: If the code communicates with devices, web services, or third-party tools:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass** - [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).

View File

@ -340,8 +340,8 @@ latex_elements = {
# (source start file, target name, title, # (source start file, target name, title,
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
(master_doc, 'Home-Assistant.tex', 'Home-Assistant Documentation', (master_doc, 'home-assistant.tex', 'Home Assistant Documentation',
'Home-Assistant Team', 'manual'), 'Home Assistant Team', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@ -382,7 +382,7 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
(master_doc, 'home-assistant', 'Home-Assistant Documentation', (master_doc, 'home-assistant', 'Home Assistant Documentation',
[author], 1) [author], 1)
] ]
@ -397,8 +397,8 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'Home-Assistant', 'Home-Assistant Documentation', (master_doc, 'Home-Assistant', 'Home Assistant Documentation',
author, 'Home-Assistant', 'One line description of project.', author, 'Home Assistant', 'Open-source home automation platform.',
'Miscellaneous'), 'Miscellaneous'),
] ]

View File

@ -4,6 +4,7 @@ Allow to setup simple automation rules via the config file.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/automation/ https://home-assistant.io/components/automation/
""" """
import asyncio
from functools import partial from functools import partial
import logging import logging
import os import os
@ -23,12 +24,15 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.loader import get_platform from homeassistant.loader import get_platform
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
DOMAIN = 'automation' DOMAIN = 'automation'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
DEPENDENCIES = ['group'] DEPENDENCIES = ['group']
GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
CONF_ALIAS = 'alias' CONF_ALIAS = 'alias'
CONF_HIDE_ENTITY = 'hide_entity' CONF_HIDE_ENTITY = 'hide_entity'
@ -36,6 +40,7 @@ CONF_CONDITION = 'condition'
CONF_ACTION = 'action' CONF_ACTION = 'action'
CONF_TRIGGER = 'trigger' CONF_TRIGGER = 'trigger'
CONF_CONDITION_TYPE = 'condition_type' CONF_CONDITION_TYPE = 'condition_type'
CONF_INITIAL_STATE = 'initial_state'
CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values' CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values'
CONDITION_TYPE_AND = 'and' CONDITION_TYPE_AND = 'and'
@ -43,9 +48,7 @@ CONDITION_TYPE_OR = 'or'
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
DEFAULT_HIDE_ENTITY = False DEFAULT_HIDE_ENTITY = False
DEFAULT_INITIAL_STATE = True
METHOD_TRIGGER = 'trigger'
METHOD_IF_ACTION = 'if_action'
ATTR_LAST_TRIGGERED = 'last_triggered' ATTR_LAST_TRIGGERED = 'last_triggered'
ATTR_VARIABLES = 'variables' ATTR_VARIABLES = 'variables'
@ -55,21 +58,14 @@ SERVICE_RELOAD = 'reload'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def _platform_validator(method, schema): def _platform_validator(config):
"""Generate platform validator for different steps.""" """Validate it is a valid platform."""
def validator(config): platform = get_platform(DOMAIN, config[CONF_PLATFORM])
"""Validate it is a valid platform."""
platform = get_platform(DOMAIN, config[CONF_PLATFORM])
if not hasattr(platform, method): if not hasattr(platform, 'TRIGGER_SCHEMA'):
raise vol.Invalid('invalid method platform') return config
if not hasattr(platform, schema): return getattr(platform, 'TRIGGER_SCHEMA')(config)
return config
return getattr(platform, schema)(config)
return validator
_TRIGGER_SCHEMA = vol.All( _TRIGGER_SCHEMA = vol.All(
cv.ensure_list, cv.ensure_list,
@ -78,33 +74,19 @@ _TRIGGER_SCHEMA = vol.All(
vol.Schema({ vol.Schema({
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN) vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN)
}, extra=vol.ALLOW_EXTRA), }, extra=vol.ALLOW_EXTRA),
_platform_validator(METHOD_TRIGGER, 'TRIGGER_SCHEMA') _platform_validator
), ),
] ]
) )
_CONDITION_SCHEMA = vol.Any( _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
CONDITION_USE_TRIGGER_VALUES,
vol.All(
cv.ensure_list,
[
vol.All(
vol.Schema({
CONF_PLATFORM: str,
CONF_CONDITION: str,
}, extra=vol.ALLOW_EXTRA),
cv.has_at_least_one_key(CONF_PLATFORM, CONF_CONDITION),
),
]
)
)
PLATFORM_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = vol.Schema({
CONF_ALIAS: cv.string, CONF_ALIAS: cv.string,
vol.Optional(CONF_INITIAL_STATE,
default=DEFAULT_INITIAL_STATE): cv.boolean,
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE):
vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)),
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
}) })
@ -163,9 +145,11 @@ def reload(hass):
def setup(hass, config): def setup(hass, config):
"""Setup the automation.""" """Setup the automation."""
component = EntityComponent(_LOGGER, DOMAIN, hass) component = EntityComponent(_LOGGER, DOMAIN, hass,
group_name=GROUP_NAME_ALL_AUTOMATIONS)
success = _process_config(hass, config, component) success = run_coroutine_threadsafe(
_async_process_config(hass, config, component), hass.loop).result()
if not success: if not success:
return False return False
@ -173,22 +157,37 @@ def setup(hass, config):
descriptions = conf_util.load_yaml_config_file( descriptions = conf_util.load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml')) os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
def trigger_service_handler(service_call): def trigger_service_handler(service_call):
"""Handle automation triggers.""" """Handle automation triggers."""
for entity in component.extract_from_service(service_call): for entity in component.extract_from_service(service_call):
entity.trigger(service_call.data.get(ATTR_VARIABLES)) hass.loop.create_task(entity.async_trigger(
service_call.data.get(ATTR_VARIABLES), True))
def service_handler(service_call): @asyncio.coroutine
"""Handle automation service calls.""" def turn_onoff_service_handler(service_call):
"""Handle automation turn on/off service calls."""
method = 'async_{}'.format(service_call.service)
for entity in component.extract_from_service(service_call): for entity in component.extract_from_service(service_call):
getattr(entity, service_call.service)() hass.loop.create_task(getattr(entity, method)())
@asyncio.coroutine
def toggle_service_handler(service_call):
"""Handle automation toggle service calls."""
for entity in component.extract_from_service(service_call):
if entity.is_on:
hass.loop.create_task(entity.async_turn_off())
else:
hass.loop.create_task(entity.async_turn_on())
@asyncio.coroutine
def reload_service_handler(service_call): def reload_service_handler(service_call):
"""Remove all automations and load new ones from config.""" """Remove all automations and load new ones from config."""
conf = component.prepare_reload() conf = yield from hass.loop.run_in_executor(
None, component.prepare_reload)
if conf is None: if conf is None:
return return
_process_config(hass, conf, component) hass.loop.create_task(_async_process_config(hass, conf, component))
hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler, hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
descriptions.get(SERVICE_TRIGGER), descriptions.get(SERVICE_TRIGGER),
@ -198,8 +197,12 @@ def setup(hass, config):
descriptions.get(SERVICE_RELOAD), descriptions.get(SERVICE_RELOAD),
schema=RELOAD_SERVICE_SCHEMA) schema=RELOAD_SERVICE_SCHEMA)
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE): hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
hass.services.register(DOMAIN, service, service_handler, descriptions.get(SERVICE_TOGGLE),
schema=SERVICE_SCHEMA)
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
hass.services.register(DOMAIN, service, turn_onoff_service_handler,
descriptions.get(service), descriptions.get(service),
schema=SERVICE_SCHEMA) schema=SERVICE_SCHEMA)
@ -209,15 +212,17 @@ def setup(hass, config):
class AutomationEntity(ToggleEntity): class AutomationEntity(ToggleEntity):
"""Entity to show status of entity.""" """Entity to show status of entity."""
# pylint: disable=abstract-method
# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-instance-attributes
def __init__(self, name, attach_triggers, cond_func, action, hidden): def __init__(self, name, async_attach_triggers, cond_func, async_action,
hidden):
"""Initialize an automation entity.""" """Initialize an automation entity."""
self._name = name self._name = name
self._attach_triggers = attach_triggers self._async_attach_triggers = async_attach_triggers
self._detach_triggers = attach_triggers(self.trigger) self._async_detach_triggers = None
self._cond_func = cond_func self._cond_func = cond_func
self._action = action self._async_action = async_action
self._enabled = True self._enabled = False
self._last_triggered = None self._last_triggered = None
self._hidden = hidden self._hidden = hidden
@ -248,41 +253,65 @@ class AutomationEntity(ToggleEntity):
"""Return True if entity is on.""" """Return True if entity is on."""
return self._enabled return self._enabled
def turn_on(self, **kwargs) -> None: @asyncio.coroutine
"""Turn the entity on.""" def async_turn_on(self, **kwargs) -> None:
"""Turn the entity on and update the state."""
if self._enabled: if self._enabled:
return return
self._detach_triggers = self._attach_triggers(self.trigger) yield from self.async_enable()
self._enabled = True self.hass.loop.create_task(self.async_update_ha_state())
self.update_ha_state()
def turn_off(self, **kwargs) -> None: @asyncio.coroutine
def async_turn_off(self, **kwargs) -> None:
"""Turn the entity off.""" """Turn the entity off."""
if not self._enabled: if not self._enabled:
return return
self._detach_triggers() self._async_detach_triggers()
self._detach_triggers = None self._async_detach_triggers = None
self._enabled = False self._enabled = False
self.update_ha_state() self.hass.loop.create_task(self.async_update_ha_state())
def trigger(self, variables): @asyncio.coroutine
"""Trigger automation.""" def async_trigger(self, variables, skip_condition=False):
if self._cond_func(variables): """Trigger automation.
self._action(variables)
This method is a coroutine.
"""
if skip_condition or self._cond_func(variables):
yield from self._async_action(variables)
self._last_triggered = utcnow() self._last_triggered = utcnow()
self.update_ha_state() self.hass.loop.create_task(self.async_update_ha_state())
def remove(self): def remove(self):
"""Remove automation from HASS.""" """Remove automation from HASS."""
self.turn_off() run_coroutine_threadsafe(self.async_turn_off(),
self.hass.loop).result()
super().remove() super().remove()
@asyncio.coroutine
def async_enable(self):
"""Enable this automation entity.
def _process_config(hass, config, component): This method is a coroutine.
"""Process config and add automations.""" """
success = False if self._enabled:
return
self._async_detach_triggers = yield from self._async_attach_triggers(
self.async_trigger)
self._enabled = True
@asyncio.coroutine
def _async_process_config(hass, config, component):
"""Process config and add automations.
This method is a coroutine.
"""
entities = []
tasks = []
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]
@ -293,10 +322,11 @@ def _process_config(hass, config, component):
hidden = config_block[CONF_HIDE_ENTITY] hidden = config_block[CONF_HIDE_ENTITY]
action = _get_action(hass, config_block.get(CONF_ACTION, {}), name) action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
name)
if CONF_CONDITION in config_block: if CONF_CONDITION in config_block:
cond_func = _process_if(hass, config, config_block) cond_func = _async_process_if(hass, config, config_block)
if cond_func is None: if cond_func is None:
continue continue
@ -305,101 +335,78 @@ def _process_config(hass, config, component):
"""Condition will always pass.""" """Condition will always pass."""
return True return True
attach_triggers = partial(_process_trigger, hass, config, async_attach_triggers = partial(
config_block.get(CONF_TRIGGER, []), name) _async_process_trigger, hass, config,
entity = AutomationEntity(name, attach_triggers, cond_func, action, config_block.get(CONF_TRIGGER, []), name)
hidden) entity = AutomationEntity(name, async_attach_triggers, cond_func,
component.add_entities((entity,)) action, hidden)
success = True if config_block[CONF_INITIAL_STATE]:
tasks.append(hass.loop.create_task(entity.async_enable()))
entities.append(entity)
return success yield from asyncio.gather(*tasks, loop=hass.loop)
yield from hass.loop.run_in_executor(
None, component.add_entities, entities)
return len(entities) > 0
def _get_action(hass, config, name): def _async_get_action(hass, config, name):
"""Return an action based on a configuration.""" """Return an action based on a configuration."""
script_obj = script.Script(hass, config, name) script_obj = script.Script(hass, config, name)
@asyncio.coroutine
def action(variables=None): 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.async_log_entry(hass, name, 'has been triggered', DOMAIN)
script_obj.run(variables) hass.loop.create_task(script_obj.async_run(variables))
return action return action
def _process_if(hass, config, p_config): def _async_process_if(hass, config, p_config):
"""Process if checks.""" """Process if checks."""
cond_type = p_config.get(CONF_CONDITION_TYPE,
DEFAULT_CONDITION_TYPE).lower()
# Deprecated since 0.19 - 5/5/2016
if cond_type != DEFAULT_CONDITION_TYPE:
_LOGGER.warning('Using condition_type: "or" 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
if use_trigger:
if_configs = p_config[CONF_TRIGGER]
checks = [] checks = []
for if_config in if_configs: for if_config in if_configs:
# Deprecated except for used by use_trigger_values
# since 0.19 - 5/5/2016
if CONF_PLATFORM in if_config:
if not use_trigger:
_LOGGER.warning("Please switch your condition configuration "
"to use 'condition' instead of 'platform'.")
if_config = dict(if_config)
if_config[CONF_CONDITION] = if_config.pop(CONF_PLATFORM)
# 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: try:
checks.append(condition.from_config(if_config)) checks.append(condition.async_from_config(if_config, False))
except HomeAssistantError as ex: except HomeAssistantError as ex:
# Invalid conditions are allowed if we base it on trigger _LOGGER.warning('Invalid condition: %s', ex)
if use_trigger: return None
_LOGGER.warning('Ignoring invalid condition: %s', ex)
else:
_LOGGER.warning('Invalid condition: %s', ex)
return None
if cond_type == CONDITION_TYPE_AND: def if_action(variables=None):
def if_action(variables=None): """AND all conditions."""
"""AND all conditions.""" return all(check(hass, variables) for check in checks)
return all(check(hass, variables) for check in checks)
else:
def if_action(variables=None):
"""OR all conditions."""
return any(check(hass, variables) for check in checks)
return if_action return if_action
def _process_trigger(hass, config, trigger_configs, name, action): @asyncio.coroutine
"""Setup the triggers.""" def _async_process_trigger(hass, config, trigger_configs, name, action):
"""Setup the triggers.
This method is a coroutine.
"""
removes = [] removes = []
for conf in trigger_configs: for conf in trigger_configs:
platform = _resolve_platform(METHOD_TRIGGER, hass, config, platform = yield from hass.loop.run_in_executor(
conf.get(CONF_PLATFORM)) None, prepare_setup_platform, hass, config, DOMAIN,
if platform is None: conf.get(CONF_PLATFORM))
continue
remove = platform.trigger(hass, conf, action) if platform is None:
return None
remove = platform.async_trigger(hass, conf, action)
if not remove: if not remove:
_LOGGER.error("Error setting up rule %s", name) _LOGGER.error("Error setting up trigger %s", name)
continue continue
_LOGGER.info("Initialized rule %s", name) _LOGGER.info("Initialized trigger %s", name)
removes.append(remove) removes.append(remove)
if not removes: if not removes:
@ -411,17 +418,3 @@ def _process_trigger(hass, config, trigger_configs, name, action):
remove() remove()
return remove_triggers return remove_triggers
def _resolve_platform(method, hass, config, platform):
"""Find the automation platform."""
if platform is None:
return None
platform = prepare_setup_platform(hass, config, DOMAIN, platform)
if platform is None or not hasattr(platform, method):
_LOGGER.error("Unknown automation platform specified for %s: %s",
method, platform)
return None
return platform

View File

@ -4,11 +4,11 @@ Offer event 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/#event-trigger at https://home-assistant.io/components/automation/#event-trigger
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import CONF_PLATFORM from homeassistant.const import CONF_PLATFORM
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
@ -24,21 +24,21 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
def trigger(hass, config, action): def async_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)
event_data = config.get(CONF_EVENT_DATA) event_data = config.get(CONF_EVENT_DATA)
@asyncio.coroutine @callback
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()):
hass.async_add_job(action, { hass.async_run_job(action, {
'trigger': { 'trigger': {
'platform': 'event', 'platform': 'event',
'event': event, 'event': event,
}, },
}) })
return hass.bus.listen(event_type, handle_event) return hass.bus.async_listen(event_type, handle_event)

View File

@ -6,6 +6,7 @@ at https://home-assistant.io/components/automation/#mqtt-trigger
""" """
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD) from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -21,15 +22,16 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
def trigger(hass, config, action): def async_trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
topic = config.get(CONF_TOPIC) topic = config.get(CONF_TOPIC)
payload = config.get(CONF_PAYLOAD) payload = config.get(CONF_PAYLOAD)
@callback
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({ hass.async_run_job(action, {
'trigger': { 'trigger': {
'platform': 'mqtt', 'platform': 'mqtt',
'topic': msg_topic, 'topic': msg_topic,
@ -38,4 +40,4 @@ def trigger(hass, config, action):
} }
}) })
return mqtt.subscribe(hass, topic, mqtt_automation_listener) return mqtt.async_subscribe(hass, topic, mqtt_automation_listener)

View File

@ -8,10 +8,11 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import ( from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
CONF_BELOW, CONF_ABOVE) CONF_BELOW, CONF_ABOVE)
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers import condition, config_validation as cv
TRIGGER_SCHEMA = vol.All(vol.Schema({ TRIGGER_SCHEMA = vol.All(vol.Schema({
@ -25,7 +26,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def trigger(hass, config, action): def async_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)
below = config.get(CONF_BELOW) below = config.get(CONF_BELOW)
@ -34,7 +35,7 @@ def trigger(hass, config, action):
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
# pylint: disable=unused-argument @callback
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."""
if to_s is None: if to_s is None:
@ -50,19 +51,19 @@ def trigger(hass, config, action):
} }
# If new one doesn't match, nothing to do # If new one doesn't match, nothing to do
if not condition.numeric_state( if not condition.async_numeric_state(
hass, to_s, below, above, value_template, variables): hass, to_s, below, above, value_template, variables):
return return
# Only match if old didn't exist or existed but didn't match # Only match if old didn't exist or existed but didn't match
# Written as: skip if old one did exist and matched # Written as: skip if old one did exist and matched
if from_s is not None and condition.numeric_state( if from_s is not None and condition.async_numeric_state(
hass, from_s, below, above, value_template, variables): hass, from_s, below, above, value_template, variables):
return return
variables['trigger']['from_state'] = from_s variables['trigger']['from_state'] = from_s
variables['trigger']['to_state'] = to_s variables['trigger']['to_state'] = to_s
action(variables) hass.async_run_job(action, variables)
return track_state_change(hass, entity_id, state_automation_listener) return async_track_state_change(hass, entity_id, state_automation_listener)

View File

@ -6,9 +6,11 @@ at https://home-assistant.io/components/automation/#state-trigger
""" """
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.const import MATCH_ALL, CONF_PLATFORM from homeassistant.const import MATCH_ALL, CONF_PLATFORM
from homeassistant.helpers.event import track_state_change, track_point_in_time from homeassistant.helpers.event import (
async_track_state_change, async_track_point_in_utc_time)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
CONF_ENTITY_ID = "entity_id" CONF_ENTITY_ID = "entity_id"
@ -32,22 +34,23 @@ TRIGGER_SCHEMA = vol.All(
) )
def trigger(hass, config, action): def async_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)
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 = config.get(CONF_FOR) time_delta = config.get(CONF_FOR)
remove_state_for_cancel = None async_remove_state_for_cancel = None
remove_state_for_listener = None async_remove_state_for_listener = None
@callback
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."""
nonlocal remove_state_for_cancel, remove_state_for_listener nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
def call_action(): def call_action():
"""Call action with right context.""" """Call action with right context."""
action({ hass.async_run_job(action, {
'trigger': { 'trigger': {
'platform': 'state', 'platform': 'state',
'entity_id': entity, 'entity_id': entity,
@ -61,35 +64,37 @@ def trigger(hass, config, action):
call_action() call_action()
return return
@callback
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."""
remove_state_for_cancel() async_remove_state_for_cancel()
call_action() call_action()
@callback
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.state == to_s.state: if inner_to_s.state == to_s.state:
return return
remove_state_for_listener() async_remove_state_for_listener()
remove_state_for_cancel() async_remove_state_for_cancel()
remove_state_for_listener = track_point_in_time( async_remove_state_for_listener = async_track_point_in_utc_time(
hass, state_for_listener, dt_util.utcnow() + time_delta) hass, state_for_listener, dt_util.utcnow() + time_delta)
remove_state_for_cancel = track_state_change( async_remove_state_for_cancel = async_track_state_change(
hass, entity, state_for_cancel_listener) hass, entity, state_for_cancel_listener)
unsub = track_state_change(hass, entity_id, state_automation_listener, unsub = async_track_state_change(
from_state, to_state) hass, entity_id, state_automation_listener, from_state, to_state)
def remove(): def async_remove():
"""Remove state listeners.""" """Remove state listeners async."""
unsub() unsub()
# pylint: disable=not-callable # pylint: disable=not-callable
if remove_state_for_cancel is not None: if async_remove_state_for_cancel is not None:
remove_state_for_cancel() async_remove_state_for_cancel()
if remove_state_for_listener is not None: if async_remove_state_for_listener is not None:
remove_state_for_listener() async_remove_state_for_listener()
return remove return async_remove

View File

@ -9,9 +9,10 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import ( from homeassistant.const import (
CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE) CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE)
from homeassistant.helpers.event import track_sunrise, track_sunset from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['sun'] DEPENDENCIES = ['sun']
@ -25,14 +26,15 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
def trigger(hass, config, action): def async_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)
@callback
def call_action(): def call_action():
"""Call action with right context.""" """Call action with right context."""
action({ hass.async_run_job(action, {
'trigger': { 'trigger': {
'platform': 'sun', 'platform': 'sun',
'event': event, 'event': event,
@ -42,6 +44,6 @@ def trigger(hass, config, action):
# Do something to call action # Do something to call action
if event == SUN_EVENT_SUNRISE: if event == SUN_EVENT_SUNRISE:
return track_sunrise(hass, call_action, offset) return async_track_sunrise(hass, call_action, offset)
else: else:
return track_sunset(hass, call_action, offset) return async_track_sunset(hass, call_action, offset)

View File

@ -4,14 +4,14 @@ Offer template 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/#template-trigger at https://home-assistant.io/components/automation/#template-trigger
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM
from homeassistant.helpers import condition from homeassistant.helpers import condition
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import async_track_state_change
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -23,7 +23,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
}) })
def trigger(hass, config, action): def async_trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
value_template.hass = hass value_template.hass = hass
@ -31,7 +31,7 @@ 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
@asyncio.coroutine @callback
def state_changed_listener(entity_id, from_s, to_s): 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
@ -40,7 +40,7 @@ def trigger(hass, config, action):
# 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
hass.async_add_job(action, { hass.async_run_job(action, {
'trigger': { 'trigger': {
'platform': 'template', 'platform': 'template',
'entity_id': entity_id, 'entity_id': entity_id,
@ -51,5 +51,5 @@ def trigger(hass, config, action):
elif not template_result: elif not template_result:
already_triggered = False already_triggered = False
return track_state_change(hass, value_template.extract_entities(), return async_track_state_change(hass, value_template.extract_entities(),
state_changed_listener) state_changed_listener)

View File

@ -8,9 +8,10 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import CONF_AFTER, CONF_PLATFORM from homeassistant.const import CONF_AFTER, CONF_PLATFORM
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_time_change from homeassistant.helpers.event import async_track_time_change
CONF_HOURS = "hours" CONF_HOURS = "hours"
CONF_MINUTES = "minutes" CONF_MINUTES = "minutes"
@ -28,7 +29,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
CONF_SECONDS, CONF_AFTER)) CONF_SECONDS, CONF_AFTER))
def trigger(hass, config, action): def async_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 = config.get(CONF_AFTER) after = config.get(CONF_AFTER)
@ -38,14 +39,15 @@ def trigger(hass, config, action):
minutes = config.get(CONF_MINUTES) minutes = config.get(CONF_MINUTES)
seconds = config.get(CONF_SECONDS) seconds = config.get(CONF_SECONDS)
@callback
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({ hass.async_run_job(action, {
'trigger': { 'trigger': {
'platform': 'time', 'platform': 'time',
'now': now, 'now': now,
}, },
}) })
return track_time_change(hass, time_automation_listener, return async_track_time_change(hass, time_automation_listener,
hour=hours, minute=minutes, second=seconds) hour=hours, minute=minutes, second=seconds)

View File

@ -6,9 +6,10 @@ at https://home-assistant.io/components/automation/#zone-trigger
""" """
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import ( from homeassistant.const import (
CONF_EVENT, CONF_ENTITY_ID, CONF_ZONE, 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 async_track_state_change
from homeassistant.helpers import ( from homeassistant.helpers import (
condition, config_validation as cv, location) condition, config_validation as cv, location)
@ -25,12 +26,13 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
def trigger(hass, config, action): def async_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)
zone_entity_id = config.get(CONF_ZONE) zone_entity_id = config.get(CONF_ZONE)
event = config.get(CONF_EVENT) event = config.get(CONF_EVENT)
@callback
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 not location.has_location(from_s) or \ if from_s and not location.has_location(from_s) or \
@ -47,7 +49,7 @@ def trigger(hass, config, action):
# 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({ hass.async_run_job(action, {
'trigger': { 'trigger': {
'platform': 'zone', 'platform': 'zone',
'entity_id': entity, 'entity_id': entity,
@ -58,5 +60,5 @@ def trigger(hass, config, action):
}, },
}) })
return track_state_change(hass, entity_id, zone_automation_listener, return async_track_state_change(hass, entity_id, zone_automation_listener,
MATCH_ALL, MATCH_ALL) MATCH_ALL, MATCH_ALL)

View File

@ -1,5 +1,5 @@
""" """
Support for exposed aREST RESTful API of a device. Support for an exposed aREST RESTful API of a device.
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.arest/ https://home-assistant.io/components/binary_sensor.arest/
@ -8,31 +8,32 @@ import logging
from datetime import timedelta from datetime import timedelta
import requests import requests
import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, SENSOR_CLASSES) BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
from homeassistant.const import CONF_RESOURCE, CONF_PIN from homeassistant.const import (
CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_SENSOR_CLASS)
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RESOURCE): cv.url,
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_PIN): cv.string,
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA,
})
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the aREST binary sensor.""" """Setup the aREST binary sensor."""
resource = config.get(CONF_RESOURCE) resource = config.get(CONF_RESOURCE)
pin = config.get(CONF_PIN) pin = config.get(CONF_PIN)
sensor_class = config.get(CONF_SENSOR_CLASS)
sensor_class = config.get('sensor_class')
if sensor_class not in SENSOR_CLASSES:
_LOGGER.warning('Unknown sensor class: %s', sensor_class)
sensor_class = None
if None in (resource, pin):
_LOGGER.error('Not all required config keys present: %s',
', '.join((CONF_RESOURCE, CONF_PIN)))
return False
try: try:
response = requests.get(resource, timeout=10).json() response = requests.get(resource, timeout=10).json()
@ -49,11 +50,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
arest = ArestData(resource, pin) arest = ArestData(resource, pin)
add_devices([ArestBinarySensor( add_devices([ArestBinarySensor(
arest, arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
resource, sensor_class, pin)])
config.get('name', response['name']),
sensor_class,
pin)])
# pylint: disable=too-many-instance-attributes, too-many-arguments # pylint: disable=too-many-instance-attributes, too-many-arguments

View File

@ -0,0 +1,91 @@
"""
Support for monitoring the state of Digital Ocean droplets.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.digital_ocean/
"""
import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.digital_ocean import (
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
ATTR_REGION, ATTR_VCPUS)
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Droplet'
DEFAULT_SENSOR_CLASS = 'motion'
DEPENDENCIES = ['digital_ocean']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Digital Ocean droplet sensor."""
digital_ocean = get_component('digital_ocean')
droplets = config.get(CONF_DROPLETS)
dev = []
for droplet in droplets:
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet)
dev.append(DigitalOceanBinarySensor(
digital_ocean.DIGITAL_OCEAN, droplet_id))
add_devices(dev)
class DigitalOceanBinarySensor(BinarySensorDevice):
"""Representation of a Digital Ocean droplet sensor."""
def __init__(self, do, droplet_id):
"""Initialize a new Digital Ocean sensor."""
self._digital_ocean = do
self._droplet_id = droplet_id
self._state = None
self.update()
@property
def name(self):
"""Return the name of the sensor."""
return self.data.name
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self.data.status == 'active'
@property
def sensor_class(self):
"""Return the class of this sensor."""
return DEFAULT_SENSOR_CLASS
@property
def state_attributes(self):
"""Return the state attributes of the Digital Ocean droplet."""
return {
ATTR_CREATED_AT: self.data.created_at,
ATTR_DROPLET_ID: self.data.id,
ATTR_DROPLET_NAME: self.data.name,
ATTR_FEATURES: self.data.features,
ATTR_IPV4_ADDRESS: self.data.ip_address,
ATTR_IPV6_ADDRESS: self.data.ip_v6_address,
ATTR_MEMORY: self.data.memory,
ATTR_REGION: self.data.region['name'],
ATTR_VCPUS: self.data.vcpus,
}
def update(self):
"""Update state of sensor."""
self._digital_ocean.update()
for droplet in self._digital_ocean.data:
if droplet.id == self._droplet_id:
self.data = droplet

View File

@ -16,6 +16,7 @@ DEPENDENCIES = ['homematic']
SENSOR_TYPES_CLASS = { SENSOR_TYPES_CLASS = {
"Remote": None, "Remote": None,
"ShutterContact": "opening", "ShutterContact": "opening",
"IPShutterContact": "opening",
"Smoke": "smoke", "Smoke": "smoke",
"SmokeV2": "smoke", "SmokeV2": "smoke",
"Motion": "motion", "Motion": "motion",

View File

@ -1,41 +1,56 @@
""" """
Support for exposing nx584 elements as sensors. Support for exposing NX584 elements as sensors.
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.nx584/ https://home-assistant.io/components/binary_sensor.nx584/
""" """
import logging import logging
import threading import threading
import time import time
import requests import requests
import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
SENSOR_CLASSES, BinarySensorDevice) SENSOR_CLASSES, BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_HOST, CONF_PORT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pynx584==0.2'] REQUIREMENTS = ['pynx584==0.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_EXCLUDE_ZONES = 'exclude_zones'
CONF_ZONE_TYPES = 'zone_types'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = '5007'
DEFAULT_SSL = False
ZONE_TYPES_SCHEMA = vol.Schema({
cv.positive_int: vol.In(SENSOR_CLASSES),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_EXCLUDE_ZONES, default=[]):
vol.All(cv.ensure_list, [cv.positive_int]),
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
})
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup nx584 binary sensor platform.""" """Setup the NX584 binary sensor platform."""
from nx584 import client as nx584_client from nx584 import client as nx584_client
host = config.get('host', 'localhost:5007') host = config.get(CONF_HOST)
exclude = config.get('exclude_zones', []) port = config.get(CONF_PORT)
zone_types = config.get('zone_types', {}) exclude = config.get(CONF_EXCLUDE_ZONES)
zone_types = config.get(CONF_ZONE_TYPES)
if not all(isinstance(zone, int) for zone in exclude):
_LOGGER.error('Invalid excluded zone specified (use zone number)')
return False
if not all(isinstance(zone, int) and ztype in SENSOR_CLASSES
for zone, ztype in zone_types.items()):
_LOGGER.error('Invalid zone_types entry')
return False
try: try:
client = nx584_client.Client('http://%s' % host) client = nx584_client.Client('http://{}:{}'.format(host, port))
zones = client.list_zones() zones = client.list_zones()
except requests.exceptions.ConnectionError as ex: except requests.exceptions.ConnectionError as ex:
_LOGGER.error('Unable to connect to NX584: %s', str(ex)) _LOGGER.error('Unable to connect to NX584: %s', str(ex))
@ -43,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
version = [int(v) for v in client.get_version().split('.')] version = [int(v) for v in client.get_version().split('.')]
if version < [1, 1]: if version < [1, 1]:
_LOGGER.error('NX584 is too old to use for sensors (>=0.2 required)') _LOGGER.error("NX584 is too old to use for sensors (>=0.2 required)")
return False return False
zone_sensors = { zone_sensors = {
@ -57,13 +72,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
watcher = NX584Watcher(client, zone_sensors) watcher = NX584Watcher(client, zone_sensors)
watcher.start() watcher.start()
else: else:
_LOGGER.warning('No zones found on NX584') _LOGGER.warning("No zones found on NX584")
return True return True
class NX584ZoneSensor(BinarySensorDevice): class NX584ZoneSensor(BinarySensorDevice):
"""Represents a NX584 zone as a sensor.""" """Representation of a NX584 zone as a sensor."""
def __init__(self, zone, zone_type): def __init__(self, zone, zone_type):
"""Initialize the nx594 binary sensor.""" """Initialize the nx594 binary sensor."""
@ -96,7 +110,7 @@ class NX584Watcher(threading.Thread):
"""Event listener thread to process NX584 events.""" """Event listener thread to process NX584 events."""
def __init__(self, client, zone_sensors): def __init__(self, client, zone_sensors):
"""Initialize nx584 watcher thread.""" """Initialize NX584 watcher thread."""
super(NX584Watcher, self).__init__() super(NX584Watcher, self).__init__()
self.daemon = True self.daemon = True
self._client = client self._client = client
@ -130,5 +144,5 @@ class NX584Watcher(threading.Thread):
try: try:
self._run() self._run()
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error('Failed to reach NX584 server') _LOGGER.error("Failed to reach NX584 server")
time.sleep(10) time.sleep(10)

View File

@ -5,15 +5,19 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.rest/ https://home-assistant.io/components/binary_sensor.rest/
""" """
import logging import logging
import json
import voluptuous as vol import voluptuous as vol
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA) BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA)
from homeassistant.components.sensor.rest import RestData from homeassistant.components.sensor.rest import RestData
from homeassistant.const import ( from homeassistant.const import (
CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE, CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE,
CONF_SENSOR_CLASS, CONF_VERIFY_SSL) CONF_SENSOR_CLASS, CONF_VERIFY_SSL, CONF_USERNAME, CONF_PASSWORD,
CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -24,16 +28,21 @@ DEFAULT_VERIFY_SSL = True
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RESOURCE): cv.url, vol.Required(CONF_RESOURCE): cv.url,
vol.Optional(CONF_AUTHENTICATION):
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
vol.Optional(CONF_HEADERS): cv.string,
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(['POST', 'GET']), vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(['POST', 'GET']),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PAYLOAD): cv.string, vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA, vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
}) })
# pylint: disable=unused-variable # pylint: disable=unused-variable, too-many-locals
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the REST binary sensor.""" """Setup the REST binary sensor."""
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
@ -41,11 +50,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
method = config.get(CONF_METHOD) method = config.get(CONF_METHOD)
payload = config.get(CONF_PAYLOAD) payload = config.get(CONF_PAYLOAD)
verify_ssl = config.get(CONF_VERIFY_SSL) verify_ssl = config.get(CONF_VERIFY_SSL)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
headers = json.loads(config.get(CONF_HEADERS, '{}'))
sensor_class = config.get(CONF_SENSOR_CLASS) sensor_class = config.get(CONF_SENSOR_CLASS)
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
rest = RestData(method, resource, payload, verify_ssl)
if username and password:
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
auth = HTTPDigestAuth(username, password)
else:
auth = HTTPBasicAuth(username, password)
else:
auth = None
rest = RestData(method, resource, auth, headers, payload, verify_ssl)
rest.update() rest.update()
if rest.data is None: if rest.data is None:

View File

@ -4,10 +4,12 @@ Support for exposing a templated binary 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/binary_sensor.template/ https://home-assistant.io/components/binary_sensor.template/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
SENSOR_CLASSES_SCHEMA) SENSOR_CLASSES_SCHEMA)
@ -81,9 +83,10 @@ class BinarySensorTemplate(BinarySensorDevice):
self.update() self.update()
@callback
def template_bsensor_state_listener(entity, old_state, new_state): def template_bsensor_state_listener(entity, old_state, new_state):
"""Called when the target device changes state.""" """Called when the target device changes state."""
self.update_ha_state(True) hass.loop.create_task(self.async_update_ha_state(True))
track_state_change(hass, entity_ids, template_bsensor_state_listener) track_state_change(hass, entity_ids, template_bsensor_state_listener)
@ -107,10 +110,11 @@ class BinarySensorTemplate(BinarySensorDevice):
"""No polling needed.""" """No polling needed."""
return False return False
def update(self): @asyncio.coroutine
def async_update(self):
"""Get the latest data and update the state.""" """Get the latest data and update the state."""
try: try:
self._state = self._template.render().lower() == 'true' self._state = self._template.async_render().lower() == 'true'
except TemplateError as ex: except TemplateError as ex:
if ex.args and ex.args[0].startswith( if ex.args and ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"): "UndefinedError: 'None' has no attribute"):

View File

@ -20,7 +20,10 @@ SENSOR_TYPES = {
"vibration": "vibration", "vibration": "vibration",
"loudness": "sound", "loudness": "sound",
"liquid_detected": "moisture", "liquid_detected": "moisture",
"motion": "motion" "motion": "motion",
"presence": "occupancy",
"co_detected": "gas",
"smoke_detected": "smoke"
} }
@ -35,6 +38,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for key in pywink.get_keys(): for key in pywink.get_keys():
add_devices([WinkBinarySensorDevice(key)]) add_devices([WinkBinarySensorDevice(key)])
for sensor in pywink.get_smoke_and_co_detectors():
add_devices([WinkBinarySensorDevice(sensor)])
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
"""Representation of a Wink binary sensor.""" """Representation of a Wink binary sensor."""
@ -58,17 +64,25 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
if self.capability == "loudness": if self.capability == "loudness":
return self.wink.loudness_boolean() state = self.wink.loudness_boolean()
elif self.capability == "vibration": elif self.capability == "vibration":
return self.wink.vibration_boolean() state = self.wink.vibration_boolean()
elif self.capability == "brightness": elif self.capability == "brightness":
return self.wink.brightness_boolean() state = self.wink.brightness_boolean()
elif self.capability == "liquid_detected": elif self.capability == "liquid_detected":
return self.wink.liquid_boolean() state = self.wink.liquid_boolean()
elif self.capability == "motion": elif self.capability == "motion":
return self.wink.motion_boolean() state = self.wink.motion_boolean()
elif self.capability == "presence":
state = self.wink.presence_boolean()
elif self.capability == "co_detected":
state = self.wink.co_detected_boolean()
elif self.capability == "smoke_detected":
state = self.wink.smoke_detected_boolean()
else: else:
return self.wink.state() state = self.wink.state()
return state
@property @property
def sensor_class(self): def sensor_class(self):

View File

@ -36,8 +36,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None or zwave.NETWORK is None: if discovery_info is None or zwave.NETWORK is None:
return return
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] value = node.values[discovery_info[zwave.const.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 # Make sure that we have values for the key before converting to int
@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
]) ])
return return
if value.command_class == zwave.COMMAND_CLASS_SENSOR_BINARY: if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
add_devices([ZWaveBinarySensor(value, None)]) add_devices([ZWaveBinarySensor(value, None)])

View File

@ -58,6 +58,12 @@ ATTR_OPERATION_LIST = "operation_list"
ATTR_SWING_MODE = "swing_mode" ATTR_SWING_MODE = "swing_mode"
ATTR_SWING_LIST = "swing_list" ATTR_SWING_LIST = "swing_list"
CONVERTIBLE_ATTRIBUTE = [
ATTR_TEMPERATURE,
ATTR_TARGET_TEMP_LOW,
ATTR_TARGET_TEMP_HIGH,
]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SET_AWAY_MODE_SCHEMA = vol.Schema({ SET_AWAY_MODE_SCHEMA = vol.Schema({
@ -73,6 +79,7 @@ SET_TEMPERATURE_SCHEMA = vol.Schema({
vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float), vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float),
vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float), vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float),
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_OPERATION_MODE): cv.string,
}) })
SET_FAN_MODE_SCHEMA = vol.Schema({ SET_FAN_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
@ -116,8 +123,10 @@ def set_aux_heat(hass, aux_heat, entity_id=None):
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
# pylint: disable=too-many-arguments
def set_temperature(hass, temperature=None, entity_id=None, def set_temperature(hass, temperature=None, entity_id=None,
target_temp_high=None, target_temp_low=None): target_temp_high=None, target_temp_low=None,
operation_mode=None):
"""Set new target temperature.""" """Set new target temperature."""
kwargs = { kwargs = {
key: value for key, value in [ key: value for key, value in [
@ -125,6 +134,7 @@ def set_temperature(hass, temperature=None, entity_id=None,
(ATTR_TARGET_TEMP_HIGH, target_temp_high), (ATTR_TARGET_TEMP_HIGH, target_temp_high),
(ATTR_TARGET_TEMP_LOW, target_temp_low), (ATTR_TARGET_TEMP_LOW, target_temp_low),
(ATTR_ENTITY_ID, entity_id), (ATTR_ENTITY_ID, entity_id),
(ATTR_OPERATION_MODE, operation_mode)
] if value is not None ] if value is not None
} }
_LOGGER.debug("set_temperature start data=%s", kwargs) _LOGGER.debug("set_temperature start data=%s", kwargs)
@ -235,10 +245,20 @@ def setup(hass, config):
def temperature_set_service(service): def temperature_set_service(service):
"""Set temperature on the target climate devices.""" """Set temperature on the target climate devices."""
target_climate = component.extract_from_service(service) target_climate = component.extract_from_service(service)
kwargs = service.data
for climate in target_climate:
climate.set_temperature(**kwargs)
for climate in target_climate:
kwargs = {}
for value, temp in service.data.items():
if value in CONVERTIBLE_ATTRIBUTE:
kwargs[value] = convert_temperature(
temp,
hass.config.units.temperature_unit,
climate.unit_of_measurement
)
else:
kwargs[value] = temp
climate.set_temperature(**kwargs)
if climate.should_poll: if climate.should_poll:
climate.update_ha_state(True) climate.update_ha_state(True)

View File

@ -13,7 +13,6 @@ from homeassistant.components.climate import (
ATTR_TEMPERATURE) ATTR_TEMPERATURE)
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN) TEMP_CELSIUS, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
from homeassistant.util.temperature import convert as convert_temperature
DEPENDENCIES = ['nest'] DEPENDENCIES = ['nest']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -127,12 +126,9 @@ class NestThermostat(ClimateDevice):
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
if kwargs.get(ATTR_TARGET_TEMP_LOW) is not None and \ target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None: target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
target_temp_high = convert_temperature(kwargs.get( if target_temp_low is not None and target_temp_high is not None:
ATTR_TARGET_TEMP_HIGH), self._unit, TEMP_CELSIUS)
target_temp_low = convert_temperature(kwargs.get(
ATTR_TARGET_TEMP_LOW), self._unit, TEMP_CELSIUS)
if self.device.mode == 'range': if self.device.mode == 'range':
temp = (target_temp_low, target_temp_high) temp = (target_temp_low, target_temp_high)

View File

@ -34,6 +34,18 @@ set_temperature:
description: New target temperature for hvac description: New target temperature for hvac
example: 25 example: 25
target_temp_high:
description: New target high tempereature for hvac
example: 26
target_temp_low:
description: New target low temperature for hvac
example: 20
operation_mode:
description: Operation mode to set temperature to. This defaults to current_operation mode if not set, or set incorrectly.
example: 'Heat'
set_humidity: set_humidity:
description: Set target humidity of climate device description: Set target humidity of climate device

View File

@ -8,9 +8,9 @@ https://home-assistant.io/components/climate.zwave/
# pylint: disable=import-error # pylint: disable=import-error
import logging import logging
from homeassistant.components.climate import DOMAIN from homeassistant.components.climate import DOMAIN
from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate import (
from homeassistant.components.zwave import ( ClimateDevice, ATTR_OPERATION_MODE)
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity) from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
@ -28,12 +28,6 @@ HORSTMANN = 0x0059
HORSTMANN_HRT4_ZW = 0x3 HORSTMANN_HRT4_ZW = 0x3
HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW) HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW)
COMMAND_CLASS_SENSOR_MULTILEVEL = 0x31
COMMAND_CLASS_THERMOSTAT_MODE = 0x40
COMMAND_CLASS_THERMOSTAT_SETPOINT = 0x43
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 0x44
COMMAND_CLASS_CONFIGURATION = 0x70
WORKAROUND_ZXT_120 = 'zxt_120' WORKAROUND_ZXT_120 = 'zxt_120'
WORKAROUND_HRT4_ZW = 'hrt4_zw' WORKAROUND_HRT4_ZW = 'hrt4_zw'
@ -67,19 +61,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
discovery_info, zwave.NETWORK) discovery_info, zwave.NETWORK)
return return
temp_unit = hass.config.units.temperature_unit temp_unit = hass.config.units.temperature_unit
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]] node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[ATTR_VALUE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
value.set_change_verified(False) value.set_change_verified(False)
add_devices([ZWaveClimate(value, temp_unit)]) add_devices([ZWaveClimate(value, temp_unit)])
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s", _LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
discovery_info, zwave.NETWORK) discovery_info, zwave.NETWORK)
# pylint: disable=too-many-arguments, abstract-method # pylint: disable=abstract-method
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Represents a ZWave Climate device.""" """Represents a ZWave Climate device."""
# pylint: disable=too-many-public-methods, too-many-instance-attributes # pylint: disable=too-many-instance-attributes
def __init__(self, value, temp_unit): def __init__(self, value, temp_unit):
"""Initialize the zwave climate device.""" """Initialize the zwave climate device."""
from openzwave.network import ZWaveNetwork from openzwave.network import ZWaveNetwork
@ -130,7 +124,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Callback on data change for the registered node/value pair.""" """Callback on data change for the registered node/value pair."""
# Operation Mode # Operation Mode
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values(): class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
self._current_operation = value.data self._current_operation = value.data
self._index_operation = SET_TEMP_TO_INDEX.get( self._index_operation = SET_TEMP_TO_INDEX.get(
self._current_operation) self._current_operation)
@ -139,14 +133,16 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
_LOGGER.debug("self._current_operation=%s", _LOGGER.debug("self._current_operation=%s",
self._current_operation) self._current_operation)
# Current Temp # Current Temp
for value in self._node.get_values( for value in (self._node.get_values(
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values(): class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
.values()):
if value.label == 'Temperature': if value.label == 'Temperature':
self._current_temperature = int(value.data) self._current_temperature = int(value.data)
self._unit = value.units self._unit = value.units
# Fan Mode # Fan Mode
for value in self._node.get_values( for value in (self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values(): class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
.values()):
self._current_fan_mode = value.data self._current_fan_mode = value.data
self._fan_list = list(value.data_items) self._fan_list = list(value.data_items)
_LOGGER.debug("self._fan_list=%s", self._fan_list) _LOGGER.debug("self._fan_list=%s", self._fan_list)
@ -154,17 +150,27 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
self._current_fan_mode) self._current_fan_mode)
# Swing mode # Swing mode
if self._zxt_120 == 1: if self._zxt_120 == 1:
for value in self._node.get_values( for value in (self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values(): class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
if value.command_class == 112 and value.index == 33: .values()):
if value.command_class == \
zwave.const.COMMAND_CLASS_CONFIGURATION and \
value.index == 33:
self._current_swing_mode = value.data self._current_swing_mode = value.data
self._swing_list = list(value.data_items) self._swing_list = list(value.data_items)
_LOGGER.debug("self._swing_list=%s", self._swing_list) _LOGGER.debug("self._swing_list=%s", self._swing_list)
_LOGGER.debug("self._current_swing_mode=%s", _LOGGER.debug("self._current_swing_mode=%s",
self._current_swing_mode) self._current_swing_mode)
# Set point # Set point
for value in self._node.get_values( for value in (self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values(): class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
.values()):
if value.data == 0:
_LOGGER.debug("Setpoint is 0, setting default to "
"current_temperature=%s",
self._current_temperature)
self._target_temperature = int(self._current_temperature)
break
if self.current_operation is not None and \ if self.current_operation is not None and \
self.current_operation != 'Off': self.current_operation != 'Off':
if self._index_operation != value.index: if self._index_operation != value.index:
@ -232,16 +238,26 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._target_temperature return self._target_temperature
# pylint: disable=too-many-branches, too-many-statements
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
if kwargs.get(ATTR_TEMPERATURE) is not None: if kwargs.get(ATTR_TEMPERATURE) is not None:
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
else: else:
return return
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
_LOGGER.debug("set_temperature operation_mode=%s", operation_mode)
for value in (self._node.get_values(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
.values()):
if operation_mode is not None:
setpoint_mode = SET_TEMP_TO_INDEX.get(operation_mode)
if value.index != setpoint_mode:
continue
_LOGGER.debug("setpoint_mode=%s", setpoint_mode)
value.data = temperature
break
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
if self.current_operation is not None: if self.current_operation is not None:
if self._hrt4_zw and self.current_operation == 'Off': if self._hrt4_zw and self.current_operation == 'Off':
# HRT4-ZW can change setpoint when off. # HRT4-ZW can change setpoint when off.
@ -279,17 +295,21 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
def set_fan_mode(self, fan): def set_fan_mode(self, fan):
"""Set new target fan mode.""" """Set new target fan mode."""
for value in self._node.get_values( for value in (self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values(): class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE).
if value.command_class == 68 and value.index == 0: values()):
if value.command_class == \
zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE and \
value.index == 0:
value.data = bytes(fan, 'utf-8') value.data = bytes(fan, 'utf-8')
break break
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set new target operation mode.""" """Set new target operation mode."""
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values(): class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
if value.command_class == 64 and value.index == 0: if value.command_class == \
zwave.const.COMMAND_CLASS_THERMOSTAT_MODE and value.index == 0:
value.data = bytes(operation_mode, 'utf-8') value.data = bytes(operation_mode, 'utf-8')
break break
@ -297,7 +317,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Set new target swing mode.""" """Set new target swing mode."""
if self._zxt_120 == 1: if self._zxt_120 == 1:
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values(): class_id=zwave.const.COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33: if value.command_class == \
zwave.const.COMMAND_CLASS_CONFIGURATION and \
value.index == 33:
value.data = bytes(swing_mode, 'utf-8') value.data = bytes(swing_mode, 'utf-8')
break break

View File

@ -29,6 +29,10 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower), vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower),
}) })
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config): def setup(hass, config):
"""Register the process service.""" """Register the process service."""
@ -48,8 +52,8 @@ def setup(hass, config):
name, command = match.groups() name, command = match.groups()
entities = {state.entity_id: state.name for state in hass.states.all()} entities = {state.entity_id: state.name for state in hass.states.all()}
entity_ids = fuzzyExtract.extractOne(name, entities, entity_ids = fuzzyExtract.extractOne(
score_cutoff=65)[2] name, entities, score_cutoff=65)[2]
if not entity_ids: if not entity_ids:
logger.error( logger.error(
@ -70,6 +74,7 @@ def setup(hass, config):
logger.error('Got unsupported command %s from text %s', logger.error('Got unsupported command %s from text %s',
command, text) command, text)
hass.services.register(DOMAIN, SERVICE_PROCESS, process, hass.services.register(
schema=SERVICE_PROCESS_SCHEMA) DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA)
return True return True

View File

@ -0,0 +1,103 @@
"""
Support for MySensors covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.mysensors/
"""
import logging
from homeassistant.components import mysensors
from homeassistant.components.cover import CoverDevice, ATTR_POSITION
from homeassistant.const import STATE_ON, STATE_OFF
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = []
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the mysensors platform for covers."""
if discovery_info is None:
return
for gateway in mysensors.GATEWAYS.values():
pres = gateway.const.Presentation
set_req = gateway.const.SetReq
map_sv_types = {
pres.S_COVER: [set_req.V_DIMMER, set_req.V_LIGHT],
}
if float(gateway.protocol_version) >= 1.5:
map_sv_types.update({
pres.S_COVER: [set_req.V_PERCENTAGE, set_req.V_STATUS],
})
devices = {}
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
map_sv_types, devices, add_devices, MySensorsCover))
class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice):
"""Representation of the value of a MySensors Cover child node."""
@property
def assumed_state(self):
"""Return True if unable to access real state of entity."""
return self.gateway.optimistic
@property
def is_closed(self):
"""Return True if cover is closed."""
set_req = self.gateway.const.SetReq
if set_req.V_DIMMER in self._values:
return self._values.get(set_req.V_DIMMER) == 0
else:
return self._values.get(set_req.V_LIGHT) == STATE_OFF
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
set_req = self.gateway.const.SetReq
return self._values.get(set_req.V_DIMMER)
def open_cover(self, **kwargs):
"""Move the cover up."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_UP, 1)
if self.gateway.optimistic:
# Optimistically assume that cover has changed state.
if set_req.V_DIMMER in self._values:
self._values[set_req.V_DIMMER] = 100
else:
self._values[set_req.V_LIGHT] = STATE_ON
self.update_ha_state()
def close_cover(self, **kwargs):
"""Move the cover down."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_DOWN, 1)
if self.gateway.optimistic:
# Optimistically assume that cover has changed state.
if set_req.V_DIMMER in self._values:
self._values[set_req.V_DIMMER] = 0
else:
self._values[set_req.V_LIGHT] = STATE_OFF
self.update_ha_state()
def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
position = kwargs.get(ATTR_POSITION)
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_DIMMER, position)
if self.gateway.optimistic:
# Optimistically assume that cover has changed state.
self._values[set_req.V_DIMMER] = position
self.update_ha_state()
def stop_cover(self, **kwargs):
"""Stop the device."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_STOP, 1)

View File

@ -12,9 +12,6 @@ from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.cover import CoverDevice from homeassistant.components.cover import CoverDevice
COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
SOMFY = 0x47 SOMFY = 0x47
SOMFY_ZRTSI = 0x5a52 SOMFY_ZRTSI = 0x5a52
SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI) SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI)
@ -32,17 +29,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None or zwave.NETWORK is None: if discovery_info is None or zwave.NETWORK is None:
return return
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
if (value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL and if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL) \
value.index == 0): and value.index == 0:
value.set_change_verified(False) value.set_change_verified(False)
add_devices([ZwaveRollershutter(value)]) add_devices([ZwaveRollershutter(value)])
elif (value.command_class == zwave.COMMAND_CLASS_SWITCH_BINARY or elif node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_BINARY) or \
value.command_class == zwave.COMMAND_CLASS_BARRIER_OPERATOR): node.has_command_class(zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
if value.type != zwave.TYPE_BOOL and \ if value.type != zwave.const.TYPE_BOOL and \
value.genre != zwave.GENRE_USER: value.genre != zwave.const.GENRE_USER:
return return
value.set_change_verified(False) value.set_change_verified(False)
add_devices([ZwaveGarageDoor(value)]) add_devices([ZwaveGarageDoor(value)])
@ -59,6 +56,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
from openzwave.network import ZWaveNetwork from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN) ZWaveDeviceEntity.__init__(self, value, DOMAIN)
# pylint: disable=no-member
self._lozwmgr = libopenzwave.PyManager() self._lozwmgr = libopenzwave.PyManager()
self._lozwmgr.create() self._lozwmgr.create()
self._node = value.node self._node = value.node
@ -88,9 +86,10 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
"""Callback on data change for the registered node/value pair.""" """Callback on data change for the registered node/value pair."""
# Position value # Position value
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ if value.command_class == \
and value.label == 'Level': zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and \
value.label == 'Level':
self._current_position = value.data self._current_position = value.data
@property @property
@ -118,22 +117,24 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
def open_cover(self, **kwargs): def open_cover(self, **kwargs):
"""Move the roller shutter up.""" """Move the roller shutter up."""
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ if value.command_class == \
and value.label == 'Open' or \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ 'Open' or value.command_class == \
and value.label == 'Down': zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
'Down':
self._lozwmgr.pressButton(value.value_id) self._lozwmgr.pressButton(value.value_id)
break break
def close_cover(self, **kwargs): def close_cover(self, **kwargs):
"""Move the roller shutter down.""" """Move the roller shutter down."""
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ if value.command_class == \
and value.label == 'Up' or \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ 'Up' or value.command_class == \
and value.label == 'Close': zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
'Close':
self._lozwmgr.pressButton(value.value_id) self._lozwmgr.pressButton(value.value_id)
break break
@ -144,11 +145,12 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
def stop_cover(self, **kwargs): def stop_cover(self, **kwargs):
"""Stop the roller shutter.""" """Stop the roller shutter."""
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ if value.command_class == \
and value.label == 'Open' or \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ 'Open' or value.command_class == \
and value.label == 'Down': zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
'Down':
self._lozwmgr.releaseButton(value.value_id) self._lozwmgr.releaseButton(value.value_id)
break break

View File

@ -46,7 +46,6 @@ CONF_TRACK_NEW = 'track_new_devices'
DEFAULT_TRACK_NEW = True DEFAULT_TRACK_NEW = True
CONF_CONSIDER_HOME = 'consider_home' CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONSIDER_HOME = 180 # seconds
CONF_SCAN_INTERVAL = 'interval_seconds' CONF_SCAN_INTERVAL = 'interval_seconds'
DEFAULT_SCAN_INTERVAL = 12 DEFAULT_SCAN_INTERVAL = 12
@ -70,8 +69,10 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
_CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [ _CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [
vol.Schema({ vol.Schema({
vol.Optional(CONF_TRACK_NEW): cv.boolean, vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
vol.Optional(CONF_CONSIDER_HOME): cv.positive_int # seconds vol.Optional(
CONF_CONSIDER_HOME, default=timedelta(seconds=180)): vol.All(
cv.time_period, cv.positive_timedelta)
}, extra=vol.ALLOW_EXTRA)])}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)])}, extra=vol.ALLOW_EXTRA)
DISCOVERY_PLATFORMS = { DISCOVERY_PLATFORMS = {
@ -118,9 +119,8 @@ def setup(hass: HomeAssistantType, config: ConfigType):
return False return False
else: else:
conf = conf[0] if len(conf) > 0 else {} conf = conf[0] if len(conf) > 0 else {}
consider_home = timedelta( consider_home = conf[CONF_CONSIDER_HOME]
seconds=conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME)) track_new = conf[CONF_TRACK_NEW]
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
devices = load_config(yaml_path, hass, consider_home) devices = load_config(yaml_path, hass, consider_home)
@ -282,7 +282,7 @@ class Device(Entity):
def __init__(self, hass: HomeAssistantType, consider_home: timedelta, def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str=None, track: bool, dev_id: str, mac: str, name: str=None,
picture: str=None, gravatar: str=None, picture: str=None, gravatar: str=None,
away_hide: bool=False) -> None: hide_if_away: bool=False) -> None:
"""Initialize a device.""" """Initialize a device."""
self.hass = hass self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id) self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
@ -307,7 +307,7 @@ class Device(Entity):
else: else:
self.config_picture = picture self.config_picture = picture
self.away_hide = away_hide self.away_hide = hide_if_away
@property @property
def name(self): def name(self):
@ -398,15 +398,29 @@ class Device(Entity):
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
"""Load devices from YAML configuration file.""" """Load devices from YAML configuration file."""
dev_schema = vol.Schema({
vol.Required('name'): cv.string,
vol.Optional('track', default=False): cv.boolean,
vol.Optional('mac', default=None): vol.Any(None, vol.All(cv.string,
vol.Upper)),
vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean,
vol.Optional('gravatar', default=None): vol.Any(None, cv.string),
vol.Optional('picture', default=None): vol.Any(None, cv.string),
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
cv.time_period, cv.positive_timedelta)
})
try: try:
return [ result = []
Device(hass, consider_home, device.get('track', False), devices = load_yaml_config_file(path)
str(dev_id).lower(), None if device.get('mac') is None for dev_id, device in devices.items():
else str(device.get('mac')).upper(), try:
device.get('name'), device.get('picture'), device = dev_schema(device)
device.get('gravatar'), device['dev_id'] = cv.slug(dev_id)
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE)) except vol.Invalid as exp:
for dev_id, device in load_yaml_config_file(path).items()] log_exception(exp, dev_id, devices)
else:
result.append(Device(hass, **device))
return result
except (HomeAssistantError, FileNotFoundError): except (HomeAssistantError, FileNotFoundError):
# When YAML file could not be loaded/did not contain a dict # When YAML file could not be loaded/did not contain a dict
return [] return []

View File

@ -85,7 +85,9 @@ class FritzBoxScanner(object):
def get_device_name(self, mac): def get_device_name(self, mac):
"""Return the name of the given device or None if is not known.""" """Return the name of the given device or None if is not known."""
ret = self.fritz_box.get_specific_host_entry(mac)['NewHostName'] ret = self.fritz_box.get_specific_host_entry(mac).get(
'NewHostName'
)
if ret == {}: if ret == {}:
return None return None
return ret return ret

View File

@ -7,6 +7,7 @@ https://home-assistant.io/components/device_tracker.owntracks/
import json import json
import logging import logging
import threading import threading
import base64
from collections import defaultdict from collections import defaultdict
import voluptuous as vol import voluptuous as vol
@ -18,53 +19,121 @@ from homeassistant.util import convert, slugify
from homeassistant.components import zone as zone_comp from homeassistant.components import zone as zone_comp
from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker import PLATFORM_SCHEMA
DEPENDENCIES = ['mqtt'] REQUIREMENTS = ['libnacl==1.5.0']
REGIONS_ENTERED = defaultdict(list)
MOBILE_BEACONS_ACTIVE = defaultdict(list)
BEACON_DEV_ID = 'beacon'
LOCATION_TOPIC = 'owntracks/+/+'
EVENT_TOPIC = 'owntracks/+/+/event'
WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
LOCK = threading.Lock() BEACON_DEV_ID = 'beacon'
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
CONF_SECRET = 'secret'
CONF_WAYPOINT_IMPORT = 'waypoints' CONF_WAYPOINT_IMPORT = 'waypoints'
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist' CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
DEPENDENCIES = ['mqtt']
EVENT_TOPIC = 'owntracks/+/+/event'
LOCATION_TOPIC = 'owntracks/+/+'
LOCK = threading.Lock()
MOBILE_BEACONS_ACTIVE = defaultdict(list)
REGIONS_ENTERED = defaultdict(list)
VALIDATE_LOCATION = 'location' VALIDATE_LOCATION = 'location'
VALIDATE_TRANSITION = 'transition' VALIDATE_TRANSITION = 'transition'
VALIDATE_WAYPOINTS = 'waypoints' VALIDATE_WAYPOINTS = 'waypoints'
WAYPOINT_LAT_KEY = 'lat' WAYPOINT_LAT_KEY = 'lat'
WAYPOINT_LON_KEY = 'lon' WAYPOINT_LON_KEY = 'lon'
WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float), vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean, vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean,
vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(cv.ensure_list, [cv.string]) vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(
cv.ensure_list, [cv.string]),
vol.Optional(CONF_SECRET): vol.Any(
vol.Schema({vol.Optional(cv.string): cv.string}),
cv.string)
}) })
def get_cipher():
"""Return decryption function and length of key."""
from libnacl import crypto_secretbox_KEYBYTES as KEYLEN
from libnacl.secret import SecretBox
def decrypt(ciphertext, key):
"""Decrypt ciphertext using key."""
return SecretBox(key).decrypt(ciphertext)
return (KEYLEN, decrypt)
def setup_scanner(hass, config, see): def setup_scanner(hass, config, see):
"""Setup an OwnTracks tracker.""" """Set up an OwnTracks tracker."""
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY) max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
waypoint_import = config.get(CONF_WAYPOINT_IMPORT) waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST) waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
secret = config.get(CONF_SECRET)
def decrypt_payload(topic, ciphertext):
"""Decrypt encrypted payload."""
try:
keylen, decrypt = get_cipher()
except OSError:
_LOGGER.warning('Ignoring encrypted payload '
'because libsodium not installed.')
return None
if isinstance(secret, dict):
key = secret.get(topic)
else:
key = secret
if key is None:
_LOGGER.warning('Ignoring encrypted payload '
'because no decryption key known '
'for topic %s.', topic)
return None
key = key.encode("utf-8")
key = key[:keylen]
key = key.ljust(keylen, b'\0')
try:
ciphertext = base64.b64decode(ciphertext)
message = decrypt(ciphertext, key)
message = message.decode("utf-8")
_LOGGER.debug("Decrypted payload: %s", message)
return message
except ValueError:
_LOGGER.warning('Ignoring encrypted payload '
'because unable to decrypt using key '
'for topic %s.', topic)
return None
def validate_payload(topic, payload, data_type):
"""Validate the OwnTracks payload."""
# pylint: disable=too-many-return-statements
def validate_payload(payload, data_type):
"""Validate OwnTracks payload."""
try: try:
data = json.loads(payload) data = json.loads(payload)
except ValueError: except ValueError:
# If invalid JSON # If invalid JSON
_LOGGER.error('Unable to parse payload as JSON: %s', payload) _LOGGER.error('Unable to parse payload as JSON: %s', payload)
return None return None
if isinstance(data, dict) and \
data.get('_type') == 'encrypted' and \
'data' in data:
plaintext_payload = decrypt_payload(topic, data['data'])
if plaintext_payload is None:
return None
else:
return validate_payload(topic, plaintext_payload, data_type)
if not isinstance(data, dict) or data.get('_type') != data_type: if not isinstance(data, dict) or data.get('_type') != data_type:
_LOGGER.debug('Skipping %s update for following data ' _LOGGER.debug('Skipping %s update for following data '
'because of missing or malformatted data: %s', 'because of missing or malformatted data: %s',
@ -90,7 +159,7 @@ def setup_scanner(hass, config, see):
"""MQTT message received.""" """MQTT message received."""
# Docs on available data: # Docs on available data:
# http://owntracks.org/booklet/tech/json/#_typelocation # http://owntracks.org/booklet/tech/json/#_typelocation
data = validate_payload(payload, VALIDATE_LOCATION) data = validate_payload(topic, payload, VALIDATE_LOCATION)
if not data: if not data:
return return
@ -111,7 +180,7 @@ def setup_scanner(hass, config, see):
"""MQTT event (geofences) received.""" """MQTT event (geofences) received."""
# Docs on available data: # Docs on available data:
# http://owntracks.org/booklet/tech/json/#_typetransition # http://owntracks.org/booklet/tech/json/#_typetransition
data = validate_payload(payload, VALIDATE_TRANSITION) data = validate_payload(topic, payload, VALIDATE_TRANSITION)
if not data: if not data:
return return
@ -206,7 +275,7 @@ def setup_scanner(hass, config, see):
"""List of waypoints published by a user.""" """List of waypoints published by a user."""
# Docs on available data: # Docs on available data:
# http://owntracks.org/booklet/tech/json/#_typewaypoints # http://owntracks.org/booklet/tech/json/#_typewaypoints
data = validate_payload(payload, VALIDATE_WAYPOINTS) data = validate_payload(topic, payload, VALIDATE_WAYPOINTS)
if not data: if not data:
return return

View File

@ -0,0 +1,100 @@
"""
Support for Volvo On Call.
http://www.volvocars.com/intl/own/owner-info/volvo-on-call
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.volvooncall/
"""
import logging
from datetime import timedelta
from urllib.parse import urljoin
import voluptuous as vol
import requests
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.util.dt import utcnow
from homeassistant.const import (
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
CONF_USERNAME)
from homeassistant.components.device_tracker import (
DEFAULT_SCAN_INTERVAL,
PLATFORM_SCHEMA)
MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1)
_LOGGER = logging.getLogger(__name__)
SERVICE_URL = 'https://vocapi.wirelesscar.net/customerapi/rest/v3.0/'
HEADERS = {"X-Device-Id": "Device",
"X-OS-Type": "Android",
"X-Originator-Type": "App"}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
})
def setup_scanner(hass, config, see):
"""Validate the configuration and return a scanner."""
session = requests.Session()
session.headers.update(HEADERS)
session.auth = (config.get(CONF_USERNAME),
config.get(CONF_PASSWORD))
interval = max(MIN_TIME_BETWEEN_SCANS.seconds,
config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))
def query(ref, rel=SERVICE_URL):
"""Perform a query to the online service."""
url = urljoin(rel, ref)
_LOGGER.debug("Request for %s", url)
res = session.get(url, timeout=15)
res.raise_for_status()
_LOGGER.debug("Received %s", res.json())
return res.json()
def update(now):
"""Update status from the online service."""
try:
_LOGGER.debug("Updating")
status = query("status", vehicle_url)
position = query("position", vehicle_url)
see(dev_id=dev_id,
host_name=host_name,
gps=(position["position"]["latitude"],
position["position"]["longitude"]),
attributes=dict(
tank_volume=attributes["fuelTankVolume"],
washer_fluid=status["washerFluidLevel"],
brake_fluid=status["brakeFluid"],
service_warning=status["serviceWarningStatus"],
fuel=status["fuelAmount"],
odometer=status["odometer"],
range=status["distanceToEmpty"]))
except requests.exceptions.RequestException as error:
_LOGGER.error("Could not query server: %s", error)
finally:
track_point_in_utc_time(hass, update,
now + timedelta(seconds=interval))
try:
_LOGGER.info('Logging in to service')
user = query("customeraccounts")
rel = query(user["accountVehicleRelations"][0])
vehicle_url = rel["vehicle"] + '/'
attributes = query("attributes", vehicle_url)
dev_id = "volvo_" + attributes["registrationNumber"]
host_name = "%s %s/%s" % (attributes["registrationNumber"],
attributes["vehicleType"],
attributes["modelYear"])
update(utcnow())
return True
except requests.exceptions.RequestException as error:
_LOGGER.error("Could not log in to service. "
"Please check configuration: "
"%s", error)
return False

View File

@ -0,0 +1,86 @@
"""
Support for Digital Ocean.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/digital_ocean/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-digitalocean==1.9.0']
_LOGGER = logging.getLogger(__name__)
ATTR_CREATED_AT = 'created_at'
ATTR_DROPLET_ID = 'droplet_id'
ATTR_DROPLET_NAME = 'droplet_name'
ATTR_FEATURES = 'features'
ATTR_IPV4_ADDRESS = 'ipv4_address'
ATTR_IPV6_ADDRESS = 'ipv6_address'
ATTR_MEMORY = 'memory'
ATTR_REGION = 'region'
ATTR_VCPUS = 'vcpus'
CONF_DROPLETS = 'droplets'
DIGITAL_OCEAN = None
DIGITAL_OCEAN_PLATFORMS = ['switch', 'binary_sensor']
DOMAIN = 'digital_ocean'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_ACCESS_TOKEN): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
# pylint: disable=unused-argument,too-few-public-methods
def setup(hass, config):
"""Setup the Digital Ocean component."""
conf = config[DOMAIN]
access_token = conf.get(CONF_ACCESS_TOKEN)
global DIGITAL_OCEAN
DIGITAL_OCEAN = DigitalOcean(access_token)
if not DIGITAL_OCEAN.manager.get_account():
_LOGGER.error("No Digital Ocean account found for the given API Token")
return False
return True
class DigitalOcean(object):
"""Handle all communication with the Digital Ocean API."""
def __init__(self, access_token):
"""Initialize the Digital Ocean connection."""
import digitalocean
self._access_token = access_token
self.data = None
self.manager = digitalocean.Manager(token=self._access_token)
def get_droplet_id(self, droplet_name):
"""Get the status of a Digital Ocean droplet."""
droplet_id = None
all_droplets = self.manager.get_all_droplets()
for droplet in all_droplets:
if droplet_name == droplet.name:
droplet_id = droplet.id
return droplet_id
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Use the data from Digital Ocean API."""
self.data = self.manager.get_all_droplets()

View File

@ -9,6 +9,8 @@ loaded before the EVENT_PLATFORM_DISCOVERED is fired.
import logging import logging
import threading import threading
import voluptuous as vol
from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover from homeassistant.helpers.discovery import load_platform, discover
@ -33,6 +35,10 @@ SERVICE_HANDLERS = {
'directv': ('media_player', 'directv'), 'directv': ('media_player', 'directv'),
} }
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config): def setup(hass, config):
"""Start a discovery service.""" """Start a discovery service."""

View File

@ -25,6 +25,8 @@ from homeassistant.components.light import (
from homeassistant.components.http import ( from homeassistant.components.http import (
HomeAssistantView, HomeAssistantWSGI HomeAssistantView, HomeAssistantWSGI
) )
# pylint: disable=unused-import
from homeassistant.components.http import REQUIREMENTS # noqa
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DOMAIN = 'emulated_hue' DOMAIN = 'emulated_hue'

View File

@ -1,14 +1,14 @@
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.""" """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
FINGERPRINTS = { FINGERPRINTS = {
"core.js": "78862c0984279b6876f594ffde45177c", "core.js": "9b3e5ab4eac7e3b074e0daf3f619a638",
"frontend.html": "c1753e1ce530f978036742477c96d2fd", "frontend.html": "5854807d361de26fe93ad474010f19d2",
"mdi.html": "6bd013a8252e19b3c1f1de52994cfbe4", "mdi.html": "46a76f877ac9848899b8ed382427c16f",
"panels/ha-panel-dev-event.html": "c4a5f70eece9f92616a65e8d26be803e", "panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002",
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169", "panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
"panels/ha-panel-dev-service.html": "07e83c6b7f79d78a59258f6dba477b54", "panels/ha-panel-dev-service.html": "c7974458ebc33412d95497e99b785e12",
"panels/ha-panel-dev-state.html": "fd8eb946856b1346a87a51d0c86854ff", "panels/ha-panel-dev-state.html": "4be627b74e683af14ef779d8203ec674",
"panels/ha-panel-dev-template.html": "7cff8a2ef3f44fdaf357a0d41696bf6d", "panels/ha-panel-dev-template.html": "d23943fa0370f168714da407c90091a2",
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295", "panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40", "panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 0a4454c68f3c29c77cd60f4315d410d8b3737543 Subproject commit db109f5dda043182a7e9647b161851e83be9b91e

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:24px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1) <html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:24px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1)
clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/balloob/home-assistant" target="_blank">server</a><a href="https://github.com/balloob/home-assistant-polymer" target="_blank">frontend-ui</a><a href="https://github.com/balloob/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html> clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p>Path to configuration.yaml: [[hassConfigDir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a><a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a><a href="https://github.com/home-assistant/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},hassConfigDir:{type:String,bindNuclear:function(r){return r.configGetters.configDir}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -22,15 +22,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None or zwave.NETWORK is None: if discovery_info is None or zwave.NETWORK is None:
return return
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
if value.command_class != zwave.COMMAND_CLASS_SWITCH_BINARY and \ if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_BINARY and \
value.command_class != zwave.COMMAND_CLASS_BARRIER_OPERATOR: value.command_class != zwave.const.COMMAND_CLASS_BARRIER_OPERATOR:
return return
if value.type != zwave.TYPE_BOOL: if value.type != zwave.const.TYPE_BOOL:
return return
if value.genre != zwave.GENRE_USER: if value.genre != zwave.const.GENRE_USER:
return return
value.set_change_verified(False) value.set_change_verified(False)

View File

@ -23,7 +23,7 @@ from homeassistant.config import load_yaml_config_file
from homeassistant.util import Throttle from homeassistant.util import Throttle
DOMAIN = 'homematic' DOMAIN = 'homematic'
REQUIREMENTS = ["pyhomematic==0.1.14"] REQUIREMENTS = ["pyhomematic==0.1.16"]
HOMEMATIC = None HOMEMATIC = None
HOMEMATIC_LINK_DELAY = 0.5 HOMEMATIC_LINK_DELAY = 0.5
@ -52,17 +52,22 @@ SERVICE_VIRTUALKEY = 'virtualkey'
SERVICE_SET_VALUE = 'set_value' SERVICE_SET_VALUE = 'set_value'
HM_DEVICE_TYPES = { HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: ['Switch', 'SwitchPowermeter'], DISCOVER_SWITCHES: [
DISCOVER_LIGHTS: ['Dimmer'], 'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch',
DISCOVER_SENSORS: ['SwitchPowermeter', 'Motion', 'MotionV2', 'IPSwitchPowermeter', 'KeyMatic'],
'RemoteMotion', 'ThermostatWall', 'AreaThermostat', DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer'],
'RotaryHandleSensor', 'WaterSensor', 'PowermeterGas', DISCOVER_SENSORS: [
'LuxSensor', 'WeatherSensor', 'WeatherStation'], 'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion',
DISCOVER_CLIMATE: ['Thermostat', 'ThermostatWall', 'MAXThermostat'], 'ThermostatWall', 'AreaThermostat', 'RotaryHandleSensor',
DISCOVER_BINARY_SENSORS: ['ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'WaterSensor', 'PowermeterGas', 'LuxSensor', 'WeatherSensor',
'MotionV2', 'RemoteMotion', 'WeatherSensor', 'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor',
'TiltSensor'], 'TemperatureSensor', 'CO2Sensor'],
DISCOVER_COVER: ['Blind'] DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2'],
DISCOVER_BINARY_SENSORS: [
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact'],
DISCOVER_COVER: ['Blind', 'KeyBlind']
} }
HM_IGNORE_DISCOVERY_NODE = [ HM_IGNORE_DISCOVERY_NODE = [
@ -87,11 +92,12 @@ HM_PRESS_EVENTS = [
'PRESS_SHORT', 'PRESS_SHORT',
'PRESS_LONG', 'PRESS_LONG',
'PRESS_CONT', 'PRESS_CONT',
'PRESS_LONG_RELEASE' 'PRESS_LONG_RELEASE',
'PRESS',
] ]
HM_IMPULSE_EVENTS = [ HM_IMPULSE_EVENTS = [
'SEQUENCE_OK' 'SEQUENCE_OK',
] ]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -111,6 +117,15 @@ CONF_RESOLVENAMES = 'resolvenames'
CONF_DELAY = 'delay' CONF_DELAY = 'delay'
CONF_VARIABLES = 'variables' CONF_VARIABLES = 'variables'
DEFAULT_LOCAL_IP = "0.0.0.0"
DEFAULT_LOCAL_PORT = 0
DEFAULT_RESOLVENAMES = False
DEFAULT_REMOTE_PORT = 2001
DEFAULT_USERNAME = "Admin"
DEFAULT_PASSWORD = ""
DEFAULT_VARIABLES = False
DEFAULT_DELAY = 0.5
DEVICE_SCHEMA = vol.Schema({ DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): "homematic", vol.Required(CONF_PLATFORM): "homematic",
@ -122,16 +137,16 @@ DEVICE_SCHEMA = vol.Schema({
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_LOCAL_IP): cv.string,
vol.Optional(CONF_LOCAL_PORT, default=8943): cv.port,
vol.Required(CONF_REMOTE_IP): cv.string, vol.Required(CONF_REMOTE_IP): cv.string,
vol.Optional(CONF_REMOTE_PORT, default=2001): cv.port, vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string,
vol.Optional(CONF_RESOLVENAMES, default=False): vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port,
vol.Optional(CONF_REMOTE_PORT, default=DEFAULT_REMOTE_PORT): cv.port,
vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES):
vol.In(CONF_RESOLVENAMES_OPTIONS), vol.In(CONF_RESOLVENAMES_OPTIONS),
vol.Optional(CONF_USERNAME, default="Admin"): cv.string, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=""): cv.string, vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_DELAY, default=0.5): vol.Coerce(float), vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): vol.Coerce(float),
vol.Optional(CONF_VARIABLES, default=False): cv.boolean, vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES): cv.boolean,
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -323,83 +338,45 @@ def _get_devices(device_type, keys):
metadata.update(device.SENSORNODE) metadata.update(device.SENSORNODE)
elif device_type == DISCOVER_BINARY_SENSORS: elif device_type == DISCOVER_BINARY_SENSORS:
metadata.update(device.BINARYNODE) metadata.update(device.BINARYNODE)
else:
metadata.update({None: device.ELEMENT})
params = _create_params_list(device, metadata, device_type) if metadata:
if params:
# Generate options for 1...n elements with 1...n params # Generate options for 1...n elements with 1...n params
for channel in range(1, device.ELEMENT + 1): for param, channels in metadata.items():
_LOGGER.debug("Handling %s:%i", key, channel) if param in HM_IGNORE_DISCOVERY_NODE:
if channel in params: continue
for param in params[channel]:
name = _create_ha_name(
name=device.NAME,
channel=channel,
param=param
)
device_dict = {
CONF_PLATFORM: "homematic",
ATTR_ADDRESS: key,
ATTR_NAME: name,
ATTR_CHANNEL: channel
}
if param is not None:
device_dict.update({ATTR_PARAM: param})
# Add new device # add devices
try: _LOGGER.debug("Handling %s: %s", param, channels)
DEVICE_SCHEMA(device_dict) for channel in channels:
device_arr.append(device_dict) name = _create_ha_name(
except vol.MultipleInvalid as err: name=device.NAME,
_LOGGER.error("Invalid device config: %s", channel=channel,
str(err)) param=param
else: )
_LOGGER.debug("Channel %i not in params", channel) device_dict = {
CONF_PLATFORM: "homematic",
ATTR_ADDRESS: key,
ATTR_NAME: name,
ATTR_CHANNEL: channel
}
if param is not None:
device_dict[ATTR_PARAM] = param
# Add new device
try:
DEVICE_SCHEMA(device_dict)
device_arr.append(device_dict)
except vol.MultipleInvalid as err:
_LOGGER.error("Invalid device config: %s",
str(err))
else: else:
_LOGGER.debug("Got no params for %s", key) _LOGGER.debug("Got no params for %s", key)
_LOGGER.debug("%s autodiscovery: %s", device_type, str(device_arr)) _LOGGER.debug("%s autodiscovery: %s", device_type, str(device_arr))
return device_arr return device_arr
def _create_params_list(hmdevice, metadata, device_type):
"""Create a list from HMDevice with all possible parameters in config."""
params = {}
merge = False
# use merge?
if device_type in (DISCOVER_SENSORS, DISCOVER_BINARY_SENSORS):
merge = True
# Search in sensor and binary metadata per elements
for channel in range(1, hmdevice.ELEMENT + 1):
param_chan = []
for node, meta_chan in metadata.items():
try:
# Is this attribute ignored?
if node in HM_IGNORE_DISCOVERY_NODE:
continue
if meta_chan == 'c' or meta_chan is None:
# Only channel linked data
param_chan.append(node)
elif channel == 1:
# First channel can have other data channel
param_chan.append(node)
except (TypeError, ValueError):
_LOGGER.error("Exception generating %s (%s)",
hmdevice.ADDRESS, str(metadata))
# default parameter is merge is off
if len(param_chan) == 0 and not merge:
param_chan.append(None)
# Add to channel
if len(param_chan) > 0:
params.update({channel: param_chan})
_LOGGER.debug("Create param list for %s with: %s", hmdevice.ADDRESS,
str(params))
return params
def _create_ha_name(name, channel, param): def _create_ha_name(name, channel, param):
"""Generate a unique object name.""" """Generate a unique object name."""
# HMDevice is a simple device # HMDevice is a simple device
@ -484,12 +461,12 @@ def _hm_service_virtualkey(call):
hmdevice = HOMEMATIC.devices.get(address) hmdevice = HOMEMATIC.devices.get(address)
# if param exists for this device # if param exists for this device
if param not in hmdevice.ACTIONNODE: if hmdevice is None or param not in hmdevice.ACTIONNODE:
_LOGGER.error("%s not datapoint in hm device %s", param, address) _LOGGER.error("%s not datapoint in hm device %s", param, address)
return return
# channel exists? # channel exists?
if channel > hmdevice.ELEMENT: if channel in hmdevice.ACTIONNODE[param]:
_LOGGER.error("%i is not a channel in hm device %s", channel, address) _LOGGER.error("%i is not a channel in hm device %s", channel, address)
return return
@ -743,19 +720,22 @@ class HMDevice(Entity):
self._hmdevice.ATTRIBUTENODE, self._hmdevice.ATTRIBUTENODE,
self._hmdevice.WRITENODE, self._hmdevice.EVENTNODE, self._hmdevice.WRITENODE, self._hmdevice.EVENTNODE,
self._hmdevice.ACTIONNODE): self._hmdevice.ACTIONNODE):
for node, channel in metadata.items(): for node, channels in metadata.items():
# Data is needed for this instance # Data is needed for this instance
if node in self._data: if node in self._data:
# chan is current channel # chan is current channel
if channel == 'c' or channel is None: if len(channels) == 1:
channel = channels[0]
else:
channel = self._channel channel = self._channel
# Prepare for subscription # Prepare for subscription
try: try:
if int(channel) >= 0: if int(channel) >= 0:
channels_to_sub.update({int(channel): True}) channels_to_sub.update({int(channel): True})
except (ValueError, TypeError): except (ValueError, TypeError):
_LOGGER("Invalid channel in metadata from %s", _LOGGER.error("Invalid channel in metadata from %s",
self._name) self._name)
# Set callbacks # Set callbacks
for channel in channels_to_sub: for channel in channels_to_sub:

View File

@ -11,18 +11,20 @@ import mimetypes
import threading import threading
import re import re
import ssl import ssl
import voluptuous as vol import voluptuous as vol
import homeassistant.remote as rem import homeassistant.remote as rem
from homeassistant import util from homeassistant import util
from homeassistant.const import ( from homeassistant.const import (
SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL, SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE_JSON,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS, HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
from homeassistant.core import split_entity_id from homeassistant.core import split_entity_id
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components import persistent_notification
DOMAIN = 'http' DOMAIN = 'http'
REQUIREMENTS = ('cherrypy==8.1.0', 'static3==0.7.0', 'Werkzeug==0.11.11') REQUIREMENTS = ('cherrypy==8.1.0', 'static3==0.7.0', 'Werkzeug==0.11.11')
@ -37,6 +39,7 @@ CONF_SSL_KEY = 'ssl_key'
CONF_CORS_ORIGINS = 'cors_allowed_origins' CONF_CORS_ORIGINS = 'cors_allowed_origins'
DATA_API_PASSWORD = 'api_password' DATA_API_PASSWORD = 'api_password'
NOTIFICATION_ID_LOGIN = 'http-login'
# TLS configuation follows the best-practice guidelines specified here: # TLS configuation follows the best-practice guidelines specified here:
# https://wiki.mozilla.org/Security/Server_Side_TLS # https://wiki.mozilla.org/Security/Server_Side_TLS
@ -106,7 +109,7 @@ def setup(hass, config):
api_password = util.convert(conf.get(CONF_API_PASSWORD), str) api_password = util.convert(conf.get(CONF_API_PASSWORD), str)
server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0') server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT) server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
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, []) cors_origins = conf.get(CONF_CORS_ORIGINS, [])
@ -365,8 +368,8 @@ class HomeAssistantWSGI(object):
context.load_cert_chain(self.ssl_certificate, self.ssl_key) context.load_cert_chain(self.ssl_certificate, self.ssl_key)
self.server.ssl_adapter = ContextSSLAdapter(context) self.server.ssl_adapter = ContextSSLAdapter(context)
threading.Thread(target=self.server.start, daemon=True, threading.Thread(
name='WSGI-server').start() target=self.server.start, daemon=True, name='WSGI-server').start()
def stop(self): def stop(self):
"""Stop the wsgi server.""" """Stop the wsgi server."""
@ -391,10 +394,10 @@ class HomeAssistantWSGI(object):
resp = ex.get_response(request.environ) resp = ex.get_response(request.environ)
if request.accept_mimetypes.accept_json: if request.accept_mimetypes.accept_json:
resp.data = json.dumps({ resp.data = json.dumps({
"result": "error", 'result': 'error',
"message": str(ex), 'message': str(ex),
}) })
resp.mimetype = "application/json" resp.mimetype = CONTENT_TYPE_JSON
return resp return resp
def base_app(self, environ, start_response): def base_app(self, environ, start_response):
@ -403,11 +406,11 @@ class HomeAssistantWSGI(object):
response = self.dispatch_request(request) response = self.dispatch_request(request)
if self.cors_origins: if self.cors_origins:
cors_check = (environ.get("HTTP_ORIGIN") in self.cors_origins) cors_check = (environ.get('HTTP_ORIGIN') in self.cors_origins)
cors_headers = ", ".join(ALLOWED_CORS_HEADERS) cors_headers = ", ".join(ALLOWED_CORS_HEADERS)
if cors_check: if cors_check:
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN] = \ response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN] = \
environ.get("HTTP_ORIGIN") environ.get('HTTP_ORIGIN')
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS] = \ response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS] = \
cors_headers cors_headers
@ -425,7 +428,7 @@ class HomeAssistantWSGI(object):
# Strip out any cachebusting MD5 fingerprints # Strip out any cachebusting MD5 fingerprints
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', '')) fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
if fingerprinted: if fingerprinted:
environ['PATH_INFO'] = "{}.{}".format(*fingerprinted.groups()) environ['PATH_INFO'] = '{}.{}'.format(*fingerprinted.groups())
return app(environ, start_response) return app(environ, start_response)
@ -489,6 +492,10 @@ class HomeAssistantView(object):
if self.requires_auth and not authenticated: if self.requires_auth and not authenticated:
_LOGGER.warning('Login attempt or request with an invalid ' _LOGGER.warning('Login attempt or request with an invalid '
'password from %s', request.remote_addr) 'password from %s', request.remote_addr)
persistent_notification.create(
self.hass,
'Invalid password used from {}'.format(request.remote_addr),
'Login attempt failed', NOTIFICATION_ID_LOGIN)
raise Unauthorized() raise Unauthorized()
request.authenticated = authenticated request.authenticated = authenticated
@ -512,12 +519,9 @@ class HomeAssistantView(object):
def json(self, result, status_code=200): def json(self, result, status_code=200):
"""Return a JSON response.""" """Return a JSON response."""
msg = json.dumps( msg = json.dumps(
result, result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8')
sort_keys=True, return self.Response(
cls=rem.JSONEncoder msg, mimetype=CONTENT_TYPE_JSON, status=status_code)
).encode('UTF-8')
return self.Response(msg, mimetype="application/json",
status=status_code)
def json_message(self, error, status_code=200): def json_message(self, error, status_code=200):
"""Return a JSON message response.""" """Return a JSON message response."""

View File

@ -9,8 +9,7 @@ https://home-assistant.io/components/hvac.zwave/
import logging import logging
from homeassistant.components.hvac import DOMAIN from homeassistant.components.hvac import DOMAIN
from homeassistant.components.hvac import HvacDevice from homeassistant.components.hvac import HvacDevice
from homeassistant.components.zwave import ( from homeassistant.components.zwave import ZWaveDeviceEntity
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity)
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)
@ -23,12 +22,6 @@ REMOTEC = 0x5254
REMOTEC_ZXT_120 = 0x8377 REMOTEC_ZXT_120 = 0x8377
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0) REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0)
COMMAND_CLASS_SENSOR_MULTILEVEL = 0x31
COMMAND_CLASS_THERMOSTAT_MODE = 0x40
COMMAND_CLASS_THERMOSTAT_SETPOINT = 0x43
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 0x44
COMMAND_CLASS_CONFIGURATION = 0x70
WORKAROUND_ZXT_120 = 'zxt_120' WORKAROUND_ZXT_120 = 'zxt_120'
DEVICE_MAPPINGS = { DEVICE_MAPPINGS = {
@ -50,8 +43,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
discovery_info, zwave.NETWORK) discovery_info, zwave.NETWORK)
return return
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]] node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[ATTR_VALUE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
value.set_change_verified(False) value.set_change_verified(False)
add_devices([ZWaveHvac(value)]) add_devices([ZWaveHvac(value)])
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s", _LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
@ -107,25 +100,29 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def update_properties(self): def update_properties(self):
"""Callback on data change for the registered node/value pair.""" """Callback on data change for the registered node/value pair."""
# Set point # Set point
for value in self._node.get_values( for value in (self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values(): class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
.values()):
if int(value.data) != 0: if int(value.data) != 0:
self._target_temperature = int(value.data) self._target_temperature = int(value.data)
# Operation Mode # Operation Mode
for value in self._node.get_values( for value in (self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values(): class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE)
.values()):
self._current_operation = value.data self._current_operation = value.data
self._operation_list = list(value.data_items) self._operation_list = list(value.data_items)
_LOGGER.debug("self._operation_list=%s", self._operation_list) _LOGGER.debug("self._operation_list=%s", self._operation_list)
# Current Temp # Current Temp
for value in self._node.get_values( for value in (self._node.get_values(
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values(): class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
.values()):
if value.label == 'Temperature': if value.label == 'Temperature':
self._current_temperature = int(value.data) self._current_temperature = int(value.data)
self._unit = value.units self._unit = value.units
# Fan Mode # Fan Mode
for value in self._node.get_values( for value in (self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values(): class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
.values()):
self._current_operation_state = value.data self._current_operation_state = value.data
self._fan_list = list(value.data_items) self._fan_list = list(value.data_items)
_LOGGER.debug("self._fan_list=%s", self._fan_list) _LOGGER.debug("self._fan_list=%s", self._fan_list)
@ -133,8 +130,9 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
self._current_operation_state) self._current_operation_state)
# Swing mode # Swing mode
if self._zxt_120 == 1: if self._zxt_120 == 1:
for value in self._node.get_values( for value in (self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values(): class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
.values()):
if value.command_class == 112 and value.index == 33: if value.command_class == 112 and value.index == 33:
self._current_swing_mode = value.data self._current_swing_mode = value.data
self._swing_list = list(value.data_items) self._swing_list = list(value.data_items)
@ -199,8 +197,9 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def set_temperature(self, temperature): def set_temperature(self, temperature):
"""Set new target temperature.""" """Set new target temperature."""
for value in self._node.get_values( for value in (self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values(): class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
.values()):
if value.command_class != 67: if value.command_class != 67:
continue continue
if self._zxt_120: if self._zxt_120:
@ -217,8 +216,9 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def set_fan_mode(self, fan): def set_fan_mode(self, fan):
"""Set new target fan mode.""" """Set new target fan mode."""
for value in self._node.get_values( for value in (self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values(): class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
.values()):
if value.command_class == 68 and value.index == 0: if value.command_class == 68 and value.index == 0:
value.data = bytes(fan, 'utf-8') value.data = bytes(fan, 'utf-8')
break break
@ -226,7 +226,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set new target operation mode.""" """Set new target operation mode."""
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values(): class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
if value.command_class == 64 and value.index == 0: if value.command_class == 64 and value.index == 0:
value.data = bytes(operation_mode, 'utf-8') value.data = bytes(operation_mode, 'utf-8')
break break
@ -235,7 +235,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
"""Set new target swing mode.""" """Set new target swing mode."""
if self._zxt_120 == 1: if self._zxt_120 == 1:
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values(): class_id=zwave.const.COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33: if value.command_class == 112 and value.index == 33:
value.data = bytes(swing_mode, 'utf-8') value.data = bytes(swing_mode, 'utf-8')
break break

View File

@ -28,6 +28,7 @@ DEFAULT_PORT = 8086
DEFAULT_SSL = False DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = False DEFAULT_VERIFY_SSL = False
DOMAIN = 'influxdb' DOMAIN = 'influxdb'
TIMEOUT = 5
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -69,7 +70,8 @@ def setup(hass, config):
try: try:
influx = InfluxDBClient( influx = InfluxDBClient(
host=host, port=port, username=username, password=password, host=host, port=port, username=username, password=password,
database=database, ssl=ssl, verify_ssl=verify_ssl) database=database, ssl=ssl, verify_ssl=verify_ssl,
timeout=TIMEOUT)
influx.query("select * from /.*/ LIMIT 1;") influx.query("select * from /.*/ LIMIT 1;")
except exceptions.InfluxDBClientError as exc: except exceptions.InfluxDBClientError as exc:
_LOGGER.error("Database host is not accessible due to '%s', please " _LOGGER.error("Database host is not accessible due to '%s', please "

View File

@ -6,8 +6,14 @@ https://home-assistant.io/components/introduction/
""" """
import logging import logging
import voluptuous as vol
DOMAIN = 'introduction' DOMAIN = 'introduction'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config=None): def setup(hass, config=None):
"""Setup the introduction component.""" """Setup the introduction component."""

View File

@ -93,7 +93,8 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({
vol.Coerce(tuple)), vol.Coerce(tuple)),
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
vol.Coerce(tuple)), vol.Coerce(tuple)),
ATTR_COLOR_TEMP: vol.All(int, vol.Range(min=154, max=500)), ATTR_COLOR_TEMP: vol.All(int, vol.Range(min=color_util.HASS_COLOR_MIN,
max=color_util.HASS_COLOR_MAX)),
ATTR_WHITE_VALUE: vol.All(int, vol.Range(min=0, max=255)), ATTR_WHITE_VALUE: vol.All(int, vol.Range(min=0, max=255)),
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
ATTR_EFFECT: vol.In([EFFECT_COLORLOOP, EFFECT_RANDOM, EFFECT_WHITE]), ATTR_EFFECT: vol.In([EFFECT_COLORLOOP, EFFECT_RANDOM, EFFECT_WHITE]),

View File

@ -17,8 +17,8 @@ from homeassistant.components.light import (
PLATFORM_SCHEMA) PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.6.zip' REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.7.zip'
'#flux_led==0.6'] '#flux_led==0.7']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -7,19 +7,35 @@ https://home-assistant.io/components/light.limitlessled/
# pylint: disable=abstract-method # pylint: disable=abstract-method
import logging import logging
import voluptuous as vol
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT)
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR,
ATTR_TRANSITION, EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG, ATTR_TRANSITION, EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH,
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light) SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['limitlessled==1.0.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['limitlessled==1.0.0']
RGB_BOUNDARY = 40 CONF_BRIDGES = 'bridges'
DEFAULT_TRANSITION = 0 CONF_GROUPS = 'groups'
DEFAULT_PORT = 8899 CONF_NUMBER = 'number'
DEFAULT_VERSION = 5 CONF_TYPE = 'type'
CONF_VERSION = 'version'
DEFAULT_LED_TYPE = 'rgbw' DEFAULT_LED_TYPE = 'rgbw'
DEFAULT_PORT = 8899
DEFAULT_TRANSITION = 0
DEFAULT_VERSION = 5
LED_TYPE = ['rgbw', 'white']
RGB_BOUNDARY = 40
WHITE = [255, 255, 255] WHITE = [255, 255, 255]
SUPPORT_LIMITLESSLED_WHITE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_LIMITLESSLED_WHITE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP |
@ -28,6 +44,25 @@ SUPPORT_LIMITLESSLED_RGB = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
SUPPORT_FLASH | SUPPORT_RGB_COLOR | SUPPORT_FLASH | SUPPORT_RGB_COLOR |
SUPPORT_TRANSITION) SUPPORT_TRANSITION)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_BRIDGES): vol.All(cv.ensure_list, [
{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_VERSION,
default=DEFAULT_VERSION): cv.positive_int,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Required(CONF_GROUPS): vol.All(cv.ensure_list, [
{
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_TYPE, default=DEFAULT_LED_TYPE):
vol.In(LED_TYPE),
vol.Required(CONF_NUMBER): cv.positive_int,
}
]),
},
]),
})
def rewrite_legacy(config): def rewrite_legacy(config):
"""Rewrite legacy configuration to new format.""" """Rewrite legacy configuration to new format."""

View File

@ -10,11 +10,12 @@ import voluptuous as vol
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS,
Light) SUPPORT_RGB_COLOR, SUPPORT_COLOR_TEMP, Light)
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON, CONF_STATE, CONF_BRIGHTNESS, CONF_RGB) CONF_PAYLOAD_ON, CONF_STATE, CONF_BRIGHTNESS, CONF_RGB,
CONF_COLOR_TEMP)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -31,6 +32,9 @@ CONF_RGB_STATE_TOPIC = 'rgb_state_topic'
CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic' CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic'
CONF_RGB_VALUE_TEMPLATE = 'rgb_value_template' CONF_RGB_VALUE_TEMPLATE = 'rgb_value_template'
CONF_BRIGHTNESS_SCALE = 'brightness_scale' CONF_BRIGHTNESS_SCALE = 'brightness_scale'
CONF_COLOR_TEMP_STATE_TOPIC = 'color_temp_state_topic'
CONF_COLOR_TEMP_COMMAND_TOPIC = 'color_temp_command_topic'
CONF_COLOR_TEMP_VALUE_TEMPLATE = 'color_temp_value_template'
DEFAULT_NAME = 'MQTT Light' DEFAULT_NAME = 'MQTT Light'
DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_ON = 'ON'
@ -44,6 +48,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template,
@ -70,12 +77,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
CONF_BRIGHTNESS_COMMAND_TOPIC, CONF_BRIGHTNESS_COMMAND_TOPIC,
CONF_RGB_STATE_TOPIC, CONF_RGB_STATE_TOPIC,
CONF_RGB_COMMAND_TOPIC, CONF_RGB_COMMAND_TOPIC,
CONF_COLOR_TEMP_STATE_TOPIC,
CONF_COLOR_TEMP_COMMAND_TOPIC
) )
}, },
{ {
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
CONF_BRIGHTNESS: config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE), CONF_BRIGHTNESS: config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE),
CONF_RGB: config.get(CONF_RGB_VALUE_TEMPLATE) CONF_RGB: config.get(CONF_RGB_VALUE_TEMPLATE),
CONF_COLOR_TEMP: config.get(CONF_COLOR_TEMP_VALUE_TEMPLATE)
}, },
config.get(CONF_QOS), config.get(CONF_QOS),
config.get(CONF_RETAIN), config.get(CONF_RETAIN),
@ -92,6 +102,7 @@ class MqttLight(Light):
"""MQTT light.""" """MQTT light."""
# pylint: disable=too-many-arguments,too-many-instance-attributes # pylint: disable=too-many-arguments,too-many-instance-attributes
# pylint: disable=too-many-locals,too-many-branches
def __init__(self, hass, name, topic, templates, qos, retain, payload, def __init__(self, hass, name, topic, templates, qos, retain, payload,
optimistic, brightness_scale): optimistic, brightness_scale):
"""Initialize MQTT light.""" """Initialize MQTT light."""
@ -106,6 +117,8 @@ class MqttLight(Light):
optimistic or topic[CONF_RGB_STATE_TOPIC] is None optimistic or topic[CONF_RGB_STATE_TOPIC] is None
self._optimistic_brightness = ( self._optimistic_brightness = (
optimistic or topic[CONF_BRIGHTNESS_STATE_TOPIC] is None) optimistic or topic[CONF_BRIGHTNESS_STATE_TOPIC] is None)
self._optimistic_color_temp = (
optimistic or topic[CONF_COLOR_TEMP_STATE_TOPIC] is None)
self._brightness_scale = brightness_scale self._brightness_scale = brightness_scale
self._state = False self._state = False
self._supported_features = 0 self._supported_features = 0
@ -114,6 +127,9 @@ class MqttLight(Light):
self._supported_features |= ( self._supported_features |= (
topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None and topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None and
SUPPORT_BRIGHTNESS) SUPPORT_BRIGHTNESS)
self._supported_features |= (
topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None and
SUPPORT_COLOR_TEMP)
for key, tpl in list(templates.items()): for key, tpl in list(templates.items()):
if tpl is None: if tpl is None:
@ -168,6 +184,21 @@ class MqttLight(Light):
else: else:
self._rgb = None self._rgb = None
def color_temp_received(topic, payload, qos):
"""A new MQTT message for color temp has been received."""
self._color_temp = int(templates[CONF_COLOR_TEMP](payload))
self.update_ha_state()
if self._topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None:
mqtt.subscribe(
self._hass, self._topic[CONF_COLOR_TEMP_STATE_TOPIC],
color_temp_received, self._qos)
self._color_temp = 150
if self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None:
self._color_temp = 150
else:
self._color_temp = None
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
@ -178,6 +209,11 @@ class MqttLight(Light):
"""Return the RGB color value.""" """Return the RGB color value."""
return self._rgb return self._rgb
@property
def color_temp(self):
"""Return the color temperature in mired."""
return self._color_temp
@property @property
def should_poll(self): def should_poll(self):
"""No polling needed for a MQTT light.""" """No polling needed for a MQTT light."""
@ -230,6 +266,16 @@ class MqttLight(Light):
self._brightness = kwargs[ATTR_BRIGHTNESS] self._brightness = kwargs[ATTR_BRIGHTNESS]
should_update = True should_update = True
if ATTR_COLOR_TEMP in kwargs and \
self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None:
color_temp = int(kwargs[ATTR_COLOR_TEMP])
mqtt.publish(
self._hass, self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC],
color_temp, self._qos, self._retain)
if self._optimistic_color_temp:
self._color_temp = kwargs[ATTR_COLOR_TEMP]
should_update = True
mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC], mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC],
self._payload['on'], self._qos, self._retain) self._payload['on'], self._qos, self._retain)

View File

@ -50,19 +50,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None or zwave.NETWORK is None: if discovery_info is None or zwave.NETWORK is None:
return return
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL: if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL:
return return
if value.type != zwave.TYPE_BYTE: if value.type != zwave.const.TYPE_BYTE:
return return
if value.genre != zwave.GENRE_USER: if value.genre != zwave.const.GENRE_USER:
return return
value.set_change_verified(False) value.set_change_verified(False)
if node.has_command_class(zwave.COMMAND_CLASS_COLOR): if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
try: try:
add_devices([ZwaveColorLight(value)]) add_devices([ZwaveColorLight(value)])
except ValueError as exception: except ValueError as exception:
@ -195,8 +195,8 @@ class ZwaveColorLight(ZwaveDimmer):
raise ValueError("No matching color command found.") raise ValueError("No matching color command found.")
for value_color_channels in value.node.get_values( for value_color_channels in value.node.get_values(
class_id=zwave.COMMAND_CLASS_COLOR, genre='System', class_id=zwave.const.COMMAND_CLASS_SWITCH_COLOR,
type="Int").values(): genre='System', type="Int").values():
self._value_color_channels = value_color_channels self._value_color_channels = value_color_channels
if self._value_color_channels is None: if self._value_color_channels is None:

View File

@ -1,28 +1,35 @@
""" """
LIRC interface to receive signals from a infrared remote control. LIRC interface to receive signals from a infrared remote control.
This sensor will momentarily set state to various values as defined For more details about this component, please refer to the documentation at
in the .lintrc file which can be interpreted in home-assistant to https://home-assistant.io/components/lirc/
trigger various actions.
Sending signals to other IR receivers can be accomplished with the
shell_command component and the irsend command for now.
""" """
# pylint: disable=import-error # pylint: disable=import-error
import threading import threading
import time import time
import logging import logging
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, import voluptuous as vol
EVENT_HOMEASSISTANT_START)
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
DOMAIN = "lirc"
REQUIREMENTS = ['python-lirc==1.2.1'] REQUIREMENTS = ['python-lirc==1.2.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ICON = 'mdi:remote'
EVENT_IR_COMMAND_RECEIVED = 'ir_command_received'
BUTTON_NAME = 'button_name' BUTTON_NAME = 'button_name'
DOMAIN = 'lirc'
EVENT_IR_COMMAND_RECEIVED = 'ir_command_received'
ICON = 'mdi:remote'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config): def setup(hass, config):
"""Setup LIRC capability.""" """Setup LIRC capability."""
@ -65,20 +72,19 @@ class LircInterface(threading.Thread):
def run(self): def run(self):
"""Main loop of LIRC interface thread.""" """Main loop of LIRC interface thread."""
import lirc import lirc
_LOGGER.debug('LIRC interface thread started') _LOGGER.debug("LIRC interface thread started")
while not self.stopped.isSet(): while not self.stopped.isSet():
try: try:
code = lirc.nextcode() # list; empty if no buttons pressed code = lirc.nextcode() # list; empty if no buttons pressed
except lirc.NextCodeError: except lirc.NextCodeError:
_LOGGER.warning('Encountered error reading ' _LOGGER.warning("Error reading next code from LIRC")
'next code from LIRC')
code = None code = None
# interpret result from python-lirc # interpret result from python-lirc
if code: if code:
code = code[0] code = code[0]
_LOGGER.info('Got new LIRC code %s', code) _LOGGER.info("Got new LIRC code %s", code)
self.hass.bus.fire(EVENT_IR_COMMAND_RECEIVED, self.hass.bus.fire(
{BUTTON_NAME: code}) EVENT_IR_COMMAND_RECEIVED, {BUTTON_NAME: code})
else: else:
time.sleep(0.2) time.sleep(0.2)
lirc.deinit() lirc.deinit()

View File

@ -16,14 +16,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None or zwave.NETWORK is None: if discovery_info is None or zwave.NETWORK is None:
return return
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
if value.command_class != zwave.COMMAND_CLASS_DOOR_LOCK: if value.command_class != zwave.const.COMMAND_CLASS_DOOR_LOCK:
return return
if value.type != zwave.TYPE_BOOL: if value.type != zwave.const.TYPE_BOOL:
return return
if value.genre != zwave.GENRE_USER: if value.genre != zwave.const.GENRE_USER:
return return
value.set_change_verified(False) value.set_change_verified(False)

View File

@ -4,6 +4,7 @@ Event parser and human readable log generator.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/logbook/ https://home-assistant.io/components/logbook/
""" """
import asyncio
import logging import logging
from datetime import timedelta from datetime import timedelta
from itertools import groupby from itertools import groupby
@ -20,6 +21,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START,
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_NOT_HOME, STATE_OFF, STATE_ON,
ATTR_HIDDEN) ATTR_HIDDEN)
from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN
from homeassistant.util.async import run_callback_threadsafe
DOMAIN = "logbook" DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'frontend'] DEPENDENCIES = ['recorder', 'frontend']
@ -57,6 +59,13 @@ LOG_MESSAGE_SCHEMA = vol.Schema({
def log_entry(hass, name, message, domain=None, entity_id=None): def log_entry(hass, name, message, domain=None, entity_id=None):
"""Add an entry to the logbook."""
run_callback_threadsafe(
hass.loop, async_log_entry, hass, name, message, domain, entity_id
).result()
def async_log_entry(hass, name, message, domain=None, entity_id=None):
"""Add an entry to the logbook.""" """Add an entry to the logbook."""
data = { data = {
ATTR_NAME: name, ATTR_NAME: name,
@ -67,11 +76,12 @@ def log_entry(hass, name, message, domain=None, entity_id=None):
data[ATTR_DOMAIN] = domain data[ATTR_DOMAIN] = domain
if entity_id is not None: if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id data[ATTR_ENTITY_ID] = entity_id
hass.bus.fire(EVENT_LOGBOOK_ENTRY, data) hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data)
def setup(hass, config): def setup(hass, config):
"""Listen for download events to download files.""" """Listen for download events to download files."""
@asyncio.coroutine
def log_message(service): def log_message(service):
"""Handle sending notification message service calls.""" """Handle sending notification message service calls."""
message = service.data[ATTR_MESSAGE] message = service.data[ATTR_MESSAGE]
@ -80,8 +90,8 @@ def setup(hass, config):
entity_id = service.data.get(ATTR_ENTITY_ID) entity_id = service.data.get(ATTR_ENTITY_ID)
message.hass = hass message.hass = hass
message = message.render() message = message.async_render()
log_entry(hass, name, message, domain, entity_id) async_log_entry(hass, name, message, domain, entity_id)
hass.wsgi.register_view(LogbookView(hass, config)) hass.wsgi.register_view(LogbookView(hass, config))

View File

@ -79,18 +79,11 @@ class LogitechMediaServer(object):
def _get_http_port(self): def _get_http_port(self):
"""Get http port from media server, it is used to get cover art.""" """Get http port from media server, it is used to get cover art."""
http_port = None http_port = self.query('pref', 'httpport', '?')
try: if not http_port:
http_port = self.query('pref', 'httpport', '?') _LOGGER.error("Failed to connect to server %s:%s",
if not http_port: self.host, self.port)
_LOGGER.error("Unable to read data from server %s:%s", return http_port
self.host, self.port)
return
return http_port
except ConnectionError as ex:
_LOGGER.error("Failed to connect to server %s:%s - %s",
self.host, self.port, ex)
return
def create_players(self): def create_players(self):
"""Create a list of SqueezeBoxDevices connected to the LMS.""" """Create a list of SqueezeBoxDevices connected to the LMS."""
@ -104,20 +97,27 @@ class LogitechMediaServer(object):
def query(self, *parameters): def query(self, *parameters):
"""Send request and await response from server.""" """Send request and await response from server."""
telnet = telnetlib.Telnet(self.host, self.port) try:
if self._username and self._password: telnet = telnetlib.Telnet(self.host, self.port)
telnet.write('login {username} {password}\n'.format( if self._username and self._password:
username=self._username, telnet.write('login {username} {password}\n'.format(
password=self._password).encode('UTF-8')) username=self._username,
telnet.read_until(b'\n', timeout=3) password=self._password).encode('UTF-8'))
message = '{}\n'.format(' '.join(parameters)) telnet.read_until(b'\n', timeout=3)
telnet.write(message.encode('UTF-8')) message = '{}\n'.format(' '.join(parameters))
response = telnet.read_until(b'\n', timeout=3)\ telnet.write(message.encode('UTF-8'))
.decode('UTF-8')\ response = telnet.read_until(b'\n', timeout=3)\
.split(' ')[-1]\ .decode('UTF-8')\
.strip() .split(' ')[-1]\
telnet.write(b'exit\n') .strip()
return urllib.parse.unquote(response) telnet.write(b'exit\n')
return urllib.parse.unquote(response)
except (OSError, ConnectionError) as error:
_LOGGER.error("Could not communicate with %s:%d: %s",
self.host,
self.port,
error)
return None
def get_player_status(self, player): def get_player_status(self, player):
"""Get ithe status of a player.""" """Get ithe status of a player."""
@ -128,18 +128,24 @@ class LogitechMediaServer(object):
# K (artwork_url): URL to remote artwork # K (artwork_url): URL to remote artwork
tags = 'adK' tags = 'adK'
new_status = {} new_status = {}
telnet = telnetlib.Telnet(self.host, self.port) try:
telnet.write('{player} status - 1 tags:{tags}\n'.format( telnet = telnetlib.Telnet(self.host, self.port)
player=player, telnet.write('{player} status - 1 tags:{tags}\n'.format(
tags=tags player=player,
tags=tags
).encode('UTF-8')) ).encode('UTF-8'))
response = telnet.read_until(b'\n', timeout=3)\ response = telnet.read_until(b'\n', timeout=3)\
.decode('UTF-8')\ .decode('UTF-8')\
.split(' ') .split(' ')
telnet.write(b'exit\n') telnet.write(b'exit\n')
for item in response: for item in response:
parts = urllib.parse.unquote(item).partition(':') parts = urllib.parse.unquote(item).partition(':')
new_status[parts[0]] = parts[2] new_status[parts[0]] = parts[2]
except (OSError, ConnectionError) as error:
_LOGGER.error("Could not communicate with %s:%d: %s",
self.host,
self.port,
error)
return new_status return new_status

View File

@ -8,24 +8,31 @@ import logging
from datetime import timedelta from datetime import timedelta
from urllib.parse import urlparse from urllib.parse import urlparse
import voluptuous as vol
import homeassistant.util as util import homeassistant.util as util
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL,
MediaPlayerDevice) MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_CUSTOMIZE, STATE_OFF, STATE_PLAYING, STATE_PAUSED, CONF_HOST, CONF_CUSTOMIZE, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
STATE_UNKNOWN) STATE_UNKNOWN, CONF_NAME)
from homeassistant.loader import get_component from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv' REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv'
'/archive/v0.1.2.zip' '/archive/v0.1.2.zip'
'#pylgtv==0.1.2'] '#pylgtv==0.1.2']
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
CONF_SOURCES = 'sources'
DEFAULT_NAME = 'LG WebOS Smart TV'
SUPPORT_WEBOSTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_WEBOSTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \ SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \
@ -44,6 +51,17 @@ WEBOS_APPS_SHORT = {
'makotv': WEBOS_APP_MAKO 'makotv': WEBOS_APP_MAKO
} }
CUSTOMIZE_SCHEMA = vol.Schema({
vol.Optional(CONF_SOURCES):
vol.All(cv.ensure_list, [vol.In(WEBOS_APPS_SHORT)]),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA,
})
# 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):
@ -51,21 +69,22 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is not None: if discovery_info is not None:
host = urlparse(discovery_info[1]).hostname host = urlparse(discovery_info[1]).hostname
else: else:
host = config.get(CONF_HOST, None) host = config.get(CONF_HOST)
if host is None: if host is None:
_LOGGER.error('No host found in configuration') _LOGGER.error("No host found in configuration")
return False return False
# Only act if we are not already configuring this host # Only act if we are not already configuring this host
if host in _CONFIGURING: if host in _CONFIGURING:
return return
customize = config.get(CONF_CUSTOMIZE, {}) name = config.get(CONF_NAME)
setup_tv(host, customize, hass, add_devices) customize = config.get(CONF_CUSTOMIZE)
setup_tv(host, name, customize, hass, add_devices)
def setup_tv(host, customize, hass, add_devices): def setup_tv(host, name, customize, hass, add_devices):
"""Setup a phue bridge based on host parameter.""" """Setup a phue bridge based on host parameter."""
from pylgtv import WebOsClient from pylgtv import WebOsClient
from pylgtv import PyLGTVPairException from pylgtv import PyLGTVPairException
@ -79,15 +98,15 @@ def setup_tv(host, customize, hass, add_devices):
client.register() client.register()
except PyLGTVPairException: except PyLGTVPairException:
_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 OSError: except OSError:
_LOGGER.error('Unable to connect to host %s.', host) _LOGGER.error("Unable to connect to host %s", host)
return return
else: else:
# Not registered, request configuration. # Not registered, request configuration.
_LOGGER.warning('LG WebOS TV at %s needs to be paired.', host) _LOGGER.warning('LG WebOS TV at %s needs to be paired.', host)
request_configuration(host, customize, hass, add_devices) request_configuration(host, name, customize, hass, add_devices)
return return
# If we came here and configuring this host, mark as done. # If we came here and configuring this host, mark as done.
@ -96,10 +115,10 @@ def setup_tv(host, customize, hass, add_devices):
configurator = get_component('configurator') configurator = get_component('configurator')
configurator.request_done(request_id) configurator.request_done(request_id)
add_devices([LgWebOSDevice(host, customize)]) add_devices([LgWebOSDevice(host, name, customize)])
def request_configuration(host, customize, hass, add_devices): def request_configuration(host, name, customize, hass, add_devices):
"""Request configuration steps from the user.""" """Request configuration steps from the user."""
configurator = get_component('configurator') configurator = get_component('configurator')
@ -112,7 +131,7 @@ def request_configuration(host, customize, hass, add_devices):
# pylint: disable=unused-argument # pylint: disable=unused-argument
def lgtv_configuration_callback(data): def lgtv_configuration_callback(data):
"""The actions to do when our configuration callback is called.""" """The actions to do when our configuration callback is called."""
setup_tv(host, customize, hass, add_devices) setup_tv(host, name, customize, hass, add_devices)
_CONFIGURING[host] = configurator.request_config( _CONFIGURING[host] = configurator.request_config(
hass, 'LG WebOS TV', lgtv_configuration_callback, hass, 'LG WebOS TV', lgtv_configuration_callback,
@ -128,13 +147,13 @@ class LgWebOSDevice(MediaPlayerDevice):
"""Representation of a LG WebOS TV.""" """Representation of a LG WebOS TV."""
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
def __init__(self, host, customize): def __init__(self, host, name, customize):
"""Initialize the webos device.""" """Initialize the webos device."""
from pylgtv import WebOsClient from pylgtv import WebOsClient
self._client = WebOsClient(host) self._client = WebOsClient(host)
self._customize = customize self._customize = customize
self._name = 'LG WebOS TV Remote' self._name = name
# Assume that the TV is not muted # Assume that the TV is not muted
self._muted = False self._muted = False
# Assume that the TV is in Play mode # Assume that the TV is in Play mode
@ -160,7 +179,7 @@ class LgWebOSDevice(MediaPlayerDevice):
self._app_list = {} self._app_list = {}
custom_sources = [] custom_sources = []
for source in self._customize.get('sources', []): for source in self._customize.get(CONF_SOURCES, []):
app_id = WEBOS_APPS_SHORT.get(source, None) app_id = WEBOS_APPS_SHORT.get(source, None)
if app_id: if app_id:
custom_sources.append(app_id) custom_sources.append(app_id)

View File

@ -4,6 +4,7 @@ Support for MQTT message handling.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mqtt/ https://home-assistant.io/components/mqtt/
""" """
import asyncio
import logging import logging
import os import os
import socket import socket
@ -14,8 +15,8 @@ import voluptuous as vol
from homeassistant.bootstrap import prepare_setup_platform from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import template from homeassistant.helpers import template, config_validation as cv
import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import threaded_listener_factory
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_VALUE_TEMPLATE) CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_VALUE_TEMPLATE)
@ -162,21 +163,28 @@ def publish_template(hass, topic, payload_template, qos=None, retain=None):
hass.services.call(DOMAIN, SERVICE_PUBLISH, data) hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
def subscribe(hass, topic, callback, qos=DEFAULT_QOS): def async_subscribe(hass, topic, callback, qos=DEFAULT_QOS):
"""Subscribe to an MQTT topic.""" """Subscribe to an MQTT topic."""
@asyncio.coroutine
def mqtt_topic_subscriber(event): def mqtt_topic_subscriber(event):
"""Match subscribed MQTT topic.""" """Match subscribed MQTT topic."""
if _match_topic(topic, event.data[ATTR_TOPIC]): if not _match_topic(topic, event.data[ATTR_TOPIC]):
callback(event.data[ATTR_TOPIC], event.data[ATTR_PAYLOAD], return
event.data[ATTR_QOS])
remove = hass.bus.listen(EVENT_MQTT_MESSAGE_RECEIVED, hass.async_run_job(callback, event.data[ATTR_TOPIC],
mqtt_topic_subscriber) event.data[ATTR_PAYLOAD], event.data[ATTR_QOS])
async_remove = hass.bus.async_listen(EVENT_MQTT_MESSAGE_RECEIVED,
mqtt_topic_subscriber)
# Future: track subscriber count and unsubscribe in remove # Future: track subscriber count and unsubscribe in remove
MQTT_CLIENT.subscribe(topic, qos) MQTT_CLIENT.subscribe(topic, qos)
return remove return async_remove
# pylint: disable=invalid-name
subscribe = threaded_listener_factory(async_subscribe)
def _setup_server(hass, config): def _setup_server(hass, config):

View File

@ -158,7 +158,8 @@ def setup(hass, config): # pylint: disable=too-many-locals
'No devices could be setup as gateways, check your configuration') 'No devices could be setup as gateways, check your configuration')
return False return False
for component in 'sensor', 'switch', 'light', 'binary_sensor', 'climate': for component in ['sensor', 'switch', 'light', 'binary_sensor', 'climate',
'cover']:
discovery.load_platform(hass, component, DOMAIN, {}, config) discovery.load_platform(hass, component, DOMAIN, {}, config)
return True return True
@ -340,5 +341,7 @@ class MySensorsDeviceEntity(object):
set_req.V_LOCK_STATUS, set_req.V_TRIPPED): set_req.V_LOCK_STATUS, set_req.V_TRIPPED):
self._values[value_type] = ( self._values[value_type] = (
STATE_ON if int(value) == 1 else STATE_OFF) STATE_ON if int(value) == 1 else STATE_OFF)
elif value_type == set_req.V_DIMMER:
self._values[value_type] = int(value)
else: else:
self._values[value_type] = value self._values[value_type] = value

View File

@ -14,7 +14,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, CONF_STRUCTURE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-nest==2.9.2'] REQUIREMENTS = ['python-nest==2.10.0']
DOMAIN = 'nest' DOMAIN = 'nest'

View File

@ -37,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config): def setup(hass, config):
"""Setup the Netatmo devices.""" """Set up the Netatmo devices."""
import lnetatmo import lnetatmo
global NETATMO_AUTH global NETATMO_AUTH
@ -47,7 +47,7 @@ def setup(hass, config):
config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD],
'read_station read_camera access_camera') 'read_station read_camera access_camera')
except HTTPError: except HTTPError:
_LOGGER.error("Unable to connect to NatAtmo API") _LOGGER.error("Unable to connect to Netatmo API")
return False return False
for component in 'camera', 'sensor': for component in 'camera', 'sensor':

View File

@ -42,7 +42,7 @@ PLATFORM_SCHEMA = vol.Schema({
NOTIFY_SERVICE_SCHEMA = vol.Schema({ NOTIFY_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_MESSAGE): cv.template, vol.Required(ATTR_MESSAGE): cv.template,
vol.Optional(ATTR_TITLE): cv.template, vol.Optional(ATTR_TITLE): cv.template,
vol.Optional(ATTR_TARGET): cv.string, vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_DATA): dict, vol.Optional(ATTR_DATA): dict,
}) })

View File

@ -79,9 +79,6 @@ class AWSLambda(BaseNotificationService):
_LOGGER.info("At least 1 target is required") _LOGGER.info("At least 1 target is required")
return return
if not isinstance(targets, list):
targets = [targets]
for target in targets: for target in targets:
cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v) cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v)
payload = {"message": message} payload = {"message": message}

View File

@ -70,9 +70,6 @@ class AWSSNS(BaseNotificationService):
_LOGGER.info("At least 1 target is required") _LOGGER.info("At least 1 target is required")
return return
if not isinstance(targets, list):
targets = [targets]
message_attributes = {k: {"StringValue": json.dumps(v), message_attributes = {k: {"StringValue": json.dumps(v),
"DataType": "String"} "DataType": "String"}
for k, v in kwargs.items() if v} for k, v in kwargs.items() if v}

View File

@ -69,9 +69,6 @@ class AWSSQS(BaseNotificationService):
_LOGGER.info("At least 1 target is required") _LOGGER.info("At least 1 target is required")
return return
if not isinstance(targets, list):
targets = [targets]
for target in targets: for target in targets:
cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v) cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v)
message_body = {"message": message} message_body = {"message": message}

View File

@ -359,8 +359,6 @@ class HTML5NotificationService(BaseNotificationService):
if not targets: if not targets:
targets = self.registrations.keys() targets = self.registrations.keys()
elif not isinstance(targets, list):
targets = [targets]
for target in targets: for target in targets:
info = self.registrations.get(target) info = self.registrations.get(target)

View File

@ -58,9 +58,6 @@ class MessageBirdNotificationService(BaseNotificationService):
_LOGGER.error('No target specified.') _LOGGER.error('No target specified.')
return return
if not isinstance(targets, list):
targets = [targets]
for target in targets: for target in targets:
try: try:
self.client.message_create(self.sender, self.client.message_create(self.sender,

View File

@ -87,10 +87,6 @@ class PushBulletNotificationService(BaseNotificationService):
_LOGGER.info('Sent notification to self') _LOGGER.info('Sent notification to self')
return return
# Make list if not so
if not isinstance(targets, list):
targets = [targets]
# Main loop, Process all targets specified # Main loop, Process all targets specified
for target in targets: for target in targets:
try: try:

View File

@ -18,45 +18,51 @@ REQUIREMENTS = ['pushetta==1.0.15']
CONF_CHANNEL_NAME = 'channel_name' CONF_CHANNEL_NAME = 'channel_name'
CONF_SEND_TEST_MSG = 'send_test_msg'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_CHANNEL_NAME): cv.string, vol.Required(CONF_CHANNEL_NAME): cv.string,
vol.Optional(CONF_SEND_TEST_MSG, default=False): cv.boolean,
}) })
def get_service(hass, config): def get_service(hass, config):
"""Get the Pushetta notification service.""" """Get the Pushetta notification service."""
from pushetta import Pushetta, exceptions pushetta_service = PushettaNotificationService(config[CONF_API_KEY],
config[CONF_CHANNEL_NAME],
config[CONF_SEND_TEST_MSG])
try: if pushetta_service.is_valid:
pushetta = Pushetta(config[CONF_API_KEY]) return pushetta_service
pushetta.pushMessage(config[CONF_CHANNEL_NAME],
"Home Assistant started")
except exceptions.TokenValidationError:
_LOGGER.error("Please check your access token")
return None
except exceptions.ChannelNotFoundError:
_LOGGER.error("Channel '%s' not found", config[CONF_CHANNEL_NAME])
return None
return PushettaNotificationService(config[CONF_API_KEY],
config[CONF_CHANNEL_NAME])
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class PushettaNotificationService(BaseNotificationService): class PushettaNotificationService(BaseNotificationService):
"""Implement the notification service for Pushetta.""" """Implement the notification service for Pushetta."""
def __init__(self, api_key, channel_name): def __init__(self, api_key, channel_name, send_test_msg):
"""Initialize the service.""" """Initialize the service."""
from pushetta import Pushetta from pushetta import Pushetta
self._api_key = api_key self._api_key = api_key
self._channel_name = channel_name self._channel_name = channel_name
self.pushetta = Pushetta(self._api_key) self.is_valid = True
self.pushetta = Pushetta(api_key)
if send_test_msg:
self.send_message("Home Assistant started")
def send_message(self, message="", **kwargs): def send_message(self, message="", **kwargs):
"""Send a message to a user.""" """Send a message to a user."""
from pushetta import exceptions
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
self.pushetta.pushMessage(self._channel_name,
"{} {}".format(title, message)) try:
self.pushetta.pushMessage(self._channel_name,
"{} {}".format(title, message))
except exceptions.TokenValidationError:
_LOGGER.error("Please check your access token")
self.is_valid = False
except exceptions.ChannelNotFoundError:
_LOGGER.error("Channel '%s' not found", self._channel_name)
self.is_valid = False

View File

@ -61,13 +61,15 @@ class PushoverNotificationService(BaseNotificationService):
data['title'] = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data['title'] = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
target = kwargs.get(ATTR_TARGET) targets = kwargs.get(ATTR_TARGET)
if target is not None:
data['device'] = target
try: for target in targets:
self.pushover.send_message(message, **data) if target is not None:
except ValueError as val_err: data['device'] = target
_LOGGER.error(str(val_err))
except RequestError: try:
_LOGGER.exception('Could not send pushover notification') self.pushover.send_message(message, **data)
except ValueError as val_err:
_LOGGER.error(str(val_err))
except RequestError:
_LOGGER.exception('Could not send pushover notification')

View File

@ -75,8 +75,10 @@ class RestNotificationService(BaseNotificationService):
data[self._title_param_name] = kwargs.get(ATTR_TITLE, data[self._title_param_name] = kwargs.get(ATTR_TITLE,
ATTR_TITLE_DEFAULT) ATTR_TITLE_DEFAULT)
if self._target_param_name is not None: if self._target_param_name is not None and ATTR_TARGET in kwargs:
data[self._target_param_name] = kwargs.get(ATTR_TARGET) # Target is a list as of 0.29 and we don't want to break existing
# integrations, so just return the first target in the list.
data[self._target_param_name] = kwargs[ATTR_TARGET][0]
if self._method == 'POST': if self._method == 'POST':
response = requests.post(self._resource, data=data, timeout=10) response = requests.post(self._resource, data=data, timeout=10)

View File

@ -11,9 +11,9 @@ notify:
example: 'Your Garage Door Friend' example: 'Your Garage Door Friend'
target: target:
description: Target of the notification. Optional depending on the platform description: An array of targets to send the notification to. Optional depending on the platform.
example: platform specific example: platform specific
data: data:
description: Extended information for notification. Optional depending on the platform description: Extended information for notification. Optional depending on the platform.
example: platform specific example: platform specific

View File

@ -9,7 +9,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.notify import ( from homeassistant.components.notify import (
PLATFORM_SCHEMA, BaseNotificationService) ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_USERNAME, CONF_ICON) CONF_API_KEY, CONF_USERNAME, CONF_ICON)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -68,16 +68,19 @@ class SlackNotificationService(BaseNotificationService):
"""Send a message to a user.""" """Send a message to a user."""
import slacker import slacker
channel = kwargs.get('target') or self._default_channel targets = kwargs.get(ATTR_TARGET, [self._default_channel])
data = kwargs.get('data') data = kwargs.get('data')
attachments = data.get('attachments') if data else None attachments = data.get('attachments') if data else None
try: for target in targets:
self.slack.chat.post_message(channel, message, try:
as_user=self._as_user, self.slack.chat.post_message(target, message,
username=self._username, as_user=self._as_user,
icon_emoji=self._icon, username=self._username,
attachments=attachments, icon_emoji=self._icon,
link_names=True) attachments=attachments,
except slacker.Error as err: link_names=True)
_LOGGER.error("Could not send slack notification. Error: %s", err) except slacker.Error as err:
_LOGGER.error("Could not send slack notification. Error: %s",
err)

View File

@ -57,9 +57,6 @@ class TwilioSMSNotificationService(BaseNotificationService):
_LOGGER.info("At least 1 target is required") _LOGGER.info("At least 1 target is required")
return return
if not isinstance(targets, list):
targets = [targets]
for target in targets: for target in targets:
self.client.messages.create(to=target, body=message, self.client.messages.create(to=target, body=message,
from_=self.from_number) from_=self.from_number)

View File

@ -27,7 +27,7 @@ DEPENDENCIES = ['ffmpeg']
REQUIREMENTS = [ REQUIREMENTS = [
'https://github.com/pvizeli/cloudapi/releases/download/1.0.2/' 'https://github.com/pvizeli/cloudapi/releases/download/1.0.2/'
'python-1.0.2.zip#cloud_api==1.0.2', 'python-1.0.2.zip#cloud_api==1.0.2',
'ha-alpr==0.2'] 'ha-alpr==0.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -68,7 +68,7 @@ DEFAULT_NAME = 'OpenAlpr'
DEFAULT_ENGINE = ENGINE_LOCAL DEFAULT_ENGINE = ENGINE_LOCAL
DEFAULT_RENDER = RENDER_FFMPEG DEFAULT_RENDER = RENDER_FFMPEG
DEFAULT_BINARY = 'alpr' DEFAULT_BINARY = 'alpr'
DEFAULT_INTERVAL = 2 DEFAULT_INTERVAL = 10
DEFAULT_CONFIDENCE = 80.0 DEFAULT_CONFIDENCE = 80.0
DEVICE_SCHEMA = vol.Schema({ DEVICE_SCHEMA = vol.Schema({

View File

@ -12,9 +12,6 @@ from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.rollershutter import RollershutterDevice from homeassistant.components.rollershutter import RollershutterDevice
COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
SOMFY = 0x47 SOMFY = 0x47
SOMFY_ZRTSI = 0x5a52 SOMFY_ZRTSI = 0x5a52
SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI) SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI)
@ -32,10 +29,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None or zwave.NETWORK is None: if discovery_info is None or zwave.NETWORK is None:
return return
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL: if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL:
return return
if value.index != 0: if value.index != 0:
return return
@ -82,9 +79,10 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
"""Callback on data change for the registered node/value pair.""" """Callback on data change for the registered node/value pair."""
# Position value # Position value
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ if value.command_class == \
and value.label == 'Level': zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and \
value.label == 'Level':
self._current_position = value.data self._current_position = value.data
@property @property
@ -101,23 +99,26 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
def move_up(self, **kwargs): def move_up(self, **kwargs):
"""Move the roller shutter up.""" """Move the roller shutter up."""
for value in self._node.get_values( for value in (self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL)
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ .values()):
and value.label == 'Open' or \ if value.command_class == \
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
and value.label == 'Down': 'Open' or value.command_class == \
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
'Down':
self._lozwmgr.pressButton(value.value_id) self._lozwmgr.pressButton(value.value_id)
break break
def move_down(self, **kwargs): def move_down(self, **kwargs):
"""Move the roller shutter down.""" """Move the roller shutter down."""
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ if value.command_class == \
and value.label == 'Up' or \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ 'Up' or value.command_class == \
and value.label == 'Close': zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
'Close':
self._lozwmgr.pressButton(value.value_id) self._lozwmgr.pressButton(value.value_id)
break break
@ -128,10 +129,11 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
def stop(self, **kwargs): def stop(self, **kwargs):
"""Stop the roller shutter.""" """Stop the roller shutter."""
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ if value.command_class == \
and value.label == 'Open' or \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \ 'Open' or value.command_class == \
and value.label == 'Down': zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
'Down':
self._lozwmgr.releaseButton(value.value_id) self._lozwmgr.releaseButton(value.value_id)
break break

View File

@ -12,7 +12,7 @@ from homeassistant.helpers.entity import generate_entity_id
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = [ REQUIREMENTS = [
'https://github.com/sander76/powerviewApi/' 'https://github.com/sander76/powerviewApi/'
'archive/master.zip#powerviewApi==0.2'] 'archive/cc6f75dd39160d4aaf46cb2ed9220136b924bcb4.zip#powerviewApi==0.2']
HUB_ADDRESS = 'address' HUB_ADDRESS = 'address'

View File

@ -23,6 +23,7 @@ from homeassistant.helpers.script import Script
DOMAIN = "script" DOMAIN = "script"
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
GROUP_NAME_ALL_SCRIPTS = 'all scripts'
DEPENDENCIES = ["group"] DEPENDENCIES = ["group"]
CONF_SEQUENCE = "sequence" CONF_SEQUENCE = "sequence"
@ -73,7 +74,8 @@ def toggle(hass, entity_id):
def setup(hass, config): def setup(hass, config):
"""Load the scripts from the configuration.""" """Load the scripts from the configuration."""
component = EntityComponent(_LOGGER, DOMAIN, hass) component = EntityComponent(_LOGGER, DOMAIN, hass,
group_name=GROUP_NAME_ALL_SCRIPTS)
def service_handler(service): def service_handler(service):
"""Execute a service call to script.<script name>.""" """Execute a service call to script.<script name>."""
@ -124,7 +126,7 @@ class ScriptEntity(ToggleEntity):
def __init__(self, hass, 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.script = Script(hass, sequence, name, self.update_ha_state) self.script = Script(hass, sequence, name, self.async_update_ha_state)
@property @property
def should_poll(self): def should_poll(self):

View File

@ -1,5 +1,5 @@
""" """
The arest sensor will consume an exposed aREST API of a device. Support for an exposed aREST RESTful API of a device.
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.arest/ https://home-assistant.io/components/sensor.arest/
@ -8,30 +8,48 @@ import logging
from datetime import timedelta from datetime import timedelta
import requests import requests
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, DEVICE_DEFAULT_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_RESOURCE,
CONF_RESOURCE, CONF_MONITORED_VARIABLES) CONF_MONITORED_VARIABLES, CONF_NAME, STATE_UNKNOWN)
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers import template
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
CONF_FUNCTIONS = 'functions'
CONF_PINS = 'pins'
DEFAULT_NAME = 'aREST sensor'
PIN_VARIABLE_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RESOURCE): cv.url,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PINS, default={}):
vol.Schema({cv.string: PIN_VARIABLE_SCHEMA}),
vol.Optional(CONF_MONITORED_VARIABLES, default={}):
vol.Schema({cv.string: PIN_VARIABLE_SCHEMA}),
})
# pylint: disable=too-many-locals
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the aREST sensor.""" """Setup the aREST sensor."""
resource = config.get(CONF_RESOURCE) resource = config.get(CONF_RESOURCE)
var_conf = config.get(CONF_MONITORED_VARIABLES) var_conf = config.get(CONF_MONITORED_VARIABLES)
pins = config.get('pins', None) pins = config.get(CONF_PINS)
if resource is None:
_LOGGER.error('Not all required config keys present: %s',
CONF_RESOURCE)
return False
try: try:
response = requests.get(resource, timeout=10).json() response = requests.get(resource, timeout=10).json()
@ -52,11 +70,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if value_template is None: if value_template is None:
return lambda value: value return lambda value: value
value_template = template.Template(value_template, hass) value_template.hass = hass
def _render(value): def _render(value):
try: try:
return value_template.render({'value': value}) return value_template.async_render({'value': value})
except TemplateError: except TemplateError:
_LOGGER.exception('Error parsing value') _LOGGER.exception('Error parsing value')
return value return value
@ -66,33 +84,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = [] dev = []
if var_conf is not None: if var_conf is not None:
for variable in var_conf: for variable, var_data in var_conf.items():
if variable['name'] not in response['variables']: if variable not in response['variables']:
_LOGGER.error('Variable: "%s" does not exist', _LOGGER.error("Variable: '%s' does not exist", variable)
variable['name'])
continue continue
renderer = make_renderer(variable.get(CONF_VALUE_TEMPLATE)) renderer = make_renderer(var_data.get(CONF_VALUE_TEMPLATE))
dev.append(ArestSensor(arest, dev.append(ArestSensor(
resource, arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
config.get('name', response['name']), variable, variable=variable,
variable['name'], unit_of_measurement=var_data.get(CONF_UNIT_OF_MEASUREMENT),
variable=variable['name'], renderer=renderer))
unit_of_measurement=variable.get(
ATTR_UNIT_OF_MEASUREMENT),
renderer=renderer))
if pins is not None: if pins is not None:
for pinnum, pin in pins.items(): for pinnum, pin in pins.items():
renderer = make_renderer(pin.get(CONF_VALUE_TEMPLATE)) renderer = make_renderer(pin.get(CONF_VALUE_TEMPLATE))
dev.append(ArestSensor(ArestData(resource, pinnum), dev.append(ArestSensor(
resource, ArestData(resource, pinnum), resource,
config.get('name', response['name']), config.get(CONF_NAME, response[CONF_NAME]), pin.get(CONF_NAME),
pin.get('name'), pin=pinnum, unit_of_measurement=pin.get(
pin=pinnum, CONF_UNIT_OF_MEASUREMENT), renderer=renderer))
unit_of_measurement=pin.get(
ATTR_UNIT_OF_MEASUREMENT),
renderer=renderer))
add_devices(dev) add_devices(dev)
@ -106,18 +117,17 @@ class ArestSensor(Entity):
"""Initialize the sensor.""" """Initialize the sensor."""
self.arest = arest self.arest = arest
self._resource = resource self._resource = resource
self._name = '{} {}'.format(location.title(), name.title()) \ self._name = '{} {}'.format(location.title(), name.title())
or DEVICE_DEFAULT_NAME
self._variable = variable self._variable = variable
self._pin = pin self._pin = pin
self._state = 'n/a' self._state = STATE_UNKNOWN
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self._renderer = renderer self._renderer = renderer
self.update() self.update()
if self._pin is not None: if self._pin is not None:
request = requests.get('{}/mode/{}/i'.format request = requests.get(
(self._resource, self._pin), timeout=10) '{}/mode/{}/i'.format(self._resource, self._pin), timeout=10)
if request.status_code is not 200: if request.status_code is not 200:
_LOGGER.error("Can't set mode. Is device offline?") _LOGGER.error("Can't set mode. Is device offline?")
@ -139,15 +149,19 @@ class ArestSensor(Entity):
if 'error' in values: if 'error' in values:
return values['error'] return values['error']
value = self._renderer(values.get('value', value = self._renderer(
values.get(self._variable, values.get('value', values.get(self._variable, STATE_UNKNOWN)))
'N/A')))
return value return value
def update(self): def update(self):
"""Get the latest data from aREST API.""" """Get the latest data from aREST API."""
self.arest.update() self.arest.update()
@property
def available(self):
"""Could the device be accessed during the last update call."""
return self.arest.available
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class ArestData(object): class ArestData(object):
@ -158,6 +172,7 @@ class ArestData(object):
self._resource = resource self._resource = resource
self._pin = pin self._pin = pin
self.data = {} self.data = {}
self.available = True
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
@ -179,7 +194,8 @@ class ArestData(object):
response = requests.get('{}/digital/{}'.format( response = requests.get('{}/digital/{}'.format(
self._resource, self._pin), timeout=10) self._resource, self._pin), timeout=10)
self.data = {'value': response.json()['return_value']} self.data = {'value': response.json()['return_value']}
self.available = True
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error("No route to device %s. Is device offline?", _LOGGER.error("No route to device %s. Is device offline?",
self._resource) self._resource)
self.data = {'error': 'error fetching'} self.available = False

View File

@ -1,8 +1,8 @@
""" """
Support for Forecast.io weather service. Support for Dark Sky weather 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.forecast/ https://home-assistant.io/components/sensor.darksky/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta
@ -18,15 +18,14 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-forecastio==1.3.4'] REQUIREMENTS = ['python-forecastio==1.3.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_UNITS = 'units' CONF_UNITS = 'units'
CONF_UPDATE_INTERVAL = 'update_interval'
DEFAULT_NAME = 'Forecast.io' DEFAULT_NAME = 'Dark Sky'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
# Sensor types are defined like so: # Sensor types are defined like so:
# Name, si unit, us unit, ca unit, uk unit, uk2 unit # Name, si unit, us unit, ca unit, uk unit, uk2 unit
@ -84,12 +83,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']) vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']),
vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=120)): (
vol.All(cv.time_period, cv.positive_timedelta)),
}) })
# pylint: disable=too-many-arguments
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Forecast.io sensor.""" """Setup the Dark Sky sensor."""
# Validate the configuration # Validate the configuration
if None in (hass.config.latitude, hass.config.longitude): if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config") _LOGGER.error("Latitude or longitude not set in Home Assistant config")
@ -105,9 +107,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
# Create a data fetcher to support all of the configured sensors. Then make # Create a data fetcher to support all of the configured sensors. Then make
# the first call to init the data and confirm we can connect. # the first call to init the data and confirm we can connect.
try: try:
forecast_data = ForeCastData( forecast_data = DarkSkyData(
config.get(CONF_API_KEY, None), hass.config.latitude, api_key=config.get(CONF_API_KEY, None),
hass.config.longitude, units) latitude=hass.config.latitude,
longitude=hass.config.longitude,
units=units,
interval=config.get(CONF_UPDATE_INTERVAL))
forecast_data.update_currently() forecast_data.update_currently()
except ValueError as error: except ValueError as error:
_LOGGER.error(error) _LOGGER.error(error)
@ -117,14 +122,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensors = [] sensors = []
for variable in config[CONF_MONITORED_CONDITIONS]: for variable in config[CONF_MONITORED_CONDITIONS]:
sensors.append(ForeCastSensor(forecast_data, variable, name)) sensors.append(DarkSkySensor(forecast_data, variable, name))
add_devices(sensors) add_devices(sensors)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class ForeCastSensor(Entity): class DarkSkySensor(Entity):
"""Implementation of a Forecast.io sensor.""" """Implementation of a Dark Sky sensor."""
def __init__(self, forecast_data, sensor_type, name): def __init__(self, forecast_data, sensor_type, name):
"""Initialize the sensor.""" """Initialize the sensor."""
@ -175,10 +180,10 @@ class ForeCastSensor(Entity):
# pylint: disable=too-many-branches,too-many-statements # pylint: disable=too-many-branches,too-many-statements
def update(self): def update(self):
"""Get the latest data from Forecast.io and updates the states.""" """Get the latest data from Dark Sky and updates the states."""
# Call the API for new forecast data. Each sensor will re-trigger this # Call the API for new forecast data. Each sensor will re-trigger this
# same exact call, but thats fine. We cache results for a short period # same exact call, but that's fine. We cache results for a short period
# of time to prevent hitting API limits. Note that forecast.io will # of time to prevent hitting API limits. Note that Dark Sky will
# charge users for too many calls in 1 day, so take care when updating. # charge users for too many calls in 1 day, so take care when updating.
self.forecast_data.update() self.forecast_data.update()
self.update_unit_of_measurement() self.update_unit_of_measurement()
@ -192,7 +197,8 @@ class ForeCastSensor(Entity):
hourly = self.forecast_data.data_hourly hourly = self.forecast_data.data_hourly
self._state = getattr(hourly, 'summary', '') self._state = getattr(hourly, 'summary', '')
elif self.type in ['daily_summary', elif self.type in ['daily_summary',
'temperature_min', 'temperature_max', 'temperature_min',
'temperature_max',
'apparent_temperature_min', 'apparent_temperature_min',
'apparent_temperature_max', 'apparent_temperature_max',
'precip_intensity_max']: 'precip_intensity_max']:
@ -242,12 +248,11 @@ def convert_to_camel(data):
return components[0] + "".join(x.title() for x in components[1:]) return components[0] + "".join(x.title() for x in components[1:])
class ForeCastData(object): class DarkSkyData(object):
"""Gets the latest data from Forecast.io.""" """Get the latest data from Darksky."""
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
def __init__(self, api_key, latitude, longitude, units, interval):
def __init__(self, api_key, latitude, longitude, units):
"""Initialize the data object.""" """Initialize the data object."""
self._api_key = api_key self._api_key = api_key
self.latitude = latitude self.latitude = latitude
@ -261,36 +266,38 @@ class ForeCastData(object):
self.data_hourly = None self.data_hourly = None
self.data_daily = None self.data_daily = None
# Apply throttling to methods using configured interval
self.update = Throttle(interval)(self._update)
self.update_currently = Throttle(interval)(self._update_currently)
self.update_minutely = Throttle(interval)(self._update_minutely)
self.update_hourly = Throttle(interval)(self._update_hourly)
self.update_daily = Throttle(interval)(self._update_daily)
self.update() self.update()
@Throttle(MIN_TIME_BETWEEN_UPDATES) def _update(self):
def update(self): """Get the latest data from Dark Sky."""
"""Get the latest data from Forecast.io."""
import forecastio import forecastio
try: try:
self.data = forecastio.load_forecast( self.data = forecastio.load_forecast(
self._api_key, self.latitude, self.longitude, units=self.units) self._api_key, self.latitude, self.longitude, units=self.units)
except (ConnectError, HTTPError, Timeout, ValueError) as error: except (ConnectError, HTTPError, Timeout, ValueError) as error:
raise ValueError("Unable to init Forecast.io. - %s", error) raise ValueError("Unable to init Dark Sky. %s", error)
self.unit_system = self.data.json['flags']['units'] self.unit_system = self.data.json['flags']['units']
@Throttle(MIN_TIME_BETWEEN_UPDATES) def _update_currently(self):
def update_currently(self):
"""Update currently data.""" """Update currently data."""
self.data_currently = self.data.currently() self.data_currently = self.data.currently()
@Throttle(MIN_TIME_BETWEEN_UPDATES) def _update_minutely(self):
def update_minutely(self):
"""Update minutely data.""" """Update minutely data."""
self.data_minutely = self.data.minutely() self.data_minutely = self.data.minutely()
@Throttle(MIN_TIME_BETWEEN_UPDATES) def _update_hourly(self):
def update_hourly(self):
"""Update hourly data.""" """Update hourly data."""
self.data_hourly = self.data.hourly() self.data_hourly = self.data.hourly()
@Throttle(MIN_TIME_BETWEEN_UPDATES) def _update_daily(self):
def update_daily(self):
"""Update daily data.""" """Update daily data."""
self.data_daily = self.data.daily() self.data_daily = self.data.daily()

View File

@ -202,8 +202,7 @@ class EmonCmsData(object):
"""Get the latest data.""" """Get the latest data."""
try: try:
req = requests.get(self._url, params={"apikey": self._apikey}, req = requests.get(self._url, params={"apikey": self._apikey},
verify=False, allow_redirects=True, allow_redirects=True, timeout=5)
timeout=5)
except requests.exceptions.RequestException as exception: except requests.exceptions.RequestException as exception:
_LOGGER.error(exception) _LOGGER.error(exception)
return return

View File

@ -1,5 +1,5 @@
""" """
Support gahtering system information of hosts which are running glances. Support gathering system information of hosts which are running glances.
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.glances/ https://home-assistant.io/components/sensor.glances/
@ -24,6 +24,8 @@ DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'Glances' DEFAULT_NAME = 'Glances'
DEFAULT_PORT = '61208' DEFAULT_PORT = '61208'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
SENSOR_TYPES = { SENSOR_TYPES = {
'disk_use_percent': ['Disk Use', '%'], 'disk_use_percent': ['Disk Use', '%'],
'disk_use': ['Disk Use', 'GiB'], 'disk_use': ['Disk Use', 'GiB'],
@ -34,11 +36,11 @@ SENSOR_TYPES = {
'swap_use_percent': ['Swap Use', '%'], 'swap_use_percent': ['Swap Use', '%'],
'swap_use': ['Swap Use', 'GiB'], 'swap_use': ['Swap Use', 'GiB'],
'swap_free': ['Swap Free', 'GiB'], 'swap_free': ['Swap Free', 'GiB'],
'processor_load': ['CPU Load', None], 'processor_load': ['CPU Load', '15 min'],
'process_running': ['Running', None], 'process_running': ['Running', 'Count'],
'process_total': ['Total', None], 'process_total': ['Total', 'Count'],
'process_thread': ['Thread', None], 'process_thread': ['Thread', 'Count'],
'process_sleeping': ['Sleeping', None] 'process_sleeping': ['Sleeping', 'Count']
} }
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -50,10 +52,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
# pylint: disable=unused-variable # pylint: disable=unused-variable
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Glances sensor.""" """Setup the Glances sensor."""
@ -66,7 +64,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
try: try:
response = requests.get(url, timeout=10) response = requests.get(url, timeout=10)
if not response.ok: if not response.ok:
_LOGGER.error('Response status is "%s"', response.status_code) _LOGGER.error("Response status is '%s'", response.status_code)
return False return False
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint: %s", url) _LOGGER.error("No route to resource/endpoint: %s", url)

View File

@ -36,14 +36,14 @@ TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ORIGIN): cv.string, vol.Required(CONF_ORIGIN): cv.string,
vol.Required(CONF_DESTINATION): cv.string, vol.Required(CONF_DESTINATION): cv.string,
vol.Required(CONF_DATA): cv.isfile, vol.Required(CONF_DATA): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}) })
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
def get_next_departure(sched, start_station_id, end_station_id): def get_next_departure(sched, start_station_id, end_station_id):
"""Get the next departure for the given sched.""" """Get the next departure for the given schedule."""
origin_station = sched.stops_by_id(start_station_id)[0] origin_station = sched.stops_by_id(start_station_id)[0]
destination_station = sched.stops_by_id(end_station_id)[0] destination_station = sched.stops_by_id(end_station_id)[0]
@ -147,7 +147,7 @@ def get_next_departure(sched, start_station_id, end_station_id):
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Get the GTFS sensor.""" """Set up the GTFS sensor."""
gtfs_dir = hass.config.path(DEFAULT_PATH) gtfs_dir = hass.config.path(DEFAULT_PATH)
data = config.get(CONF_DATA) data = config.get(CONF_DATA)
origin = config.get(CONF_ORIGIN) origin = config.get(CONF_ORIGIN)

View File

@ -18,7 +18,8 @@ DEPENDENCIES = ['homematic']
HM_STATE_HA_CAST = { HM_STATE_HA_CAST = {
"RotaryHandleSensor": {0: "closed", 1: "tilted", 2: "open"}, "RotaryHandleSensor": {0: "closed", 1: "tilted", 2: "open"},
"WaterSensor": {0: "dry", 1: "wet", 2: "water"} "WaterSensor": {0: "dry", 1: "wet", 2: "water"},
"CO2Sensor": {0: "normal", 1: "added", 2: "strong"},
} }
HM_UNIT_HA_CAST = { HM_UNIT_HA_CAST = {
@ -38,6 +39,7 @@ HM_UNIT_HA_CAST = {
"WIND_DIRECTION_RANGE": "°", "WIND_DIRECTION_RANGE": "°",
"SUNSHINEDURATION": "#", "SUNSHINEDURATION": "#",
"AIR_PRESSURE": "hPa", "AIR_PRESSURE": "hPa",
"FREQUENCY": "Hz",
} }

View File

@ -104,7 +104,7 @@ class EmailReader:
self.connection.select() self.connection.select()
if len(self._unread_ids) == 0: if len(self._unread_ids) == 0:
search = "SINCE {0:%d-%b-%y}".format(datetime.date.today()) search = "SINCE {0:%d-%b-%Y}".format(datetime.date.today())
if self._last_id is not None: if self._last_id is not None:
search = "UID {}:*".format(self._last_id) search = "UID {}:*".format(self._last_id)

View File

@ -15,7 +15,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyloopenergy==0.0.14'] REQUIREMENTS = ['pyloopenergy==0.0.15']
CONF_ELEC = 'electricity' CONF_ELEC = 'electricity'
CONF_GAS = 'gas' CONF_GAS = 'gas'

View File

@ -11,7 +11,6 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = []
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -66,6 +65,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
}) })
map_sv_types[pres.S_LIGHT_LEVEL].append(set_req.V_LEVEL) map_sv_types[pres.S_LIGHT_LEVEL].append(set_req.V_LEVEL)
if float(gateway.protocol_version) >= 2.0:
map_sv_types.update({
pres.S_INFO: [set_req.V_TEXT],
pres.S_GAS: [set_req.V_FLOW, set_req.V_VOLUME],
pres.S_GPS: [set_req.V_POSITION],
pres.S_WATER_QUALITY: [set_req.V_TEMP, set_req.V_PH,
set_req.V_ORP, set_req.V_EC]
})
map_sv_types[pres.S_CUSTOM].append(set_req.V_CUSTOM)
map_sv_types[pres.S_POWER].extend(
[set_req.V_VAR, set_req.V_VA, set_req.V_POWER_FACTOR])
devices = {} devices = {}
gateway.platform_callbacks.append(mysensors.pf_callback_factory( gateway.platform_callbacks.append(mysensors.pf_callback_factory(
map_sv_types, devices, add_devices, MySensorsSensor)) map_sv_types, devices, add_devices, MySensorsSensor))
@ -74,6 +85,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity): class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity):
"""Representation of a MySensors Sensor child node.""" """Representation of a MySensors Sensor child node."""
@property
def force_update(self):
"""Return True if state updates should be forced.
If True, a state change will be triggered anytime the state property is
updated, not just when the value changes.
"""
return True
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
@ -104,4 +124,11 @@ class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity):
return self._values[ return self._values[
set_req.V_UNIT_PREFIX] set_req.V_UNIT_PREFIX]
unit_map.update({set_req.V_PERCENTAGE: '%'}) unit_map.update({set_req.V_PERCENTAGE: '%'})
if float(self.gateway.protocol_version) >= 2.0:
unit_map.update({
set_req.V_ORP: 'mV',
set_req.V_EC: 'μS/cm',
set_req.V_VAR: 'var',
set_req.V_VA: 'VA',
})
return unit_map.get(self.value_type) return unit_map.get(self.value_type)

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