mirror of
https://github.com/home-assistant/core.git
synced 2025-07-29 08:07:45 +00:00
commit
4239a2b844
14
.coveragerc
14
.coveragerc
@ -16,6 +16,9 @@ omit =
|
||||
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
|
||||
|
||||
@ -46,6 +49,9 @@ omit =
|
||||
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
|
||||
|
||||
@ -77,7 +83,7 @@ omit =
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/zwave.py
|
||||
homeassistant/components/zwave/*
|
||||
homeassistant/components/*/zwave.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
@ -135,6 +141,7 @@ omit =
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
homeassistant/components/device_tracker/volvooncall.py
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
@ -213,6 +220,7 @@ omit =
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/darksky.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
@ -222,7 +230,6 @@ omit =
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
@ -255,17 +262,20 @@ omit =
|
||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/torque.py
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/worldclock.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yahoo_finance.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/anel_pwrctrl.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -15,7 +15,7 @@
|
||||
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)
|
||||
|
||||
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**
|
||||
- [ ] 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]).
|
||||
|
@ -340,8 +340,8 @@ latex_elements = {
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Home-Assistant.tex', 'Home-Assistant Documentation',
|
||||
'Home-Assistant Team', 'manual'),
|
||||
(master_doc, 'home-assistant.tex', 'Home Assistant Documentation',
|
||||
'Home Assistant Team', 'manual'),
|
||||
]
|
||||
|
||||
# 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
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'home-assistant', 'Home-Assistant Documentation',
|
||||
(master_doc, 'home-assistant', 'Home Assistant Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
@ -397,8 +397,8 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Home-Assistant', 'Home-Assistant Documentation',
|
||||
author, 'Home-Assistant', 'One line description of project.',
|
||||
(master_doc, 'Home-Assistant', 'Home Assistant Documentation',
|
||||
author, 'Home Assistant', 'Open-source home automation platform.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/automation/
|
||||
"""
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
import os
|
||||
@ -23,12 +24,15 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.loader import get_platform
|
||||
from homeassistant.util.dt import utcnow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
DOMAIN = 'automation'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
DEPENDENCIES = ['group']
|
||||
|
||||
GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
|
||||
|
||||
CONF_ALIAS = 'alias'
|
||||
CONF_HIDE_ENTITY = 'hide_entity'
|
||||
|
||||
@ -36,6 +40,7 @@ CONF_CONDITION = 'condition'
|
||||
CONF_ACTION = 'action'
|
||||
CONF_TRIGGER = 'trigger'
|
||||
CONF_CONDITION_TYPE = 'condition_type'
|
||||
CONF_INITIAL_STATE = 'initial_state'
|
||||
|
||||
CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values'
|
||||
CONDITION_TYPE_AND = 'and'
|
||||
@ -43,9 +48,7 @@ CONDITION_TYPE_OR = 'or'
|
||||
|
||||
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
|
||||
DEFAULT_HIDE_ENTITY = False
|
||||
|
||||
METHOD_TRIGGER = 'trigger'
|
||||
METHOD_IF_ACTION = 'if_action'
|
||||
DEFAULT_INITIAL_STATE = True
|
||||
|
||||
ATTR_LAST_TRIGGERED = 'last_triggered'
|
||||
ATTR_VARIABLES = 'variables'
|
||||
@ -55,21 +58,14 @@ SERVICE_RELOAD = 'reload'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _platform_validator(method, schema):
|
||||
"""Generate platform validator for different steps."""
|
||||
def validator(config):
|
||||
def _platform_validator(config):
|
||||
"""Validate it is a valid platform."""
|
||||
platform = get_platform(DOMAIN, config[CONF_PLATFORM])
|
||||
|
||||
if not hasattr(platform, method):
|
||||
raise vol.Invalid('invalid method platform')
|
||||
|
||||
if not hasattr(platform, schema):
|
||||
if not hasattr(platform, 'TRIGGER_SCHEMA'):
|
||||
return config
|
||||
|
||||
return getattr(platform, schema)(config)
|
||||
|
||||
return validator
|
||||
return getattr(platform, 'TRIGGER_SCHEMA')(config)
|
||||
|
||||
_TRIGGER_SCHEMA = vol.All(
|
||||
cv.ensure_list,
|
||||
@ -78,33 +74,19 @@ _TRIGGER_SCHEMA = vol.All(
|
||||
vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN)
|
||||
}, extra=vol.ALLOW_EXTRA),
|
||||
_platform_validator(METHOD_TRIGGER, 'TRIGGER_SCHEMA')
|
||||
_platform_validator
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
_CONDITION_SCHEMA = vol.Any(
|
||||
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),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
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.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.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
})
|
||||
@ -163,9 +145,11 @@ def reload(hass):
|
||||
|
||||
def setup(hass, config):
|
||||
"""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:
|
||||
return False
|
||||
@ -173,22 +157,37 @@ def setup(hass, config):
|
||||
descriptions = conf_util.load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@asyncio.coroutine
|
||||
def trigger_service_handler(service_call):
|
||||
"""Handle automation triggers."""
|
||||
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):
|
||||
"""Handle automation service calls."""
|
||||
@asyncio.coroutine
|
||||
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):
|
||||
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):
|
||||
"""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:
|
||||
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,
|
||||
descriptions.get(SERVICE_TRIGGER),
|
||||
@ -198,8 +197,12 @@ def setup(hass, config):
|
||||
descriptions.get(SERVICE_RELOAD),
|
||||
schema=RELOAD_SERVICE_SCHEMA)
|
||||
|
||||
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE):
|
||||
hass.services.register(DOMAIN, service, service_handler,
|
||||
hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_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),
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
@ -209,15 +212,17 @@ def setup(hass, config):
|
||||
class AutomationEntity(ToggleEntity):
|
||||
"""Entity to show status of entity."""
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
# 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."""
|
||||
self._name = name
|
||||
self._attach_triggers = attach_triggers
|
||||
self._detach_triggers = attach_triggers(self.trigger)
|
||||
self._async_attach_triggers = async_attach_triggers
|
||||
self._async_detach_triggers = None
|
||||
self._cond_func = cond_func
|
||||
self._action = action
|
||||
self._enabled = True
|
||||
self._async_action = async_action
|
||||
self._enabled = False
|
||||
self._last_triggered = None
|
||||
self._hidden = hidden
|
||||
|
||||
@ -248,41 +253,65 @@ class AutomationEntity(ToggleEntity):
|
||||
"""Return True if entity is on."""
|
||||
return self._enabled
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Turn the entity on."""
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn the entity on and update the state."""
|
||||
if self._enabled:
|
||||
return
|
||||
|
||||
self._detach_triggers = self._attach_triggers(self.trigger)
|
||||
self._enabled = True
|
||||
self.update_ha_state()
|
||||
yield from self.async_enable()
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn the entity off."""
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
self._detach_triggers()
|
||||
self._detach_triggers = None
|
||||
self._async_detach_triggers()
|
||||
self._async_detach_triggers = None
|
||||
self._enabled = False
|
||||
self.update_ha_state()
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
|
||||
def trigger(self, variables):
|
||||
"""Trigger automation."""
|
||||
if self._cond_func(variables):
|
||||
self._action(variables)
|
||||
@asyncio.coroutine
|
||||
def async_trigger(self, variables, skip_condition=False):
|
||||
"""Trigger automation.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if skip_condition or self._cond_func(variables):
|
||||
yield from self._async_action(variables)
|
||||
self._last_triggered = utcnow()
|
||||
self.update_ha_state()
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
|
||||
def remove(self):
|
||||
"""Remove automation from HASS."""
|
||||
self.turn_off()
|
||||
run_coroutine_threadsafe(self.async_turn_off(),
|
||||
self.hass.loop).result()
|
||||
super().remove()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_enable(self):
|
||||
"""Enable this automation entity.
|
||||
|
||||
def _process_config(hass, config, component):
|
||||
"""Process config and add automations."""
|
||||
success = False
|
||||
This method is a coroutine.
|
||||
"""
|
||||
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):
|
||||
conf = config[config_key]
|
||||
@ -293,10 +322,11 @@ def _process_config(hass, config, component):
|
||||
|
||||
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:
|
||||
cond_func = _process_if(hass, config, config_block)
|
||||
cond_func = _async_process_if(hass, config, config_block)
|
||||
|
||||
if cond_func is None:
|
||||
continue
|
||||
@ -305,101 +335,78 @@ def _process_config(hass, config, component):
|
||||
"""Condition will always pass."""
|
||||
return True
|
||||
|
||||
attach_triggers = partial(_process_trigger, hass, config,
|
||||
async_attach_triggers = partial(
|
||||
_async_process_trigger, hass, config,
|
||||
config_block.get(CONF_TRIGGER, []), name)
|
||||
entity = AutomationEntity(name, attach_triggers, cond_func, action,
|
||||
hidden)
|
||||
component.add_entities((entity,))
|
||||
success = True
|
||||
entity = AutomationEntity(name, async_attach_triggers, cond_func,
|
||||
action, hidden)
|
||||
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."""
|
||||
script_obj = script.Script(hass, config, name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def action(variables=None):
|
||||
"""Action to be executed."""
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
script_obj.run(variables)
|
||||
logbook.async_log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
hass.loop.create_task(script_obj.async_run(variables))
|
||||
|
||||
return action
|
||||
|
||||
|
||||
def _process_if(hass, config, p_config):
|
||||
def _async_process_if(hass, config, p_config):
|
||||
"""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)
|
||||
use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES
|
||||
|
||||
if use_trigger:
|
||||
if_configs = p_config[CONF_TRIGGER]
|
||||
|
||||
checks = []
|
||||
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:
|
||||
checks.append(condition.from_config(if_config))
|
||||
checks.append(condition.async_from_config(if_config, False))
|
||||
except HomeAssistantError as ex:
|
||||
# Invalid conditions are allowed if we base it on trigger
|
||||
if use_trigger:
|
||||
_LOGGER.warning('Ignoring invalid condition: %s', ex)
|
||||
else:
|
||||
_LOGGER.warning('Invalid condition: %s', ex)
|
||||
return None
|
||||
|
||||
if cond_type == CONDITION_TYPE_AND:
|
||||
def if_action(variables=None):
|
||||
"""AND all conditions."""
|
||||
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
|
||||
|
||||
|
||||
def _process_trigger(hass, config, trigger_configs, name, action):
|
||||
"""Setup the triggers."""
|
||||
@asyncio.coroutine
|
||||
def _async_process_trigger(hass, config, trigger_configs, name, action):
|
||||
"""Setup the triggers.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
removes = []
|
||||
|
||||
for conf in trigger_configs:
|
||||
platform = _resolve_platform(METHOD_TRIGGER, hass, config,
|
||||
platform = yield from hass.loop.run_in_executor(
|
||||
None, prepare_setup_platform, hass, config, DOMAIN,
|
||||
conf.get(CONF_PLATFORM))
|
||||
if platform is None:
|
||||
continue
|
||||
|
||||
remove = platform.trigger(hass, conf, action)
|
||||
if platform is None:
|
||||
return None
|
||||
|
||||
remove = platform.async_trigger(hass, conf, action)
|
||||
|
||||
if not remove:
|
||||
_LOGGER.error("Error setting up rule %s", name)
|
||||
_LOGGER.error("Error setting up trigger %s", name)
|
||||
continue
|
||||
|
||||
_LOGGER.info("Initialized rule %s", name)
|
||||
_LOGGER.info("Initialized trigger %s", name)
|
||||
removes.append(remove)
|
||||
|
||||
if not removes:
|
||||
@ -411,17 +418,3 @@ def _process_trigger(hass, config, trigger_configs, name, action):
|
||||
remove()
|
||||
|
||||
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
|
||||
|
@ -4,11 +4,11 @@ Offer event listening automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#event-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
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."""
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
event_data = config.get(CONF_EVENT_DATA)
|
||||
|
||||
@asyncio.coroutine
|
||||
@callback
|
||||
def handle_event(event):
|
||||
"""Listen for events and calls the action when data matches."""
|
||||
if not event_data or all(val == event.data.get(key) for key, val
|
||||
in event_data.items()):
|
||||
hass.async_add_job(action, {
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'event',
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
|
||||
return hass.bus.listen(event_type, handle_event)
|
||||
return hass.bus.async_listen(event_type, handle_event)
|
||||
|
@ -6,6 +6,7 @@ at https://home-assistant.io/components/automation/#mqtt-trigger
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD)
|
||||
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."""
|
||||
topic = config.get(CONF_TOPIC)
|
||||
payload = config.get(CONF_PAYLOAD)
|
||||
|
||||
@callback
|
||||
def mqtt_automation_listener(msg_topic, msg_payload, qos):
|
||||
"""Listen for MQTT messages."""
|
||||
if payload is None or payload == msg_payload:
|
||||
action({
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'mqtt',
|
||||
'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)
|
||||
|
@ -8,10 +8,11 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers import condition, config_validation as cv
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
@ -25,7 +26,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
below = config.get(CONF_BELOW)
|
||||
@ -34,7 +35,7 @@ def trigger(hass, config, action):
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
if to_s is None:
|
||||
@ -50,19 +51,19 @@ def trigger(hass, config, action):
|
||||
}
|
||||
|
||||
# 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):
|
||||
return
|
||||
|
||||
# Only match if old didn't exist or existed but didn't match
|
||||
# Written as: skip if old one did exist and matched
|
||||
if from_s is not None and condition.numeric_state(
|
||||
if from_s is not None and condition.async_numeric_state(
|
||||
hass, from_s, below, above, value_template, variables):
|
||||
return
|
||||
|
||||
variables['trigger']['from_state'] = from_s
|
||||
variables['trigger']['to_state'] = to_s
|
||||
|
||||
action(variables)
|
||||
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)
|
||||
|
@ -6,9 +6,11 @@ at https://home-assistant.io/components/automation/#state-trigger
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.util.dt as dt_util
|
||||
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
|
||||
|
||||
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."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
|
||||
time_delta = config.get(CONF_FOR)
|
||||
remove_state_for_cancel = None
|
||||
remove_state_for_listener = None
|
||||
async_remove_state_for_cancel = None
|
||||
async_remove_state_for_listener = None
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""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():
|
||||
"""Call action with right context."""
|
||||
action({
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'state',
|
||||
'entity_id': entity,
|
||||
@ -61,35 +64,37 @@ def trigger(hass, config, action):
|
||||
call_action()
|
||||
return
|
||||
|
||||
@callback
|
||||
def state_for_listener(now):
|
||||
"""Fire on state changes after a delay and calls action."""
|
||||
remove_state_for_cancel()
|
||||
async_remove_state_for_cancel()
|
||||
call_action()
|
||||
|
||||
@callback
|
||||
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
|
||||
"""Fire on changes and cancel for listener if changed."""
|
||||
if inner_to_s.state == to_s.state:
|
||||
return
|
||||
remove_state_for_listener()
|
||||
remove_state_for_cancel()
|
||||
async_remove_state_for_listener()
|
||||
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)
|
||||
|
||||
remove_state_for_cancel = track_state_change(
|
||||
async_remove_state_for_cancel = async_track_state_change(
|
||||
hass, entity, state_for_cancel_listener)
|
||||
|
||||
unsub = track_state_change(hass, entity_id, state_automation_listener,
|
||||
from_state, to_state)
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
|
||||
def remove():
|
||||
"""Remove state listeners."""
|
||||
def async_remove():
|
||||
"""Remove state listeners async."""
|
||||
unsub()
|
||||
# pylint: disable=not-callable
|
||||
if remove_state_for_cancel is not None:
|
||||
remove_state_for_cancel()
|
||||
if async_remove_state_for_cancel is not None:
|
||||
async_remove_state_for_cancel()
|
||||
|
||||
if remove_state_for_listener is not None:
|
||||
remove_state_for_listener()
|
||||
if async_remove_state_for_listener is not None:
|
||||
async_remove_state_for_listener()
|
||||
|
||||
return remove
|
||||
return async_remove
|
||||
|
@ -9,9 +9,10 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
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
|
||||
|
||||
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."""
|
||||
event = config.get(CONF_EVENT)
|
||||
offset = config.get(CONF_OFFSET)
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
action({
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'sun',
|
||||
'event': event,
|
||||
@ -42,6 +44,6 @@ def trigger(hass, config, action):
|
||||
|
||||
# Do something to call action
|
||||
if event == SUN_EVENT_SUNRISE:
|
||||
return track_sunrise(hass, call_action, offset)
|
||||
return async_track_sunrise(hass, call_action, offset)
|
||||
else:
|
||||
return track_sunset(hass, call_action, offset)
|
||||
return async_track_sunset(hass, call_action, offset)
|
||||
|
@ -4,14 +4,14 @@ Offer template automation rules.
|
||||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#template-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM
|
||||
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
|
||||
|
||||
|
||||
@ -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."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
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
|
||||
already_triggered = False
|
||||
|
||||
@asyncio.coroutine
|
||||
@callback
|
||||
def state_changed_listener(entity_id, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal already_triggered
|
||||
@ -40,7 +40,7 @@ def trigger(hass, config, action):
|
||||
# Check to see if template returns true
|
||||
if template_result and not already_triggered:
|
||||
already_triggered = True
|
||||
hass.async_add_job(action, {
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'template',
|
||||
'entity_id': entity_id,
|
||||
@ -51,5 +51,5 @@ def trigger(hass, config, action):
|
||||
elif not template_result:
|
||||
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)
|
||||
|
@ -8,9 +8,10 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_AFTER, CONF_PLATFORM
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
from homeassistant.helpers.event import async_track_time_change
|
||||
|
||||
CONF_HOURS = "hours"
|
||||
CONF_MINUTES = "minutes"
|
||||
@ -28,7 +29,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
CONF_SECONDS, CONF_AFTER))
|
||||
|
||||
|
||||
def trigger(hass, config, action):
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
if CONF_AFTER in config:
|
||||
after = config.get(CONF_AFTER)
|
||||
@ -38,14 +39,15 @@ def trigger(hass, config, action):
|
||||
minutes = config.get(CONF_MINUTES)
|
||||
seconds = config.get(CONF_SECONDS)
|
||||
|
||||
@callback
|
||||
def time_automation_listener(now):
|
||||
"""Listen for time changes and calls action."""
|
||||
action({
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'time',
|
||||
'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)
|
||||
|
@ -6,9 +6,10 @@ at https://home-assistant.io/components/automation/#zone-trigger
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
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 (
|
||||
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."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
zone_entity_id = config.get(CONF_ZONE)
|
||||
event = config.get(CONF_EVENT)
|
||||
|
||||
@callback
|
||||
def zone_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
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
|
||||
if event == EVENT_ENTER and not from_match and to_match or \
|
||||
event == EVENT_LEAVE and from_match and not to_match:
|
||||
action({
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'zone',
|
||||
'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)
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/binary_sensor.arest/
|
||||
@ -8,31 +8,32 @@ import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, SENSOR_CLASSES)
|
||||
from homeassistant.const import CONF_RESOURCE, CONF_PIN
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_SENSOR_CLASS)
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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):
|
||||
"""Setup the aREST binary sensor."""
|
||||
resource = config.get(CONF_RESOURCE)
|
||||
pin = config.get(CONF_PIN)
|
||||
|
||||
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
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
add_devices([ArestBinarySensor(
|
||||
arest,
|
||||
resource,
|
||||
config.get('name', response['name']),
|
||||
sensor_class,
|
||||
pin)])
|
||||
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
|
||||
sensor_class, pin)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
|
91
homeassistant/components/binary_sensor/digital_ocean.py
Normal file
91
homeassistant/components/binary_sensor/digital_ocean.py
Normal 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
|
@ -16,6 +16,7 @@ DEPENDENCIES = ['homematic']
|
||||
SENSOR_TYPES_CLASS = {
|
||||
"Remote": None,
|
||||
"ShutterContact": "opening",
|
||||
"IPShutterContact": "opening",
|
||||
"Smoke": "smoke",
|
||||
"SmokeV2": "smoke",
|
||||
"Motion": "motion",
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/sensor.nx584/
|
||||
https://home-assistant.io/components/binary_sensor.nx584/
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
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']
|
||||
|
||||
_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):
|
||||
"""Setup nx584 binary sensor platform."""
|
||||
"""Setup the NX584 binary sensor platform."""
|
||||
from nx584 import client as nx584_client
|
||||
|
||||
host = config.get('host', 'localhost:5007')
|
||||
exclude = config.get('exclude_zones', [])
|
||||
zone_types = config.get('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
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
exclude = config.get(CONF_EXCLUDE_ZONES)
|
||||
zone_types = config.get(CONF_ZONE_TYPES)
|
||||
|
||||
try:
|
||||
client = nx584_client.Client('http://%s' % host)
|
||||
client = nx584_client.Client('http://{}:{}'.format(host, port))
|
||||
zones = client.list_zones()
|
||||
except requests.exceptions.ConnectionError as 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('.')]
|
||||
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
|
||||
|
||||
zone_sensors = {
|
||||
@ -57,13 +72,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
watcher = NX584Watcher(client, zone_sensors)
|
||||
watcher.start()
|
||||
else:
|
||||
_LOGGER.warning('No zones found on NX584')
|
||||
|
||||
_LOGGER.warning("No zones found on NX584")
|
||||
return True
|
||||
|
||||
|
||||
class NX584ZoneSensor(BinarySensorDevice):
|
||||
"""Represents a NX584 zone as a sensor."""
|
||||
"""Representation of a NX584 zone as a sensor."""
|
||||
|
||||
def __init__(self, zone, zone_type):
|
||||
"""Initialize the nx594 binary sensor."""
|
||||
@ -96,7 +110,7 @@ class NX584Watcher(threading.Thread):
|
||||
"""Event listener thread to process NX584 events."""
|
||||
|
||||
def __init__(self, client, zone_sensors):
|
||||
"""Initialize nx584 watcher thread."""
|
||||
"""Initialize NX584 watcher thread."""
|
||||
super(NX584Watcher, self).__init__()
|
||||
self.daemon = True
|
||||
self._client = client
|
||||
@ -130,5 +144,5 @@ class NX584Watcher(threading.Thread):
|
||||
try:
|
||||
self._run()
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('Failed to reach NX584 server')
|
||||
_LOGGER.error("Failed to reach NX584 server")
|
||||
time.sleep(10)
|
||||
|
@ -5,15 +5,19 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rest/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
import voluptuous as vol
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.sensor.rest import RestData
|
||||
from homeassistant.const import (
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -24,16 +28,21 @@ DEFAULT_VERIFY_SSL = True
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD): cv.string,
|
||||
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
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):
|
||||
"""Setup the REST binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
@ -41,11 +50,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
method = config.get(CONF_METHOD)
|
||||
payload = config.get(CONF_PAYLOAD)
|
||||
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)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
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()
|
||||
|
||||
if rest.data is None:
|
||||
|
@ -4,10 +4,12 @@ Support for exposing a templated binary sensor.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.template/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
|
||||
SENSOR_CLASSES_SCHEMA)
|
||||
@ -81,9 +83,10 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
|
||||
self.update()
|
||||
|
||||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_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)
|
||||
|
||||
@ -107,10 +110,11 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
try:
|
||||
self._state = self._template.render().lower() == 'true'
|
||||
self._state = self._template.async_render().lower() == 'true'
|
||||
except TemplateError as ex:
|
||||
if ex.args and ex.args[0].startswith(
|
||||
"UndefinedError: 'None' has no attribute"):
|
||||
|
@ -20,7 +20,10 @@ SENSOR_TYPES = {
|
||||
"vibration": "vibration",
|
||||
"loudness": "sound",
|
||||
"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():
|
||||
add_devices([WinkBinarySensorDevice(key)])
|
||||
|
||||
for sensor in pywink.get_smoke_and_co_detectors():
|
||||
add_devices([WinkBinarySensorDevice(sensor)])
|
||||
|
||||
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink binary sensor."""
|
||||
@ -58,17 +64,25 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
if self.capability == "loudness":
|
||||
return self.wink.loudness_boolean()
|
||||
state = self.wink.loudness_boolean()
|
||||
elif self.capability == "vibration":
|
||||
return self.wink.vibration_boolean()
|
||||
state = self.wink.vibration_boolean()
|
||||
elif self.capability == "brightness":
|
||||
return self.wink.brightness_boolean()
|
||||
state = self.wink.brightness_boolean()
|
||||
elif self.capability == "liquid_detected":
|
||||
return self.wink.liquid_boolean()
|
||||
state = self.wink.liquid_boolean()
|
||||
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:
|
||||
return self.wink.state()
|
||||
state = self.wink.state()
|
||||
|
||||
return state
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
|
@ -36,8 +36,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
|
||||
# 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
|
||||
|
||||
if value.command_class == zwave.COMMAND_CLASS_SENSOR_BINARY:
|
||||
if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
|
||||
|
||||
|
@ -58,6 +58,12 @@ ATTR_OPERATION_LIST = "operation_list"
|
||||
ATTR_SWING_MODE = "swing_mode"
|
||||
ATTR_SWING_LIST = "swing_list"
|
||||
|
||||
CONVERTIBLE_ATTRIBUTE = [
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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_LOW, 'temperature'): vol.Coerce(float),
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_OPERATION_MODE): cv.string,
|
||||
})
|
||||
SET_FAN_MODE_SCHEMA = vol.Schema({
|
||||
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)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
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."""
|
||||
kwargs = {
|
||||
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_LOW, target_temp_low),
|
||||
(ATTR_ENTITY_ID, entity_id),
|
||||
(ATTR_OPERATION_MODE, operation_mode)
|
||||
] if value is not None
|
||||
}
|
||||
_LOGGER.debug("set_temperature start data=%s", kwargs)
|
||||
@ -235,10 +245,20 @@ def setup(hass, config):
|
||||
def temperature_set_service(service):
|
||||
"""Set temperature on the target climate devices."""
|
||||
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:
|
||||
climate.update_ha_state(True)
|
||||
|
||||
|
@ -13,7 +13,6 @@ from homeassistant.components.climate import (
|
||||
ATTR_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -127,12 +126,9 @@ class NestThermostat(ClimateDevice):
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if kwargs.get(ATTR_TARGET_TEMP_LOW) is not None and \
|
||||
kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None:
|
||||
target_temp_high = convert_temperature(kwargs.get(
|
||||
ATTR_TARGET_TEMP_HIGH), self._unit, TEMP_CELSIUS)
|
||||
target_temp_low = convert_temperature(kwargs.get(
|
||||
ATTR_TARGET_TEMP_LOW), self._unit, TEMP_CELSIUS)
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if target_temp_low is not None and target_temp_high is not None:
|
||||
|
||||
if self.device.mode == 'range':
|
||||
temp = (target_temp_low, target_temp_high)
|
||||
|
@ -34,6 +34,18 @@ set_temperature:
|
||||
description: New target temperature for hvac
|
||||
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:
|
||||
description: Set target humidity of climate device
|
||||
|
||||
|
@ -8,9 +8,9 @@ https://home-assistant.io/components/climate.zwave/
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.climate import DOMAIN
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.zwave import (
|
||||
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity)
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, ATTR_OPERATION_MODE)
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
@ -28,12 +28,6 @@ HORSTMANN = 0x0059
|
||||
HORSTMANN_HRT4_ZW = 0x3
|
||||
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_HRT4_ZW = 'hrt4_zw'
|
||||
|
||||
@ -67,19 +61,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
discovery_info, zwave.NETWORK)
|
||||
return
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[ATTR_VALUE_ID]]
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZWaveClimate(value, temp_unit)])
|
||||
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, abstract-method
|
||||
# pylint: disable=abstract-method
|
||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""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):
|
||||
"""Initialize the zwave climate device."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
@ -130,7 +124,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
# Operation Mode
|
||||
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._index_operation = SET_TEMP_TO_INDEX.get(
|
||||
self._current_operation)
|
||||
@ -139,14 +133,16 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("self._current_operation=%s",
|
||||
self._current_operation)
|
||||
# Current Temp
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values():
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||
.values()):
|
||||
if value.label == 'Temperature':
|
||||
self._current_temperature = int(value.data)
|
||||
self._unit = value.units
|
||||
# Fan Mode
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||
.values()):
|
||||
self._current_fan_mode = value.data
|
||||
self._fan_list = list(value.data_items)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
@ -154,17 +150,27 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._current_fan_mode)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == 112 and value.index == 33:
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||
.values()):
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_CONFIGURATION and \
|
||||
value.index == 33:
|
||||
self._current_swing_mode = value.data
|
||||
self._swing_list = list(value.data_items)
|
||||
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
||||
_LOGGER.debug("self._current_swing_mode=%s",
|
||||
self._current_swing_mode)
|
||||
# Set point
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
||||
for value in (self._node.get_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 \
|
||||
self.current_operation != 'Off':
|
||||
if self._index_operation != value.index:
|
||||
@ -232,16 +238,26 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-statements
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
else:
|
||||
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._hrt4_zw and self.current_operation == 'Off':
|
||||
# HRT4-ZW can change setpoint when off.
|
||||
@ -279,17 +295,21 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
||||
if value.command_class == 68 and value.index == 0:
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE).
|
||||
values()):
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE and \
|
||||
value.index == 0:
|
||||
value.data = bytes(fan, 'utf-8')
|
||||
break
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
if value.command_class == 64 and value.index == 0:
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_THERMOSTAT_MODE and value.index == 0:
|
||||
value.data = bytes(operation_mode, 'utf-8')
|
||||
break
|
||||
|
||||
@ -297,7 +317,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Set new target swing mode."""
|
||||
if self._zxt_120 == 1:
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == 112 and value.index == 33:
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_CONFIGURATION and \
|
||||
value.index == 33:
|
||||
value.data = bytes(swing_mode, 'utf-8')
|
||||
break
|
||||
|
@ -29,6 +29,10 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
|
||||
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):
|
||||
"""Register the process service."""
|
||||
@ -48,8 +52,8 @@ def setup(hass, config):
|
||||
|
||||
name, command = match.groups()
|
||||
entities = {state.entity_id: state.name for state in hass.states.all()}
|
||||
entity_ids = fuzzyExtract.extractOne(name, entities,
|
||||
score_cutoff=65)[2]
|
||||
entity_ids = fuzzyExtract.extractOne(
|
||||
name, entities, score_cutoff=65)[2]
|
||||
|
||||
if not entity_ids:
|
||||
logger.error(
|
||||
@ -70,6 +74,7 @@ def setup(hass, config):
|
||||
logger.error('Got unsupported command %s from text %s',
|
||||
command, text)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PROCESS, process,
|
||||
schema=SERVICE_PROCESS_SCHEMA)
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA)
|
||||
|
||||
return True
|
||||
|
103
homeassistant/components/cover/mysensors.py
Normal file
103
homeassistant/components/cover/mysensors.py
Normal 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)
|
@ -12,9 +12,6 @@ from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
|
||||
COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38
|
||||
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
|
||||
|
||||
SOMFY = 0x47
|
||||
SOMFY_ZRTSI = 0x5a52
|
||||
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:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
|
||||
if (value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL and
|
||||
value.index == 0):
|
||||
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL) \
|
||||
and value.index == 0:
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveRollershutter(value)])
|
||||
elif (value.command_class == zwave.COMMAND_CLASS_SWITCH_BINARY or
|
||||
value.command_class == zwave.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
if value.type != zwave.TYPE_BOOL and \
|
||||
value.genre != zwave.GENRE_USER:
|
||||
elif node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_BINARY) or \
|
||||
node.has_command_class(zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
if value.type != zwave.const.TYPE_BOOL and \
|
||||
value.genre != zwave.const.GENRE_USER:
|
||||
return
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
@ -59,6 +56,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
# pylint: disable=no-member
|
||||
self._lozwmgr = libopenzwave.PyManager()
|
||||
self._lozwmgr.create()
|
||||
self._node = value.node
|
||||
@ -88,9 +86,10 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
# Position value
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Level':
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and \
|
||||
value.label == 'Level':
|
||||
self._current_position = value.data
|
||||
|
||||
@property
|
||||
@ -118,22 +117,24 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Open' or \
|
||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Down':
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Open' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Down':
|
||||
self._lozwmgr.pressButton(value.value_id)
|
||||
break
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Move the roller shutter down."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Up' or \
|
||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Close':
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Up' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Close':
|
||||
self._lozwmgr.pressButton(value.value_id)
|
||||
break
|
||||
|
||||
@ -144,11 +145,12 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Open' or \
|
||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Down':
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Open' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Down':
|
||||
self._lozwmgr.releaseButton(value.value_id)
|
||||
break
|
||||
|
||||
|
@ -46,7 +46,6 @@ CONF_TRACK_NEW = 'track_new_devices'
|
||||
DEFAULT_TRACK_NEW = True
|
||||
|
||||
CONF_CONSIDER_HOME = 'consider_home'
|
||||
DEFAULT_CONSIDER_HOME = 180 # seconds
|
||||
|
||||
CONF_SCAN_INTERVAL = 'interval_seconds'
|
||||
DEFAULT_SCAN_INTERVAL = 12
|
||||
@ -70,8 +69,10 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
||||
|
||||
_CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [
|
||||
vol.Schema({
|
||||
vol.Optional(CONF_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(CONF_CONSIDER_HOME): cv.positive_int # seconds
|
||||
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
|
||||
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)
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
@ -118,9 +119,8 @@ def setup(hass: HomeAssistantType, config: ConfigType):
|
||||
return False
|
||||
else:
|
||||
conf = conf[0] if len(conf) > 0 else {}
|
||||
consider_home = timedelta(
|
||||
seconds=conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME))
|
||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
consider_home = conf[CONF_CONSIDER_HOME]
|
||||
track_new = conf[CONF_TRACK_NEW]
|
||||
|
||||
devices = load_config(yaml_path, hass, consider_home)
|
||||
|
||||
@ -282,7 +282,7 @@ class Device(Entity):
|
||||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||
track: bool, dev_id: str, mac: str, name: str=None,
|
||||
picture: str=None, gravatar: str=None,
|
||||
away_hide: bool=False) -> None:
|
||||
hide_if_away: bool=False) -> None:
|
||||
"""Initialize a device."""
|
||||
self.hass = hass
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||
@ -307,7 +307,7 @@ class Device(Entity):
|
||||
else:
|
||||
self.config_picture = picture
|
||||
|
||||
self.away_hide = away_hide
|
||||
self.away_hide = hide_if_away
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -398,15 +398,29 @@ class Device(Entity):
|
||||
|
||||
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
"""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:
|
||||
return [
|
||||
Device(hass, consider_home, device.get('track', False),
|
||||
str(dev_id).lower(), None if device.get('mac') is None
|
||||
else str(device.get('mac')).upper(),
|
||||
device.get('name'), device.get('picture'),
|
||||
device.get('gravatar'),
|
||||
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||
for dev_id, device in load_yaml_config_file(path).items()]
|
||||
result = []
|
||||
devices = load_yaml_config_file(path)
|
||||
for dev_id, device in devices.items():
|
||||
try:
|
||||
device = dev_schema(device)
|
||||
device['dev_id'] = cv.slug(dev_id)
|
||||
except vol.Invalid as exp:
|
||||
log_exception(exp, dev_id, devices)
|
||||
else:
|
||||
result.append(Device(hass, **device))
|
||||
return result
|
||||
except (HomeAssistantError, FileNotFoundError):
|
||||
# When YAML file could not be loaded/did not contain a dict
|
||||
return []
|
||||
|
@ -85,7 +85,9 @@ class FritzBoxScanner(object):
|
||||
|
||||
def get_device_name(self, mac):
|
||||
"""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 == {}:
|
||||
return None
|
||||
return ret
|
||||
|
@ -7,6 +7,7 @@ https://home-assistant.io/components/device_tracker.owntracks/
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import base64
|
||||
from collections import defaultdict
|
||||
|
||||
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.device_tracker import PLATFORM_SCHEMA
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
REGIONS_ENTERED = defaultdict(list)
|
||||
MOBILE_BEACONS_ACTIVE = defaultdict(list)
|
||||
|
||||
BEACON_DEV_ID = 'beacon'
|
||||
|
||||
LOCATION_TOPIC = 'owntracks/+/+'
|
||||
EVENT_TOPIC = 'owntracks/+/+/event'
|
||||
WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint'
|
||||
REQUIREMENTS = ['libnacl==1.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LOCK = threading.Lock()
|
||||
BEACON_DEV_ID = 'beacon'
|
||||
|
||||
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
|
||||
CONF_SECRET = 'secret'
|
||||
CONF_WAYPOINT_IMPORT = 'waypoints'
|
||||
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_TRANSITION = 'transition'
|
||||
VALIDATE_WAYPOINTS = 'waypoints'
|
||||
|
||||
WAYPOINT_LAT_KEY = 'lat'
|
||||
WAYPOINT_LON_KEY = 'lon'
|
||||
WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
|
||||
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):
|
||||
"""Setup an OwnTracks tracker."""
|
||||
"""Set up an OwnTracks tracker."""
|
||||
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
||||
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
|
||||
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:
|
||||
data = json.loads(payload)
|
||||
except ValueError:
|
||||
# If invalid JSON
|
||||
_LOGGER.error('Unable to parse payload as JSON: %s', payload)
|
||||
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:
|
||||
_LOGGER.debug('Skipping %s update for following data '
|
||||
'because of missing or malformatted data: %s',
|
||||
@ -90,7 +159,7 @@ def setup_scanner(hass, config, see):
|
||||
"""MQTT message received."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typelocation
|
||||
data = validate_payload(payload, VALIDATE_LOCATION)
|
||||
data = validate_payload(topic, payload, VALIDATE_LOCATION)
|
||||
if not data:
|
||||
return
|
||||
|
||||
@ -111,7 +180,7 @@ def setup_scanner(hass, config, see):
|
||||
"""MQTT event (geofences) received."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typetransition
|
||||
data = validate_payload(payload, VALIDATE_TRANSITION)
|
||||
data = validate_payload(topic, payload, VALIDATE_TRANSITION)
|
||||
if not data:
|
||||
return
|
||||
|
||||
@ -206,7 +275,7 @@ def setup_scanner(hass, config, see):
|
||||
"""List of waypoints published by a user."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typewaypoints
|
||||
data = validate_payload(payload, VALIDATE_WAYPOINTS)
|
||||
data = validate_payload(topic, payload, VALIDATE_WAYPOINTS)
|
||||
if not data:
|
||||
return
|
||||
|
||||
|
100
homeassistant/components/device_tracker/volvooncall.py
Normal file
100
homeassistant/components/device_tracker/volvooncall.py
Normal 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
|
86
homeassistant/components/digital_ocean.py
Normal file
86
homeassistant/components/digital_ocean.py
Normal 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()
|
@ -9,6 +9,8 @@ loaded before the EVENT_PLATFORM_DISCOVERED is fired.
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
@ -33,6 +35,10 @@ SERVICE_HANDLERS = {
|
||||
'directv': ('media_player', 'directv'),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Start a discovery service."""
|
||||
|
@ -25,6 +25,8 @@ from homeassistant.components.light import (
|
||||
from homeassistant.components.http import (
|
||||
HomeAssistantView, HomeAssistantWSGI
|
||||
)
|
||||
# pylint: disable=unused-import
|
||||
from homeassistant.components.http import REQUIREMENTS # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = 'emulated_hue'
|
||||
|
@ -1,14 +1,14 @@
|
||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "78862c0984279b6876f594ffde45177c",
|
||||
"frontend.html": "c1753e1ce530f978036742477c96d2fd",
|
||||
"mdi.html": "6bd013a8252e19b3c1f1de52994cfbe4",
|
||||
"panels/ha-panel-dev-event.html": "c4a5f70eece9f92616a65e8d26be803e",
|
||||
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
|
||||
"panels/ha-panel-dev-service.html": "07e83c6b7f79d78a59258f6dba477b54",
|
||||
"panels/ha-panel-dev-state.html": "fd8eb946856b1346a87a51d0c86854ff",
|
||||
"panels/ha-panel-dev-template.html": "7cff8a2ef3f44fdaf357a0d41696bf6d",
|
||||
"core.js": "9b3e5ab4eac7e3b074e0daf3f619a638",
|
||||
"frontend.html": "5854807d361de26fe93ad474010f19d2",
|
||||
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
||||
"panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002",
|
||||
"panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
|
||||
"panels/ha-panel-dev-service.html": "c7974458ebc33412d95497e99b785e12",
|
||||
"panels/ha-panel-dev-state.html": "4be627b74e683af14ef779d8203ec674",
|
||||
"panels/ha-panel-dev-template.html": "d23943fa0370f168714da407c90091a2",
|
||||
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
|
||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1 +1 @@
|
||||
Subproject commit 0a4454c68f3c29c77cd60f4315d410d8b3737543
|
||||
Subproject commit db109f5dda043182a7e9647b161851e83be9b91e
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -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)
|
||||
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>
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -22,15 +22,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
|
||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_BINARY and \
|
||||
value.command_class != zwave.COMMAND_CLASS_BARRIER_OPERATOR:
|
||||
if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_BINARY and \
|
||||
value.command_class != zwave.const.COMMAND_CLASS_BARRIER_OPERATOR:
|
||||
return
|
||||
if value.type != zwave.TYPE_BOOL:
|
||||
if value.type != zwave.const.TYPE_BOOL:
|
||||
return
|
||||
if value.genre != zwave.GENRE_USER:
|
||||
if value.genre != zwave.const.GENRE_USER:
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
|
@ -23,7 +23,7 @@ from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
REQUIREMENTS = ["pyhomematic==0.1.14"]
|
||||
REQUIREMENTS = ["pyhomematic==0.1.16"]
|
||||
|
||||
HOMEMATIC = None
|
||||
HOMEMATIC_LINK_DELAY = 0.5
|
||||
@ -52,17 +52,22 @@ SERVICE_VIRTUALKEY = 'virtualkey'
|
||||
SERVICE_SET_VALUE = 'set_value'
|
||||
|
||||
HM_DEVICE_TYPES = {
|
||||
DISCOVER_SWITCHES: ['Switch', 'SwitchPowermeter'],
|
||||
DISCOVER_LIGHTS: ['Dimmer'],
|
||||
DISCOVER_SENSORS: ['SwitchPowermeter', 'Motion', 'MotionV2',
|
||||
'RemoteMotion', 'ThermostatWall', 'AreaThermostat',
|
||||
'RotaryHandleSensor', 'WaterSensor', 'PowermeterGas',
|
||||
'LuxSensor', 'WeatherSensor', 'WeatherStation'],
|
||||
DISCOVER_CLIMATE: ['Thermostat', 'ThermostatWall', 'MAXThermostat'],
|
||||
DISCOVER_BINARY_SENSORS: ['ShutterContact', 'Smoke', 'SmokeV2', 'Motion',
|
||||
'MotionV2', 'RemoteMotion', 'WeatherSensor',
|
||||
'TiltSensor'],
|
||||
DISCOVER_COVER: ['Blind']
|
||||
DISCOVER_SWITCHES: [
|
||||
'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch',
|
||||
'IPSwitchPowermeter', 'KeyMatic'],
|
||||
DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer'],
|
||||
DISCOVER_SENSORS: [
|
||||
'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion',
|
||||
'ThermostatWall', 'AreaThermostat', 'RotaryHandleSensor',
|
||||
'WaterSensor', 'PowermeterGas', 'LuxSensor', 'WeatherSensor',
|
||||
'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor',
|
||||
'TemperatureSensor', 'CO2Sensor'],
|
||||
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 = [
|
||||
@ -87,11 +92,12 @@ HM_PRESS_EVENTS = [
|
||||
'PRESS_SHORT',
|
||||
'PRESS_LONG',
|
||||
'PRESS_CONT',
|
||||
'PRESS_LONG_RELEASE'
|
||||
'PRESS_LONG_RELEASE',
|
||||
'PRESS',
|
||||
]
|
||||
|
||||
HM_IMPULSE_EVENTS = [
|
||||
'SEQUENCE_OK'
|
||||
'SEQUENCE_OK',
|
||||
]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -111,6 +117,15 @@ CONF_RESOLVENAMES = 'resolvenames'
|
||||
CONF_DELAY = 'delay'
|
||||
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({
|
||||
vol.Required(CONF_PLATFORM): "homematic",
|
||||
@ -122,16 +137,16 @@ DEVICE_SCHEMA = vol.Schema({
|
||||
|
||||
CONFIG_SCHEMA = 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.Optional(CONF_REMOTE_PORT, default=2001): cv.port,
|
||||
vol.Optional(CONF_RESOLVENAMES, default=False):
|
||||
vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string,
|
||||
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.Optional(CONF_USERNAME, default="Admin"): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=""): cv.string,
|
||||
vol.Optional(CONF_DELAY, default=0.5): vol.Coerce(float),
|
||||
vol.Optional(CONF_VARIABLES, default=False): cv.boolean,
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): vol.Coerce(float),
|
||||
vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES): cv.boolean,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@ -323,14 +338,18 @@ def _get_devices(device_type, keys):
|
||||
metadata.update(device.SENSORNODE)
|
||||
elif device_type == DISCOVER_BINARY_SENSORS:
|
||||
metadata.update(device.BINARYNODE)
|
||||
else:
|
||||
metadata.update({None: device.ELEMENT})
|
||||
|
||||
params = _create_params_list(device, metadata, device_type)
|
||||
if params:
|
||||
if metadata:
|
||||
# Generate options for 1...n elements with 1...n params
|
||||
for channel in range(1, device.ELEMENT + 1):
|
||||
_LOGGER.debug("Handling %s:%i", key, channel)
|
||||
if channel in params:
|
||||
for param in params[channel]:
|
||||
for param, channels in metadata.items():
|
||||
if param in HM_IGNORE_DISCOVERY_NODE:
|
||||
continue
|
||||
|
||||
# add devices
|
||||
_LOGGER.debug("Handling %s: %s", param, channels)
|
||||
for channel in channels:
|
||||
name = _create_ha_name(
|
||||
name=device.NAME,
|
||||
channel=channel,
|
||||
@ -343,7 +362,7 @@ def _get_devices(device_type, keys):
|
||||
ATTR_CHANNEL: channel
|
||||
}
|
||||
if param is not None:
|
||||
device_dict.update({ATTR_PARAM: param})
|
||||
device_dict[ATTR_PARAM] = param
|
||||
|
||||
# Add new device
|
||||
try:
|
||||
@ -352,54 +371,12 @@ def _get_devices(device_type, keys):
|
||||
except vol.MultipleInvalid as err:
|
||||
_LOGGER.error("Invalid device config: %s",
|
||||
str(err))
|
||||
else:
|
||||
_LOGGER.debug("Channel %i not in params", channel)
|
||||
else:
|
||||
_LOGGER.debug("Got no params for %s", key)
|
||||
_LOGGER.debug("%s autodiscovery: %s", device_type, str(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):
|
||||
"""Generate a unique object name."""
|
||||
# HMDevice is a simple device
|
||||
@ -484,12 +461,12 @@ def _hm_service_virtualkey(call):
|
||||
hmdevice = HOMEMATIC.devices.get(address)
|
||||
|
||||
# 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)
|
||||
return
|
||||
|
||||
# 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)
|
||||
return
|
||||
|
||||
@ -743,18 +720,21 @@ class HMDevice(Entity):
|
||||
self._hmdevice.ATTRIBUTENODE,
|
||||
self._hmdevice.WRITENODE, self._hmdevice.EVENTNODE,
|
||||
self._hmdevice.ACTIONNODE):
|
||||
for node, channel in metadata.items():
|
||||
for node, channels in metadata.items():
|
||||
# Data is needed for this instance
|
||||
if node in self._data:
|
||||
# chan is current channel
|
||||
if channel == 'c' or channel is None:
|
||||
if len(channels) == 1:
|
||||
channel = channels[0]
|
||||
else:
|
||||
channel = self._channel
|
||||
|
||||
# Prepare for subscription
|
||||
try:
|
||||
if int(channel) >= 0:
|
||||
channels_to_sub.update({int(channel): True})
|
||||
except (ValueError, TypeError):
|
||||
_LOGGER("Invalid channel in metadata from %s",
|
||||
_LOGGER.error("Invalid channel in metadata from %s",
|
||||
self._name)
|
||||
|
||||
# Set callbacks
|
||||
|
@ -11,18 +11,20 @@ import mimetypes
|
||||
import threading
|
||||
import re
|
||||
import ssl
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant import util
|
||||
from homeassistant.const import (
|
||||
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,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.core import split_entity_id
|
||||
import homeassistant.util.dt as dt_util
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components import persistent_notification
|
||||
|
||||
DOMAIN = 'http'
|
||||
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'
|
||||
|
||||
DATA_API_PASSWORD = 'api_password'
|
||||
NOTIFICATION_ID_LOGIN = 'http-login'
|
||||
|
||||
# TLS configuation follows the best-practice guidelines specified here:
|
||||
# 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)
|
||||
server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
|
||||
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_key = conf.get(CONF_SSL_KEY)
|
||||
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
||||
@ -365,8 +368,8 @@ class HomeAssistantWSGI(object):
|
||||
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
|
||||
self.server.ssl_adapter = ContextSSLAdapter(context)
|
||||
|
||||
threading.Thread(target=self.server.start, daemon=True,
|
||||
name='WSGI-server').start()
|
||||
threading.Thread(
|
||||
target=self.server.start, daemon=True, name='WSGI-server').start()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the wsgi server."""
|
||||
@ -391,10 +394,10 @@ class HomeAssistantWSGI(object):
|
||||
resp = ex.get_response(request.environ)
|
||||
if request.accept_mimetypes.accept_json:
|
||||
resp.data = json.dumps({
|
||||
"result": "error",
|
||||
"message": str(ex),
|
||||
'result': 'error',
|
||||
'message': str(ex),
|
||||
})
|
||||
resp.mimetype = "application/json"
|
||||
resp.mimetype = CONTENT_TYPE_JSON
|
||||
return resp
|
||||
|
||||
def base_app(self, environ, start_response):
|
||||
@ -403,11 +406,11 @@ class HomeAssistantWSGI(object):
|
||||
response = self.dispatch_request(request)
|
||||
|
||||
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)
|
||||
if cors_check:
|
||||
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN] = \
|
||||
environ.get("HTTP_ORIGIN")
|
||||
environ.get('HTTP_ORIGIN')
|
||||
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS] = \
|
||||
cors_headers
|
||||
|
||||
@ -425,7 +428,7 @@ class HomeAssistantWSGI(object):
|
||||
# Strip out any cachebusting MD5 fingerprints
|
||||
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
|
||||
if fingerprinted:
|
||||
environ['PATH_INFO'] = "{}.{}".format(*fingerprinted.groups())
|
||||
environ['PATH_INFO'] = '{}.{}'.format(*fingerprinted.groups())
|
||||
return app(environ, start_response)
|
||||
|
||||
|
||||
@ -489,6 +492,10 @@ class HomeAssistantView(object):
|
||||
if self.requires_auth and not authenticated:
|
||||
_LOGGER.warning('Login attempt or request with an invalid '
|
||||
'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()
|
||||
|
||||
request.authenticated = authenticated
|
||||
@ -512,12 +519,9 @@ class HomeAssistantView(object):
|
||||
def json(self, result, status_code=200):
|
||||
"""Return a JSON response."""
|
||||
msg = json.dumps(
|
||||
result,
|
||||
sort_keys=True,
|
||||
cls=rem.JSONEncoder
|
||||
).encode('UTF-8')
|
||||
return self.Response(msg, mimetype="application/json",
|
||||
status=status_code)
|
||||
result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8')
|
||||
return self.Response(
|
||||
msg, mimetype=CONTENT_TYPE_JSON, status=status_code)
|
||||
|
||||
def json_message(self, error, status_code=200):
|
||||
"""Return a JSON message response."""
|
||||
|
@ -9,8 +9,7 @@ https://home-assistant.io/components/hvac.zwave/
|
||||
import logging
|
||||
from homeassistant.components.hvac import DOMAIN
|
||||
from homeassistant.components.hvac import HvacDevice
|
||||
from homeassistant.components.zwave import (
|
||||
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity)
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||
|
||||
@ -23,12 +22,6 @@ REMOTEC = 0x5254
|
||||
REMOTEC_ZXT_120 = 0x8377
|
||||
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'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
@ -50,8 +43,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
discovery_info, zwave.NETWORK)
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[ATTR_VALUE_ID]]
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZWaveHvac(value)])
|
||||
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
||||
@ -107,25 +100,29 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
def update_properties(self):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
# Set point
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
if int(value.data) != 0:
|
||||
self._target_temperature = int(value.data)
|
||||
# Operation Mode
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE)
|
||||
.values()):
|
||||
self._current_operation = value.data
|
||||
self._operation_list = list(value.data_items)
|
||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||
# Current Temp
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values():
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||
.values()):
|
||||
if value.label == 'Temperature':
|
||||
self._current_temperature = int(value.data)
|
||||
self._unit = value.units
|
||||
# Fan Mode
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||
.values()):
|
||||
self._current_operation_state = value.data
|
||||
self._fan_list = list(value.data_items)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
@ -133,8 +130,9 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
self._current_operation_state)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||
.values()):
|
||||
if value.command_class == 112 and value.index == 33:
|
||||
self._current_swing_mode = value.data
|
||||
self._swing_list = list(value.data_items)
|
||||
@ -199,8 +197,9 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
"""Set new target temperature."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
if value.command_class != 67:
|
||||
continue
|
||||
if self._zxt_120:
|
||||
@ -217,8 +216,9 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||
.values()):
|
||||
if value.command_class == 68 and value.index == 0:
|
||||
value.data = bytes(fan, 'utf-8')
|
||||
break
|
||||
@ -226,7 +226,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
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:
|
||||
value.data = bytes(operation_mode, 'utf-8')
|
||||
break
|
||||
@ -235,7 +235,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
"""Set new target swing mode."""
|
||||
if self._zxt_120 == 1:
|
||||
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:
|
||||
value.data = bytes(swing_mode, 'utf-8')
|
||||
break
|
||||
|
@ -28,6 +28,7 @@ DEFAULT_PORT = 8086
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_VERIFY_SSL = False
|
||||
DOMAIN = 'influxdb'
|
||||
TIMEOUT = 5
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@ -69,7 +70,8 @@ def setup(hass, config):
|
||||
try:
|
||||
influx = InfluxDBClient(
|
||||
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;")
|
||||
except exceptions.InfluxDBClientError as exc:
|
||||
_LOGGER.error("Database host is not accessible due to '%s', please "
|
||||
|
@ -6,8 +6,14 @@ https://home-assistant.io/components/introduction/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
DOMAIN = 'introduction'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config=None):
|
||||
"""Setup the introduction component."""
|
||||
|
@ -93,7 +93,8 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({
|
||||
vol.Coerce(tuple)),
|
||||
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
||||
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_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
|
||||
ATTR_EFFECT: vol.In([EFFECT_COLORLOOP, EFFECT_RANDOM, EFFECT_WHITE]),
|
||||
|
@ -17,8 +17,8 @@ from homeassistant.components.light import (
|
||||
PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.6.zip'
|
||||
'#flux_led==0.6']
|
||||
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.7.zip'
|
||||
'#flux_led==0.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -7,19 +7,35 @@ https://home-assistant.io/components/light.limitlessled/
|
||||
# pylint: disable=abstract-method
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR,
|
||||
ATTR_TRANSITION, EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG,
|
||||
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__)
|
||||
REQUIREMENTS = ['limitlessled==1.0.0']
|
||||
RGB_BOUNDARY = 40
|
||||
DEFAULT_TRANSITION = 0
|
||||
DEFAULT_PORT = 8899
|
||||
DEFAULT_VERSION = 5
|
||||
|
||||
CONF_BRIDGES = 'bridges'
|
||||
CONF_GROUPS = 'groups'
|
||||
CONF_NUMBER = 'number'
|
||||
CONF_TYPE = 'type'
|
||||
CONF_VERSION = 'version'
|
||||
|
||||
DEFAULT_LED_TYPE = 'rgbw'
|
||||
DEFAULT_PORT = 8899
|
||||
DEFAULT_TRANSITION = 0
|
||||
DEFAULT_VERSION = 5
|
||||
|
||||
LED_TYPE = ['rgbw', 'white']
|
||||
|
||||
RGB_BOUNDARY = 40
|
||||
|
||||
WHITE = [255, 255, 255]
|
||||
|
||||
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_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):
|
||||
"""Rewrite legacy configuration to new format."""
|
||||
|
@ -10,11 +10,12 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR,
|
||||
Light)
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_RGB_COLOR, SUPPORT_COLOR_TEMP, Light)
|
||||
from homeassistant.const import (
|
||||
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 (
|
||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
|
||||
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_VALUE_TEMPLATE = 'rgb_value_template'
|
||||
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_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_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
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_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
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_RGB_STATE_TOPIC,
|
||||
CONF_RGB_COMMAND_TOPIC,
|
||||
CONF_COLOR_TEMP_STATE_TOPIC,
|
||||
CONF_COLOR_TEMP_COMMAND_TOPIC
|
||||
)
|
||||
},
|
||||
{
|
||||
CONF_STATE: config.get(CONF_STATE_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_RETAIN),
|
||||
@ -92,6 +102,7 @@ class MqttLight(Light):
|
||||
"""MQTT light."""
|
||||
|
||||
# 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,
|
||||
optimistic, brightness_scale):
|
||||
"""Initialize MQTT light."""
|
||||
@ -106,6 +117,8 @@ class MqttLight(Light):
|
||||
optimistic or topic[CONF_RGB_STATE_TOPIC] is None
|
||||
self._optimistic_brightness = (
|
||||
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._state = False
|
||||
self._supported_features = 0
|
||||
@ -114,6 +127,9 @@ class MqttLight(Light):
|
||||
self._supported_features |= (
|
||||
topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None and
|
||||
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()):
|
||||
if tpl is None:
|
||||
@ -168,6 +184,21 @@ class MqttLight(Light):
|
||||
else:
|
||||
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
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
@ -178,6 +209,11 @@ class MqttLight(Light):
|
||||
"""Return the RGB color value."""
|
||||
return self._rgb
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the color temperature in mired."""
|
||||
return self._color_temp
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for a MQTT light."""
|
||||
@ -230,6 +266,16 @@ class MqttLight(Light):
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
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],
|
||||
self._payload['on'], self._qos, self._retain)
|
||||
|
||||
|
@ -50,19 +50,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_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
|
||||
if value.type != zwave.TYPE_BYTE:
|
||||
if value.type != zwave.const.TYPE_BYTE:
|
||||
return
|
||||
if value.genre != zwave.GENRE_USER:
|
||||
if value.genre != zwave.const.GENRE_USER:
|
||||
return
|
||||
|
||||
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:
|
||||
add_devices([ZwaveColorLight(value)])
|
||||
except ValueError as exception:
|
||||
@ -195,8 +195,8 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||
raise ValueError("No matching color command found.")
|
||||
|
||||
for value_color_channels in value.node.get_values(
|
||||
class_id=zwave.COMMAND_CLASS_COLOR, genre='System',
|
||||
type="Int").values():
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_COLOR,
|
||||
genre='System', type="Int").values():
|
||||
self._value_color_channels = value_color_channels
|
||||
|
||||
if self._value_color_channels is None:
|
||||
|
@ -1,28 +1,35 @@
|
||||
"""
|
||||
LIRC interface to receive signals from a infrared remote control.
|
||||
|
||||
This sensor will momentarily set state to various values as defined
|
||||
in the .lintrc file which can be interpreted in home-assistant to
|
||||
trigger various actions.
|
||||
|
||||
Sending signals to other IR receivers can be accomplished with the
|
||||
shell_command component and the irsend command for now.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/lirc/
|
||||
"""
|
||||
# pylint: disable=import-error
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||
|
||||
DOMAIN = "lirc"
|
||||
REQUIREMENTS = ['python-lirc==1.2.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ICON = 'mdi:remote'
|
||||
EVENT_IR_COMMAND_RECEIVED = 'ir_command_received'
|
||||
|
||||
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):
|
||||
"""Setup LIRC capability."""
|
||||
@ -65,20 +72,19 @@ class LircInterface(threading.Thread):
|
||||
def run(self):
|
||||
"""Main loop of LIRC interface thread."""
|
||||
import lirc
|
||||
_LOGGER.debug('LIRC interface thread started')
|
||||
_LOGGER.debug("LIRC interface thread started")
|
||||
while not self.stopped.isSet():
|
||||
try:
|
||||
code = lirc.nextcode() # list; empty if no buttons pressed
|
||||
except lirc.NextCodeError:
|
||||
_LOGGER.warning('Encountered error reading '
|
||||
'next code from LIRC')
|
||||
_LOGGER.warning("Error reading next code from LIRC")
|
||||
code = None
|
||||
# interpret result from python-lirc
|
||||
if code:
|
||||
code = code[0]
|
||||
_LOGGER.info('Got new LIRC code %s', code)
|
||||
self.hass.bus.fire(EVENT_IR_COMMAND_RECEIVED,
|
||||
{BUTTON_NAME: code})
|
||||
_LOGGER.info("Got new LIRC code %s", code)
|
||||
self.hass.bus.fire(
|
||||
EVENT_IR_COMMAND_RECEIVED, {BUTTON_NAME: code})
|
||||
else:
|
||||
time.sleep(0.2)
|
||||
lirc.deinit()
|
||||
|
@ -16,14 +16,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_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
|
||||
if value.type != zwave.TYPE_BOOL:
|
||||
if value.type != zwave.const.TYPE_BOOL:
|
||||
return
|
||||
if value.genre != zwave.GENRE_USER:
|
||||
if value.genre != zwave.const.GENRE_USER:
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
|
@ -4,6 +4,7 @@ Event parser and human readable log generator.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/logbook/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
@ -20,6 +21,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON,
|
||||
ATTR_HIDDEN)
|
||||
from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
||||
DOMAIN = "logbook"
|
||||
DEPENDENCIES = ['recorder', 'frontend']
|
||||
@ -57,6 +59,13 @@ LOG_MESSAGE_SCHEMA = vol.Schema({
|
||||
|
||||
|
||||
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."""
|
||||
data = {
|
||||
ATTR_NAME: name,
|
||||
@ -67,11 +76,12 @@ def log_entry(hass, name, message, domain=None, entity_id=None):
|
||||
data[ATTR_DOMAIN] = domain
|
||||
if entity_id is not None:
|
||||
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):
|
||||
"""Listen for download events to download files."""
|
||||
@asyncio.coroutine
|
||||
def log_message(service):
|
||||
"""Handle sending notification message service calls."""
|
||||
message = service.data[ATTR_MESSAGE]
|
||||
@ -80,8 +90,8 @@ def setup(hass, config):
|
||||
entity_id = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
message.hass = hass
|
||||
message = message.render()
|
||||
log_entry(hass, name, message, domain, entity_id)
|
||||
message = message.async_render()
|
||||
async_log_entry(hass, name, message, domain, entity_id)
|
||||
|
||||
hass.wsgi.register_view(LogbookView(hass, config))
|
||||
|
||||
|
@ -79,18 +79,11 @@ class LogitechMediaServer(object):
|
||||
|
||||
def _get_http_port(self):
|
||||
"""Get http port from media server, it is used to get cover art."""
|
||||
http_port = None
|
||||
try:
|
||||
http_port = self.query('pref', 'httpport', '?')
|
||||
if not http_port:
|
||||
_LOGGER.error("Unable to read data from server %s:%s",
|
||||
_LOGGER.error("Failed to connect to server %s:%s",
|
||||
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):
|
||||
"""Create a list of SqueezeBoxDevices connected to the LMS."""
|
||||
@ -104,6 +97,7 @@ class LogitechMediaServer(object):
|
||||
|
||||
def query(self, *parameters):
|
||||
"""Send request and await response from server."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host, self.port)
|
||||
if self._username and self._password:
|
||||
telnet.write('login {username} {password}\n'.format(
|
||||
@ -118,6 +112,12 @@ class LogitechMediaServer(object):
|
||||
.strip()
|
||||
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):
|
||||
"""Get ithe status of a player."""
|
||||
@ -128,6 +128,7 @@ class LogitechMediaServer(object):
|
||||
# K (artwork_url): URL to remote artwork
|
||||
tags = 'adK'
|
||||
new_status = {}
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host, self.port)
|
||||
telnet.write('{player} status - 1 tags:{tags}\n'.format(
|
||||
player=player,
|
||||
@ -140,6 +141,11 @@ class LogitechMediaServer(object):
|
||||
for item in response:
|
||||
parts = urllib.parse.unquote(item).partition(':')
|
||||
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
|
||||
|
||||
|
||||
|
@ -8,24 +8,31 @@ import logging
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.util as util
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
||||
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL,
|
||||
MediaPlayerDevice)
|
||||
MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_CUSTOMIZE, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
|
||||
STATE_UNKNOWN)
|
||||
STATE_UNKNOWN, CONF_NAME)
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv'
|
||||
'/archive/v0.1.2.zip'
|
||||
'#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_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \
|
||||
@ -44,6 +51,17 @@ WEBOS_APPS_SHORT = {
|
||||
'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
|
||||
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:
|
||||
host = urlparse(discovery_info[1]).hostname
|
||||
else:
|
||||
host = config.get(CONF_HOST, None)
|
||||
host = config.get(CONF_HOST)
|
||||
|
||||
if host is None:
|
||||
_LOGGER.error('No host found in configuration')
|
||||
_LOGGER.error("No host found in configuration")
|
||||
return False
|
||||
|
||||
# Only act if we are not already configuring this host
|
||||
if host in _CONFIGURING:
|
||||
return
|
||||
|
||||
customize = config.get(CONF_CUSTOMIZE, {})
|
||||
setup_tv(host, customize, hass, add_devices)
|
||||
name = config.get(CONF_NAME)
|
||||
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."""
|
||||
from pylgtv import WebOsClient
|
||||
from pylgtv import PyLGTVPairException
|
||||
@ -79,15 +98,15 @@ def setup_tv(host, customize, hass, add_devices):
|
||||
client.register()
|
||||
except PyLGTVPairException:
|
||||
_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
|
||||
except OSError:
|
||||
_LOGGER.error('Unable to connect to host %s.', host)
|
||||
_LOGGER.error("Unable to connect to host %s", host)
|
||||
return
|
||||
else:
|
||||
# Not registered, request configuration.
|
||||
_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
|
||||
|
||||
# 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.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."""
|
||||
configurator = get_component('configurator')
|
||||
|
||||
@ -112,7 +131,7 @@ def request_configuration(host, customize, hass, add_devices):
|
||||
# pylint: disable=unused-argument
|
||||
def lgtv_configuration_callback(data):
|
||||
"""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(
|
||||
hass, 'LG WebOS TV', lgtv_configuration_callback,
|
||||
@ -128,13 +147,13 @@ class LgWebOSDevice(MediaPlayerDevice):
|
||||
"""Representation of a LG WebOS TV."""
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
def __init__(self, host, customize):
|
||||
def __init__(self, host, name, customize):
|
||||
"""Initialize the webos device."""
|
||||
from pylgtv import WebOsClient
|
||||
self._client = WebOsClient(host)
|
||||
self._customize = customize
|
||||
|
||||
self._name = 'LG WebOS TV Remote'
|
||||
self._name = name
|
||||
# Assume that the TV is not muted
|
||||
self._muted = False
|
||||
# Assume that the TV is in Play mode
|
||||
@ -160,7 +179,7 @@ class LgWebOSDevice(MediaPlayerDevice):
|
||||
self._app_list = {}
|
||||
|
||||
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)
|
||||
if app_id:
|
||||
custom_sources.append(app_id)
|
||||
|
@ -4,6 +4,7 @@ Support for MQTT message handling.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
@ -14,8 +15,8 @@ import voluptuous as vol
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import template
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import template, config_validation as cv
|
||||
from homeassistant.helpers.event import threaded_listener_factory
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
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)
|
||||
|
||||
|
||||
def subscribe(hass, topic, callback, qos=DEFAULT_QOS):
|
||||
def async_subscribe(hass, topic, callback, qos=DEFAULT_QOS):
|
||||
"""Subscribe to an MQTT topic."""
|
||||
@asyncio.coroutine
|
||||
def mqtt_topic_subscriber(event):
|
||||
"""Match subscribed MQTT topic."""
|
||||
if _match_topic(topic, event.data[ATTR_TOPIC]):
|
||||
callback(event.data[ATTR_TOPIC], event.data[ATTR_PAYLOAD],
|
||||
event.data[ATTR_QOS])
|
||||
if not _match_topic(topic, event.data[ATTR_TOPIC]):
|
||||
return
|
||||
|
||||
remove = hass.bus.listen(EVENT_MQTT_MESSAGE_RECEIVED,
|
||||
hass.async_run_job(callback, event.data[ATTR_TOPIC],
|
||||
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
|
||||
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):
|
||||
|
@ -158,7 +158,8 @@ def setup(hass, config): # pylint: disable=too-many-locals
|
||||
'No devices could be setup as gateways, check your configuration')
|
||||
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)
|
||||
|
||||
return True
|
||||
@ -340,5 +341,7 @@ class MySensorsDeviceEntity(object):
|
||||
set_req.V_LOCK_STATUS, set_req.V_TRIPPED):
|
||||
self._values[value_type] = (
|
||||
STATE_ON if int(value) == 1 else STATE_OFF)
|
||||
elif value_type == set_req.V_DIMMER:
|
||||
self._values[value_type] = int(value)
|
||||
else:
|
||||
self._values[value_type] = value
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, CONF_STRUCTURE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['python-nest==2.9.2']
|
||||
REQUIREMENTS = ['python-nest==2.10.0']
|
||||
|
||||
DOMAIN = 'nest'
|
||||
|
||||
|
@ -37,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the Netatmo devices."""
|
||||
"""Set up the Netatmo devices."""
|
||||
import lnetatmo
|
||||
|
||||
global NETATMO_AUTH
|
||||
@ -47,7 +47,7 @@ def setup(hass, config):
|
||||
config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD],
|
||||
'read_station read_camera access_camera')
|
||||
except HTTPError:
|
||||
_LOGGER.error("Unable to connect to NatAtmo API")
|
||||
_LOGGER.error("Unable to connect to Netatmo API")
|
||||
return False
|
||||
|
||||
for component in 'camera', 'sensor':
|
||||
|
@ -42,7 +42,7 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||
NOTIFY_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_MESSAGE): 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,
|
||||
})
|
||||
|
||||
|
@ -79,9 +79,6 @@ class AWSLambda(BaseNotificationService):
|
||||
_LOGGER.info("At least 1 target is required")
|
||||
return
|
||||
|
||||
if not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
for target in targets:
|
||||
cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v)
|
||||
payload = {"message": message}
|
||||
|
@ -70,9 +70,6 @@ class AWSSNS(BaseNotificationService):
|
||||
_LOGGER.info("At least 1 target is required")
|
||||
return
|
||||
|
||||
if not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
message_attributes = {k: {"StringValue": json.dumps(v),
|
||||
"DataType": "String"}
|
||||
for k, v in kwargs.items() if v}
|
||||
|
@ -69,9 +69,6 @@ class AWSSQS(BaseNotificationService):
|
||||
_LOGGER.info("At least 1 target is required")
|
||||
return
|
||||
|
||||
if not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
for target in targets:
|
||||
cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v)
|
||||
message_body = {"message": message}
|
||||
|
@ -359,8 +359,6 @@ class HTML5NotificationService(BaseNotificationService):
|
||||
|
||||
if not targets:
|
||||
targets = self.registrations.keys()
|
||||
elif not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
for target in targets:
|
||||
info = self.registrations.get(target)
|
||||
|
@ -58,9 +58,6 @@ class MessageBirdNotificationService(BaseNotificationService):
|
||||
_LOGGER.error('No target specified.')
|
||||
return
|
||||
|
||||
if not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
for target in targets:
|
||||
try:
|
||||
self.client.message_create(self.sender,
|
||||
|
@ -87,10 +87,6 @@ class PushBulletNotificationService(BaseNotificationService):
|
||||
_LOGGER.info('Sent notification to self')
|
||||
return
|
||||
|
||||
# Make list if not so
|
||||
if not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
# Main loop, Process all targets specified
|
||||
for target in targets:
|
||||
try:
|
||||
|
@ -18,45 +18,51 @@ REQUIREMENTS = ['pushetta==1.0.15']
|
||||
|
||||
|
||||
CONF_CHANNEL_NAME = 'channel_name'
|
||||
CONF_SEND_TEST_MSG = 'send_test_msg'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_CHANNEL_NAME): cv.string,
|
||||
vol.Optional(CONF_SEND_TEST_MSG, default=False): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
"""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:
|
||||
pushetta = Pushetta(config[CONF_API_KEY])
|
||||
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])
|
||||
if pushetta_service.is_valid:
|
||||
return pushetta_service
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class PushettaNotificationService(BaseNotificationService):
|
||||
"""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."""
|
||||
from pushetta import Pushetta
|
||||
self._api_key = api_key
|
||||
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):
|
||||
"""Send a message to a user."""
|
||||
from pushetta import exceptions
|
||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
|
||||
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
|
||||
|
@ -61,7 +61,9 @@ class PushoverNotificationService(BaseNotificationService):
|
||||
|
||||
data['title'] = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
|
||||
target = kwargs.get(ATTR_TARGET)
|
||||
targets = kwargs.get(ATTR_TARGET)
|
||||
|
||||
for target in targets:
|
||||
if target is not None:
|
||||
data['device'] = target
|
||||
|
||||
|
@ -75,8 +75,10 @@ class RestNotificationService(BaseNotificationService):
|
||||
data[self._title_param_name] = kwargs.get(ATTR_TITLE,
|
||||
ATTR_TITLE_DEFAULT)
|
||||
|
||||
if self._target_param_name is not None:
|
||||
data[self._target_param_name] = kwargs.get(ATTR_TARGET)
|
||||
if self._target_param_name is not None and ATTR_TARGET in kwargs:
|
||||
# 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':
|
||||
response = requests.post(self._resource, data=data, timeout=10)
|
||||
|
@ -11,9 +11,9 @@ notify:
|
||||
example: 'Your Garage Door Friend'
|
||||
|
||||
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
|
||||
|
||||
data:
|
||||
description: Extended information for notification. Optional depending on the platform
|
||||
description: Extended information for notification. Optional depending on the platform.
|
||||
example: platform specific
|
||||
|
@ -9,7 +9,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
PLATFORM_SCHEMA, BaseNotificationService)
|
||||
ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService)
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -68,16 +68,19 @@ class SlackNotificationService(BaseNotificationService):
|
||||
"""Send a message to a user."""
|
||||
import slacker
|
||||
|
||||
channel = kwargs.get('target') or self._default_channel
|
||||
targets = kwargs.get(ATTR_TARGET, [self._default_channel])
|
||||
|
||||
data = kwargs.get('data')
|
||||
attachments = data.get('attachments') if data else None
|
||||
|
||||
for target in targets:
|
||||
try:
|
||||
self.slack.chat.post_message(channel, message,
|
||||
self.slack.chat.post_message(target, message,
|
||||
as_user=self._as_user,
|
||||
username=self._username,
|
||||
icon_emoji=self._icon,
|
||||
attachments=attachments,
|
||||
link_names=True)
|
||||
except slacker.Error as err:
|
||||
_LOGGER.error("Could not send slack notification. Error: %s", err)
|
||||
_LOGGER.error("Could not send slack notification. Error: %s",
|
||||
err)
|
||||
|
@ -57,9 +57,6 @@ class TwilioSMSNotificationService(BaseNotificationService):
|
||||
_LOGGER.info("At least 1 target is required")
|
||||
return
|
||||
|
||||
if not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
for target in targets:
|
||||
self.client.messages.create(to=target, body=message,
|
||||
from_=self.from_number)
|
||||
|
@ -27,7 +27,7 @@ DEPENDENCIES = ['ffmpeg']
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/pvizeli/cloudapi/releases/download/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__)
|
||||
|
||||
@ -68,7 +68,7 @@ DEFAULT_NAME = 'OpenAlpr'
|
||||
DEFAULT_ENGINE = ENGINE_LOCAL
|
||||
DEFAULT_RENDER = RENDER_FFMPEG
|
||||
DEFAULT_BINARY = 'alpr'
|
||||
DEFAULT_INTERVAL = 2
|
||||
DEFAULT_INTERVAL = 10
|
||||
DEFAULT_CONFIDENCE = 80.0
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
|
@ -12,9 +12,6 @@ from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.rollershutter import RollershutterDevice
|
||||
|
||||
COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38
|
||||
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
|
||||
|
||||
SOMFY = 0x47
|
||||
SOMFY_ZRTSI = 0x5a52
|
||||
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:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_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
|
||||
if value.index != 0:
|
||||
return
|
||||
@ -82,9 +79,10 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
# Position value
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Level':
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and \
|
||||
value.label == 'Level':
|
||||
self._current_position = value.data
|
||||
|
||||
@property
|
||||
@ -101,23 +99,26 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
||||
|
||||
def move_up(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Open' or \
|
||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Down':
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL)
|
||||
.values()):
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Open' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Down':
|
||||
self._lozwmgr.pressButton(value.value_id)
|
||||
break
|
||||
|
||||
def move_down(self, **kwargs):
|
||||
"""Move the roller shutter down."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Up' or \
|
||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Close':
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Up' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Close':
|
||||
self._lozwmgr.pressButton(value.value_id)
|
||||
break
|
||||
|
||||
@ -128,10 +129,11 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
||||
def stop(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Open' or \
|
||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Down':
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Open' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Down':
|
||||
self._lozwmgr.releaseButton(value.value_id)
|
||||
break
|
||||
|
@ -12,7 +12,7 @@ from homeassistant.helpers.entity import generate_entity_id
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/sander76/powerviewApi/'
|
||||
'archive/master.zip#powerviewApi==0.2']
|
||||
'archive/cc6f75dd39160d4aaf46cb2ed9220136b924bcb4.zip#powerviewApi==0.2']
|
||||
|
||||
HUB_ADDRESS = 'address'
|
||||
|
||||
|
@ -23,6 +23,7 @@ from homeassistant.helpers.script import Script
|
||||
|
||||
DOMAIN = "script"
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
GROUP_NAME_ALL_SCRIPTS = 'all scripts'
|
||||
DEPENDENCIES = ["group"]
|
||||
|
||||
CONF_SEQUENCE = "sequence"
|
||||
@ -73,7 +74,8 @@ def toggle(hass, entity_id):
|
||||
|
||||
def setup(hass, config):
|
||||
"""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):
|
||||
"""Execute a service call to script.<script name>."""
|
||||
@ -124,7 +126,7 @@ class ScriptEntity(ToggleEntity):
|
||||
def __init__(self, hass, object_id, name, sequence):
|
||||
"""Initialize the script."""
|
||||
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
|
||||
def should_poll(self):
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/sensor.arest/
|
||||
@ -8,30 +8,48 @@ import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, DEVICE_DEFAULT_NAME,
|
||||
CONF_RESOURCE, CONF_MONITORED_VARIABLES)
|
||||
CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_RESOURCE,
|
||||
CONF_MONITORED_VARIABLES, CONF_NAME, STATE_UNKNOWN)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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):
|
||||
"""Setup the aREST sensor."""
|
||||
resource = config.get(CONF_RESOURCE)
|
||||
var_conf = config.get(CONF_MONITORED_VARIABLES)
|
||||
pins = config.get('pins', None)
|
||||
|
||||
if resource is None:
|
||||
_LOGGER.error('Not all required config keys present: %s',
|
||||
CONF_RESOURCE)
|
||||
return False
|
||||
pins = config.get(CONF_PINS)
|
||||
|
||||
try:
|
||||
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:
|
||||
return lambda value: value
|
||||
|
||||
value_template = template.Template(value_template, hass)
|
||||
value_template.hass = hass
|
||||
|
||||
def _render(value):
|
||||
try:
|
||||
return value_template.render({'value': value})
|
||||
return value_template.async_render({'value': value})
|
||||
except TemplateError:
|
||||
_LOGGER.exception('Error parsing value')
|
||||
return value
|
||||
@ -66,33 +84,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
dev = []
|
||||
|
||||
if var_conf is not None:
|
||||
for variable in var_conf:
|
||||
if variable['name'] not in response['variables']:
|
||||
_LOGGER.error('Variable: "%s" does not exist',
|
||||
variable['name'])
|
||||
for variable, var_data in var_conf.items():
|
||||
if variable not in response['variables']:
|
||||
_LOGGER.error("Variable: '%s' does not exist", variable)
|
||||
continue
|
||||
|
||||
renderer = make_renderer(variable.get(CONF_VALUE_TEMPLATE))
|
||||
dev.append(ArestSensor(arest,
|
||||
resource,
|
||||
config.get('name', response['name']),
|
||||
variable['name'],
|
||||
variable=variable['name'],
|
||||
unit_of_measurement=variable.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT),
|
||||
renderer = make_renderer(var_data.get(CONF_VALUE_TEMPLATE))
|
||||
dev.append(ArestSensor(
|
||||
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
|
||||
variable, variable=variable,
|
||||
unit_of_measurement=var_data.get(CONF_UNIT_OF_MEASUREMENT),
|
||||
renderer=renderer))
|
||||
|
||||
if pins is not None:
|
||||
for pinnum, pin in pins.items():
|
||||
renderer = make_renderer(pin.get(CONF_VALUE_TEMPLATE))
|
||||
dev.append(ArestSensor(ArestData(resource, pinnum),
|
||||
resource,
|
||||
config.get('name', response['name']),
|
||||
pin.get('name'),
|
||||
pin=pinnum,
|
||||
unit_of_measurement=pin.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT),
|
||||
renderer=renderer))
|
||||
dev.append(ArestSensor(
|
||||
ArestData(resource, pinnum), resource,
|
||||
config.get(CONF_NAME, response[CONF_NAME]), pin.get(CONF_NAME),
|
||||
pin=pinnum, unit_of_measurement=pin.get(
|
||||
CONF_UNIT_OF_MEASUREMENT), renderer=renderer))
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
@ -106,18 +117,17 @@ class ArestSensor(Entity):
|
||||
"""Initialize the sensor."""
|
||||
self.arest = arest
|
||||
self._resource = resource
|
||||
self._name = '{} {}'.format(location.title(), name.title()) \
|
||||
or DEVICE_DEFAULT_NAME
|
||||
self._name = '{} {}'.format(location.title(), name.title())
|
||||
self._variable = variable
|
||||
self._pin = pin
|
||||
self._state = 'n/a'
|
||||
self._state = STATE_UNKNOWN
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._renderer = renderer
|
||||
self.update()
|
||||
|
||||
if self._pin is not None:
|
||||
request = requests.get('{}/mode/{}/i'.format
|
||||
(self._resource, self._pin), timeout=10)
|
||||
request = requests.get(
|
||||
'{}/mode/{}/i'.format(self._resource, self._pin), timeout=10)
|
||||
if request.status_code is not 200:
|
||||
_LOGGER.error("Can't set mode. Is device offline?")
|
||||
|
||||
@ -139,15 +149,19 @@ class ArestSensor(Entity):
|
||||
if 'error' in values:
|
||||
return values['error']
|
||||
|
||||
value = self._renderer(values.get('value',
|
||||
values.get(self._variable,
|
||||
'N/A')))
|
||||
value = self._renderer(
|
||||
values.get('value', values.get(self._variable, STATE_UNKNOWN)))
|
||||
return value
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from aREST API."""
|
||||
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
|
||||
class ArestData(object):
|
||||
@ -158,6 +172,7 @@ class ArestData(object):
|
||||
self._resource = resource
|
||||
self._pin = pin
|
||||
self.data = {}
|
||||
self.available = True
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
@ -179,7 +194,8 @@ class ArestData(object):
|
||||
response = requests.get('{}/digital/{}'.format(
|
||||
self._resource, self._pin), timeout=10)
|
||||
self.data = {'value': response.json()['return_value']}
|
||||
self.available = True
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to device %s. Is device offline?",
|
||||
self._resource)
|
||||
self.data = {'error': 'error fetching'}
|
||||
self.available = False
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/sensor.forecast/
|
||||
https://home-assistant.io/components/sensor.darksky/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
@ -18,15 +18,14 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-forecastio==1.3.4']
|
||||
REQUIREMENTS = ['python-forecastio==1.3.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_UNITS = 'units'
|
||||
CONF_UPDATE_INTERVAL = 'update_interval'
|
||||
|
||||
DEFAULT_NAME = 'Forecast.io'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
||||
DEFAULT_NAME = 'Dark Sky'
|
||||
|
||||
# Sensor types are defined like so:
|
||||
# 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.Required(CONF_API_KEY): 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):
|
||||
"""Setup the Forecast.io sensor."""
|
||||
"""Setup the Dark Sky sensor."""
|
||||
# Validate the configuration
|
||||
if None in (hass.config.latitude, hass.config.longitude):
|
||||
_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
|
||||
# the first call to init the data and confirm we can connect.
|
||||
try:
|
||||
forecast_data = ForeCastData(
|
||||
config.get(CONF_API_KEY, None), hass.config.latitude,
|
||||
hass.config.longitude, units)
|
||||
forecast_data = DarkSkyData(
|
||||
api_key=config.get(CONF_API_KEY, None),
|
||||
latitude=hass.config.latitude,
|
||||
longitude=hass.config.longitude,
|
||||
units=units,
|
||||
interval=config.get(CONF_UPDATE_INTERVAL))
|
||||
forecast_data.update_currently()
|
||||
except ValueError as error:
|
||||
_LOGGER.error(error)
|
||||
@ -117,14 +122,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
sensors = []
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]:
|
||||
sensors.append(ForeCastSensor(forecast_data, variable, name))
|
||||
sensors.append(DarkSkySensor(forecast_data, variable, name))
|
||||
|
||||
add_devices(sensors)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class ForeCastSensor(Entity):
|
||||
"""Implementation of a Forecast.io sensor."""
|
||||
class DarkSkySensor(Entity):
|
||||
"""Implementation of a Dark Sky sensor."""
|
||||
|
||||
def __init__(self, forecast_data, sensor_type, name):
|
||||
"""Initialize the sensor."""
|
||||
@ -175,10 +180,10 @@ class ForeCastSensor(Entity):
|
||||
|
||||
# pylint: disable=too-many-branches,too-many-statements
|
||||
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
|
||||
# same exact call, but thats fine. We cache results for a short period
|
||||
# of time to prevent hitting API limits. Note that forecast.io will
|
||||
# same exact call, but that's fine. We cache results for a short period
|
||||
# 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.
|
||||
self.forecast_data.update()
|
||||
self.update_unit_of_measurement()
|
||||
@ -192,7 +197,8 @@ class ForeCastSensor(Entity):
|
||||
hourly = self.forecast_data.data_hourly
|
||||
self._state = getattr(hourly, 'summary', '')
|
||||
elif self.type in ['daily_summary',
|
||||
'temperature_min', 'temperature_max',
|
||||
'temperature_min',
|
||||
'temperature_max',
|
||||
'apparent_temperature_min',
|
||||
'apparent_temperature_max',
|
||||
'precip_intensity_max']:
|
||||
@ -242,12 +248,11 @@ def convert_to_camel(data):
|
||||
return components[0] + "".join(x.title() for x in components[1:])
|
||||
|
||||
|
||||
class ForeCastData(object):
|
||||
"""Gets the latest data from Forecast.io."""
|
||||
class DarkSkyData(object):
|
||||
"""Get the latest data from Darksky."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def __init__(self, api_key, latitude, longitude, units):
|
||||
def __init__(self, api_key, latitude, longitude, units, interval):
|
||||
"""Initialize the data object."""
|
||||
self._api_key = api_key
|
||||
self.latitude = latitude
|
||||
@ -261,36 +266,38 @@ class ForeCastData(object):
|
||||
self.data_hourly = 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()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from Forecast.io."""
|
||||
def _update(self):
|
||||
"""Get the latest data from Dark Sky."""
|
||||
import forecastio
|
||||
|
||||
try:
|
||||
self.data = forecastio.load_forecast(
|
||||
self._api_key, self.latitude, self.longitude, units=self.units)
|
||||
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']
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update_currently(self):
|
||||
def _update_currently(self):
|
||||
"""Update currently data."""
|
||||
self.data_currently = self.data.currently()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update_minutely(self):
|
||||
def _update_minutely(self):
|
||||
"""Update minutely data."""
|
||||
self.data_minutely = self.data.minutely()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update_hourly(self):
|
||||
def _update_hourly(self):
|
||||
"""Update hourly data."""
|
||||
self.data_hourly = self.data.hourly()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update_daily(self):
|
||||
def _update_daily(self):
|
||||
"""Update daily data."""
|
||||
self.data_daily = self.data.daily()
|
@ -202,8 +202,7 @@ class EmonCmsData(object):
|
||||
"""Get the latest data."""
|
||||
try:
|
||||
req = requests.get(self._url, params={"apikey": self._apikey},
|
||||
verify=False, allow_redirects=True,
|
||||
timeout=5)
|
||||
allow_redirects=True, timeout=5)
|
||||
except requests.exceptions.RequestException as exception:
|
||||
_LOGGER.error(exception)
|
||||
return
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/sensor.glances/
|
||||
@ -24,6 +24,8 @@ DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'Glances'
|
||||
DEFAULT_PORT = '61208'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'disk_use_percent': ['Disk Use', '%'],
|
||||
'disk_use': ['Disk Use', 'GiB'],
|
||||
@ -34,11 +36,11 @@ SENSOR_TYPES = {
|
||||
'swap_use_percent': ['Swap Use', '%'],
|
||||
'swap_use': ['Swap Use', 'GiB'],
|
||||
'swap_free': ['Swap Free', 'GiB'],
|
||||
'processor_load': ['CPU Load', None],
|
||||
'process_running': ['Running', None],
|
||||
'process_total': ['Total', None],
|
||||
'process_thread': ['Thread', None],
|
||||
'process_sleeping': ['Sleeping', None]
|
||||
'processor_load': ['CPU Load', '15 min'],
|
||||
'process_running': ['Running', 'Count'],
|
||||
'process_total': ['Total', 'Count'],
|
||||
'process_thread': ['Thread', 'Count'],
|
||||
'process_sleeping': ['Sleeping', 'Count']
|
||||
}
|
||||
|
||||
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
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Glances sensor."""
|
||||
@ -66,7 +64,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
if not response.ok:
|
||||
_LOGGER.error('Response status is "%s"', response.status_code)
|
||||
_LOGGER.error("Response status is '%s'", response.status_code)
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to resource/endpoint: %s", url)
|
||||
|
@ -36,14 +36,14 @@ TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ORIGIN): 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,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
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]
|
||||
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):
|
||||
"""Get the GTFS sensor."""
|
||||
"""Set up the GTFS sensor."""
|
||||
gtfs_dir = hass.config.path(DEFAULT_PATH)
|
||||
data = config.get(CONF_DATA)
|
||||
origin = config.get(CONF_ORIGIN)
|
||||
|
@ -18,7 +18,8 @@ DEPENDENCIES = ['homematic']
|
||||
|
||||
HM_STATE_HA_CAST = {
|
||||
"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 = {
|
||||
@ -38,6 +39,7 @@ HM_UNIT_HA_CAST = {
|
||||
"WIND_DIRECTION_RANGE": "°",
|
||||
"SUNSHINEDURATION": "#",
|
||||
"AIR_PRESSURE": "hPa",
|
||||
"FREQUENCY": "Hz",
|
||||
}
|
||||
|
||||
|
||||
|
@ -104,7 +104,7 @@ class EmailReader:
|
||||
self.connection.select()
|
||||
|
||||
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:
|
||||
search = "UID {}:*".format(self._last_id)
|
||||
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pyloopenergy==0.0.14']
|
||||
REQUIREMENTS = ['pyloopenergy==0.0.15']
|
||||
|
||||
CONF_ELEC = 'electricity'
|
||||
CONF_GAS = 'gas'
|
||||
|
@ -11,7 +11,6 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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 = {}
|
||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||
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):
|
||||
"""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
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
@ -104,4 +124,11 @@ class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity):
|
||||
return self._values[
|
||||
set_req.V_UNIT_PREFIX]
|
||||
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)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user