Merge pull request #6509 from home-assistant/release-0-40

0.40
This commit is contained in:
Paulus Schoutsen 2017-03-11 11:08:40 -08:00 committed by GitHub
commit a8b32edc8e
432 changed files with 10211 additions and 4303 deletions

View File

@ -14,9 +14,15 @@ omit =
homeassistant/components/arduino.py homeassistant/components/arduino.py
homeassistant/components/*/arduino.py homeassistant/components/*/arduino.py
homeassistant/components/android_ip_webcam.py
homeassistant/components/*/android_ip_webcam.py
homeassistant/components/bbb_gpio.py homeassistant/components/bbb_gpio.py
homeassistant/components/*/bbb_gpio.py homeassistant/components/*/bbb_gpio.py
homeassistant/components/blink.py
homeassistant/components/*/blink.py
homeassistant/components/bloomsky.py homeassistant/components/bloomsky.py
homeassistant/components/*/bloomsky.py homeassistant/components/*/bloomsky.py
@ -85,6 +91,10 @@ omit =
homeassistant/components/*/thinkingcleaner.py homeassistant/components/*/thinkingcleaner.py
homeassistant/components/twilio.py
homeassistant/components/notify/twilio_sms.py
homeassistant/components/notify/twilio_call.py
homeassistant/components/vera.py homeassistant/components/vera.py
homeassistant/components/*/vera.py homeassistant/components/*/vera.py
@ -132,6 +142,9 @@ omit =
homeassistant/components/zabbix.py homeassistant/components/zabbix.py
homeassistant/components/*/zabbix.py homeassistant/components/*/zabbix.py
homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py
homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/nx584.py
@ -231,6 +244,7 @@ omit =
homeassistant/components/media_player/dunehd.py homeassistant/components/media_player/dunehd.py
homeassistant/components/media_player/emby.py homeassistant/components/media_player/emby.py
homeassistant/components/media_player/firetv.py homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/frontier_silicon.py
homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/gstreamer.py homeassistant/components/media_player/gstreamer.py
homeassistant/components/media_player/hdmi_cec.py homeassistant/components/media_player/hdmi_cec.py
@ -259,6 +273,7 @@ omit =
homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_lambda.py
homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sns.py
homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/ciscospark.py
homeassistant/components/notify/discord.py homeassistant/components/notify/discord.py
homeassistant/components/notify/facebook.py homeassistant/components/notify/facebook.py
homeassistant/components/notify/free_mobile.py homeassistant/components/notify/free_mobile.py
@ -286,8 +301,6 @@ omit =
homeassistant/components/notify/syslog.py homeassistant/components/notify/syslog.py
homeassistant/components/notify/telegram.py homeassistant/components/notify/telegram.py
homeassistant/components/notify/telstra.py homeassistant/components/notify/telstra.py
homeassistant/components/notify/twilio_sms.py
homeassistant/components/notify/twilio_call.py
homeassistant/components/notify/twitter.py homeassistant/components/notify/twitter.py
homeassistant/components/notify/xmpp.py homeassistant/components/notify/xmpp.py
homeassistant/components/nuimo_controller.py homeassistant/components/nuimo_controller.py
@ -303,12 +316,14 @@ omit =
homeassistant/components/sensor/broadlink.py homeassistant/components/sensor/broadlink.py
homeassistant/components/sensor/dublin_bus_transport.py homeassistant/components/sensor/dublin_bus_transport.py
homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/coinmarketcap.py
homeassistant/components/sensor/comed_hourly_pricing.py
homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/cpuspeed.py
homeassistant/components/sensor/cups.py homeassistant/components/sensor/cups.py
homeassistant/components/sensor/currencylayer.py homeassistant/components/sensor/currencylayer.py
homeassistant/components/sensor/darksky.py homeassistant/components/sensor/darksky.py
homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/deutsche_bahn.py
homeassistant/components/sensor/dht.py homeassistant/components/sensor/dht.py
homeassistant/components/sensor/dnsip.py
homeassistant/components/sensor/dovado.py homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/ebox.py homeassistant/components/sensor/ebox.py
@ -333,11 +348,12 @@ omit =
homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/influxdb.py
homeassistant/components/sensor/kwb.py
homeassistant/components/sensor/lastfm.py homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/linux_battery.py
homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/mhz19.py
homeassistant/components/sensor/miflora.py homeassistant/components/sensor/miflora.py
homeassistant/components/sensor/modem_callerid.py
homeassistant/components/sensor/mqtt_room.py homeassistant/components/sensor/mqtt_room.py
homeassistant/components/sensor/netdata.py homeassistant/components/sensor/netdata.py
homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/neurio_energy.py
@ -411,6 +427,7 @@ omit =
homeassistant/components/upnp.py homeassistant/components/upnp.py
homeassistant/components/weather/bom.py homeassistant/components/weather/bom.py
homeassistant/components/weather/openweathermap.py homeassistant/components/weather/openweathermap.py
homeassistant/components/weather/zamg.py
homeassistant/components/zeroconf.py homeassistant/components/zeroconf.py

View File

@ -1,16 +1,16 @@
**Description:** ## Description:
**Related issue (if applicable):** fixes #<home-assistant issue number goes here> **Related issue (if applicable):** fixes #<home-assistant issue number goes here>
**Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here> **Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>
**Example entry for `configuration.yaml` (if applicable):** ## Example entry for `configuration.yaml` (if applicable):
```yaml ```yaml
``` ```
**Checklist:** ## Checklist:
If user exposed functionality or configuration variables are added/changed: If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) - [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)

View File

@ -14,6 +14,8 @@ matrix:
env: TOXENV=py35 env: TOXENV=py35
- python: "3.6" - python: "3.6"
env: TOXENV=py36 env: TOXENV=py36
- python: "3.6-dev"
env: TOXENV=py36
# allow_failures: # allow_failures:
# - python: "3.5" # - python: "3.5"
# env: TOXENV=typing # env: TOXENV=typing

View File

@ -4,320 +4,31 @@ import logging
import logging.handlers import logging.handlers
import os import os
import sys import sys
from time import time
from collections import OrderedDict from collections import OrderedDict
from types import ModuleType
from typing import Any, Optional, Dict from typing import Any, Optional, Dict
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error
import homeassistant.components as core_components import homeassistant.components as core_components
from homeassistant.components import persistent_notification from homeassistant.components import persistent_notification
import homeassistant.config as conf_util import homeassistant.config as conf_util
import homeassistant.core as core import homeassistant.core as core
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
from homeassistant.setup import async_setup_component
import homeassistant.loader as loader import homeassistant.loader as loader
import homeassistant.util.package as pkg_util
from homeassistant.util.async import (
run_coroutine_threadsafe, run_callback_threadsafe)
from homeassistant.util.logging import AsyncHandler from homeassistant.util.logging import AsyncHandler
from homeassistant.util.yaml import clear_secret_cache from homeassistant.util.yaml import clear_secret_cache
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import ( from homeassistant.helpers import event_decorators, service
event_decorators, service, config_per_platform, extract_domain_configs)
from homeassistant.helpers.signal import async_register_signal_handling from homeassistant.helpers.signal import async_register_signal_handling
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_COMPONENT = 'component'
ERROR_LOG_FILENAME = 'home-assistant.log' ERROR_LOG_FILENAME = 'home-assistant.log'
_PERSISTENT_ERRORS = {} FIRST_INIT_COMPONENT = set((
HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)' 'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction'))
def setup_component(hass: core.HomeAssistant, domain: str,
config: Optional[Dict]=None) -> bool:
"""Setup a component and all its dependencies."""
return run_coroutine_threadsafe(
async_setup_component(hass, domain, config), loop=hass.loop).result()
@asyncio.coroutine
def async_setup_component(hass: core.HomeAssistant, domain: str,
config: Optional[Dict]=None) -> bool:
"""Setup a component and all its dependencies.
This method is a coroutine.
"""
if domain in hass.config.components:
_LOGGER.debug('Component %s already set up.', domain)
return True
if not loader.PREPARED:
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
if config is None:
config = {}
components = loader.load_order_component(domain)
# OrderedSet is empty if component or dependencies could not be resolved
if not components:
_async_persistent_notification(hass, domain, True)
return False
for component in components:
res = yield from _async_setup_component(hass, component, config)
if not res:
_LOGGER.error('Component %s failed to setup', component)
_async_persistent_notification(hass, component, True)
return False
return True
def _handle_requirements(hass: core.HomeAssistant, component,
name: str) -> bool:
"""Install the requirements for a component.
This method needs to run in an executor.
"""
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
return True
for req in component.REQUIREMENTS:
if not pkg_util.install_package(req, target=hass.config.path('deps')):
_LOGGER.error('Not initializing %s because could not install '
'dependency %s', name, req)
_async_persistent_notification(hass, name)
return False
return True
@asyncio.coroutine
def _async_setup_component(hass: core.HomeAssistant,
domain: str, config) -> bool:
"""Setup a component for Home Assistant.
This method is a coroutine.
"""
# pylint: disable=too-many-return-statements
if domain in hass.config.components:
return True
setup_lock = hass.data.get('setup_lock')
if setup_lock is None:
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
setup_progress = hass.data.get('setup_progress')
if setup_progress is None:
setup_progress = hass.data['setup_progress'] = []
if domain in setup_progress:
_LOGGER.error('Attempt made to setup %s during setup of %s',
domain, domain)
_async_persistent_notification(hass, domain, True)
return False
try:
# Used to indicate to discovery that a setup is ongoing and allow it
# to wait till it is done.
did_lock = False
if not setup_lock.locked():
yield from setup_lock.acquire()
did_lock = True
setup_progress.append(domain)
config = yield from async_prepare_setup_component(hass, config, domain)
if config is None:
return False
component = loader.get_component(domain)
if component is None:
_async_persistent_notification(hass, domain)
return False
async_comp = hasattr(component, 'async_setup')
try:
_LOGGER.info("Setting up %s", domain)
if async_comp:
result = yield from component.async_setup(hass, config)
else:
result = yield from hass.loop.run_in_executor(
None, component.setup, hass, config)
except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error during setup of component %s', domain)
_async_persistent_notification(hass, domain, True)
return False
if result is False:
_LOGGER.error('component %s failed to initialize', domain)
_async_persistent_notification(hass, domain, True)
return False
elif result is not True:
_LOGGER.error('component %s did not return boolean if setup '
'was successful. Disabling component.', domain)
_async_persistent_notification(hass, domain, True)
loader.set_component(domain, None)
return False
hass.config.components.add(component.DOMAIN)
hass.bus.async_fire(
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
)
return True
finally:
setup_progress.remove(domain)
if did_lock:
setup_lock.release()
def prepare_setup_component(hass: core.HomeAssistant, config: dict,
domain: str):
"""Prepare setup of a component and return processed config."""
return run_coroutine_threadsafe(
async_prepare_setup_component(hass, config, domain), loop=hass.loop
).result()
@asyncio.coroutine
def async_prepare_setup_component(hass: core.HomeAssistant, config: dict,
domain: str):
"""Prepare setup of a component and return processed config.
This method is a coroutine.
"""
# pylint: disable=too-many-return-statements
component = loader.get_component(domain)
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
if dep not in hass.config.components]
if missing_deps:
_LOGGER.error(
'Not initializing %s because not all dependencies loaded: %s',
domain, ", ".join(missing_deps))
return None
if hasattr(component, 'CONFIG_SCHEMA'):
try:
config = component.CONFIG_SCHEMA(config)
except vol.Invalid as ex:
async_log_exception(ex, domain, config, hass)
return None
elif hasattr(component, 'PLATFORM_SCHEMA'):
platforms = []
for p_name, p_config in config_per_platform(config, domain):
# Validate component specific platform schema
try:
p_validated = component.PLATFORM_SCHEMA(p_config)
except vol.Invalid as ex:
async_log_exception(ex, domain, config, hass)
continue
# Not all platform components follow same pattern for platforms
# So if p_name is None we are not going to validate platform
# (the automation component is one of them)
if p_name is None:
platforms.append(p_validated)
continue
platform = yield from async_prepare_setup_platform(
hass, config, domain, p_name)
if platform is None:
continue
# Validate platform specific schema
if hasattr(platform, 'PLATFORM_SCHEMA'):
try:
# pylint: disable=no-member
p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.Invalid as ex:
async_log_exception(ex, '{}.{}'.format(domain, p_name),
p_validated, hass)
continue
platforms.append(p_validated)
# Create a copy of the configuration with all config for current
# component removed and add validated config back in.
filter_keys = extract_domain_configs(config, domain)
config = {key: value for key, value in config.items()
if key not in filter_keys}
config[domain] = platforms
res = yield from hass.loop.run_in_executor(
None, _handle_requirements, hass, component, domain)
if not res:
return None
return config
def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
platform_name: str) -> Optional[ModuleType]:
"""Load a platform and makes sure dependencies are setup."""
return run_coroutine_threadsafe(
async_prepare_setup_platform(hass, config, domain, platform_name),
loop=hass.loop
).result()
@asyncio.coroutine
def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
platform_name: str) \
-> Optional[ModuleType]:
"""Load a platform and makes sure dependencies are setup.
This method is a coroutine.
"""
if not loader.PREPARED:
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
platform = loader.get_platform(domain, platform_name)
# Not found
if platform is None:
_LOGGER.error('Unable to find platform %s', platform_path)
_async_persistent_notification(hass, platform_path)
return None
# Already loaded
elif platform_path in hass.config.components:
return platform
# Load dependencies
for component in getattr(platform, 'DEPENDENCIES', []):
if component in loader.DEPENDENCY_BLACKLIST:
raise HomeAssistantError(
'{} is not allowed to be a dependency.'.format(component))
res = yield from async_setup_component(hass, component, config)
if not res:
_LOGGER.error(
'Unable to prepare setup for platform %s because '
'dependency %s could not be initialized', platform_path,
component)
_async_persistent_notification(hass, platform_path, True)
return None
res = yield from hass.loop.run_in_executor(
None, _handle_requirements, hass, platform, platform_path)
if not res:
return None
return platform
def from_config_dict(config: Dict[str, Any], def from_config_dict(config: Dict[str, Any],
@ -339,23 +50,14 @@ def from_config_dict(config: Dict[str, Any],
hass.config.config_dir = config_dir hass.config.config_dir = config_dir
mount_local_lib_path(config_dir) mount_local_lib_path(config_dir)
@asyncio.coroutine
def _async_init_from_config_dict(future):
try:
re_hass = yield from async_from_config_dict(
config, hass, config_dir, enable_log, verbose, skip_pip,
log_rotate_days)
future.set_result(re_hass)
# pylint: disable=broad-except
except Exception as exc:
future.set_exception(exc)
# run task # run task
future = asyncio.Future(loop=hass.loop) hass = hass.loop.run_until_complete(
hass.async_add_job(_async_init_from_config_dict(future)) async_from_config_dict(
hass.loop.run_until_complete(future) config, hass, config_dir, enable_log, verbose, skip_pip,
log_rotate_days)
)
return future.result() return hass
@asyncio.coroutine @asyncio.coroutine
@ -372,19 +74,15 @@ def async_from_config_dict(config: Dict[str, Any],
Dynamically loads required components and its dependencies. Dynamically loads required components and its dependencies.
This method is a coroutine. This method is a coroutine.
""" """
start = time()
hass.async_track_tasks() hass.async_track_tasks()
setup_lock = hass.data.get('setup_lock')
if setup_lock is None:
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
yield from setup_lock.acquire()
core_config = config.get(core.DOMAIN, {}) core_config = config.get(core.DOMAIN, {})
try: try:
yield from conf_util.async_process_ha_core_config(hass, core_config) yield from conf_util.async_process_ha_core_config(hass, core_config)
except vol.Invalid as ex: except vol.Invalid as ex:
async_log_exception(ex, 'homeassistant', core_config, hass) conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
return None return None
yield from hass.loop.run_in_executor( yield from hass.loop.run_in_executor(
@ -433,20 +131,25 @@ def async_from_config_dict(config: Dict[str, Any],
event_decorators.HASS = hass event_decorators.HASS = hass
service.HASS = hass service.HASS = hass
# Setup the components # stage 1
dependency_blacklist = loader.DEPENDENCY_BLACKLIST - set(components) for component in components:
if component not in FIRST_INIT_COMPONENT:
continue
hass.async_add_job(async_setup_component(hass, component, config))
for domain in loader.load_order_components(components): yield from hass.async_block_till_done()
if domain in dependency_blacklist:
raise HomeAssistantError(
'{} is not allowed to be a dependency'.format(domain))
yield from _async_setup_component(hass, domain, config) # stage 2
for component in components:
setup_lock.release() if component in FIRST_INIT_COMPONENT:
continue
hass.async_add_job(async_setup_component(hass, component, config))
yield from hass.async_stop_track_tasks() yield from hass.async_stop_track_tasks()
stop = time()
_LOGGER.info('Home Assistant initialized in %ss', round(stop-start, 2))
async_register_signal_handling(hass) async_register_signal_handling(hass)
return hass return hass
@ -464,22 +167,13 @@ def from_config_file(config_path: str,
if hass is None: if hass is None:
hass = core.HomeAssistant() hass = core.HomeAssistant()
@asyncio.coroutine
def _async_init_from_config_file(future):
try:
re_hass = yield from async_from_config_file(
config_path, hass, verbose, skip_pip, log_rotate_days)
future.set_result(re_hass)
# pylint: disable=broad-except
except Exception as exc:
future.set_exception(exc)
# run task # run task
future = asyncio.Future(loop=hass.loop) hass = hass.loop.run_until_complete(
hass.loop.create_task(_async_init_from_config_file(future)) async_from_config_file(
hass.loop.run_until_complete(future) config_path, hass, verbose, skip_pip, log_rotate_days)
)
return future.result() return hass
@asyncio.coroutine @asyncio.coroutine
@ -504,7 +198,8 @@ def async_from_config_file(config_path: str,
try: try:
config_dict = yield from hass.loop.run_in_executor( config_dict = yield from hass.loop.run_in_executor(
None, conf_util.load_yaml_config_file, config_path) None, conf_util.load_yaml_config_file, config_path)
except HomeAssistantError: except HomeAssistantError as err:
_LOGGER.error('Error loading %s: %s', config_path, err)
return None return None
finally: finally:
clear_secret_cache() clear_secret_cache()
@ -588,57 +283,6 @@ def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
'Unable to setup error log %s (access denied)', err_log_path) 'Unable to setup error log %s (access denied)', err_log_path)
def log_exception(ex, domain, config, hass):
"""Generate log exception for config validation."""
run_callback_threadsafe(
hass.loop, async_log_exception, ex, domain, config, hass).result()
@core.callback
def _async_persistent_notification(hass: core.HomeAssistant, component: str,
link: Optional[bool]=False):
"""Print a persistent notification.
This method must be run in the event loop.
"""
_PERSISTENT_ERRORS[component] = _PERSISTENT_ERRORS.get(component) or link
_lst = [HA_COMPONENT_URL.format(name.replace('_', '-'), name)
if link else name for name, link in _PERSISTENT_ERRORS.items()]
message = ('The following components and platforms could not be set up:\n'
'* ' + '\n* '.join(list(_lst)) + '\nPlease check your config')
persistent_notification.async_create(
hass, message, 'Invalid config', 'invalid_config')
@core.callback
def async_log_exception(ex, domain, config, hass):
"""Generate log exception for config validation.
This method must be run in the event loop.
"""
message = 'Invalid config for [{}]: '.format(domain)
if hass is not None:
_async_persistent_notification(hass, domain, True)
if 'extra keys not allowed' in ex.error_message:
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
.format(ex.path[-1], domain, domain,
'->'.join(str(m) for m in ex.path))
else:
message += '{}.'.format(humanize_error(config, ex))
domain_config = config.get(domain, config)
message += " (See {}, line {}). ".format(
getattr(domain_config, '__config_file__', '?'),
getattr(domain_config, '__line__', '?'))
if domain != 'homeassistant':
message += ('Please check the docs at '
'https://home-assistant.io/components/{}/'.format(domain))
_LOGGER.error(message)
def mount_local_lib_path(config_dir: str) -> str: def mount_local_lib_path(config_dir: str) -> str:
"""Add local library to Python Path. """Add local library to Python Path.

View File

@ -55,7 +55,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
) )
devices.append(device) devices.append(device)
yield from async_add_devices(devices) async_add_devices(devices)
@callback @callback
def alarm_keypress_handler(service): def alarm_keypress_handler(service):
@ -94,10 +94,13 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
_LOGGER.debug("Setting up alarm: %s", alarm_name) _LOGGER.debug("Setting up alarm: %s", alarm_name)
super().__init__(alarm_name, info, controller) super().__init__(alarm_name, info, controller)
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect( async_dispatcher_connect(
hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
async_dispatcher_connect( async_dispatcher_connect(
hass, SIGNAL_PARTITION_UPDATE, self._update_callback) self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback)
@callback @callback
def _update_callback(self, partition): def _update_callback(self, partition):

View File

@ -46,7 +46,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the MQTT platform.""" """Setup the MQTT platform."""
yield from async_add_devices([MqttAlarm( async_add_devices([MqttAlarm(
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC), config.get(CONF_STATE_TOPIC),
config.get(CONF_COMMAND_TOPIC), config.get(CONF_COMMAND_TOPIC),

View File

@ -18,7 +18,6 @@ from homeassistant.const import (
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers import service, event from homeassistant.helpers import service, event
from homeassistant.util.async import run_callback_threadsafe
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -62,8 +61,7 @@ def is_on(hass, entity_id):
def turn_on(hass, entity_id): def turn_on(hass, entity_id):
"""Reset the alert.""" """Reset the alert."""
run_callback_threadsafe( hass.add_job(async_turn_on, hass, entity_id)
hass.loop, async_turn_on, hass, entity_id).result()
@callback @callback
@ -76,8 +74,7 @@ def async_turn_on(hass, entity_id):
def turn_off(hass, entity_id): def turn_off(hass, entity_id):
"""Acknowledge alert.""" """Acknowledge alert."""
run_callback_threadsafe( hass.add_job(async_turn_off, hass, entity_id)
hass.loop, async_turn_off, hass, entity_id).result()
@callback @callback
@ -90,7 +87,7 @@ def async_turn_off(hass, entity_id):
def toggle(hass, entity_id): def toggle(hass, entity_id):
"""Toggle acknowledgement of alert.""" """Toggle acknowledgement of alert."""
run_callback_threadsafe(hass.loop, async_toggle, hass, entity_id) hass.add_job(async_toggle, hass, entity_id)
@callback @callback

View File

@ -0,0 +1,303 @@
"""
Support for IP Webcam, an Android app that acts as a full-featured webcam.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/android_ip_webcam/
"""
import asyncio
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, CONF_SCAN_INTERVAL,
CONF_PLATFORM)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_send, async_dispatcher_connect)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
DOMAIN = 'android_ip_webcam'
REQUIREMENTS = ["pydroid-ipcam==0.4"]
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10)
DATA_IP_WEBCAM = 'android_ip_webcam'
ATTR_HOST = 'host'
ATTR_VID_CONNS = 'Video Connections'
ATTR_AUD_CONNS = 'Audio Connections'
KEY_MAP = {
'audio_connections': 'Audio Connections',
'adet_limit': 'Audio Trigger Limit',
'antibanding': 'Anti-banding',
'audio_only': 'Audio Only',
'battery_level': 'Battery Level',
'battery_temp': 'Battery Temperature',
'battery_voltage': 'Battery Voltage',
'coloreffect': 'Color Effect',
'exposure': 'Exposure Level',
'exposure_lock': 'Exposure Lock',
'ffc': 'Front-facing Camera',
'flashmode': 'Flash Mode',
'focus': 'Focus',
'focus_homing': 'Focus Homing',
'focus_region': 'Focus Region',
'focusmode': 'Focus Mode',
'gps_active': 'GPS Active',
'idle': 'Idle',
'ip_address': 'IPv4 Address',
'ipv6_address': 'IPv6 Address',
'ivideon_streaming': 'Ivideon Streaming',
'light': 'Light Level',
'mirror_flip': 'Mirror Flip',
'motion': 'Motion',
'motion_active': 'Motion Active',
'motion_detect': 'Motion Detection',
'motion_event': 'Motion Event',
'motion_limit': 'Motion Limit',
'night_vision': 'Night Vision',
'night_vision_average': 'Night Vision Average',
'night_vision_gain': 'Night Vision Gain',
'orientation': 'Orientation',
'overlay': 'Overlay',
'photo_size': 'Photo Size',
'pressure': 'Pressure',
'proximity': 'Proximity',
'quality': 'Quality',
'scenemode': 'Scene Mode',
'sound': 'Sound',
'sound_event': 'Sound Event',
'sound_timeout': 'Sound Timeout',
'torch': 'Torch',
'video_connections': 'Video Connections',
'video_chunk_len': 'Video Chunk Length',
'video_recording': 'Video Recording',
'video_size': 'Video Size',
'whitebalance': 'White Balance',
'whitebalance_lock': 'White Balance Lock',
'zoom': 'Zoom'
}
ICON_MAP = {
'audio_connections': 'mdi:speaker',
'battery_level': 'mdi:battery',
'battery_temp': 'mdi:thermometer',
'battery_voltage': 'mdi:battery-charging-100',
'exposure_lock': 'mdi:camera',
'ffc': 'mdi:camera-front-variant',
'focus': 'mdi:image-filter-center-focus',
'gps_active': 'mdi:crosshairs-gps',
'light': 'mdi:flashlight',
'motion': 'mdi:run',
'night_vision': 'mdi:weather-night',
'overlay': 'mdi:monitor',
'pressure': 'mdi:gauge',
'proximity': 'mdi:map-marker-radius',
'quality': 'mdi:quality-high',
'sound': 'mdi:speaker',
'sound_event': 'mdi:speaker',
'sound_timeout': 'mdi:speaker',
'torch': 'mdi:white-balance-sunny',
'video_chunk_len': 'mdi:video',
'video_connections': 'mdi:eye',
'video_recording': 'mdi:record-rec',
'whitebalance_lock': 'mdi:white-balance-auto'
}
SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision',
'overlay', 'torch', 'whitebalance_lock', 'video_recording']
SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
'battery_voltage', 'light', 'motion', 'pressure', 'proximity',
'sound', 'video_connections']
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
CONF_AUTO_DISCOVERY = 'auto_discovery'
CONF_MOTION_SENSOR = 'motion_sensor'
DEFAULT_AUTO_DISCOVERY = True
DEFAULT_MOTION_SENSOR = False
DEFAULT_NAME = 'IP Webcam'
DEFAULT_PORT = 8080
DEFAULT_TIMEOUT = 10
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period,
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
vol.Optional(CONF_AUTO_DISCOVERY, default=DEFAULT_AUTO_DISCOVERY):
cv.boolean,
vol.Optional(CONF_SWITCHES, default=[]):
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
vol.Optional(CONF_SENSORS, default=[]):
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
vol.Optional(CONF_MOTION_SENSOR, default=DEFAULT_MOTION_SENSOR):
cv.boolean,
})])
}, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine
def async_setup(hass, config):
"""Setup the IP Webcam component."""
from pydroid_ipcam import PyDroidIPCam
webcams = hass.data[DATA_IP_WEBCAM] = {}
websession = async_get_clientsession(hass)
@asyncio.coroutine
def async_setup_ipcamera(cam_config):
"""Setup a ip camera."""
host = cam_config[CONF_HOST]
username = cam_config.get(CONF_USERNAME)
password = cam_config.get(CONF_PASSWORD)
name = cam_config[CONF_NAME]
interval = cam_config[CONF_SCAN_INTERVAL]
switches = cam_config[CONF_SWITCHES]
sensors = cam_config[CONF_SENSORS]
motion = cam_config[CONF_MOTION_SENSOR]
# init ip webcam
cam = PyDroidIPCam(
hass.loop, websession, host, cam_config[CONF_PORT],
username=username, password=password,
timeout=cam_config[CONF_TIMEOUT]
)
@asyncio.coroutine
def async_update_data(now):
"""Update data from ipcam in SCAN_INTERVAL."""
yield from cam.update()
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
async_track_point_in_utc_time(
hass, async_update_data, utcnow() + interval)
yield from async_update_data(None)
# use autodiscovery to detect sensors/configs
if cam_config[CONF_AUTO_DISCOVERY]:
if not cam.available:
_LOGGER.error(
"Android webcam %s not found for discovery!", cam.base_url)
return
sensors = [sensor for sensor in cam.enabled_sensors
if sensor in SENSORS]
switches = [setting for setting in cam.enabled_settings
if setting in SWITCHES]
motion = True if 'motion_active' in cam.enabled_sensors else False
sensors.extend(['audio_connections', 'video_connections'])
# load platforms
webcams[host] = cam
mjpeg_camera = {
CONF_PLATFORM: 'mjpeg',
CONF_MJPEG_URL: cam.mjpeg_url,
CONF_STILL_IMAGE_URL: cam.image_url,
CONF_NAME: name,
}
if username and password:
mjpeg_camera.update({
CONF_USERNAME: username,
CONF_PASSWORD: password
})
hass.async_add_job(discovery.async_load_platform(
hass, 'camera', 'mjpeg', mjpeg_camera, config))
if sensors:
hass.async_add_job(discovery.async_load_platform(
hass, 'sensor', DOMAIN, {
CONF_NAME: name,
CONF_HOST: host,
CONF_SENSORS: sensors,
}, config))
if switches:
hass.async_add_job(discovery.async_load_platform(
hass, 'switch', DOMAIN, {
CONF_NAME: name,
CONF_HOST: host,
CONF_SWITCHES: switches,
}, config))
if motion:
hass.async_add_job(discovery.async_load_platform(
hass, 'binary_sensor', DOMAIN, {
CONF_HOST: host,
CONF_NAME: name,
}, config))
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
return True
class AndroidIPCamEntity(Entity):
"""The Android device running IP Webcam."""
def __init__(self, host, ipcam):
"""Initialize the data oject."""
self._host = host
self._ipcam = ipcam
@asyncio.coroutine
def async_added_to_hass(self):
"""Register update dispatcher."""
@callback
def async_ipcam_update(host):
"""Update callback."""
if self._host != host:
return
self.hass.async_add_job(self.async_update_ha_state(True))
async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update)
@property
def should_poll(self):
"""Return True if entity has to be polled for state."""
return False
@property
def available(self):
"""Return True if entity is available."""
return self._ipcam.available
@property
def device_state_attributes(self):
"""Return the state attributes."""
state_attr = {ATTR_HOST: self._host}
if self._ipcam.status_data is None:
return state_attr
state_attr[ATTR_VID_CONNS] = \
self._ipcam.status_data.get('video_connections')
state_attr[ATTR_AUD_CONNS] = \
self._ipcam.status_data.get('audio_connections')
return state_attr

View File

@ -11,16 +11,17 @@ import os
import voluptuous as vol import voluptuous as vol
from homeassistant.bootstrap import async_prepare_setup_platform from homeassistant.setup import async_prepare_setup_platform
from homeassistant import config as conf_util from homeassistant import config as conf_util
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
SERVICE_TOGGLE) SERVICE_TOGGLE, SERVICE_RELOAD)
from homeassistant.components import logbook from homeassistant.components import logbook
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import extract_domain_configs, script, condition from homeassistant.helpers import extract_domain_configs, script, condition
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.loader import get_platform from homeassistant.loader import get_platform
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -28,8 +29,6 @@ import homeassistant.helpers.config_validation as cv
DOMAIN = 'automation' DOMAIN = 'automation'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
DEPENDENCIES = ['group']
GROUP_NAME_ALL_AUTOMATIONS = 'all automations' GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
CONF_ALIAS = 'alias' CONF_ALIAS = 'alias'
@ -52,7 +51,6 @@ DEFAULT_INITIAL_STATE = True
ATTR_LAST_TRIGGERED = 'last_triggered' ATTR_LAST_TRIGGERED = 'last_triggered'
ATTR_VARIABLES = 'variables' ATTR_VARIABLES = 'variables'
SERVICE_TRIGGER = 'trigger' SERVICE_TRIGGER = 'trigger'
SERVICE_RELOAD = 'reload'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -226,7 +224,7 @@ class AutomationEntity(ToggleEntity):
"""Entity to show status of entity.""" """Entity to show status of entity."""
def __init__(self, name, async_attach_triggers, cond_func, async_action, def __init__(self, name, async_attach_triggers, cond_func, async_action,
hidden): hidden, initial_state):
"""Initialize an automation entity.""" """Initialize an automation entity."""
self._name = name self._name = name
self._async_attach_triggers = async_attach_triggers self._async_attach_triggers = async_attach_triggers
@ -236,6 +234,7 @@ class AutomationEntity(ToggleEntity):
self._enabled = False self._enabled = False
self._last_triggered = None self._last_triggered = None
self._hidden = hidden self._hidden = hidden
self._initial_state = initial_state
@property @property
def name(self): def name(self):
@ -264,6 +263,18 @@ class AutomationEntity(ToggleEntity):
"""Return True if entity is on.""" """Return True if entity is on."""
return self._enabled return self._enabled
@asyncio.coroutine
def async_added_to_hass(self) -> None:
"""Startup with initial state or previous state."""
state = yield from async_get_last_state(self.hass, self.entity_id)
if state is None:
if self._initial_state:
yield from self.async_enable()
else:
self._last_triggered = state.attributes.get('last_triggered')
if state.state == STATE_ON:
yield from self.async_enable()
@asyncio.coroutine @asyncio.coroutine
def async_turn_on(self, **kwargs) -> None: def async_turn_on(self, **kwargs) -> None:
"""Turn the entity on and update the state.""" """Turn the entity on and update the state."""
@ -322,7 +333,6 @@ def _async_process_config(hass, config, component):
This method is a coroutine. This method is a coroutine.
""" """
entities = [] entities = []
tasks = []
for config_key in extract_domain_configs(config, DOMAIN): for config_key in extract_domain_configs(config, DOMAIN):
conf = config[config_key] conf = config[config_key]
@ -332,6 +342,7 @@ def _async_process_config(hass, config, component):
list_no) list_no)
hidden = config_block[CONF_HIDE_ENTITY] hidden = config_block[CONF_HIDE_ENTITY]
initial_state = config_block[CONF_INITIAL_STATE]
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
name) name)
@ -348,15 +359,14 @@ def _async_process_config(hass, config, component):
async_attach_triggers = partial( async_attach_triggers = partial(
_async_process_trigger, hass, config, _async_process_trigger, hass, config,
config_block.get(CONF_TRIGGER, []), name) config_block.get(CONF_TRIGGER, []), name
entity = AutomationEntity(name, async_attach_triggers, cond_func, )
action, hidden) entity = AutomationEntity(
if config_block[CONF_INITIAL_STATE]: name, async_attach_triggers, cond_func, action, hidden,
tasks.append(entity.async_enable()) initial_state)
entities.append(entity) entities.append(entity)
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
if entities: if entities:
yield from component.async_add_entities(entities) yield from component.async_add_entities(entities)

View File

@ -0,0 +1,62 @@
"""
Support for IP Webcam binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.android_ip_webcam/
"""
import asyncio
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.android_ip_webcam import (
KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME)
DEPENDENCIES = ['android_ip_webcam']
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup IP Webcam binary sensors."""
if discovery_info is None:
return
host = discovery_info[CONF_HOST]
name = discovery_info[CONF_NAME]
ipcam = hass.data[DATA_IP_WEBCAM][host]
async_add_devices(
[IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True)
class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
"""Represents an IP Webcam binary sensor."""
def __init__(self, name, host, ipcam, sensor):
"""Initialize the binary sensor."""
super().__init__(host, ipcam)
self._sensor = sensor
self._mapped_name = KEY_MAP.get(self._sensor, self._sensor)
self._name = '{} {}'.format(name, self._mapped_name)
self._state = None
self._unit = None
@property
def name(self):
"""Return the name of the binary sensor, if any."""
return self._name
@property
def is_on(self):
"""True if the binary sensor is on."""
return self._state
@asyncio.coroutine
def async_update(self):
"""Retrieve latest state."""
state, _ = self._ipcam.export_sensor(self._sensor)
self._state = state == 1.0
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'motion'

View File

@ -0,0 +1,74 @@
"""
Support for Blink system camera control.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.blink/
"""
from homeassistant.components.blink import DOMAIN
from homeassistant.components.binary_sensor import BinarySensorDevice
DEPENDENCIES = ['blink']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the blink binary sensors."""
if discovery_info is None:
return
data = hass.data[DOMAIN].blink
devs = list()
for name in data.cameras:
devs.append(BlinkCameraMotionSensor(name, data))
devs.append(BlinkSystemSensor(data))
add_devices(devs, True)
class BlinkCameraMotionSensor(BinarySensorDevice):
"""A representation of a Blink binary sensor."""
def __init__(self, name, data):
"""Initialize the sensor."""
self._name = 'blink_' + name + '_motion_enabled'
self._camera_name = name
self.data = data
self._state = self.data.cameras[self._camera_name].armed
@property
def name(self):
"""Return the name of the blink sensor."""
return self._name
@property
def is_on(self):
"""Return the status of the sensor."""
return self._state
def update(self):
"""Update sensor state."""
self.data.refresh()
self._state = self.data.cameras[self._camera_name].armed
class BlinkSystemSensor(BinarySensorDevice):
"""A representation of a Blink system sensor."""
def __init__(self, data):
"""Initialize the sensor."""
self._name = 'blink armed status'
self.data = data
self._state = self.data.arm
@property
def name(self):
"""Return the name of the blink sensor."""
return self._name.replace(" ", "_")
@property
def is_on(self):
"""Return the status of the sensor."""
return self._state
def update(self):
"""Update sensor state."""
self.data.refresh()
self._state = self.data.arm

View File

@ -67,7 +67,7 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
This method is called when there is an incoming packet associated This method is called when there is an incoming packet associated
with this platform. with this platform.
""" """
self.update_ha_state() self.schedule_update_ha_state()
if value2 == 0x70: if value2 == 0x70:
self.which = 0 self.which = 0
self.onoff = 0 self.onoff = 0

View File

@ -37,7 +37,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
) )
devices.append(device) devices.append(device)
yield from async_add_devices(devices) async_add_devices(devices)
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
@ -52,8 +52,11 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
_LOGGER.debug('Setting up zone: ' + zone_name) _LOGGER.debug('Setting up zone: ' + zone_name)
super().__init__(zone_name, info, controller) super().__init__(zone_name, info, controller)
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect( async_dispatcher_connect(
hass, SIGNAL_ZONE_UPDATE, self._update_callback) self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
@property @property
def device_state_attributes(self): def device_state_attributes(self):

View File

@ -57,16 +57,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
# generate sensor object # generate sensor object
entity = FFmpegMotion(hass, manager, config) entity = FFmpegMotion(hass, manager, config)
async_add_devices([entity])
# add to system
manager.async_register_device(entity)
yield from async_add_devices([entity])
class FFmpegBinarySensor(FFmpegBase, BinarySensorDevice): class FFmpegBinarySensor(FFmpegBase, BinarySensorDevice):
"""A binary sensor which use ffmpeg for noise detection.""" """A binary sensor which use ffmpeg for noise detection."""
def __init__(self, hass, config): def __init__(self, config):
"""Constructor for binary sensor noise detection.""" """Constructor for binary sensor noise detection."""
super().__init__(config.get(CONF_INITIAL_STATE)) super().__init__(config.get(CONF_INITIAL_STATE))
@ -98,15 +95,19 @@ class FFmpegMotion(FFmpegBinarySensor):
"""Initialize ffmpeg motion binary sensor.""" """Initialize ffmpeg motion binary sensor."""
from haffmpeg import SensorMotion from haffmpeg import SensorMotion
super().__init__(hass, config) super().__init__(config)
self.ffmpeg = SensorMotion( self.ffmpeg = SensorMotion(
manager.binary, hass.loop, self._async_callback) manager.binary, hass.loop, self._async_callback)
def async_start_ffmpeg(self): @asyncio.coroutine
def _async_start_ffmpeg(self, entity_ids):
"""Start a FFmpeg instance. """Start a FFmpeg instance.
This method must be run in the event loop and returns a coroutine. This method is a coroutine.
""" """
if entity_ids is not None and self.entity_id not in entity_ids:
return
# init config # init config
self.ffmpeg.set_options( self.ffmpeg.set_options(
time_reset=self._config.get(CONF_RESET), time_reset=self._config.get(CONF_RESET),
@ -116,7 +117,7 @@ class FFmpegMotion(FFmpegBinarySensor):
) )
# run # run
return self.ffmpeg.open_sensor( yield from self.ffmpeg.open_sensor(
input_source=self._config.get(CONF_INPUT), input_source=self._config.get(CONF_INPUT),
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
) )

View File

@ -54,10 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
# generate sensor object # generate sensor object
entity = FFmpegNoise(hass, manager, config) entity = FFmpegNoise(hass, manager, config)
async_add_devices([entity])
# add to system
manager.async_register_device(entity)
yield from async_add_devices([entity])
class FFmpegNoise(FFmpegBinarySensor): class FFmpegNoise(FFmpegBinarySensor):
@ -67,15 +64,19 @@ class FFmpegNoise(FFmpegBinarySensor):
"""Initialize ffmpeg noise binary sensor.""" """Initialize ffmpeg noise binary sensor."""
from haffmpeg import SensorNoise from haffmpeg import SensorNoise
super().__init__(hass, config) super().__init__(config)
self.ffmpeg = SensorNoise( self.ffmpeg = SensorNoise(
manager.binary, hass.loop, self._async_callback) manager.binary, hass.loop, self._async_callback)
def async_start_ffmpeg(self): @asyncio.coroutine
def _async_start_ffmpeg(self, entity_ids):
"""Start a FFmpeg instance. """Start a FFmpeg instance.
This method must be run in the event loop and returns a coroutine. This method is a coroutine.
""" """
if entity_ids is not None and self.entity_id not in entity_ids:
return
# init config # init config
self.ffmpeg.set_options( self.ffmpeg.set_options(
time_duration=self._config.get(CONF_DURATION), time_duration=self._config.get(CONF_DURATION),
@ -84,7 +85,7 @@ class FFmpegNoise(FFmpegBinarySensor):
) )
# run # run
return self.ffmpeg.open_sensor( yield from self.ffmpeg.open_sensor(
input_source=self._config.get(CONF_INPUT), input_source=self._config.get(CONF_INPUT),
output_dest=self._config.get(CONF_OUTPUT), output_dest=self._config.get(CONF_OUTPUT),
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),

View File

@ -15,9 +15,10 @@ from homeassistant.components.binary_sensor import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_SSL, EVENT_HOMEASSISTANT_STOP, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE) CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
REQUIREMENTS = ['pyhik==0.0.7', 'pydispatcher==2.0.5'] REQUIREMENTS = ['pyhik==0.1.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_IGNORED = 'ignored' CONF_IGNORED = 'ignored'
@ -119,30 +120,32 @@ class HikvisionData(object):
self._password = password self._password = password
# Establish camera # Establish camera
self._cam = HikCamera(self._url, self._port, self.camdata = HikCamera(self._url, self._port,
self._username, self._password) self._username, self._password)
if self._name is None: if self._name is None:
self._name = self._cam.get_name self._name = self.camdata.get_name
# Start event stream
self._cam.start_stream()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop_hik) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop_hik)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.start_hik)
def stop_hik(self, event): def stop_hik(self, event):
"""Shutdown Hikvision subscriptions and subscription thread on exit.""" """Shutdown Hikvision subscriptions and subscription thread on exit."""
self._cam.disconnect() self.camdata.disconnect()
def start_hik(self, event):
"""Start Hikvision event stream thread."""
self.camdata.start_stream()
@property @property
def sensors(self): def sensors(self):
"""Return list of available sensors and their states.""" """Return list of available sensors and their states."""
return self._cam.current_event_states return self.camdata.current_event_states
@property @property
def cam_id(self): def cam_id(self):
"""Return camera id.""" """Return camera id."""
return self._cam.get_id return self.camdata.get_id
@property @property
def name(self): def name(self):
@ -155,8 +158,6 @@ class HikvisionBinarySensor(BinarySensorDevice):
def __init__(self, hass, sensor, cam, delay): def __init__(self, hass, sensor, cam, delay):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
from pydispatch import dispatcher
self._hass = hass self._hass = hass
self._cam = cam self._cam = cam
self._name = self._cam.name + ' ' + sensor self._name = self._cam.name + ' ' + sensor
@ -170,12 +171,8 @@ class HikvisionBinarySensor(BinarySensorDevice):
self._timer = None self._timer = None
# Form signal for dispatcher # Register callback function with pyHik
signal = 'ValueChanged.{}'.format(self._cam.cam_id) self._cam.camdata.add_update_callback(self._update_callback, self._id)
dispatcher.connect(self._update_callback,
signal=signal,
sender=self._sensor)
def _sensor_state(self): def _sensor_state(self):
"""Extract sensor state.""" """Extract sensor state."""
@ -225,13 +222,9 @@ class HikvisionBinarySensor(BinarySensorDevice):
return attr return attr
def _update_callback(self, signal, sender): def _update_callback(self, msg):
"""Update the sensor's state, if needed.""" """Update the sensor's state, if needed."""
_LOGGER.debug('Dispatcher callback, signal: %s, sender: %s', _LOGGER.debug('Callback signal from: %s', msg)
signal, sender)
if sender is not self._sensor:
return
if self._delay > 0 and not self.is_on: if self._delay > 0 and not self.is_on:
# Set timer to wait until updating the state # Set timer to wait until updating the state

View File

@ -0,0 +1,76 @@
"""
Support for MAX! Window Shutter via MAX! Cube.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/maxcube/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.maxcube import MAXCUBE_HANDLE
from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Iterate through all MAX! Devices and add window shutters to HASS."""
cube = hass.data[MAXCUBE_HANDLE].cube
# List of devices
devices = []
for device in cube.devices:
# Create device name by concatenating room name + device name
name = "%s %s" % (cube.room_by_id(device.room_id).name, device.name)
# Only add Window Shutters
if cube.is_windowshutter(device):
# add device to HASS
devices.append(MaxCubeShutter(hass, name, device.rf_address))
if len(devices) > 0:
add_devices(devices)
class MaxCubeShutter(BinarySensorDevice):
"""MAX! Cube BinarySensor device."""
def __init__(self, hass, name, rf_address):
"""Initialize MAX! Cube BinarySensorDevice."""
self._name = name
self._sensor_type = 'opening'
self._rf_address = rf_address
self._cubehandle = hass.data[MAXCUBE_HANDLE]
self._state = STATE_UNKNOWN
@property
def should_poll(self):
"""Polling is required."""
return True
@property
def name(self):
"""Return the name of the BinarySensorDevice."""
return self._name
@property
def device_class(self):
"""Return the class of this sensor."""
return self._sensor_type
@property
def is_on(self):
"""Return true if the binary sensor is on/open."""
return self._state
def update(self):
"""Get latest data from MAX! Cube."""
self._cubehandle.update()
# Get the device we want to update
device = self._cubehandle.cube.device_by_rf(self._rf_address)
# Update our internal state
self._state = device.is_open

View File

@ -46,7 +46,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
yield from async_add_devices([MqttBinarySensor( async_add_devices([MqttBinarySensor(
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC), config.get(CONF_STATE_TOPIC),
get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS), get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS),

View File

@ -15,12 +15,14 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA) DEVICE_CLASSES_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE, ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
CONF_SENSOR_CLASS, CONF_SENSORS, CONF_DEVICE_CLASS) CONF_SENSOR_CLASS, CONF_SENSORS, CONF_DEVICE_CLASS,
EVENT_HOMEASSISTANT_START, STATE_ON)
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_track_state_change
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.deprecation import get_deprecated from homeassistant.helpers.deprecation import get_deprecated
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.restore_state import async_get_last_state
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -66,7 +68,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
_LOGGER.error('No sensors added') _LOGGER.error('No sensors added')
return False return False
yield from async_add_devices(sensors, True) async_add_devices(sensors, True)
return True return True
@ -83,14 +85,30 @@ class BinarySensorTemplate(BinarySensorDevice):
self._device_class = device_class self._device_class = device_class
self._template = value_template self._template = value_template
self._state = None self._state = None
self._entities = entity_ids
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
state = yield from async_get_last_state(self.hass, self.entity_id)
if state:
self._state = state.state == STATE_ON
@callback @callback
def template_bsensor_state_listener(entity, old_state, new_state): def template_bsensor_state_listener(entity, old_state, new_state):
"""Called when the target device changes state.""" """Called when the target device changes state."""
hass.async_add_job(self.async_update_ha_state, True) self.hass.async_add_job(self.async_update_ha_state(True))
async_track_state_change( @callback
hass, entity_ids, template_bsensor_state_listener) def template_bsensor_startup(event):
"""Update template on startup."""
async_track_state_change(
self.hass, self._entities, template_bsensor_state_listener)
self.hass.async_add_job(self.async_update_ha_state(True))
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, template_bsensor_startup)
@property @property
def name(self): def name(self):

View File

@ -52,7 +52,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
limit_type = config.get(CONF_TYPE) limit_type = config.get(CONF_TYPE)
device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS) device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
yield from async_add_devices( async_add_devices(
[ThresholdSensor(hass, entity_id, name, threshold, limit_type, [ThresholdSensor(hass, entity_id, name, threshold, limit_type,
device_class)], True) device_class)], True)
return True return True

View File

@ -7,9 +7,9 @@ https://home-assistant.io/components/binary_sensor.vera/
import logging import logging
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice) BinarySensorDevice, ENTITY_ID_FORMAT)
from homeassistant.components.vera import ( from homeassistant.components.vera import (
VeraDevice, VERA_DEVICES, VERA_CONTROLLER) VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
DEPENDENCIES = ['vera'] DEPENDENCIES = ['vera']
@ -30,6 +30,7 @@ class VeraBinarySensor(VeraDevice, BinarySensorDevice):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
self._state = False self._state = False
VeraDevice.__init__(self, vera_device, controller) VeraDevice.__init__(self, vera_device, controller)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property @property
def is_on(self): def is_on(self):

View File

@ -10,6 +10,7 @@ import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_point_in_time from homeassistant.helpers.event import track_point_in_time
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.zwave import workaround from homeassistant.components.zwave import workaround
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DOMAIN, DOMAIN,
BinarySensorDevice) BinarySensorDevice)
@ -18,31 +19,20 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = [] DEPENDENCIES = []
def setup_platform(hass, config, add_devices, discovery_info=None): def get_device(value, **kwargs):
"""Setup the Z-Wave platform for binary sensors.""" """Create zwave entity device."""
if discovery_info is None or zwave.NETWORK is None:
return
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)
device_mapping = workaround.get_device_mapping(value) device_mapping = workaround.get_device_mapping(value)
if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT: if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT:
# Default the multiplier to 4 # Default the multiplier to 4
re_arm_multiplier = (zwave.get_config_value(value.node, 9) or 4) re_arm_multiplier = (zwave.get_config_value(value.node, 9) or 4)
add_devices([ return ZWaveTriggerSensor(value, "motion", re_arm_multiplier * 8)
ZWaveTriggerSensor(value, "motion",
hass, re_arm_multiplier * 8)
])
return
if workaround.get_device_component_mapping(value) == DOMAIN: if workaround.get_device_component_mapping(value) == DOMAIN:
add_devices([ZWaveBinarySensor(value, None)]) return ZWaveBinarySensor(value, None)
return
if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY: if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
add_devices([ZWaveBinarySensor(value, None)]) return ZWaveBinarySensor(value, None)
return None
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity): class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
@ -77,26 +67,23 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
class ZWaveTriggerSensor(ZWaveBinarySensor): class ZWaveTriggerSensor(ZWaveBinarySensor):
"""Representation of a stateless sensor within Z-Wave.""" """Representation of a stateless sensor within Z-Wave."""
def __init__(self, value, device_class, hass, re_arm_sec=60): def __init__(self, value, device_class, re_arm_sec=60):
"""Initialize the sensor.""" """Initialize the sensor."""
super(ZWaveTriggerSensor, self).__init__(value, device_class) super(ZWaveTriggerSensor, self).__init__(value, device_class)
self._hass = hass
self.re_arm_sec = re_arm_sec self.re_arm_sec = re_arm_sec
self.invalidate_after = dt_util.utcnow() + datetime.timedelta( self.invalidate_after = None
seconds=self.re_arm_sec)
# If it's active make sure that we set the timeout tracker
track_point_in_time(
self._hass, self.async_update_ha_state,
self.invalidate_after)
def update_properties(self): def update_properties(self):
"""Called when a value for this entity's node has changed.""" """Called when a value for this entity's node has changed."""
self._state = self._value.data self._state = self._value.data
# only allow this value to be true for re_arm secs # only allow this value to be true for re_arm secs
if not self.hass:
return
self.invalidate_after = dt_util.utcnow() + datetime.timedelta( self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
seconds=self.re_arm_sec) seconds=self.re_arm_sec)
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self.hass, self.async_update_ha_state,
self.invalidate_after) self.invalidate_after)
@property @property

View File

@ -0,0 +1,87 @@
"""
Support for Blink Home Camera System.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/blink/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (CONF_USERNAME,
CONF_PASSWORD,
ATTR_FRIENDLY_NAME,
ATTR_ARMED)
from homeassistant.helpers import discovery
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'blink'
REQUIREMENTS = ['blinkpy==0.4.4']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
}, extra=vol.ALLOW_EXTRA)
ARM_SYSTEM_SCHEMA = vol.Schema({
vol.Optional(ATTR_ARMED): cv.boolean
})
ARM_CAMERA_SCHEMA = vol.Schema({
vol.Required(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_ARMED): cv.boolean
})
SNAP_PICTURE_SCHEMA = vol.Schema({
vol.Required(ATTR_FRIENDLY_NAME): cv.string
})
class BlinkSystem(object):
"""Blink System class."""
def __init__(self, config_info):
"""Initialize the system."""
import blinkpy
self.blink = blinkpy.Blink(username=config_info[DOMAIN][CONF_USERNAME],
password=config_info[DOMAIN][CONF_PASSWORD])
self.blink.setup_system()
def setup(hass, config):
"""Setup Blink System."""
hass.data[DOMAIN] = BlinkSystem(config)
discovery.load_platform(hass, 'camera', DOMAIN, {}, config)
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
def snap_picture(call):
"""Take a picture."""
cameras = hass.data[DOMAIN].blink.cameras
name = call.data.get(ATTR_FRIENDLY_NAME, '')
if name in cameras:
cameras[name].snap_picture()
def arm_camera(call):
"""Arm a camera."""
cameras = hass.data[DOMAIN].blink.cameras
name = call.data.get(ATTR_FRIENDLY_NAME, '')
value = call.data.get(ATTR_ARMED, True)
if name in cameras:
cameras[name].set_motion_detect(value)
def arm_system(call):
"""Arm the system."""
value = call.data.get(ATTR_ARMED, True)
hass.data[DOMAIN].blink.arm = value
hass.data[DOMAIN].blink.refresh()
hass.services.register(DOMAIN, 'snap_picture', snap_picture,
schema=SNAP_PICTURE_SCHEMA)
hass.services.register(DOMAIN, 'arm_camera', arm_camera,
schema=ARM_CAMERA_SCHEMA)
hass.services.register(DOMAIN, 'arm_system', arm_system,
schema=ARM_SYSTEM_SCHEMA)
return True

View File

@ -3,8 +3,8 @@ Support for Google Calendar event device sensors.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/calendar/ https://home-assistant.io/components/calendar/
""" """
import asyncio
import logging import logging
from datetime import timedelta from datetime import timedelta
@ -27,13 +27,13 @@ DOMAIN = 'calendar'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
def setup(hass, config): @asyncio.coroutine
def async_setup(hass, config):
"""Track states and offer events for calendars.""" """Track states and offer events for calendars."""
component = EntityComponent( component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, DOMAIN) logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, DOMAIN)
component.setup(config) yield from component.async_setup(config)
return True return True
@ -155,7 +155,7 @@ class CalendarEventDevice(Entity):
start = _get_date(self.data.event['start']) start = _get_date(self.data.event['start'])
end = _get_date(self.data.event['end']) end = _get_date(self.data.event['end'])
summary = self.data.event['summary'] summary = self.data.event.get('summary', '')
# check if we have an offset tag in the message # check if we have an offset tag in the message
# time is HH:MM or MM # time is HH:MM or MM

View File

@ -0,0 +1,81 @@
"""
Support for Blink system camera.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.blink/
"""
import logging
from datetime import timedelta
import requests
from homeassistant.components.blink import DOMAIN
from homeassistant.components.camera import Camera
from homeassistant.util import Throttle
DEPENDENCIES = ['blink']
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup a Blink Camera."""
if discovery_info is None:
return
data = hass.data[DOMAIN].blink
devs = list()
for name in data.cameras:
devs.append(BlinkCamera(hass, config, data, name))
add_devices(devs)
class BlinkCamera(Camera):
"""An implementation of a Blink Camera."""
def __init__(self, hass, config, data, name):
"""Initialize a camera."""
super().__init__()
self.data = data
self.hass = hass
self._name = name
self.notifications = self.data.cameras[self._name].notifications
self.response = None
_LOGGER.info("Initialized blink camera %s", self._name)
@property
def name(self):
"""A camera name."""
return self._name
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def request_image(self):
"""An image request from Blink servers."""
_LOGGER.info("Requesting new image from blink servers")
image_url = self.check_for_motion()
header = self.data.cameras[self._name].header
self.response = requests.get(image_url, headers=header, stream=True)
def check_for_motion(self):
"""A method to check if motion has been detected since last update."""
self.data.refresh()
notifs = self.data.cameras[self._name].notifications
if notifs > self.notifications:
# We detected motion at some point
self.data.last_motion()
self.notifications = notifs
# returning motion image currently not working
# return self.data.cameras[self._name].motion['image']
elif notifs < self.notifications:
self.notifications = notifs
return self.data.camera_thumbs[self._name]
def camera_image(self):
"""Return a still image reponse from the camera."""
self.request_image()
return self.response.content

View File

@ -0,0 +1,67 @@
"""
Support for internal dispatcher image push to Camera.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.dispatcher/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import CONF_NAME
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
CONF_SIGNAL = 'signal'
DEFAULT_NAME = 'Dispatcher Camera'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SIGNAL): cv.slugify,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a dispatcher camera."""
if discovery_info:
config = PLATFORM_SCHEMA(discovery_info)
async_add_devices(
[DispatcherCamera(config[CONF_NAME], config[CONF_SIGNAL])])
class DispatcherCamera(Camera):
"""A dispatcher implementation of an camera."""
def __init__(self, name, signal):
"""Initialize a dispatcher camera."""
super().__init__()
self._name = name
self._signal = signal
self._image = None
@asyncio.coroutine
def async_added_to_hass(self):
"""Register dispatcher and callbacks."""
@callback
def async_update_image(image):
"""Update image from dispatcher call."""
self._image = image
async_dispatcher_connect(self.hass, self._signal, async_update_image)
@asyncio.coroutine
def async_camera_image(self):
"""Return a still image response from the camera."""
return self._image
@property
def name(self):
"""Return the name of this device."""
return self._name

View File

@ -34,7 +34,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a FFmpeg Camera.""" """Setup a FFmpeg Camera."""
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)): if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
return return
yield from async_add_devices([FFmpegCamera(hass, config)]) async_add_devices([FFmpegCamera(hass, config)])
class FFmpegCamera(Camera): class FFmpegCamera(Camera):

View File

@ -47,15 +47,21 @@ class FoscamCamera(Camera):
port = device_info.get(CONF_PORT) port = device_info.get(CONF_PORT)
self._base_url = 'http://{}:{}/'.format(ip_address, port) self._base_url = 'http://{}:{}/'.format(ip_address, port)
uri_template = self._base_url \
+ 'cgi-bin/CGIProxy.fcgi?' \
+ 'cmd=snapPicture2&usr={}&pwd={}'
self._username = device_info.get(CONF_USERNAME) self._username = device_info.get(CONF_USERNAME)
self._password = device_info.get(CONF_PASSWORD) self._password = device_info.get(CONF_PASSWORD)
self._snap_picture_url = self._base_url \ self._snap_picture_url = uri_template.format(
+ 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=' \ self._username,
+ self._username + '&pwd=' + self._password self._password
)
self._name = device_info.get(CONF_NAME) self._name = device_info.get(CONF_NAME)
_LOGGER.info('Using the following URL for %s: %s', _LOGGER.info('Using the following URL for %s: %s',
self._name, self._snap_picture_url) self._name, uri_template.format('***', '***'))
def camera_image(self): def camera_image(self):
"""Return a still image reponse from the camera.""" """Return a still image reponse from the camera."""

View File

@ -44,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument # pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a generic IP Camera.""" """Setup a generic IP Camera."""
yield from async_add_devices([GenericCamera(hass, config)]) async_add_devices([GenericCamera(hass, config)])
class GenericCamera(Camera): class GenericCamera(Camera):

View File

@ -20,7 +20,7 @@ CONF_FILE_PATH = 'file_path'
DEFAULT_NAME = 'Local File' DEFAULT_NAME = 'Local File'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_FILE_PATH): cv.isfile, vol.Required(CONF_FILE_PATH): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
}) })
@ -31,8 +31,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
# check filepath given is readable # check filepath given is readable
if not os.access(file_path, os.R_OK): if not os.access(file_path, os.R_OK):
_LOGGER.error("file path is not readable") _LOGGER.warning("Could not read camera %s image from file: %s",
return False config[CONF_NAME], file_path)
add_devices([LocalFile(config[CONF_NAME], file_path)]) add_devices([LocalFile(config[CONF_NAME], file_path)])
@ -49,8 +49,12 @@ class LocalFile(Camera):
def camera_image(self): def camera_image(self):
"""Return image response.""" """Return image response."""
with open(self._file_path, 'rb') as file: try:
return file.read() with open(self._file_path, 'rb') as file:
return file.read()
except FileNotFoundError:
_LOGGER.warning("Could not read camera %s image from file: %s",
self._name, self._file_path)
@property @property
def name(self): def name(self):

View File

@ -45,7 +45,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument # pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a MJPEG IP Camera.""" """Setup a MJPEG IP Camera."""
yield from async_add_devices([MjpegCamera(hass, config)]) if discovery_info:
config = PLATFORM_SCHEMA(discovery_info)
async_add_devices([MjpegCamera(hass, config)])
def extract_image_from_mjpeg(stream): def extract_image_from_mjpeg(stream):

View File

@ -153,7 +153,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
) )
devices.append(device) devices.append(device)
yield from async_add_devices(devices) async_add_devices(devices)
@asyncio.coroutine @asyncio.coroutine

View File

@ -75,4 +75,4 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
_LOGGER.warning('No active cameras found') _LOGGER.warning('No active cameras found')
return return
yield from async_add_devices(cameras) async_add_devices(cameras)

View File

@ -25,6 +25,8 @@ ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time'
ATTR_RESUME_ALL = 'resume_all' ATTR_RESUME_ALL = 'resume_all'
DEFAULT_RESUME_ALL = False DEFAULT_RESUME_ALL = False
TEMPERATURE_HOLD = 'temp'
VACATION_HOLD = 'vacation'
DEPENDENCIES = ['ecobee'] DEPENDENCIES = ['ecobee']
@ -112,6 +114,8 @@ class Thermostat(ClimateDevice):
self.thermostat_index) self.thermostat_index)
self._name = self.thermostat['name'] self._name = self.thermostat['name']
self.hold_temp = hold_temp self.hold_temp = hold_temp
self.vacation = None
self._climate_list = self.climate_list
self._operation_list = ['auto', 'auxHeatOnly', 'cool', self._operation_list = ['auto', 'auxHeatOnly', 'cool',
'heat', 'off'] 'heat', 'off']
self.update_without_throttle = False self.update_without_throttle = False
@ -187,29 +191,30 @@ class Thermostat(ClimateDevice):
def current_hold_mode(self): def current_hold_mode(self):
"""Return current hold mode.""" """Return current hold mode."""
events = self.thermostat['events'] events = self.thermostat['events']
if any((event['holdClimateRef'] == 'away' and for event in events:
int(event['endDate'][0:4])-int(event['startDate'][0:4]) <= 1) if event['running']:
or event['type'] == 'autoAway' if event['type'] == 'hold':
for event in events): if event['holdClimateRef'] == 'away':
# away hold is auto away or a temporary hold from away climate if int(event['endDate'][0:4]) - \
hold = 'away' int(event['startDate'][0:4]) <= 1:
elif any(event['holdClimateRef'] == 'away' and # a temporary hold from away climate is a hold
int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1 return 'away'
for event in events): else:
# a permanent away is not considered a hold, but away_mode # a premanent hold from away climate is away_mode
hold = None return None
elif any(event['holdClimateRef'] == 'home' or elif event['holdClimateRef'] != "":
event['type'] == 'autoHome' # any other hold based on climate
for event in events): return event['holdClimateRef']
# home mode is auto home or any home hold else:
hold = 'home' # any hold not based on a climate is a temp hold
elif any(event['type'] == 'hold' and event['running'] return TEMPERATURE_HOLD
for event in events): elif event['type'].startswith('auto'):
hold = 'temp' # all auto modes are treated as holds
# temperature hold is any other hold not based on climate return event['type'][4:].lower()
else: elif event['type'] == 'vacation':
hold = None self.vacation = event['name']
return hold return VACATION_HOLD
return None
@property @property
def current_operation(self): def current_operation(self):
@ -232,8 +237,11 @@ class Thermostat(ClimateDevice):
@property @property
def mode(self): def mode(self):
"""Return current mode ie. home, away, sleep.""" """Return current mode, as the user-visible name."""
return self.thermostat['program']['currentClimateRef'] cur = self.thermostat['program']['currentClimateRef']
climates = self.thermostat['program']['climates']
current = list(filter(lambda x: x['climateRef'] == cur, climates))
return current[0]['name']
@property @property
def fan_min_on_time(self): def fan_min_on_time(self):
@ -261,52 +269,44 @@ class Thermostat(ClimateDevice):
"fan": self.fan, "fan": self.fan,
"mode": self.mode, "mode": self.mode,
"operation": operation, "operation": operation,
"climate_list": self.climate_list,
"fan_min_on_time": self.fan_min_on_time "fan_min_on_time": self.fan_min_on_time
} }
def is_vacation_on(self):
"""Return true if vacation mode is on."""
events = self.thermostat['events']
return any(event['type'] == 'vacation' and event['running']
for event in events)
@property @property
def is_away_mode_on(self): def is_away_mode_on(self):
"""Return true if away mode is on.""" """Return true if away mode is on."""
events = self.thermostat['events'] return self.current_hold_mode == 'away'
return any(event['holdClimateRef'] == 'away' and
int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1
for event in events)
def turn_away_mode_on(self): def turn_away_mode_on(self):
"""Turn away on.""" """Turn away on."""
self.data.ecobee.set_climate_hold(self.thermostat_index, self.set_hold_mode('away')
"away", 'indefinite')
self.update_without_throttle = True
def turn_away_mode_off(self): def turn_away_mode_off(self):
"""Turn away off.""" """Turn away off."""
self.data.ecobee.resume_program(self.thermostat_index) self.set_hold_mode(None)
self.update_without_throttle = True
def set_hold_mode(self, hold_mode): def set_hold_mode(self, hold_mode):
"""Set hold mode (away, home, temp).""" """Set hold mode (away, home, temp, sleep, etc.)."""
hold = self.current_hold_mode hold = self.current_hold_mode
if hold == hold_mode: if hold == hold_mode:
# no change, so no action required # no change, so no action required
return return
elif hold_mode == 'away': elif hold_mode == 'None' or hold_mode is None:
self.data.ecobee.set_climate_hold(self.thermostat_index, if hold == VACATION_HOLD:
"away", self.hold_preference()) self.data.ecobee.delete_vacation(self.thermostat_index,
elif hold_mode == 'home': self.vacation)
self.data.ecobee.set_climate_hold(self.thermostat_index, else:
"home", self.hold_preference()) self.data.ecobee.resume_program(self.thermostat_index)
elif hold_mode == 'temp':
self.set_temp_hold(int(self.current_temperature))
else: else:
self.data.ecobee.resume_program(self.thermostat_index) if hold_mode == TEMPERATURE_HOLD:
self.update_without_throttle = True self.set_temp_hold(int(self.current_temperature))
else:
self.data.ecobee.set_climate_hold(self.thermostat_index,
hold_mode,
self.hold_preference())
self.update_without_throttle = True
def set_auto_temp_hold(self, heat_temp, cool_temp): def set_auto_temp_hold(self, heat_temp, cool_temp):
"""Set temperature hold in auto mode.""" """Set temperature hold in auto mode."""
@ -382,3 +382,9 @@ class Thermostat(ClimateDevice):
# as an indefinite away hold is interpreted as away_mode # as an indefinite away hold is interpreted as away_mode
else: else:
return 'nextTransition' return 'nextTransition'
@property
def climate_list(self):
"""Return the list of climates currently available."""
climates = self.thermostat['program']['climates']
return list(map((lambda x: x['name']), climates))

View File

@ -16,7 +16,8 @@ from homeassistant.components.climate import (
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE) ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE)
from homeassistant.helpers import condition from homeassistant.helpers import condition
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import (
async_track_state_change, async_track_time_interval)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -35,6 +36,7 @@ CONF_TARGET_TEMP = 'target_temp'
CONF_AC_MODE = 'ac_mode' CONF_AC_MODE = 'ac_mode'
CONF_MIN_DUR = 'min_cycle_duration' CONF_MIN_DUR = 'min_cycle_duration'
CONF_TOLERANCE = 'tolerance' CONF_TOLERANCE = 'tolerance'
CONF_KEEP_ALIVE = 'keep_alive'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -47,6 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float), vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float),
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float), vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
vol.Optional(CONF_KEEP_ALIVE): vol.All(
cv.time_period, cv.positive_timedelta),
}) })
@ -62,10 +66,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
ac_mode = config.get(CONF_AC_MODE) ac_mode = config.get(CONF_AC_MODE)
min_cycle_duration = config.get(CONF_MIN_DUR) min_cycle_duration = config.get(CONF_MIN_DUR)
tolerance = config.get(CONF_TOLERANCE) tolerance = config.get(CONF_TOLERANCE)
keep_alive = config.get(CONF_KEEP_ALIVE)
yield from async_add_devices([GenericThermostat( async_add_devices([GenericThermostat(
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
target_temp, ac_mode, min_cycle_duration, tolerance)]) target_temp, ac_mode, min_cycle_duration, tolerance, keep_alive)])
class GenericThermostat(ClimateDevice): class GenericThermostat(ClimateDevice):
@ -73,7 +78,7 @@ class GenericThermostat(ClimateDevice):
def __init__(self, hass, name, heater_entity_id, sensor_entity_id, def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration, min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
tolerance): tolerance, keep_alive):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self.hass = hass self.hass = hass
self._name = name self._name = name
@ -81,6 +86,7 @@ class GenericThermostat(ClimateDevice):
self.ac_mode = ac_mode self.ac_mode = ac_mode
self.min_cycle_duration = min_cycle_duration self.min_cycle_duration = min_cycle_duration
self._tolerance = tolerance self._tolerance = tolerance
self._keep_alive = keep_alive
self._active = False self._active = False
self._cur_temp = None self._cur_temp = None
@ -94,6 +100,10 @@ class GenericThermostat(ClimateDevice):
async_track_state_change( async_track_state_change(
hass, heater_entity_id, self._async_switch_changed) hass, heater_entity_id, self._async_switch_changed)
if self._keep_alive:
async_track_time_interval(
hass, self._async_keep_alive, self._keep_alive)
sensor_state = hass.states.get(sensor_entity_id) sensor_state = hass.states.get(sensor_entity_id)
if sensor_state: if sensor_state:
self._async_update_temp(sensor_state) self._async_update_temp(sensor_state)
@ -180,6 +190,14 @@ class GenericThermostat(ClimateDevice):
return return
self.hass.async_add_job(self.async_update_ha_state()) self.hass.async_add_job(self.async_update_ha_state())
@callback
def _async_keep_alive(self, time):
"""Called at constant intervals for keep-alive purposes."""
if self.current_operation in [STATE_COOL, STATE_HEAT]:
switch.async_turn_on(self.hass, self.heater_entity_id)
else:
switch.async_turn_off(self.hass, self.heater_entity_id)
@callback @callback
def _async_update_temp(self, state): def _async_update_temp(self, state):
"""Update thermostat with latest state from sensor.""" """Update thermostat with latest state from sensor."""

View File

@ -6,10 +6,15 @@ https://home-assistant.io/components/climate.honeywell/
""" """
import logging import logging
import socket import socket
import datetime
import voluptuous as vol import voluptuous as vol
import requests
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA) from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA,
ATTR_FAN_MODE, ATTR_FAN_LIST,
ATTR_OPERATION_MODE,
ATTR_OPERATION_LIST)
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
ATTR_TEMPERATURE) ATTR_TEMPERATURE)
@ -21,27 +26,35 @@ REQUIREMENTS = ['evohomeclient==0.2.5',
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_FAN = 'fan' ATTR_FAN = 'fan'
ATTR_FANMODE = 'fanmode'
ATTR_SYSTEM_MODE = 'system_mode' ATTR_SYSTEM_MODE = 'system_mode'
ATTR_CURRENT_OPERATION = 'equipment_output_status'
CONF_AWAY_TEMPERATURE = 'away_temperature' CONF_AWAY_TEMPERATURE = 'away_temperature'
CONF_COOL_AWAY_TEMPERATURE = 'away_cool_temperature'
CONF_HEAT_AWAY_TEMPERATURE = 'away_heat_temperature'
CONF_REGION = 'region' CONF_REGION = 'region'
DEFAULT_AWAY_TEMPERATURE = 16 DEFAULT_AWAY_TEMPERATURE = 16
DEFAULT_COOL_AWAY_TEMPERATURE = 30
DEFAULT_HEAT_AWAY_TEMPERATURE = 16
DEFAULT_REGION = 'eu' DEFAULT_REGION = 'eu'
REGIONS = ['eu', 'us'] REGIONS = ['eu', 'us']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_AWAY_TEMPERATURE, default=DEFAULT_AWAY_TEMPERATURE): vol.Optional(CONF_AWAY_TEMPERATURE,
vol.Coerce(float), default=DEFAULT_AWAY_TEMPERATURE): vol.Coerce(float),
vol.Optional(CONF_COOL_AWAY_TEMPERATURE,
default=DEFAULT_COOL_AWAY_TEMPERATURE): vol.Coerce(float),
vol.Optional(CONF_HEAT_AWAY_TEMPERATURE,
default=DEFAULT_HEAT_AWAY_TEMPERATURE): vol.Coerce(float),
vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(REGIONS), vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(REGIONS),
}) })
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the HoneywelL thermostat.""" """Setup the Honeywell thermostat."""
username = config.get(CONF_USERNAME) username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD) password = config.get(CONF_PASSWORD)
region = config.get(CONF_REGION) region = config.get(CONF_REGION)
@ -88,8 +101,11 @@ def _setup_us(username, password, config, add_devices):
dev_id = config.get('thermostat') dev_id = config.get('thermostat')
loc_id = config.get('location') loc_id = config.get('location')
cool_away_temp = config.get(CONF_COOL_AWAY_TEMPERATURE)
heat_away_temp = config.get(CONF_HEAT_AWAY_TEMPERATURE)
add_devices([HoneywellUSThermostat(client, device) add_devices([HoneywellUSThermostat(client, device, cool_away_temp,
heat_away_temp, username, password)
for location in client.locations_by_id.values() for location in client.locations_by_id.values()
for device in location.devices_by_id.values() for device in location.devices_by_id.values()
if ((not loc_id or location.locationid == loc_id) and if ((not loc_id or location.locationid == loc_id) and
@ -160,7 +176,7 @@ class RoundThermostat(ClimateDevice):
def turn_away_mode_on(self): def turn_away_mode_on(self):
"""Turn away on. """Turn away on.
Evohome does have a proprietary away mode, but it doesn't really work Honeywell does have a proprietary away mode, but it doesn't really work
the way it should. For example: If you set a temperature manually the way it should. For example: If you set a temperature manually
it doesn't get overwritten when away mode is switched on. it doesn't get overwritten when away mode is switched on.
""" """
@ -199,10 +215,16 @@ class RoundThermostat(ClimateDevice):
class HoneywellUSThermostat(ClimateDevice): class HoneywellUSThermostat(ClimateDevice):
"""Representation of a Honeywell US Thermostat.""" """Representation of a Honeywell US Thermostat."""
def __init__(self, client, device): def __init__(self, client, device, cool_away_temp,
heat_away_temp, username, password):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self._client = client self._client = client
self._device = device self._device = device
self._cool_away_temp = cool_away_temp
self._heat_away_temp = heat_away_temp
self._away = False
self._username = username
self._password = password
@property @property
def is_fan_on(self): def is_fan_on(self):
@ -236,7 +258,10 @@ class HoneywellUSThermostat(ClimateDevice):
@property @property
def current_operation(self: ClimateDevice) -> str: def current_operation(self: ClimateDevice) -> str:
"""Return current operation ie. heat, cool, idle.""" """Return current operation ie. heat, cool, idle."""
return getattr(self._device, ATTR_SYSTEM_MODE, None) oper = getattr(self._device, ATTR_CURRENT_OPERATION, None)
if oper == "off":
oper = "idle"
return oper
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set target temperature.""" """Set target temperature."""
@ -245,29 +270,84 @@ class HoneywellUSThermostat(ClimateDevice):
return return
import somecomfort import somecomfort
try: try:
if self._device.system_mode == 'cool': # Get current mode
self._device.setpoint_cool = temperature mode = self._device.system_mode
else: # Set hold if this is not the case
self._device.setpoint_heat = temperature if getattr(self._device, "hold_{}".format(mode)) is False:
# Get next period key
next_period_key = '{}NextPeriod'.format(mode.capitalize())
# Get next period raw value
next_period = self._device.raw_ui_data.get(next_period_key)
# Get next period time
hour, minute = divmod(next_period * 15, 60)
# Set hold time
setattr(self._device,
"hold_{}".format(mode),
datetime.time(hour, minute))
# Set temperature
setattr(self._device,
"setpoint_{}".format(mode),
temperature)
except somecomfort.SomeComfortError: except somecomfort.SomeComfortError:
_LOGGER.error('Temperature %.1f out of range', temperature) _LOGGER.error('Temperature %.1f out of range', temperature)
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the device specific state attributes.""" """Return the device specific state attributes."""
return { import somecomfort
data = {
ATTR_FAN: (self.is_fan_on and 'running' or 'idle'), ATTR_FAN: (self.is_fan_on and 'running' or 'idle'),
ATTR_FANMODE: self._device.fan_mode, ATTR_FAN_MODE: self._device.fan_mode,
ATTR_SYSTEM_MODE: self._device.system_mode, ATTR_OPERATION_MODE: self._device.system_mode,
} }
data[ATTR_FAN_LIST] = somecomfort.FAN_MODES
data[ATTR_OPERATION_LIST] = somecomfort.SYSTEM_MODES
return data
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._away
def turn_away_mode_on(self): def turn_away_mode_on(self):
"""Turn away on.""" """Turn away on.
pass
Somecomfort does have a proprietary away mode, but it doesn't really
work the way it should. For example: If you set a temperature manually
it doesn't get overwritten when away mode is switched on.
"""
self._away = True
import somecomfort
try:
# Get current mode
mode = self._device.system_mode
except somecomfort.SomeComfortError:
_LOGGER.error('Can not get system mode')
return
try:
# Set permanent hold
setattr(self._device,
"hold_{}".format(mode),
True)
# Set temperature
setattr(self._device,
"setpoint_{}".format(mode),
getattr(self, "_{}_away_temp".format(mode)))
except somecomfort.SomeComfortError:
_LOGGER.error('Temperature %.1f out of range',
getattr(self, "_{}_away_temp".format(mode)))
def turn_away_mode_off(self): def turn_away_mode_off(self):
"""Turn away off.""" """Turn away off."""
pass self._away = False
import somecomfort
try:
# Disabling all hold modes
self._device.hold_cool = False
self._device.hold_heat = False
except somecomfort.SomeComfortError:
_LOGGER.error('Can not stop hold mode')
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None: def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
"""Set the system mode (Cool, Heat, etc).""" """Set the system mode (Cool, Heat, etc)."""
@ -276,4 +356,49 @@ class HoneywellUSThermostat(ClimateDevice):
def update(self): def update(self):
"""Update the state.""" """Update the state."""
self._device.refresh() import somecomfort
retries = 3
while retries > 0:
try:
self._device.refresh()
break
except (somecomfort.client.APIRateLimited, OSError,
requests.exceptions.ReadTimeout) as exp:
retries -= 1
if retries == 0:
raise exp
if not self._retry():
raise exp
_LOGGER.error("SomeComfort update failed, Retrying "
"- Error: %s", exp)
def _retry(self):
"""Recreate a new somecomfort client.
When we got an error, the best way to be sure that the next query
will succeed, is to recreate a new somecomfort client.
"""
import somecomfort
try:
self._client = somecomfort.SomeComfort(self._username,
self._password)
except somecomfort.AuthError:
_LOGGER.error('Failed to login to honeywell account %s',
self._username)
return False
except somecomfort.SomeComfortError as ex:
_LOGGER.error('Failed to initialize honeywell client: %s',
str(ex))
return False
devices = [device
for location in self._client.locations_by_id.values()
for device in location.devices_by_id.values()
if device.name == self._device.name]
if len(devices) != 1:
_LOGGER.error('Failed to find device %s', self._device.name)
return False
self._device = devices[0]
return True

View File

@ -0,0 +1,216 @@
"""
Support for MAX! Thermostats via MAX! Cube.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/maxcube/
"""
import socket
import logging
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
from homeassistant.components.maxcube import MAXCUBE_HANDLE
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__)
STATE_MANUAL = "manual"
STATE_BOOST = "boost"
STATE_VACATION = "vacation"
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Iterate through all MAX! Devices and add thermostats to HASS."""
cube = hass.data[MAXCUBE_HANDLE].cube
# List of devices
devices = []
for device in cube.devices:
# Create device name by concatenating room name + device name
name = "%s %s" % (cube.room_by_id(device.room_id).name, device.name)
# Only add thermostats and wallthermostats
if cube.is_thermostat(device) or cube.is_wallthermostat(device):
# Add device to HASS
devices.append(MaxCubeClimate(hass, name, device.rf_address))
# Add all devices at once
if len(devices) > 0:
add_devices(devices)
class MaxCubeClimate(ClimateDevice):
"""MAX! Cube ClimateDevice."""
def __init__(self, hass, name, rf_address):
"""Initialize MAX! Cube ClimateDevice."""
self._name = name
self._unit_of_measurement = TEMP_CELSIUS
self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST,
STATE_VACATION]
self._rf_address = rf_address
self._cubehandle = hass.data[MAXCUBE_HANDLE]
@property
def should_poll(self):
"""Polling is required."""
return True
@property
def name(self):
"""Return the name of the ClimateDevice."""
return self._name
@property
def min_temp(self):
"""Return the minimum temperature."""
# Get the device we want (does not do any IO, just reads from memory)
device = self._cubehandle.cube.device_by_rf(self._rf_address)
# Map and return minimum temperature
return self.map_temperature_max_hass(device.min_temperature)
@property
def max_temp(self):
"""Return the maximum temperature."""
# Get the device we want (does not do any IO, just reads from memory)
device = self._cubehandle.cube.device_by_rf(self._rf_address)
# Map and return maximum temperature
return self.map_temperature_max_hass(device.max_temperature)
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def current_temperature(self):
"""Return the current temperature."""
# Get the device we want (does not do any IO, just reads from memory)
device = self._cubehandle.cube.device_by_rf(self._rf_address)
# Map and return current temperature
return self.map_temperature_max_hass(device.actual_temperature)
@property
def current_operation(self):
"""Return current operation (auto, manual, boost, vacation)."""
# Get the device we want (does not do any IO, just reads from memory)
device = self._cubehandle.cube.device_by_rf(self._rf_address)
# Mode Mapping
return self.map_mode_max_hass(device.mode)
@property
def operation_list(self):
"""List of available operation modes."""
return self._operation_list
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
# Get the device we want (does not do any IO, just reads from memory)
device = self._cubehandle.cube.device_by_rf(self._rf_address)
# Map and return target temperature
return self.map_temperature_max_hass(device.target_temperature)
def set_temperature(self, **kwargs):
"""Set new target temperatures."""
# Fail is target temperature has not been supplied as argument
if kwargs.get(ATTR_TEMPERATURE) is None:
return False
# Determine the new target temperature
target_temperature = kwargs.get(ATTR_TEMPERATURE)
# Write the target temperature to the MAX! Cube.
device = self._cubehandle.cube.device_by_rf(self._rf_address)
cube = self._cubehandle.cube
with self._cubehandle.mutex:
try:
cube.set_target_temperature(device, target_temperature)
except (socket.timeout, socket.error):
_LOGGER.error("Setting target temperature failed")
return False
def set_operation_mode(self, operation_mode):
"""Set new operation mode."""
# Get the device we want to update
device = self._cubehandle.cube.device_by_rf(self._rf_address)
# Mode Mapping
mode = self.map_mode_hass_max(operation_mode)
# Write new mode to thermostat
if mode is None:
return False
with self._cubehandle.mutex:
try:
self._cubehandle.cube.set_mode(device, mode)
except (socket.timeout, socket.error):
_LOGGER.error("Setting operation mode failed")
return False
def update(self):
"""Get latest data from MAX! Cube."""
# Update the CubeHandle
self._cubehandle.update()
@staticmethod
def map_temperature_max_hass(temperature):
"""Map Temperature from MAX! to HASS."""
if temperature is None:
return STATE_UNKNOWN
return temperature
@staticmethod
def map_mode_hass_max(operation_mode):
"""Map HASS Operation Modes to MAX! Operation Modes."""
from maxcube.device import \
MAX_DEVICE_MODE_AUTOMATIC, \
MAX_DEVICE_MODE_MANUAL, \
MAX_DEVICE_MODE_VACATION, \
MAX_DEVICE_MODE_BOOST
if operation_mode == STATE_AUTO:
mode = MAX_DEVICE_MODE_AUTOMATIC
elif operation_mode == STATE_MANUAL:
mode = MAX_DEVICE_MODE_MANUAL
elif operation_mode == STATE_VACATION:
mode = MAX_DEVICE_MODE_VACATION
elif operation_mode == STATE_BOOST:
mode = MAX_DEVICE_MODE_BOOST
else:
mode = None
return mode
@staticmethod
def map_mode_max_hass(mode):
"""Map MAX! Operation Modes to HASS Operation Modes."""
from maxcube.device import \
MAX_DEVICE_MODE_AUTOMATIC, \
MAX_DEVICE_MODE_MANUAL, \
MAX_DEVICE_MODE_VACATION, \
MAX_DEVICE_MODE_BOOST
if mode == MAX_DEVICE_MODE_AUTOMATIC:
operation_mode = STATE_AUTO
elif mode == MAX_DEVICE_MODE_MANUAL:
operation_mode = STATE_MANUAL
elif mode == MAX_DEVICE_MODE_VACATION:
operation_mode = STATE_VACATION
elif mode == MAX_DEVICE_MODE_BOOST:
operation_mode = STATE_BOOST
else:
operation_mode = None
return operation_mode

View File

@ -7,14 +7,14 @@ https://home-assistant.io/components/switch.vera/
import logging import logging
from homeassistant.util import convert from homeassistant.util import convert
from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT
from homeassistant.const import ( from homeassistant.const import (
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
TEMP_CELSIUS, TEMP_CELSIUS,
ATTR_TEMPERATURE) ATTR_TEMPERATURE)
from homeassistant.components.vera import ( from homeassistant.components.vera import (
VeraDevice, VERA_DEVICES, VERA_CONTROLLER) VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
DEPENDENCIES = ['vera'] DEPENDENCIES = ['vera']
@ -37,6 +37,7 @@ class VeraThermostat(VeraDevice, ClimateDevice):
def __init__(self, vera_device, controller): def __init__(self, vera_device, controller):
"""Initialize the Vera device.""" """Initialize the Vera device."""
VeraDevice.__init__(self, vera_device, controller) VeraDevice.__init__(self, vera_device, controller)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property @property
def current_operation(self): def current_operation(self):

View File

@ -11,6 +11,7 @@ from homeassistant.components.climate import DOMAIN
from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate import ClimateDevice
from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
@ -32,19 +33,10 @@ DEVICE_MAPPINGS = {
} }
def setup_platform(hass, config, add_devices, discovery_info=None): def get_device(hass, value, **kwargs):
"""Set up the Z-Wave Climate devices.""" """Create zwave entity device."""
if discovery_info is None or zwave.NETWORK is None:
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
discovery_info, zwave.NETWORK)
return
temp_unit = hass.config.units.temperature_unit temp_unit = hass.config.units.temperature_unit
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] return ZWaveClimate(value, temp_unit)
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)
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
@ -224,7 +216,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
self.set_value( self.set_value(
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT, class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT,
index=self._index, data=temperature) index=self._index, data=temperature)
self.update_ha_state() self.schedule_update_ha_state()
def set_fan_mode(self, fan): def set_fan_mode(self, fan):
"""Set new target fan mode.""" """Set new target fan mode."""
@ -254,3 +246,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
if self._fan_state: if self._fan_state:
data[ATTR_FAN_STATE] = self._fan_state data[ATTR_FAN_STATE] = self._fan_state
return data return data
@property
def dependent_value_ids(self):
"""List of value IDs a device depends on."""
return None

View File

@ -6,7 +6,7 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.bootstrap import ( from homeassistant.setup import (
async_prepare_setup_platform, ATTR_COMPONENT) async_prepare_setup_platform, ATTR_COMPONENT)
from homeassistant.components.frontend import register_built_in_panel from homeassistant.components.frontend import register_built_in_panel
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView

View File

@ -54,7 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
yield from async_add_devices([MqttCover( async_add_devices([MqttCover(
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC), config.get(CONF_STATE_TOPIC),
config.get(CONF_COMMAND_TOPIC), config.get(CONF_COMMAND_TOPIC),

View File

@ -14,8 +14,8 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = [ REQUIREMENTS = [
'https://github.com/arraylabs/pymyq/archive/v0.0.6.zip' 'https://github.com/arraylabs/pymyq/archive/v0.0.7.zip'
'#pymyq==0.0.6'] '#pymyq==0.0.7']
COVER_SCHEMA = vol.Schema({ COVER_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): cv.string, vol.Required(CONF_TYPE): cv.string,

View File

@ -6,9 +6,9 @@ https://home-assistant.io/components/cover.vera/
""" """
import logging import logging
from homeassistant.components.cover import CoverDevice from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT
from homeassistant.components.vera import ( from homeassistant.components.vera import (
VeraDevice, VERA_DEVICES, VERA_CONTROLLER) VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
DEPENDENCIES = ['vera'] DEPENDENCIES = ['vera']
@ -28,6 +28,7 @@ class VeraCover(VeraDevice, CoverDevice):
def __init__(self, vera_device, controller): def __init__(self, vera_device, controller):
"""Initialize the Vera device.""" """Initialize the Vera device."""
VeraDevice.__init__(self, vera_device, controller) VeraDevice.__init__(self, vera_device, controller)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property @property
def current_cover_position(self): def current_cover_position(self):

View File

@ -11,6 +11,7 @@ from homeassistant.components.cover import (
DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE) DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.components.zwave import workaround from homeassistant.components.zwave import workaround
from homeassistant.components.cover import CoverDevice from homeassistant.components.cover import CoverDevice
@ -19,27 +20,15 @@ _LOGGER = logging.getLogger(__name__)
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
def setup_platform(hass, config, add_devices, discovery_info=None): def get_device(value, **kwargs):
"""Find and return Z-Wave covers.""" """Create zwave entity device."""
if discovery_info is None or zwave.NETWORK is None:
return
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.const.COMMAND_CLASS_SWITCH_MULTILEVEL if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
and value.index == 0): and value.index == 0):
value.set_change_verified(False) return ZwaveRollershutter(value)
add_devices([ZwaveRollershutter(value)])
elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR): value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
if (value.type != zwave.const.TYPE_BOOL and return ZwaveGarageDoor(value)
value.genre != zwave.const.GENRE_USER): return None
return
value.set_change_verified(False)
add_devices([ZwaveGarageDoor(value)])
else:
return
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
@ -52,6 +41,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
self._node = value.node self._node = value.node
self._open_id = None self._open_id = None
self._close_id = None self._close_id = None
self._current_position_id = None
self._current_position = None self._current_position = None
self._workaround = workaround.get_device_mapping(value) self._workaround = workaround.get_device_mapping(value)
@ -59,20 +49,35 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
_LOGGER.debug("Using workaround %s", self._workaround) _LOGGER.debug("Using workaround %s", self._workaround)
self.update_properties() self.update_properties()
@property
def dependent_value_ids(self):
"""List of value IDs a device depends on."""
if not self._node.is_ready:
return None
return [self._current_position_id]
def update_properties(self): def update_properties(self):
"""Callback on data changes for node values.""" """Callback on data changes for node values."""
# Position value # Position value
self._current_position = self.get_value( if not self._node.is_ready:
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL, if self._current_position_id is None:
label=['Level'], member='data') self._current_position_id = self.get_value(
self._open_id = self.get_value( class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL, label=['Level'], member='value_id')
label=['Open', 'Up'], member='value_id') if self._open_id is None:
self._close_id = self.get_value( self._open_id = self.get_value(
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL, class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
label=['Close', 'Down'], member='value_id') label=['Open', 'Up'], member='value_id')
if self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE: if self._close_id is None:
self._close_id = self.get_value(
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
label=['Close', 'Down'], member='value_id')
if self._open_id and self._close_id and \
self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE:
self._open_id, self._close_id = self._close_id, self._open_id self._open_id, self._close_id = self._close_id, self._open_id
self._workaround = None
self._current_position = self._node.get_dimmer_level(
self._current_position_id)
@property @property
def is_closed(self): def is_closed(self):

View File

@ -4,6 +4,7 @@ Sets up a demo environment that mimics interaction with devices.
For more details about this component, please refer to the documentation For more details about this component, please refer to the documentation
https://home-assistant.io/components/demo/ https://home-assistant.io/components/demo/
""" """
import asyncio
import time import time
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
@ -34,7 +35,8 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
] ]
def setup(hass, config): @asyncio.coroutine
def async_setup(hass, config):
"""Setup a demo environment.""" """Setup a demo environment."""
group = loader.get_component('group') group = loader.get_component('group')
configurator = loader.get_component('configurator') configurator = loader.get_component('configurator')
@ -44,7 +46,7 @@ def setup(hass, config):
config.setdefault(DOMAIN, {}) config.setdefault(DOMAIN, {})
if config[DOMAIN].get('hide_demo_state') != 1: if config[DOMAIN].get('hide_demo_state') != 1:
hass.states.set('a.Demo_Mode', 'Enabled') hass.states.async_set('a.Demo_Mode', 'Enabled')
# Setup sun # Setup sun
if not hass.config.latitude: if not hass.config.latitude:
@ -53,50 +55,71 @@ def setup(hass, config):
if not hass.config.longitude: if not hass.config.longitude:
hass.config.longitude = 117.22743 hass.config.longitude = 117.22743
bootstrap.setup_component(hass, 'sun') tasks = [
bootstrap.async_setup_component(hass, 'sun')
]
# Setup demo platforms # Setup demo platforms
demo_config = config.copy() demo_config = config.copy()
for component in COMPONENTS_WITH_DEMO_PLATFORM: for component in COMPONENTS_WITH_DEMO_PLATFORM:
demo_config[component] = {CONF_PLATFORM: 'demo'} demo_config[component] = {CONF_PLATFORM: 'demo'}
bootstrap.setup_component(hass, component, demo_config) tasks.append(
bootstrap.async_setup_component(hass, component, demo_config))
# Set up input select
tasks.append(bootstrap.async_setup_component(
hass, 'input_select',
{'input_select':
{'living_room_preset': {'options': ['Visitors',
'Visitors with kids',
'Home Alone']},
'who_cooks': {'icon': 'mdi:panda',
'initial': 'Anne Therese',
'name': 'Cook today',
'options': ['Paulus', 'Anne Therese']}}}))
# Set up input boolean
tasks.append(bootstrap.async_setup_component(
hass, 'input_boolean',
{'input_boolean': {'notify': {
'icon': 'mdi:car',
'initial': False,
'name': 'Notify Anne Therese is home'}}}))
# Set up input boolean
tasks.append(bootstrap.async_setup_component(
hass, 'input_slider',
{'input_slider': {
'noise_allowance': {'icon': 'mdi:bell-ring',
'min': 0,
'max': 10,
'name': 'Allowed Noise',
'unit_of_measurement': 'dB'}}}))
# Set up weblink
tasks.append(bootstrap.async_setup_component(
hass, 'weblink',
{'weblink': {'entities': [{'name': 'Router',
'url': 'http://192.168.1.1'}]}}))
results = yield from asyncio.gather(*tasks, loop=hass.loop)
if any(not result for result in results):
return False
# Setup example persistent notification # Setup example persistent notification
persistent_notification.create( persistent_notification.async_create(
hass, 'This is an example of a persistent notification.', hass, 'This is an example of a persistent notification.',
title='Example Notification') title='Example Notification')
# Setup room groups # Setup room groups
lights = sorted(hass.states.entity_ids('light')) lights = sorted(hass.states.async_entity_ids('light'))
switches = sorted(hass.states.entity_ids('switch')) switches = sorted(hass.states.async_entity_ids('switch'))
media_players = sorted(hass.states.entity_ids('media_player')) media_players = sorted(hass.states.async_entity_ids('media_player'))
group.Group.create_group(hass, 'living room', [ tasks2 = []
lights[1], switches[0], 'input_select.living_room_preset',
'rollershutter.living_room_window', media_players[1],
'scene.romantic_lights'])
group.Group.create_group(hass, 'bedroom', [
lights[0], switches[1], media_players[0],
'input_slider.noise_allowance'])
group.Group.create_group(hass, 'kitchen', [
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door'])
group.Group.create_group(hass, 'doors', [
'lock.front_door', 'lock.kitchen_door',
'garage_door.right_garage_door', 'garage_door.left_garage_door'])
group.Group.create_group(hass, 'automations', [
'input_select.who_cooks', 'input_boolean.notify', ])
group.Group.create_group(hass, 'people', [
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
'device_tracker.demo_paulus'])
group.Group.create_group(hass, 'downstairs', [
'group.living_room', 'group.kitchen',
'scene.romantic_lights', 'rollershutter.kitchen_window',
'rollershutter.living_room_window', 'group.doors',
'thermostat.ecobee',
], view=True)
# Setup scripts # Setup scripts
bootstrap.setup_component( tasks2.append(bootstrap.async_setup_component(
hass, 'script', hass, 'script',
{'script': { {'script': {
'demo': { 'demo': {
@ -115,10 +138,10 @@ def setup(hass, config):
'service': 'light.turn_off', 'service': 'light.turn_off',
'data': {ATTR_ENTITY_ID: lights[0]} 'data': {ATTR_ENTITY_ID: lights[0]}
}] }]
}}}) }}}))
# Setup scenes # Setup scenes
bootstrap.setup_component( tasks2.append(bootstrap.async_setup_component(
hass, 'scene', hass, 'scene',
{'scene': [ {'scene': [
{'name': 'Romantic lights', {'name': 'Romantic lights',
@ -132,41 +155,37 @@ def setup(hass, config):
switches[0]: True, switches[0]: True,
switches[1]: False, switches[1]: False,
}}, }},
]}) ]}))
# Set up input select tasks2.append(group.Group.async_create_group(hass, 'living room', [
bootstrap.setup_component( lights[1], switches[0], 'input_select.living_room_preset',
hass, 'input_select', 'rollershutter.living_room_window', media_players[1],
{'input_select': 'scene.romantic_lights']))
{'living_room_preset': {'options': ['Visitors', tasks2.append(group.Group.async_create_group(hass, 'bedroom', [
'Visitors with kids', lights[0], switches[1], media_players[0],
'Home Alone']}, 'input_slider.noise_allowance']))
'who_cooks': {'icon': 'mdi:panda', tasks2.append(group.Group.async_create_group(hass, 'kitchen', [
'initial': 'Anne Therese', lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door']))
'name': 'Cook today', tasks2.append(group.Group.async_create_group(hass, 'doors', [
'options': ['Paulus', 'Anne Therese']}}}) 'lock.front_door', 'lock.kitchen_door',
# Set up input boolean 'garage_door.right_garage_door', 'garage_door.left_garage_door']))
bootstrap.setup_component( tasks2.append(group.Group.async_create_group(hass, 'automations', [
hass, 'input_boolean', 'input_select.who_cooks', 'input_boolean.notify', ]))
{'input_boolean': {'notify': {'icon': 'mdi:car', tasks2.append(group.Group.async_create_group(hass, 'people', [
'initial': False, 'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
'name': 'Notify Anne Therese is home'}}}) 'device_tracker.demo_paulus']))
tasks2.append(group.Group.async_create_group(hass, 'downstairs', [
'group.living_room', 'group.kitchen',
'scene.romantic_lights', 'rollershutter.kitchen_window',
'rollershutter.living_room_window', 'group.doors',
'thermostat.ecobee',
], view=True))
# Set up input boolean results = yield from asyncio.gather(*tasks2, loop=hass.loop)
bootstrap.setup_component(
hass, 'input_slider', if any(not result for result in results):
{'input_slider': { return False
'noise_allowance': {'icon': 'mdi:bell-ring',
'min': 0,
'max': 10,
'name': 'Allowed Noise',
'unit_of_measurement': 'dB'}}})
# Set up weblink
bootstrap.setup_component(
hass, 'weblink',
{'weblink': {'entities': [{'name': 'Router',
'url': 'http://192.168.1.1'}]}})
# Setup configurator # Setup configurator
configurator_ids = [] configurator_ids = []
@ -184,14 +203,17 @@ def setup(hass, config):
else: else:
configurator.request_done(configurator_ids[0]) configurator.request_done(configurator_ids[0])
request_id = configurator.request_config( def setup_configurator():
hass, "Philips Hue", hue_configuration_callback, """Setup configurator."""
description=("Press the button on the bridge to register Philips Hue " request_id = configurator.request_config(
"with Home Assistant."), hass, "Philips Hue", hue_configuration_callback,
description_image="/static/images/config_philips_hue.jpg", description=("Press the button on the bridge to register Philips "
submit_caption="I have pressed the button" "Hue with Home Assistant."),
) description_image="/static/images/config_philips_hue.jpg",
submit_caption="I have pressed the button"
)
configurator_ids.append(request_id)
configurator_ids.append(request_id) hass.async_add_job(setup_configurator)
return True return True

View File

@ -14,12 +14,11 @@ import aiohttp
import async_timeout import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.bootstrap import ( from homeassistant.setup import async_prepare_setup_platform
async_prepare_setup_platform, async_log_exception)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components import group, zone from homeassistant.components import group, zone
from homeassistant.components.discovery import SERVICE_NETGEAR from homeassistant.components.discovery import SERVICE_NETGEAR
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers import config_per_platform, discovery
@ -133,18 +132,6 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
devices = yield from async_load_config(yaml_path, hass, consider_home) devices = yield from async_load_config(yaml_path, hass, consider_home)
tracker = DeviceTracker(hass, consider_home, track_new, devices) tracker = DeviceTracker(hass, consider_home, track_new, devices)
# added_to_hass
add_tasks = [device.async_added_to_hass() for device in devices
if device.track]
if add_tasks:
yield from asyncio.wait(add_tasks, loop=hass.loop)
# update tracked devices
update_tasks = [device.async_update_ha_state() for device in devices
if device.track]
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(p_type, p_config, disc_info=None): def async_setup_platform(p_type, p_config, disc_info=None):
"""Setup a device tracker platform.""" """Setup a device tracker platform."""
@ -227,6 +214,8 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE)) DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE))
# restore
yield from tracker.async_setup_tracked_device()
return True return True
@ -357,6 +346,27 @@ class DeviceTracker(object):
device.stale(now): device.stale(now):
self.hass.async_add_job(device.async_update_ha_state(True)) self.hass.async_add_job(device.async_update_ha_state(True))
@asyncio.coroutine
def async_setup_tracked_device(self):
"""Setup all not exists tracked devices.
This method is a coroutine.
"""
@asyncio.coroutine
def async_init_single_device(dev):
"""Init a single device_tracker entity."""
yield from dev.async_added_to_hass()
yield from dev.async_update_ha_state()
tasks = []
for device in self.devices.values():
if device.track and not device.last_seen:
tasks.append(self.hass.async_add_job(
async_init_single_device(device)))
if tasks:
yield from asyncio.wait(tasks, loop=self.hass.loop)
class Device(Entity): class Device(Entity):
"""Represent a tracked device.""" """Represent a tracked device."""

View File

@ -110,7 +110,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
_LOGGER.info("Discovered Bluetooth LE device %s", address) _LOGGER.info("Discovered Bluetooth LE device %s", address)
see_device(address, devs[address], new_device=True) see_device(address, devs[address], new_device=True)
track_point_in_utc_time(hass, update_ble, now + interval) track_point_in_utc_time(hass, update_ble, dt_util.utcnow() + interval)
update_ble(dt_util.utcnow()) update_ble(dt_util.utcnow())

View File

@ -82,7 +82,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
see_device((mac, result)) see_device((mac, result))
except bluetooth.BluetoothError: except bluetooth.BluetoothError:
_LOGGER.exception('Error looking up bluetooth device!') _LOGGER.exception('Error looking up bluetooth device!')
track_point_in_utc_time(hass, update_bluetooth, now + interval) track_point_in_utc_time(
hass, update_bluetooth, dt_util.utcnow() + interval)
update_bluetooth(dt_util.utcnow()) update_bluetooth(dt_util.utcnow())

View File

@ -197,13 +197,22 @@ class Icloud(DeviceScanner):
def icloud_trusted_device_callback(self, callback_data): def icloud_trusted_device_callback(self, callback_data):
"""The trusted device is chosen.""" """The trusted device is chosen."""
self._trusted_device = int(callback_data.get('0', '0')) self._trusted_device = int(callback_data.get('trusted_device'))
self._trusted_device = self.api.trusted_devices[self._trusted_device] self._trusted_device = self.api.trusted_devices[self._trusted_device]
if not self.api.send_verification_code(self._trusted_device):
_LOGGER.error('Failed to send verification code')
self._trusted_device = None
return
if self.accountname in _CONFIGURING: if self.accountname in _CONFIGURING:
request_id = _CONFIGURING.pop(self.accountname) request_id = _CONFIGURING.pop(self.accountname)
configurator = get_component('configurator') configurator = get_component('configurator')
configurator.request_done(request_id) configurator.request_done(request_id)
# Trigger the next step immediately
self.icloud_need_verification_code()
def icloud_need_trusted_device(self): def icloud_need_trusted_device(self):
"""We need a trusted device.""" """We need a trusted device."""
configurator = get_component('configurator') configurator = get_component('configurator')
@ -213,7 +222,10 @@ class Icloud(DeviceScanner):
devicesstring = '' devicesstring = ''
devices = self.api.trusted_devices devices = self.api.trusted_devices
for i, device in enumerate(devices): for i, device in enumerate(devices):
devicesstring += "{}: {};".format(i, device.get('deviceName')) devicename = device.get(
'deviceName',
'SMS to %s' % device.get('phoneNumber'))
devicesstring += "{}: {};".format(i, devicename)
_CONFIGURING[self.accountname] = configurator.request_config( _CONFIGURING[self.accountname] = configurator.request_config(
self.hass, 'iCloud {}'.format(self.accountname), self.hass, 'iCloud {}'.format(self.accountname),
@ -223,12 +235,27 @@ class Icloud(DeviceScanner):
' the index from this list: ' + devicesstring), ' the index from this list: ' + devicesstring),
entity_picture="/static/images/config_icloud.png", entity_picture="/static/images/config_icloud.png",
submit_caption='Confirm', submit_caption='Confirm',
fields=[{'id': '0'}] fields=[{'id': 'trusted_device', 'name': 'Trusted Device'}]
) )
def icloud_verification_callback(self, callback_data): def icloud_verification_callback(self, callback_data):
"""The trusted device is chosen.""" """The trusted device is chosen."""
self._verification_code = callback_data.get('0') from pyicloud.exceptions import PyiCloudException
self._verification_code = callback_data.get('code')
try:
if not self.api.validate_verification_code(
self._trusted_device, self._verification_code):
raise PyiCloudException('Unknown failure')
except PyiCloudException as error:
# Reset to the inital 2FA state to allow the user to retry
_LOGGER.error('Failed to verify verification code: %s', error)
self._trusted_device = None
self._verification_code = None
# Trigger the next step immediately
self.icloud_need_trusted_device()
if self.accountname in _CONFIGURING: if self.accountname in _CONFIGURING:
request_id = _CONFIGURING.pop(self.accountname) request_id = _CONFIGURING.pop(self.accountname)
configurator = get_component('configurator') configurator = get_component('configurator')
@ -240,22 +267,17 @@ class Icloud(DeviceScanner):
if self.accountname in _CONFIGURING: if self.accountname in _CONFIGURING:
return return
if self.api.send_verification_code(self._trusted_device):
self._verification_code = 'waiting'
_CONFIGURING[self.accountname] = configurator.request_config( _CONFIGURING[self.accountname] = configurator.request_config(
self.hass, 'iCloud {}'.format(self.accountname), self.hass, 'iCloud {}'.format(self.accountname),
self.icloud_verification_callback, self.icloud_verification_callback,
description=('Please enter the validation code:'), description=('Please enter the validation code:'),
entity_picture="/static/images/config_icloud.png", entity_picture="/static/images/config_icloud.png",
submit_caption='Confirm', submit_caption='Confirm',
fields=[{'code': '0'}] fields=[{'id': 'code', 'name': 'code'}]
) )
def keep_alive(self, now): def keep_alive(self, now):
"""Keep the api alive.""" """Keep the api alive."""
from pyicloud.exceptions import PyiCloud2FARequiredError
if self.api is None: if self.api is None:
self.reset_account_icloud() self.reset_account_icloud()
@ -263,9 +285,8 @@ class Icloud(DeviceScanner):
return return
if self.api.requires_2fa: if self.api.requires_2fa:
from pyicloud.exceptions import PyiCloudException
try: try:
self.api.authenticate()
except PyiCloud2FARequiredError:
if self._trusted_device is None: if self._trusted_device is None:
self.icloud_need_trusted_device() self.icloud_need_trusted_device()
return return
@ -274,12 +295,14 @@ class Icloud(DeviceScanner):
self.icloud_need_verification_code() self.icloud_need_verification_code()
return return
if self._verification_code == 'waiting': self.api.authenticate()
return if self.api.requires_2fa:
raise Exception('Unknown failure')
if self.api.validate_verification_code( self._trusted_device = None
self._trusted_device, self._verification_code): self._verification_code = None
self._verification_code = None except PyiCloudException as error:
_LOGGER.error("Error setting up 2fa: %s", error)
else: else:
self.api.authenticate() self.api.authenticate()
@ -397,13 +420,13 @@ class Icloud(DeviceScanner):
try: try:
if devicename is not None: if devicename is not None:
if devicename in self.devices: if devicename in self.devices:
self.devices[devicename].update_icloud() self.devices[devicename].location()
else: else:
_LOGGER.error("devicename %s unknown for account %s", _LOGGER.error("devicename %s unknown for account %s",
devicename, self._attrs[ATTR_ACCOUNTNAME]) devicename, self._attrs[ATTR_ACCOUNTNAME])
else: else:
for device in self.devices: for device in self.devices:
self.devices[device].update_icloud() self.devices[device].location()
except PyiCloudNoDevicesException: except PyiCloudNoDevicesException:
_LOGGER.error('No iCloud Devices found!') _LOGGER.error('No iCloud Devices found!')

View File

@ -4,11 +4,13 @@ Support for tracking MQTT enabled devices.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.mqtt/ https://home-assistant.io/components/device_tracker.mqtt/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.core import callback
from homeassistant.const import CONF_DEVICES from homeassistant.const import CONF_DEVICES
from homeassistant.components.mqtt import CONF_QOS from homeassistant.components.mqtt import CONF_QOS
from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker import PLATFORM_SCHEMA
@ -23,19 +25,23 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({
}) })
def setup_scanner(hass, config, see, discovery_info=None): @asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Setup the MQTT tracker.""" """Setup the MQTT tracker."""
devices = config[CONF_DEVICES] devices = config[CONF_DEVICES]
qos = config[CONF_QOS] qos = config[CONF_QOS]
dev_id_lookup = {} dev_id_lookup = {}
def device_tracker_message_received(topic, payload, qos): @callback
def async_tracker_message_received(topic, payload, qos):
"""MQTT message received.""" """MQTT message received."""
see(dev_id=dev_id_lookup[topic], location_name=payload) hass.async_add_job(
async_see(dev_id=dev_id_lookup[topic], location_name=payload))
for dev_id, topic in devices.items(): for dev_id, topic in devices.items():
dev_id_lookup[topic] = dev_id dev_id_lookup[topic] = dev_id
mqtt.subscribe(hass, topic, device_tracker_message_received, qos) yield from mqtt.async_subscribe(
hass, topic, async_tracker_message_received, qos)
return True return True

View File

@ -4,14 +4,15 @@ Support the OwnTracks platform.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.owntracks/ https://home-assistant.io/components/device_tracker.owntracks/
""" """
import asyncio
import json import json
import logging import logging
import threading
import base64 import base64
from collections import defaultdict from collections import defaultdict
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import STATE_HOME from homeassistant.const import STATE_HOME
@ -19,6 +20,7 @@ from homeassistant.util import convert, slugify
from homeassistant.components import zone as zone_comp from homeassistant.components import zone as zone_comp
from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker import PLATFORM_SCHEMA
DEPENDENCIES = ['mqtt']
REQUIREMENTS = ['libnacl==1.5.0'] REQUIREMENTS = ['libnacl==1.5.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,16 +32,9 @@ CONF_SECRET = 'secret'
CONF_WAYPOINT_IMPORT = 'waypoints' CONF_WAYPOINT_IMPORT = 'waypoints'
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist' CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
DEPENDENCIES = ['mqtt']
EVENT_TOPIC = 'owntracks/+/+/event' EVENT_TOPIC = 'owntracks/+/+/event'
LOCATION_TOPIC = 'owntracks/+/+' LOCATION_TOPIC = 'owntracks/+/+'
LOCK = threading.Lock()
MOBILE_BEACONS_ACTIVE = defaultdict(list)
REGIONS_ENTERED = defaultdict(list)
VALIDATE_LOCATION = 'location' VALIDATE_LOCATION = 'location'
VALIDATE_TRANSITION = 'transition' VALIDATE_TRANSITION = 'transition'
@ -61,7 +56,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def get_cipher(): def get_cipher():
"""Return decryption function and length of key.""" """Return decryption function and length of key.
Async friendly.
"""
from libnacl import crypto_secretbox_KEYBYTES as KEYLEN from libnacl import crypto_secretbox_KEYBYTES as KEYLEN
from libnacl.secret import SecretBox from libnacl.secret import SecretBox
@ -71,13 +69,17 @@ def get_cipher():
return (KEYLEN, decrypt) return (KEYLEN, decrypt)
def setup_scanner(hass, config, see, discovery_info=None): @asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an OwnTracks tracker.""" """Set up an OwnTracks tracker."""
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY) max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
waypoint_import = config.get(CONF_WAYPOINT_IMPORT) waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST) waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
secret = config.get(CONF_SECRET) secret = config.get(CONF_SECRET)
mobile_beacons_active = defaultdict(list)
regions_entered = defaultdict(list)
def decrypt_payload(topic, ciphertext): def decrypt_payload(topic, ciphertext):
"""Decrypt encrypted payload.""" """Decrypt encrypted payload."""
try: try:
@ -154,7 +156,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
return data return data
def owntracks_location_update(topic, payload, qos): @callback
def async_owntracks_location_update(topic, payload, qos):
"""MQTT message received.""" """MQTT message received."""
# Docs on available data: # Docs on available data:
# http://owntracks.org/booklet/tech/json/#_typelocation # http://owntracks.org/booklet/tech/json/#_typelocation
@ -164,18 +167,17 @@ def setup_scanner(hass, config, see, discovery_info=None):
dev_id, kwargs = _parse_see_args(topic, data) dev_id, kwargs = _parse_see_args(topic, data)
# Block updates if we're in a region if regions_entered[dev_id]:
with LOCK: _LOGGER.debug(
if REGIONS_ENTERED[dev_id]: "location update ignored - inside region %s",
_LOGGER.debug( regions_entered[-1])
"location update ignored - inside region %s", return
REGIONS_ENTERED[-1])
return
see(**kwargs) hass.async_add_job(async_see(**kwargs))
see_beacons(dev_id, kwargs) async_see_beacons(dev_id, kwargs)
def owntracks_event_update(topic, payload, qos): @callback
def async_owntracks_event_update(topic, payload, qos):
"""MQTT event (geofences) received.""" """MQTT event (geofences) received."""
# Docs on available data: # Docs on available data:
# http://owntracks.org/booklet/tech/json/#_typetransition # http://owntracks.org/booklet/tech/json/#_typetransition
@ -199,67 +201,65 @@ def setup_scanner(hass, config, see, discovery_info=None):
def enter_event(): def enter_event():
"""Execute enter event.""" """Execute enter event."""
zone = hass.states.get("zone.{}".format(slugify(location))) zone = hass.states.get("zone.{}".format(slugify(location)))
with LOCK: if zone is None and data.get('t') == 'b':
if zone is None and data.get('t') == 'b': # Not a HA zone, and a beacon so assume mobile
# Not a HA zone, and a beacon so assume mobile beacons = mobile_beacons_active[dev_id]
beacons = MOBILE_BEACONS_ACTIVE[dev_id] if location not in beacons:
if location not in beacons: beacons.append(location)
beacons.append(location) _LOGGER.info("Added beacon %s", location)
_LOGGER.info("Added beacon %s", location) else:
else: # Normal region
# Normal region regions = regions_entered[dev_id]
regions = REGIONS_ENTERED[dev_id] if location not in regions:
if location not in regions: regions.append(location)
regions.append(location) _LOGGER.info("Enter region %s", location)
_LOGGER.info("Enter region %s", location) _set_gps_from_zone(kwargs, location, zone)
_set_gps_from_zone(kwargs, location, zone)
see(**kwargs) hass.async_add_job(async_see(**kwargs))
see_beacons(dev_id, kwargs) async_see_beacons(dev_id, kwargs)
def leave_event(): def leave_event():
"""Execute leave event.""" """Execute leave event."""
with LOCK: regions = regions_entered[dev_id]
regions = REGIONS_ENTERED[dev_id] if location in regions:
if location in regions: regions.remove(location)
regions.remove(location) new_region = regions[-1] if regions else None
new_region = regions[-1] if regions else None
if new_region: if new_region:
# Exit to previous region # Exit to previous region
zone = hass.states.get( zone = hass.states.get(
"zone.{}".format(slugify(new_region))) "zone.{}".format(slugify(new_region)))
_set_gps_from_zone(kwargs, new_region, zone) _set_gps_from_zone(kwargs, new_region, zone)
_LOGGER.info("Exit to %s", new_region) _LOGGER.info("Exit to %s", new_region)
see(**kwargs) hass.async_add_job(async_see(**kwargs))
see_beacons(dev_id, kwargs) async_see_beacons(dev_id, kwargs)
else: else:
_LOGGER.info("Exit to GPS") _LOGGER.info("Exit to GPS")
# Check for GPS accuracy # Check for GPS accuracy
valid_gps = True valid_gps = True
if 'acc' in data: if 'acc' in data:
if data['acc'] == 0.0: if data['acc'] == 0.0:
valid_gps = False valid_gps = False
_LOGGER.warning( _LOGGER.warning(
'Ignoring GPS in region exit because accuracy' 'Ignoring GPS in region exit because accuracy'
'is zero: %s', 'is zero: %s',
payload) payload)
if (max_gps_accuracy is not None and if (max_gps_accuracy is not None and
data['acc'] > max_gps_accuracy): data['acc'] > max_gps_accuracy):
valid_gps = False valid_gps = False
_LOGGER.info( _LOGGER.info(
'Ignoring GPS in region exit because expected ' 'Ignoring GPS in region exit because expected '
'GPS accuracy %s is not met: %s', 'GPS accuracy %s is not met: %s',
max_gps_accuracy, payload) max_gps_accuracy, payload)
if valid_gps: if valid_gps:
see(**kwargs) hass.async_add_job(async_see(**kwargs))
see_beacons(dev_id, kwargs) async_see_beacons(dev_id, kwargs)
beacons = MOBILE_BEACONS_ACTIVE[dev_id] beacons = mobile_beacons_active[dev_id]
if location in beacons: if location in beacons:
beacons.remove(location) beacons.remove(location)
_LOGGER.info("Remove beacon %s", location) _LOGGER.info("Remove beacon %s", location)
if data['event'] == 'enter': if data['event'] == 'enter':
enter_event() enter_event()
@ -271,7 +271,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
data['event']) data['event'])
return return
def owntracks_waypoint_update(topic, payload, qos): @callback
def async_owntracks_waypoint_update(topic, payload, qos):
"""List of waypoints published by a user.""" """List of waypoints published by a user."""
# Docs on available data: # Docs on available data:
# http://owntracks.org/booklet/tech/json/#_typewaypoints # http://owntracks.org/booklet/tech/json/#_typewaypoints
@ -298,36 +299,43 @@ def setup_scanner(hass, config, see, discovery_info=None):
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad, zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
zone_comp.ICON_IMPORT, False) zone_comp.ICON_IMPORT, False)
zone.entity_id = entity_id zone.entity_id = entity_id
zone.update_ha_state() hass.async_add_job(zone.async_update_ha_state())
def see_beacons(dev_id, kwargs_param): @callback
def async_see_beacons(dev_id, kwargs_param):
"""Set active beacons to the current location.""" """Set active beacons to the current location."""
kwargs = kwargs_param.copy() kwargs = kwargs_param.copy()
# the battery state applies to the tracking device, not the beacon # the battery state applies to the tracking device, not the beacon
kwargs.pop('battery', None) kwargs.pop('battery', None)
for beacon in MOBILE_BEACONS_ACTIVE[dev_id]: for beacon in mobile_beacons_active[dev_id]:
kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon) kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon)
kwargs['host_name'] = beacon kwargs['host_name'] = beacon
see(**kwargs) hass.async_add_job(async_see(**kwargs))
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) yield from mqtt.async_subscribe(
mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) hass, LOCATION_TOPIC, async_owntracks_location_update, 1)
yield from mqtt.async_subscribe(
hass, EVENT_TOPIC, async_owntracks_event_update, 1)
if waypoint_import: if waypoint_import:
if waypoint_whitelist is None: if waypoint_whitelist is None:
mqtt.subscribe(hass, WAYPOINT_TOPIC.format('+', '+'), yield from mqtt.async_subscribe(
owntracks_waypoint_update, 1) hass, WAYPOINT_TOPIC.format('+', '+'),
async_owntracks_waypoint_update, 1)
else: else:
for whitelist_user in waypoint_whitelist: for whitelist_user in waypoint_whitelist:
mqtt.subscribe(hass, WAYPOINT_TOPIC.format(whitelist_user, yield from mqtt.async_subscribe(
'+'), hass, WAYPOINT_TOPIC.format(whitelist_user, '+'),
owntracks_waypoint_update, 1) async_owntracks_waypoint_update, 1)
return True return True
def parse_topic(topic, pretty=False): def parse_topic(topic, pretty=False):
"""Parse an MQTT topic owntracks/user/dev, return (user, dev) tuple.""" """Parse an MQTT topic owntracks/user/dev, return (user, dev) tuple.
Async friendly.
"""
parts = topic.split('/') parts = topic.split('/')
dev_id_format = '' dev_id_format = ''
if pretty: if pretty:
@ -340,7 +348,10 @@ def parse_topic(topic, pretty=False):
def _parse_see_args(topic, data): def _parse_see_args(topic, data):
"""Parse the OwnTracks location parameters, into the format see expects.""" """Parse the OwnTracks location parameters, into the format see expects.
Async friendly.
"""
(host_name, dev_id) = parse_topic(topic, False) (host_name, dev_id) = parse_topic(topic, False)
kwargs = { kwargs = {
'dev_id': dev_id, 'dev_id': dev_id,
@ -355,7 +366,10 @@ def _parse_see_args(topic, data):
def _set_gps_from_zone(kwargs, location, zone): def _set_gps_from_zone(kwargs, location, zone):
"""Set the see parameters from the zone parameters.""" """Set the see parameters from the zone parameters.
Async friendly.
"""
if zone is not None: if zone is not None:
kwargs['gps'] = ( kwargs['gps'] = (
zone.attributes['latitude'], zone.attributes['latitude'],

View File

@ -86,7 +86,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
"""Update all the hosts on every interval time.""" """Update all the hosts on every interval time."""
for host in hosts: for host in hosts:
host.update(see) host.update(see)
track_point_in_utc_time(hass, update, now + interval) track_point_in_utc_time(hass, update, util.dt.utcnow() + interval)
return True return True
return update(util.dt.utcnow()) return update(util.dt.utcnow())

View File

@ -19,7 +19,7 @@ from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pysnmp==4.3.3'] REQUIREMENTS = ['pysnmp==4.3.4']
CONF_COMMUNITY = 'community' CONF_COMMUNITY = 'community'
CONF_AUTHKEY = 'authkey' CONF_AUTHKEY = 'authkey'

View File

@ -142,7 +142,7 @@ class TadoDeviceScanner(DeviceScanner):
# Find devices that have geofencing enabled, and are currently at home. # Find devices that have geofencing enabled, and are currently at home.
for mobile_device in tado_json: for mobile_device in tado_json:
if 'location' in mobile_device: if mobile_device.get('location'):
if mobile_device['location']['atHome']: if mobile_device['location']['atHome']:
device_id = mobile_device['id'] device_id = mobile_device['id']
device_name = mobile_device['name'] device_name = mobile_device['name']

View File

@ -13,13 +13,15 @@ import homeassistant.loader as loader
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.const import CONF_VERIFY_SSL
# Unifi package doesn't list urllib3 as a requirement # Unifi package doesn't list urllib3 as a requirement
REQUIREMENTS = ['urllib3', 'pyunifi==1.3'] REQUIREMENTS = ['pyunifi==2.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_PORT = 'port' CONF_PORT = 'port'
CONF_SITE_ID = 'site_id' CONF_SITE_ID = 'site_id'
DEFAULT_VERIFY_SSL = True
NOTIFICATION_ID = 'unifi_notification' NOTIFICATION_ID = 'unifi_notification'
NOTIFICATION_TITLE = 'Unifi Device Tracker Setup' NOTIFICATION_TITLE = 'Unifi Device Tracker Setup'
@ -29,7 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SITE_ID, default='default'): cv.string, vol.Optional(CONF_SITE_ID, default='default'): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PORT, default=8443): cv.port vol.Required(CONF_PORT, default=8443): cv.port,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
}) })
@ -42,10 +45,12 @@ def get_scanner(hass, config):
password = config[DOMAIN].get(CONF_PASSWORD) password = config[DOMAIN].get(CONF_PASSWORD)
site_id = config[DOMAIN].get(CONF_SITE_ID) site_id = config[DOMAIN].get(CONF_SITE_ID)
port = config[DOMAIN].get(CONF_PORT) port = config[DOMAIN].get(CONF_PORT)
verify_ssl = config[DOMAIN].get(CONF_VERIFY_SSL)
persistent_notification = loader.get_component('persistent_notification') persistent_notification = loader.get_component('persistent_notification')
try: try:
ctrl = Controller(host, username, password, port, 'v4', site_id) ctrl = Controller(host, username, password, port, version='v4',
site_id=site_id, ssl_verify=verify_ssl)
except urllib.error.HTTPError as ex: except urllib.error.HTTPError as ex:
_LOGGER.error('Failed to connect to Unifi: %s', ex) _LOGGER.error('Failed to connect to Unifi: %s', ex)
persistent_notification.create( persistent_notification.create(

View File

@ -19,7 +19,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==0.9.1'] REQUIREMENTS = ['netdisco==0.9.2']
DOMAIN = 'discovery' DOMAIN = 'discovery'
@ -46,6 +46,7 @@ SERVICE_HANDLERS = {
'yeelight': ('light', 'yeelight'), 'yeelight': ('light', 'yeelight'),
'flux_led': ('light', 'flux_led'), 'flux_led': ('light', 'flux_led'),
'apple_tv': ('media_player', 'apple_tv'), 'apple_tv': ('media_player', 'apple_tv'),
'frontier_silicon': ('media_player', 'frontier_silicon'),
'openhome': ('media_player', 'openhome'), 'openhome': ('media_player', 'openhome'),
} }

View File

@ -78,7 +78,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup MQTT fan platform.""" """Setup MQTT fan platform."""
yield from async_add_devices([MqttFan( async_add_devices([MqttFan(
config.get(CONF_NAME), config.get(CONF_NAME),
{ {
key: config.get(key) for key in ( key: config.get(key) for key in (

View File

@ -14,6 +14,8 @@ from homeassistant.core import callback
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.dispatcher import (
async_dispatcher_send, async_dispatcher_connect)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@ -26,6 +28,10 @@ SERVICE_START = 'start'
SERVICE_STOP = 'stop' SERVICE_STOP = 'stop'
SERVICE_RESTART = 'restart' SERVICE_RESTART = 'restart'
SIGNAL_FFMPEG_START = 'ffmpeg.start'
SIGNAL_FFMPEG_STOP = 'ffmpeg.stop'
SIGNAL_FFMPEG_RESTART = 'ffmpeg.restart'
DATA_FFMPEG = 'ffmpeg' DATA_FFMPEG = 'ffmpeg'
CONF_INITIAL_STATE = 'initial_state' CONF_INITIAL_STATE = 'initial_state'
@ -50,22 +56,25 @@ SERVICE_FFMPEG_SCHEMA = vol.Schema({
}) })
def start(hass, entity_id=None): @callback
def async_start(hass, entity_id=None):
"""Start a ffmpeg process on entity.""" """Start a ffmpeg process on entity."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_START, data) hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_START, data))
def stop(hass, entity_id=None): @callback
def async_stop(hass, entity_id=None):
"""Stop a ffmpeg process on entity.""" """Stop a ffmpeg process on entity."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_STOP, data) hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_STOP, data))
def restart(hass, entity_id=None): @callback
def async_restart(hass, entity_id=None):
"""Restart a ffmpeg process on entity.""" """Restart a ffmpeg process on entity."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_RESTART, data) hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RESTART, data))
@asyncio.coroutine @asyncio.coroutine
@ -89,30 +98,12 @@ def async_setup(hass, config):
"""Handle service ffmpeg process.""" """Handle service ffmpeg process."""
entity_ids = service.data.get(ATTR_ENTITY_ID) entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids: if service.service == SERVICE_START:
devices = [device for device in manager.entities async_dispatcher_send(hass, SIGNAL_FFMPEG_START, entity_ids)
if device.entity_id in entity_ids] elif service.service == SERVICE_STOP:
async_dispatcher_send(hass, SIGNAL_FFMPEG_STOP, entity_ids)
else: else:
devices = manager.entities async_dispatcher_send(hass, SIGNAL_FFMPEG_RESTART, entity_ids)
tasks = []
for device in devices:
if service.service == SERVICE_START:
tasks.append(device.async_start_ffmpeg())
elif service.service == SERVICE_STOP:
tasks.append(device.async_stop_ffmpeg())
else:
tasks.append(device.async_restart_ffmpeg())
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
tasks.clear()
for device in devices:
tasks.append(device.async_update_ha_state())
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_START, async_service_handle, DOMAIN, SERVICE_START, async_service_handle,
@ -140,42 +131,12 @@ class FFmpegManager(object):
self._cache = {} self._cache = {}
self._bin = ffmpeg_bin self._bin = ffmpeg_bin
self._run_test = run_test self._run_test = run_test
self._entities = []
@property @property
def binary(self): def binary(self):
"""Return ffmpeg binary from config.""" """Return ffmpeg binary from config."""
return self._bin return self._bin
@property
def entities(self):
"""Return ffmpeg entities for services."""
return self._entities
@callback
def async_register_device(self, device):
"""Register a ffmpeg process/device."""
self._entities.append(device)
@asyncio.coroutine
def async_shutdown(event):
"""Stop ffmpeg process."""
yield from device.async_stop_ffmpeg()
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, async_shutdown)
# start on startup
if device.initial_state:
@asyncio.coroutine
def async_start(event):
"""Start ffmpeg process."""
yield from device.async_start_ffmpeg()
yield from device.async_update_ha_state()
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, async_start)
@asyncio.coroutine @asyncio.coroutine
def async_run_test(self, input_source): def async_run_test(self, input_source):
"""Run test on this input. TRUE is deactivate or run correct. """Run test on this input. TRUE is deactivate or run correct.
@ -208,6 +169,22 @@ class FFmpegBase(Entity):
self.ffmpeg = None self.ffmpeg = None
self.initial_state = initial_state self.initial_state = initial_state
@asyncio.coroutine
def async_added_to_hass(self):
"""Register dispatcher & events.
This method is a coroutine.
"""
async_dispatcher_connect(
self.hass, SIGNAL_FFMPEG_START, self._async_start_ffmpeg)
async_dispatcher_connect(
self.hass, SIGNAL_FFMPEG_STOP, self._async_stop_ffmpeg)
async_dispatcher_connect(
self.hass, SIGNAL_FFMPEG_RESTART, self._async_restart_ffmpeg)
# register start/stop
self._async_register_events()
@property @property
def available(self): def available(self):
"""Return True if entity is available.""" """Return True if entity is available."""
@ -218,22 +195,53 @@ class FFmpegBase(Entity):
"""Return True if entity has to be polled for state.""" """Return True if entity has to be polled for state."""
return False return False
def async_start_ffmpeg(self): @asyncio.coroutine
def _async_start_ffmpeg(self, entity_ids):
"""Start a ffmpeg process. """Start a ffmpeg process.
This method must be run in the event loop and returns a coroutine. This method is a coroutine.
""" """
raise NotImplementedError() raise NotImplementedError()
def async_stop_ffmpeg(self): @asyncio.coroutine
def _async_stop_ffmpeg(self, entity_ids):
"""Stop a ffmpeg process. """Stop a ffmpeg process.
This method must be run in the event loop and returns a coroutine. This method is a coroutine.
""" """
return self.ffmpeg.close() if entity_ids is None or self.entity_id in entity_ids:
yield from self.ffmpeg.close()
@asyncio.coroutine @asyncio.coroutine
def async_restart_ffmpeg(self): def _async_restart_ffmpeg(self, entity_ids):
"""Stop a ffmpeg process.""" """Stop a ffmpeg process.
yield from self.async_stop_ffmpeg()
yield from self.async_start_ffmpeg() This method is a coroutine.
"""
if entity_ids is None or self.entity_id in entity_ids:
yield from self._async_stop_ffmpeg(None)
yield from self._async_start_ffmpeg(None)
@callback
def _async_register_events(self):
"""Register a ffmpeg process/device."""
@asyncio.coroutine
def async_shutdown_handle(event):
"""Stop ffmpeg process."""
yield from self._async_stop_ffmpeg(None)
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, async_shutdown_handle)
# start on startup
if not self.initial_state:
return
@asyncio.coroutine
def async_start_handle(event):
"""Start ffmpeg process."""
yield from self._async_start_ffmpeg(None)
self.hass.async_add_job(self.async_update_ha_state())
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, async_start_handle)

View File

@ -3,13 +3,13 @@
FINGERPRINTS = { FINGERPRINTS = {
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0", "compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
"core.js": "1f7f88d8f5dada08bce1d935cfa5f33e", "core.js": "1f7f88d8f5dada08bce1d935cfa5f33e",
"frontend.html": "ca9efa7e4506aa6b1a668703c8d0f800", "frontend.html": "418f6ef8354ce71f1b9594ee2068ebef",
"mdi.html": "c1dde43ccf5667f687c418fc8daf9668", "mdi.html": "65413cdf82f822bd6480e577852f0292",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-config.html": "412b3e24515ffa1ee8074ce974cf4057", "panels/ha-panel-config.html": "412b3e24515ffa1ee8074ce974cf4057",
"panels/ha-panel-dev-event.html": "91347dedf3b4fa9b49ccf4c0a28a03c4", "panels/ha-panel-dev-event.html": "91347dedf3b4fa9b49ccf4c0a28a03c4",
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d", "panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
"panels/ha-panel-dev-service.html": "a9247f255174b084fad2c04bdb9ec7a9", "panels/ha-panel-dev-service.html": "153aad076f98bbd626466bac50986874",
"panels/ha-panel-dev-state.html": "90f3bede9602241552ef7bb7958198c6", "panels/ha-panel-dev-state.html": "90f3bede9602241552ef7bb7958198c6",
"panels/ha-panel-dev-template.html": "c249a4fc18a3a6994de3d6330cfe6cbb", "panels/ha-panel-dev-template.html": "c249a4fc18a3a6994de3d6330cfe6cbb",
"panels/ha-panel-history.html": "fdaa4d2402d49d4c8bd64a1708ab7a50", "panels/ha-panel-history.html": "fdaa4d2402d49d4c8bd64a1708ab7a50",

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit e509ed07a08d35152b9eea6e263411dfc027867b Subproject commit de1b20b70a16aeb7c48a1b4867c97864c88adb1c

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -18,7 +18,7 @@ from voluptuous.error import Error as VoluptuousError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.loader as loader import homeassistant.loader as loader
from homeassistant import bootstrap from homeassistant.setup import setup_component
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import track_time_change from homeassistant.helpers.event import track_time_change
@ -118,8 +118,8 @@ def do_authentication(hass, config):
return False return False
persistent_notification.create( persistent_notification.create(
hass, 'In order to authorize Home-Assistant to view your calendars' hass, 'In order to authorize Home-Assistant to view your calendars '
'You must visit: <a href="{}" target="_blank">{}</a> and enter' 'you must visit: <a href="{}" target="_blank">{}</a> and enter '
'code: {}'.format(dev_flow.verification_url, 'code: {}'.format(dev_flow.verification_url,
dev_flow.verification_url, dev_flow.verification_url,
dev_flow.user_code), dev_flow.user_code),
@ -223,7 +223,7 @@ def do_setup(hass, config):
setup_services(hass, track_new_found_calendars, calendar_service) setup_services(hass, track_new_found_calendars, calendar_service)
# Ensure component is loaded # Ensure component is loaded
bootstrap.setup_component(hass, 'calendar', config) setup_component(hass, 'calendar', config)
for calendar in hass.data[DATA_INDEX].values(): for calendar in hass.data[DATA_INDEX].values():
discovery.load_platform(hass, 'calendar', DOMAIN, calendar) discovery.load_platform(hass, 'calendar', DOMAIN, calendar)

View File

@ -14,14 +14,13 @@ from homeassistant import config as conf_util, core as ha
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE) STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE, SERVICE_RELOAD)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import ( from homeassistant.util.async import run_coroutine_threadsafe
run_callback_threadsafe, run_coroutine_threadsafe)
DOMAIN = 'group' DOMAIN = 'group'
@ -43,7 +42,6 @@ SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_VISIBLE): cv.boolean vol.Required(ATTR_VISIBLE): cv.boolean
}) })
SERVICE_RELOAD = 'reload'
RELOAD_SERVICE_SCHEMA = vol.Schema({}) RELOAD_SERVICE_SCHEMA = vol.Schema({})
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -98,7 +96,7 @@ def is_on(hass, entity_id):
def reload(hass): def reload(hass):
"""Reload the automation from config.""" """Reload the automation from config."""
hass.services.call(DOMAIN, SERVICE_RELOAD) hass.add_job(async_reload, hass)
@asyncio.coroutine @asyncio.coroutine
@ -365,7 +363,7 @@ class Group(Entity):
def start(self): def start(self):
"""Start tracking members.""" """Start tracking members."""
run_callback_threadsafe(self.hass.loop, self.async_start).result() self.hass.add_job(self.async_start)
@callback @callback
def async_start(self): def async_start(self):
@ -396,17 +394,16 @@ class Group(Entity):
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._async_update_group_state() self._async_update_group_state()
@asyncio.coroutine
def async_remove(self): def async_remove(self):
"""Remove group from HASS. """Remove group from HASS.
This method must be run in the event loop. This method must be run in the event loop and returns a coroutine.
""" """
if self._async_unsub_state_changed: if self._async_unsub_state_changed:
self._async_unsub_state_changed() self._async_unsub_state_changed()
self._async_unsub_state_changed = None self._async_unsub_state_changed = None
yield from super().async_remove() return super().async_remove()
@asyncio.coroutine @asyncio.coroutine
def _async_state_changed_listener(self, entity_id, old_state, new_state): def _async_state_changed_listener(self, entity_id, old_state, new_state):

View File

@ -20,6 +20,7 @@ from homeassistant.components import recorder, script
from homeassistant.components.frontend import register_built_in_panel from homeassistant.components.frontend import register_built_in_panel
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.const import ATTR_HIDDEN from homeassistant.const import ATTR_HIDDEN
from homeassistant.components.recorder.util import session_scope, execute
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -34,19 +35,20 @@ SIGNIFICANT_DOMAINS = ('thermostat', 'climate')
IGNORE_DOMAINS = ('zone', 'scene',) IGNORE_DOMAINS = ('zone', 'scene',)
def last_recorder_run(): def last_recorder_run(hass):
"""Retireve the last closed recorder run from the DB.""" """Retireve the last closed recorder run from the DB."""
recorder.get_instance() from homeassistant.components.recorder.models import RecorderRuns
rec_runs = recorder.get_model('RecorderRuns')
with recorder.session_scope() as session: with session_scope(hass=hass) as session:
res = recorder.query(rec_runs).order_by(rec_runs.end.desc()).first() res = (session.query(RecorderRuns)
.order_by(RecorderRuns.end.desc()).first())
if res is None: if res is None:
return None return None
session.expunge(res) session.expunge(res)
return res return res
def get_significant_states(start_time, end_time=None, entity_id=None, def get_significant_states(hass, start_time, end_time=None, entity_id=None,
filters=None): filters=None):
""" """
Return states changes during UTC period start_time - end_time. Return states changes during UTC period start_time - end_time.
@ -55,50 +57,60 @@ def get_significant_states(start_time, end_time=None, entity_id=None,
as well as all states from certain domains (for instance as well as all states from certain domains (for instance
thermostat so that we get current temperature in our graphs). thermostat so that we get current temperature in our graphs).
""" """
from homeassistant.components.recorder.models import States
entity_ids = (entity_id.lower(), ) if entity_id is not None else None entity_ids = (entity_id.lower(), ) if entity_id is not None else None
states = recorder.get_model('States')
query = recorder.query(states).filter(
(states.domain.in_(SIGNIFICANT_DOMAINS) |
(states.last_changed == states.last_updated)) &
(states.last_updated > start_time))
if filters:
query = filters.apply(query, entity_ids)
if end_time is not None: with session_scope(hass=hass) as session:
query = query.filter(states.last_updated < end_time) query = session.query(States).filter(
(States.domain.in_(SIGNIFICANT_DOMAINS) |
(States.last_changed == States.last_updated)) &
(States.last_updated > start_time))
states = ( if filters:
state for state in recorder.execute( query = filters.apply(query, entity_ids)
query.order_by(states.entity_id, states.last_updated))
if (_is_significant(state) and
not state.attributes.get(ATTR_HIDDEN, False)))
return states_to_json(states, start_time, entity_id, filters) if end_time is not None:
query = query.filter(States.last_updated < end_time)
states = (
state for state in execute(
query.order_by(States.entity_id, States.last_updated))
if (_is_significant(state) and
not state.attributes.get(ATTR_HIDDEN, False)))
return states_to_json(hass, states, start_time, entity_id, filters)
def state_changes_during_period(start_time, end_time=None, entity_id=None): def state_changes_during_period(hass, start_time, end_time=None,
entity_id=None):
"""Return states changes during UTC period start_time - end_time.""" """Return states changes during UTC period start_time - end_time."""
states = recorder.get_model('States') from homeassistant.components.recorder.models import States
query = recorder.query(states).filter(
(states.last_changed == states.last_updated) &
(states.last_changed > start_time))
if end_time is not None: with session_scope(hass=hass) as session:
query = query.filter(states.last_updated < end_time) query = session.query(States).filter(
(States.last_changed == States.last_updated) &
(States.last_changed > start_time))
if entity_id is not None: if end_time is not None:
query = query.filter_by(entity_id=entity_id.lower()) query = query.filter(States.last_updated < end_time)
states = recorder.execute( if entity_id is not None:
query.order_by(states.entity_id, states.last_updated)) query = query.filter_by(entity_id=entity_id.lower())
return states_to_json(states, start_time, entity_id) states = execute(
query.order_by(States.entity_id, States.last_updated))
return states_to_json(hass, states, start_time, entity_id)
def get_states(utc_point_in_time, entity_ids=None, run=None, filters=None): def get_states(hass, utc_point_in_time, entity_ids=None, run=None,
filters=None):
"""Return the states at a specific point in time.""" """Return the states at a specific point in time."""
from homeassistant.components.recorder.models import States
if run is None: if run is None:
run = recorder.run_information(utc_point_in_time) run = recorder.run_information(hass, utc_point_in_time)
# History did not run before utc_point_in_time # History did not run before utc_point_in_time
if run is None: if run is None:
@ -106,29 +118,29 @@ def get_states(utc_point_in_time, entity_ids=None, run=None, filters=None):
from sqlalchemy import and_, func from sqlalchemy import and_, func
states = recorder.get_model('States') with session_scope(hass=hass) as session:
most_recent_state_ids = recorder.query( most_recent_state_ids = session.query(
func.max(states.state_id).label('max_state_id') func.max(States.state_id).label('max_state_id')
).filter( ).filter(
(states.created >= run.start) & (States.created >= run.start) &
(states.created < utc_point_in_time) & (States.created < utc_point_in_time) &
(~states.domain.in_(IGNORE_DOMAINS))) (~States.domain.in_(IGNORE_DOMAINS)))
if filters:
most_recent_state_ids = filters.apply(most_recent_state_ids,
entity_ids)
most_recent_state_ids = most_recent_state_ids.group_by( if filters:
states.entity_id).subquery() most_recent_state_ids = filters.apply(most_recent_state_ids,
entity_ids)
query = recorder.query(states).join(most_recent_state_ids, and_( most_recent_state_ids = most_recent_state_ids.group_by(
states.state_id == most_recent_state_ids.c.max_state_id)) States.entity_id).subquery()
for state in recorder.execute(query): query = session.query(States).join(most_recent_state_ids, and_(
if not state.attributes.get(ATTR_HIDDEN, False): States.state_id == most_recent_state_ids.c.max_state_id))
yield state
return [state for state in execute(query)
if not state.attributes.get(ATTR_HIDDEN, False)]
def states_to_json(states, start_time, entity_id, filters=None): def states_to_json(hass, states, start_time, entity_id, filters=None):
"""Convert SQL results into JSON friendly data structure. """Convert SQL results into JSON friendly data structure.
This takes our state list and turns it into a JSON friendly data This takes our state list and turns it into a JSON friendly data
@ -143,7 +155,7 @@ def states_to_json(states, start_time, entity_id, filters=None):
entity_ids = [entity_id] if entity_id is not None else None entity_ids = [entity_id] if entity_id is not None else None
# Get the states at the start time # Get the states at the start time
for state in get_states(start_time, entity_ids, filters=filters): for state in get_states(hass, start_time, entity_ids, filters=filters):
state.last_changed = start_time state.last_changed = start_time
state.last_updated = start_time state.last_updated = start_time
result[state.entity_id].append(state) result[state.entity_id].append(state)
@ -154,9 +166,9 @@ def states_to_json(states, start_time, entity_id, filters=None):
return result return result
def get_state(utc_point_in_time, entity_id, run=None): def get_state(hass, utc_point_in_time, entity_id, run=None):
"""Return a state at a specific point in time.""" """Return a state at a specific point in time."""
states = list(get_states(utc_point_in_time, (entity_id,), run)) states = list(get_states(hass, utc_point_in_time, (entity_id,), run))
return states[0] if states else None return states[0] if states else None
@ -173,7 +185,6 @@ def setup(hass, config):
filters.included_entities = include[CONF_ENTITIES] filters.included_entities = include[CONF_ENTITIES]
filters.included_domains = include[CONF_DOMAINS] filters.included_domains = include[CONF_DOMAINS]
recorder.get_instance()
hass.http.register_view(HistoryPeriodView(filters)) hass.http.register_view(HistoryPeriodView(filters))
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box') register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
@ -223,8 +234,8 @@ class HistoryPeriodView(HomeAssistantView):
entity_id = request.GET.get('filter_entity_id') entity_id = request.GET.get('filter_entity_id')
result = yield from request.app['hass'].loop.run_in_executor( result = yield from request.app['hass'].loop.run_in_executor(
None, get_significant_states, start_time, end_time, entity_id, None, get_significant_states, request.app['hass'], start_time,
self.filters) end_time, entity_id, self.filters)
result = result.values() result = result.values()
if _LOGGER.isEnabledFor(logging.DEBUG): if _LOGGER.isEnabledFor(logging.DEBUG):
elapsed = time.perf_counter() - timer_start elapsed = time.perf_counter() - timer_start
@ -254,41 +265,42 @@ class Filters(object):
* if include and exclude is defined - select the entities specified in * if include and exclude is defined - select the entities specified in
the include and filter out the ones from the exclude list. the include and filter out the ones from the exclude list.
""" """
states = recorder.get_model('States') from homeassistant.components.recorder.models import States
# specific entities requested - do not in/exclude anything # specific entities requested - do not in/exclude anything
if entity_ids is not None: if entity_ids is not None:
return query.filter(states.entity_id.in_(entity_ids)) return query.filter(States.entity_id.in_(entity_ids))
query = query.filter(~states.domain.in_(IGNORE_DOMAINS)) query = query.filter(~States.domain.in_(IGNORE_DOMAINS))
filter_query = None filter_query = None
# filter if only excluded domain is configured # filter if only excluded domain is configured
if self.excluded_domains and not self.included_domains: if self.excluded_domains and not self.included_domains:
filter_query = ~states.domain.in_(self.excluded_domains) filter_query = ~States.domain.in_(self.excluded_domains)
if self.included_entities: if self.included_entities:
filter_query &= states.entity_id.in_(self.included_entities) filter_query &= States.entity_id.in_(self.included_entities)
# filter if only included domain is configured # filter if only included domain is configured
elif not self.excluded_domains and self.included_domains: elif not self.excluded_domains and self.included_domains:
filter_query = states.domain.in_(self.included_domains) filter_query = States.domain.in_(self.included_domains)
if self.included_entities: if self.included_entities:
filter_query |= states.entity_id.in_(self.included_entities) filter_query |= States.entity_id.in_(self.included_entities)
# filter if included and excluded domain is configured # filter if included and excluded domain is configured
elif self.excluded_domains and self.included_domains: elif self.excluded_domains and self.included_domains:
filter_query = ~states.domain.in_(self.excluded_domains) filter_query = ~States.domain.in_(self.excluded_domains)
if self.included_entities: if self.included_entities:
filter_query &= (states.domain.in_(self.included_domains) | filter_query &= (States.domain.in_(self.included_domains) |
states.entity_id.in_(self.included_entities)) States.entity_id.in_(self.included_entities))
else: else:
filter_query &= (states.domain.in_(self.included_domains) & ~ filter_query &= (States.domain.in_(self.included_domains) & ~
states.domain.in_(self.excluded_domains)) States.domain.in_(self.excluded_domains))
# no domain filter just included entities # no domain filter just included entities
elif not self.excluded_domains and not self.included_domains and \ elif not self.excluded_domains and not self.included_domains and \
self.included_entities: self.included_entities:
filter_query = states.entity_id.in_(self.included_entities) filter_query = States.entity_id.in_(self.included_entities)
if filter_query is not None: if filter_query is not None:
query = query.filter(filter_query) query = query.filter(filter_query)
# finally apply excluded entities filter if configured # finally apply excluded entities filter if configured
if self.excluded_entities: if self.excluded_entities:
query = query.filter(~states.entity_id.in_(self.excluded_entities)) query = query.filter(~States.entity_id.in_(self.excluded_entities))
return query return query

View File

@ -4,6 +4,7 @@ from collections import defaultdict
from datetime import datetime from datetime import datetime
from ipaddress import ip_address from ipaddress import ip_address
import logging import logging
import os
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
import voluptuous as vol import voluptuous as vol
@ -115,13 +116,14 @@ def load_ip_bans_config(path: str):
"""Loading list of banned IPs from config file.""" """Loading list of banned IPs from config file."""
ip_list = [] ip_list = []
if not os.path.isfile(path):
return ip_list
try: try:
list_ = load_yaml_config_file(path) list_ = load_yaml_config_file(path)
except FileNotFoundError:
return []
except HomeAssistantError as err: except HomeAssistantError as err:
_LOGGER.error('Unable to load %s: %s', path, str(err)) _LOGGER.error('Unable to load %s: %s', path, str(err))
return [] return ip_list
for ip_ban, ip_info in list_.items(): for ip_ban, ip_info in list_.items():
try: try:

View File

@ -60,7 +60,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
camera[CONF_ENTITY_ID], api, attributes, camera.get(CONF_NAME) camera[CONF_ENTITY_ID], api, attributes, camera.get(CONF_NAME)
)) ))
yield from async_add_devices(entities) async_add_devices(entities)
class MicrosoftFaceDetectEntity(ImageProcessingFaceEntity): class MicrosoftFaceDetectEntity(ImageProcessingFaceEntity):

View File

@ -54,7 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
camera.get(CONF_NAME) camera.get(CONF_NAME)
)) ))
yield from async_add_devices(entities) async_add_devices(entities)
class ImageProcessingFaceEntity(ImageProcessingEntity): class ImageProcessingFaceEntity(ImageProcessingEntity):
@ -108,8 +108,7 @@ class ImageProcessingFaceEntity(ImageProcessingEntity):
def process_faces(self, faces, total): def process_faces(self, faces, total):
"""Send event with detected faces and store data.""" """Send event with detected faces and store data."""
run_callback_threadsafe( run_callback_threadsafe(
self.hass.loop, self.async_process_faces, faces, total self.hass.loop, self.async_process_faces, faces, total).result()
).result()
@callback @callback
def async_process_faces(self, faces, total): def async_process_faces(self, faces, total):

View File

@ -66,7 +66,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
camera[CONF_ENTITY_ID], params, confidence, camera.get(CONF_NAME) camera[CONF_ENTITY_ID], params, confidence, camera.get(CONF_NAME)
)) ))
yield from async_add_devices(entities) async_add_devices(entities)
class OpenAlprCloudEntity(ImageProcessingAlprEntity): class OpenAlprCloudEntity(ImageProcessingAlprEntity):

View File

@ -70,7 +70,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
camera[CONF_ENTITY_ID], command, confidence, camera.get(CONF_NAME) camera[CONF_ENTITY_ID], command, confidence, camera.get(CONF_NAME)
)) ))
yield from async_add_devices(entities) async_add_devices(entities)
class ImageProcessingAlprEntity(ImageProcessingEntity): class ImageProcessingAlprEntity(ImageProcessingEntity):

View File

@ -85,7 +85,7 @@ def setup(hass, config):
try: try:
influx = InfluxDBClient(**kwargs) influx = InfluxDBClient(**kwargs)
influx.query("SELECT * FROM /.*/ LIMIT 1;") influx.query("SHOW DIAGNOSTICS;")
except exceptions.InfluxDBClientError as exc: except exceptions.InfluxDBClientError as exc:
_LOGGER.error("Database host is not accessible due to '%s', please " _LOGGER.error("Database host is not accessible due to '%s', please "
"check your entries in the configuration file and that " "check your entries in the configuration file and that "

View File

@ -14,6 +14,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import async_get_last_state
DOMAIN = 'input_slider' DOMAIN = 'input_slider'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -165,6 +166,18 @@ class InputSlider(Entity):
ATTR_STEP: self._step ATTR_STEP: self._step
} }
@asyncio.coroutine
def async_added_to_hass(self):
"""Called when entity about to be added to hass."""
state = yield from async_get_last_state(self.hass, self.entity_id)
if not state:
return
num_value = float(state.state)
if num_value < self._minimum or num_value > self._maximum:
return
self._current_value = num_value
@asyncio.coroutine @asyncio.coroutine
def async_select_value(self, value): def async_select_value(self, value):
"""Select new value.""" """Select new value."""

View File

@ -13,7 +13,7 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_HOST, CONF_PORT, CONF_TIMEOUT) CONF_PASSWORD, CONF_USERNAME, CONF_HOST, CONF_PORT, CONF_TIMEOUT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['insteonlocal==0.39'] REQUIREMENTS = ['insteonlocal==0.48']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -231,7 +231,7 @@ class ISYDevice(Entity):
# pylint: disable=unused-argument # pylint: disable=unused-argument
def on_update(self, event: object) -> None: def on_update(self, event: object) -> None:
"""Handle the update event from the ISY994 Node.""" """Handle the update event from the ISY994 Node."""
self.update_ha_state() self.schedule_update_ha_state()
@property @property
def domain(self) -> str: def domain(self) -> str:

View File

@ -24,8 +24,6 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import async_restore_state from homeassistant.helpers.restore_state import async_restore_state
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from homeassistant.util.async import run_callback_threadsafe
DOMAIN = "light" DOMAIN = "light"
SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = timedelta(seconds=30)
@ -88,7 +86,7 @@ PROP_TO_ATTR = {
} }
# Service call validation schemas # Service call validation schemas
VALID_TRANSITION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)) VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553))
VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
LIGHT_TURN_ON_SCHEMA = vol.Schema({ LIGHT_TURN_ON_SCHEMA = vol.Schema({
@ -145,10 +143,10 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, color_temp=None, white_value=None, rgb_color=None, xy_color=None, color_temp=None, white_value=None,
profile=None, flash=None, effect=None, color_name=None): profile=None, flash=None, effect=None, color_name=None):
"""Turn all or specified light on.""" """Turn all or specified light on."""
run_callback_threadsafe( hass.add_job(
hass.loop, async_turn_on, hass, entity_id, transition, brightness, async_turn_on, hass, entity_id, transition, brightness,
rgb_color, xy_color, color_temp, white_value, rgb_color, xy_color, color_temp, white_value,
profile, flash, effect, color_name).result() profile, flash, effect, color_name)
@callback @callback
@ -178,8 +176,7 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
def turn_off(hass, entity_id=None, transition=None): def turn_off(hass, entity_id=None, transition=None):
"""Turn all or specified light off.""" """Turn all or specified light off."""
run_callback_threadsafe( hass.add_job(async_turn_off, hass, entity_id, transition)
hass.loop, async_turn_off, hass, entity_id, transition).result()
@callback @callback

View File

@ -106,4 +106,4 @@ class EnOceanLight(enocean.EnOceanDevice, Light):
"""Update the internal state of this device.""" """Update the internal state of this device."""
self._brightness = math.floor(val / 100.0 * 256.0) self._brightness = math.floor(val / 100.0 * 256.0)
self._on_state = bool(val != 0) self._on_state = bool(val != 0)
self.update_ha_state() self.schedule_update_ha_state()

View File

@ -18,7 +18,7 @@ from homeassistant.components.light import (
PLATFORM_SCHEMA) PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['flux_led==0.13'] REQUIREMENTS = ['flux_led==0.15']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -47,9 +47,19 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
PHUE_CONFIG_FILE = 'phue.conf' PHUE_CONFIG_FILE = 'phue.conf'
SUPPORT_HUE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION | SUPPORT_FLASH)
SUPPORT_FLASH | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION | SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS)
SUPPORT_XY_COLOR) SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP)
SUPPORT_HUE_COLOR = (SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT |
SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR)
SUPPORT_HUE_EXTENDED = (SUPPORT_HUE_COLOR_TEMP | SUPPORT_HUE_COLOR)
SUPPORT_HUE = {
'Extended color light': SUPPORT_HUE_EXTENDED,
'Color light': SUPPORT_HUE_COLOR,
'Dimmable light': SUPPORT_HUE_DIMMABLE,
'Color temperature light': SUPPORT_HUE_COLOR_TEMP
}
CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue" CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue"
DEFAULT_ALLOW_IN_EMULATED_HUE = True DEFAULT_ALLOW_IN_EMULATED_HUE = True
@ -354,7 +364,7 @@ class HueLight(Light):
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_HUE return SUPPORT_HUE.get(self.info.get('type'), SUPPORT_HUE_EXTENDED)
@property @property
def effect_list(self): def effect_list(self):
@ -366,15 +376,30 @@ class HueLight(Light):
command = {'on': True} command = {'on': True}
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
command['transitiontime'] = kwargs[ATTR_TRANSITION] * 10 command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10)
if ATTR_XY_COLOR in kwargs: if ATTR_XY_COLOR in kwargs:
command['xy'] = kwargs[ATTR_XY_COLOR] if self.info.get('manufacturername') == "OSRAM":
hsv = color_util.color_xy_brightness_to_hsv(
*kwargs[ATTR_XY_COLOR],
ibrightness=self.info['bri'])
command['hue'] = hsv[0]
command['sat'] = hsv[1]
command['bri'] = hsv[2]
else:
command['xy'] = kwargs[ATTR_XY_COLOR]
elif ATTR_RGB_COLOR in kwargs: elif ATTR_RGB_COLOR in kwargs:
xyb = color_util.color_RGB_to_xy( if self.info.get('manufacturername') == "OSRAM":
*(int(val) for val in kwargs[ATTR_RGB_COLOR])) hsv = color_util.color_RGB_to_hsv(
command['xy'] = xyb[0], xyb[1] *(int(val) for val in kwargs[ATTR_RGB_COLOR]))
command['bri'] = xyb[2] command['hue'] = hsv[0]
command['sat'] = hsv[1]
command['bri'] = hsv[2]
else:
xyb = color_util.color_RGB_to_xy(
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
command['xy'] = xyb[0], xyb[1]
command['bri'] = xyb[2]
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
command['bri'] = kwargs[ATTR_BRIGHTNESS] command['bri'] = kwargs[ATTR_BRIGHTNESS]
@ -401,7 +426,8 @@ class HueLight(Light):
command['hue'] = random.randrange(0, 65535) command['hue'] = random.randrange(0, 65535)
command['sat'] = random.randrange(150, 254) command['sat'] = random.randrange(150, 254)
elif self.bridge_type == 'hue': elif self.bridge_type == 'hue':
command['effect'] = 'none' if self.info.get('manufacturername') != "OSRAM":
command['effect'] = 'none'
self._command_func(self.light_id, command) self._command_func(self.light_id, command)
@ -410,9 +436,7 @@ class HueLight(Light):
command = {'on': False} command = {'on': False}
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
# Transition time is in 1/10th seconds and cannot exceed command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10)
# 900 seconds.
command['transitiontime'] = min(9000, kwargs[ATTR_TRANSITION] * 10)
flash = kwargs.get(ATTR_FLASH) flash = kwargs.get(ATTR_FLASH)

View File

@ -152,6 +152,10 @@ class InsteonLocalDimmerDevice(Light):
def update(self): def update(self):
"""Update state of the light.""" """Update state of the light."""
resp = self.node.status(0) resp = self.node.status(0)
while 'error' in resp and resp['error'] is True:
resp = self.node.status(0)
if 'cmd2' in resp: if 'cmd2' in resp:
self._value = int(resp['cmd2'], 16) self._value = int(resp['cmd2'], 16)

View File

@ -98,7 +98,7 @@ class LIFX(object):
ipaddr, name, power, hue, sat, bri, kel) ipaddr, name, power, hue, sat, bri, kel)
bulb.set_power(power) bulb.set_power(power)
bulb.set_color(hue, sat, bri, kel) bulb.set_color(hue, sat, bri, kel)
bulb.update_ha_state() bulb.schedule_update_ha_state()
def on_color(self, ipaddr, hue, sat, bri, kel): def on_color(self, ipaddr, hue, sat, bri, kel):
"""Initialize the light.""" """Initialize the light."""
@ -106,7 +106,7 @@ class LIFX(object):
if bulb is not None: if bulb is not None:
bulb.set_color(hue, sat, bri, kel) bulb.set_color(hue, sat, bri, kel)
bulb.update_ha_state() bulb.schedule_update_ha_state()
def on_power(self, ipaddr, power): def on_power(self, ipaddr, power):
"""Initialize the light.""" """Initialize the light."""
@ -114,7 +114,7 @@ class LIFX(object):
if bulb is not None: if bulb is not None:
bulb.set_power(power) bulb.set_power(power)
bulb.update_ha_state() bulb.schedule_update_ha_state()
# pylint: disable=unused-argument # pylint: disable=unused-argument
def poll(self, now): def poll(self, now):
@ -202,7 +202,7 @@ class LIFXLight(Light):
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the device on.""" """Turn the device on."""
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
fade = kwargs[ATTR_TRANSITION] * 1000 fade = int(kwargs[ATTR_TRANSITION] * 1000)
else: else:
fade = 0 fade = 0
@ -230,15 +230,17 @@ class LIFXLight(Light):
hue, saturation, brightness, kelvin, fade) hue, saturation, brightness, kelvin, fade)
if self._power == 0: if self._power == 0:
self._liffylights.set_color(self._ip, hue, saturation,
brightness, kelvin, 0)
self._liffylights.set_power(self._ip, 65535, fade) self._liffylights.set_power(self._ip, 65535, fade)
else:
self._liffylights.set_color(self._ip, hue, saturation, self._liffylights.set_color(self._ip, hue, saturation,
brightness, kelvin, fade) brightness, kelvin, fade)
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the device off.""" """Turn the device off."""
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
fade = kwargs[ATTR_TRANSITION] * 1000 fade = int(kwargs[ATTR_TRANSITION] * 1000)
else: else:
fade = 0 fade = 0

View File

@ -17,7 +17,7 @@ from homeassistant.components.light import (
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['limitlessled==1.0.4'] REQUIREMENTS = ['limitlessled==1.0.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -143,7 +143,7 @@ def state(new_state):
pipeline.on() pipeline.on()
# Set transition time. # Set transition time.
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
transition_time = kwargs[ATTR_TRANSITION] transition_time = int(kwargs[ATTR_TRANSITION])
# Do group type-specific work. # Do group type-specific work.
function(self, transition_time, pipeline, **kwargs) function(self, transition_time, pipeline, **kwargs)
# Update state. # Update state.

View File

@ -12,83 +12,122 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR,
SUPPORT_RGB_COLOR, SUPPORT_COLOR_TEMP, Light) ATTR_WHITE_VALUE, ATTR_XY_COLOR, Light, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR,
SUPPORT_WHITE_VALUE, SUPPORT_XY_COLOR)
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME,
CONF_PAYLOAD_ON, CONF_STATE, CONF_BRIGHTNESS, CONF_RGB, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON,
CONF_COLOR_TEMP) CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
CONF_STATE_VALUE_TEMPLATE = 'state_value_template'
CONF_BRIGHTNESS_STATE_TOPIC = 'brightness_state_topic'
CONF_BRIGHTNESS_COMMAND_TOPIC = 'brightness_command_topic' CONF_BRIGHTNESS_COMMAND_TOPIC = 'brightness_command_topic'
CONF_BRIGHTNESS_VALUE_TEMPLATE = 'brightness_value_template'
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_BRIGHTNESS_SCALE = 'brightness_scale'
CONF_COLOR_TEMP_STATE_TOPIC = 'color_temp_state_topic' CONF_BRIGHTNESS_STATE_TOPIC = 'brightness_state_topic'
CONF_BRIGHTNESS_VALUE_TEMPLATE = 'brightness_value_template'
CONF_COLOR_TEMP_COMMAND_TOPIC = 'color_temp_command_topic' CONF_COLOR_TEMP_COMMAND_TOPIC = 'color_temp_command_topic'
CONF_COLOR_TEMP_STATE_TOPIC = 'color_temp_state_topic'
CONF_COLOR_TEMP_VALUE_TEMPLATE = 'color_temp_value_template' CONF_COLOR_TEMP_VALUE_TEMPLATE = 'color_temp_value_template'
CONF_EFFECT_COMMAND_TOPIC = 'effect_command_topic'
CONF_EFFECT_LIST = 'effect_list'
CONF_EFFECT_STATE_TOPIC = 'effect_state_topic'
CONF_EFFECT_VALUE_TEMPLATE = 'effect_value_template'
CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic'
CONF_RGB_STATE_TOPIC = 'rgb_state_topic'
CONF_RGB_VALUE_TEMPLATE = 'rgb_value_template'
CONF_STATE_VALUE_TEMPLATE = 'state_value_template'
CONF_XY_COMMAND_TOPIC = 'xy_command_topic'
CONF_XY_STATE_TOPIC = 'xy_state_topic'
CONF_XY_VALUE_TEMPLATE = 'xy_value_template'
CONF_WHITE_VALUE_COMMAND_TOPIC = 'white_value_command_topic'
CONF_WHITE_VALUE_SCALE = 'white_value_scale'
CONF_WHITE_VALUE_STATE_TOPIC = 'white_value_state_topic'
CONF_WHITE_VALUE_TEMPLATE = 'white_value_template'
DEFAULT_NAME = 'MQTT Light'
DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_OPTIMISTIC = False
DEFAULT_BRIGHTNESS_SCALE = 255 DEFAULT_BRIGHTNESS_SCALE = 255
DEFAULT_NAME = 'MQTT Light'
DEFAULT_OPTIMISTIC = False
DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_WHITE_VALUE_SCALE = 255
PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_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,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE): vol.Optional(CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE):
vol.All(vol.Coerce(int), vol.Range(min=1)), vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_EFFECT_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_WHITE_VALUE_SCALE, default=DEFAULT_WHITE_VALUE_SCALE):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_WHITE_VALUE_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_XY_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_XY_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template,
}) })
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Add MQTT Light.""" """Add MQTT Light."""
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
config.setdefault( config.setdefault(
CONF_STATE_VALUE_TEMPLATE, config.get(CONF_VALUE_TEMPLATE)) CONF_STATE_VALUE_TEMPLATE, config.get(CONF_VALUE_TEMPLATE))
yield from async_add_devices([MqttLight( async_add_devices([MqttLight(
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_EFFECT_LIST),
{ {
key: config.get(key) for key in ( key: config.get(key) for key in (
CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC,
CONF_BRIGHTNESS_STATE_TOPIC,
CONF_BRIGHTNESS_COMMAND_TOPIC, CONF_BRIGHTNESS_COMMAND_TOPIC,
CONF_RGB_STATE_TOPIC, CONF_BRIGHTNESS_STATE_TOPIC,
CONF_RGB_COMMAND_TOPIC, CONF_COLOR_TEMP_COMMAND_TOPIC,
CONF_COLOR_TEMP_STATE_TOPIC, CONF_COLOR_TEMP_STATE_TOPIC,
CONF_COLOR_TEMP_COMMAND_TOPIC CONF_COMMAND_TOPIC,
CONF_EFFECT_COMMAND_TOPIC,
CONF_EFFECT_STATE_TOPIC,
CONF_RGB_COMMAND_TOPIC,
CONF_RGB_STATE_TOPIC,
CONF_STATE_TOPIC,
CONF_WHITE_VALUE_COMMAND_TOPIC,
CONF_WHITE_VALUE_STATE_TOPIC,
CONF_XY_COMMAND_TOPIC,
CONF_XY_STATE_TOPIC,
) )
}, },
{ {
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
CONF_BRIGHTNESS: config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE), CONF_BRIGHTNESS: config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE),
CONF_COLOR_TEMP: config.get(CONF_COLOR_TEMP_VALUE_TEMPLATE),
CONF_EFFECT: config.get(CONF_EFFECT_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) CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
CONF_WHITE_VALUE: config.get(CONF_WHITE_VALUE_TEMPLATE),
CONF_XY: config.get(CONF_XY_VALUE_TEMPLATE),
}, },
config.get(CONF_QOS), config.get(CONF_QOS),
config.get(CONF_RETAIN), config.get(CONF_RETAIN),
@ -98,16 +137,19 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
}, },
config.get(CONF_OPTIMISTIC), config.get(CONF_OPTIMISTIC),
config.get(CONF_BRIGHTNESS_SCALE), config.get(CONF_BRIGHTNESS_SCALE),
config.get(CONF_WHITE_VALUE_SCALE),
)]) )])
class MqttLight(Light): class MqttLight(Light):
"""MQTT light.""" """MQTT light."""
def __init__(self, name, topic, templates, qos, retain, payload, def __init__(self, name, effect_list, topic, templates, qos,
optimistic, brightness_scale): retain, payload, optimistic, brightness_scale,
white_value_scale):
"""Initialize MQTT light.""" """Initialize MQTT light."""
self._name = name self._name = name
self._effect_list = effect_list
self._topic = topic self._topic = topic
self._qos = qos self._qos = qos
self._retain = retain self._retain = retain
@ -120,11 +162,21 @@ class MqttLight(Light):
optimistic or topic[CONF_BRIGHTNESS_STATE_TOPIC] is None) optimistic or topic[CONF_BRIGHTNESS_STATE_TOPIC] is None)
self._optimistic_color_temp = ( self._optimistic_color_temp = (
optimistic or topic[CONF_COLOR_TEMP_STATE_TOPIC] is None) optimistic or topic[CONF_COLOR_TEMP_STATE_TOPIC] is None)
self._optimistic_effect = (
optimistic or topic[CONF_EFFECT_STATE_TOPIC] is None)
self._optimistic_white_value = (
optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None)
self._optimistic_xy = \
optimistic or topic[CONF_XY_STATE_TOPIC] is None
self._brightness_scale = brightness_scale self._brightness_scale = brightness_scale
self._white_value_scale = white_value_scale
self._state = False self._state = False
self._brightness = None self._brightness = None
self._rgb = None self._rgb = None
self._color_temp = None self._color_temp = None
self._effect = None
self._white_value = None
self._xy = None
self._supported_features = 0 self._supported_features = 0
self._supported_features |= ( self._supported_features |= (
topic[CONF_RGB_COMMAND_TOPIC] is not None and SUPPORT_RGB_COLOR) topic[CONF_RGB_COMMAND_TOPIC] is not None and SUPPORT_RGB_COLOR)
@ -134,6 +186,14 @@ class MqttLight(Light):
self._supported_features |= ( self._supported_features |= (
topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None and topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None and
SUPPORT_COLOR_TEMP) SUPPORT_COLOR_TEMP)
self._supported_features |= (
topic[CONF_EFFECT_STATE_TOPIC] is not None and
SUPPORT_EFFECT)
self._supported_features |= (
topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None and
SUPPORT_WHITE_VALUE)
self._supported_features |= (
topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_XY_COLOR)
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
@ -215,6 +275,57 @@ class MqttLight(Light):
else: else:
self._color_temp = None self._color_temp = None
@callback
def effect_received(topic, payload, qos):
"""A new MQTT message for effect has been received."""
self._effect = templates[CONF_EFFECT](payload)
self.hass.async_add_job(self.async_update_ha_state())
if self._topic[CONF_EFFECT_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe(
self.hass, self._topic[CONF_EFFECT_STATE_TOPIC],
effect_received, self._qos)
self._effect = 'none'
if self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None:
self._effect = 'none'
else:
self._effect = None
@callback
def white_value_received(topic, payload, qos):
"""A new MQTT message for the white value has been received."""
device_value = float(templates[CONF_WHITE_VALUE](payload))
percent_white = device_value / self._white_value_scale
self._white_value = int(percent_white * 255)
self.hass.async_add_job(self.async_update_ha_state())
if self._topic[CONF_WHITE_VALUE_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe(
self.hass, self._topic[CONF_WHITE_VALUE_STATE_TOPIC],
white_value_received, self._qos)
self._white_value = 255
elif self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None:
self._white_value = 255
else:
self._white_value = None
@callback
def xy_received(topic, payload, qos):
"""A new MQTT message has been received."""
self._xy = [float(val) for val in
templates[CONF_XY](payload).split(',')]
self.hass.async_add_job(self.async_update_ha_state())
if self._topic[CONF_XY_STATE_TOPIC] is not None:
yield from mqtt.async_subscribe(
self.hass, self._topic[CONF_XY_STATE_TOPIC], xy_received,
self._qos)
self._xy = [1, 1]
if self._topic[CONF_XY_COMMAND_TOPIC] is not None:
self._xy = [1, 1]
else:
self._xy = None
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
@ -230,6 +341,16 @@ class MqttLight(Light):
"""Return the color temperature in mired.""" """Return the color temperature in mired."""
return self._color_temp return self._color_temp
@property
def white_value(self):
"""Return the white property."""
return self._white_value
@property
def xy_color(self):
"""Return the RGB color value."""
return self._xy
@property @property
def should_poll(self): def should_poll(self):
"""No polling needed for a MQTT light.""" """No polling needed for a MQTT light."""
@ -250,6 +371,16 @@ class MqttLight(Light):
"""Return true if we do optimistic updates.""" """Return true if we do optimistic updates."""
return self._optimistic return self._optimistic
@property
def effect_list(self):
"""Return the list of supported effects."""
return self._effect_list
@property
def effect(self):
"""Return the current effect."""
return self._effect
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
@ -297,6 +428,41 @@ class MqttLight(Light):
self._color_temp = kwargs[ATTR_COLOR_TEMP] self._color_temp = kwargs[ATTR_COLOR_TEMP]
should_update = True should_update = True
if ATTR_EFFECT in kwargs and \
self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None:
effect = kwargs[ATTR_EFFECT]
if effect in self._effect_list:
mqtt.async_publish(
self.hass, self._topic[CONF_EFFECT_COMMAND_TOPIC],
effect, self._qos, self._retain)
if self._optimistic_effect:
self._effect = kwargs[ATTR_EFFECT]
should_update = True
if ATTR_WHITE_VALUE in kwargs and \
self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None:
percent_white = float(kwargs[ATTR_WHITE_VALUE]) / 255
device_white_value = int(percent_white * self._white_value_scale)
mqtt.async_publish(
self.hass, self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC],
device_white_value, self._qos, self._retain)
if self._optimistic_white_value:
self._white_value = kwargs[ATTR_WHITE_VALUE]
should_update = True
if ATTR_XY_COLOR in kwargs and \
self._topic[CONF_XY_COMMAND_TOPIC] is not None:
mqtt.async_publish(
self.hass, self._topic[CONF_XY_COMMAND_TOPIC],
'{},{}'.format(*kwargs[ATTR_XY_COLOR]), self._qos,
self._retain)
if self._optimistic_xy:
self._xy = kwargs[ATTR_XY_COLOR]
should_update = True
mqtt.async_publish( mqtt.async_publish(
self.hass, self._topic[CONF_COMMAND_TOPIC], self._payload['on'], self.hass, self._topic[CONF_COMMAND_TOPIC], self._payload['on'],
self._qos, self._retain) self._qos, self._retain)

View File

@ -12,11 +12,14 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH,
ATTR_FLASH, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR,
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light) FLASH_LONG, FLASH_SHORT, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR,
SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, SUPPORT_XY_COLOR)
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_OPTIMISTIC, CONF_BRIGHTNESS, CONF_RGB) CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT,
CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -27,42 +30,53 @@ DOMAIN = 'mqtt_json'
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
DEFAULT_BRIGHTNESS = False
DEFAULT_COLOR_TEMP = False
DEFAULT_EFFECT = False
DEFAULT_FLASH_TIME_LONG = 10
DEFAULT_FLASH_TIME_SHORT = 2
DEFAULT_NAME = 'MQTT JSON Light' DEFAULT_NAME = 'MQTT JSON Light'
DEFAULT_OPTIMISTIC = False DEFAULT_OPTIMISTIC = False
DEFAULT_BRIGHTNESS = False
DEFAULT_RGB = False DEFAULT_RGB = False
DEFAULT_FLASH_TIME_SHORT = 2 DEFAULT_WHITE_VALUE = False
DEFAULT_FLASH_TIME_LONG = 10 DEFAULT_XY = False
CONF_EFFECT_LIST = 'effect_list'
CONF_FLASH_TIME_SHORT = 'flash_time_short'
CONF_FLASH_TIME_LONG = 'flash_time_long' CONF_FLASH_TIME_LONG = 'flash_time_long'
CONF_FLASH_TIME_SHORT = 'flash_time_short'
SUPPORT_MQTT_JSON = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH | SUPPORT_RGB_COLOR |
SUPPORT_TRANSITION)
# Stealing some of these from the base MQTT configs. # Stealing some of these from the base MQTT configs.
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS):
vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean, vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean,
vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean, vol.Optional(CONF_COLOR_TEMP, default=DEFAULT_COLOR_TEMP): cv.boolean,
vol.Optional(CONF_EFFECT, default=DEFAULT_EFFECT): cv.boolean,
vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_FLASH_TIME_SHORT, default=DEFAULT_FLASH_TIME_SHORT): vol.Optional(CONF_FLASH_TIME_SHORT, default=DEFAULT_FLASH_TIME_SHORT):
cv.positive_int, cv.positive_int,
vol.Optional(CONF_FLASH_TIME_LONG, default=DEFAULT_FLASH_TIME_LONG): vol.Optional(CONF_FLASH_TIME_LONG, default=DEFAULT_FLASH_TIME_LONG):
cv.positive_int cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS):
vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean,
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean,
vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean,
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
}) })
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a MQTT JSON Light.""" """Setup a MQTT JSON Light."""
yield from async_add_devices([MqttJson( if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
async_add_devices([MqttJson(
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_EFFECT_LIST),
{ {
key: config.get(key) for key in ( key: config.get(key) for key in (
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
@ -73,7 +87,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_RETAIN), config.get(CONF_RETAIN),
config.get(CONF_OPTIMISTIC), config.get(CONF_OPTIMISTIC),
config.get(CONF_BRIGHTNESS), config.get(CONF_BRIGHTNESS),
config.get(CONF_COLOR_TEMP),
config.get(CONF_EFFECT),
config.get(CONF_RGB), config.get(CONF_RGB),
config.get(CONF_WHITE_VALUE),
config.get(CONF_XY),
{ {
key: config.get(key) for key in ( key: config.get(key) for key in (
CONF_FLASH_TIME_SHORT, CONF_FLASH_TIME_SHORT,
@ -86,10 +104,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class MqttJson(Light): class MqttJson(Light):
"""Representation of a MQTT JSON light.""" """Representation of a MQTT JSON light."""
def __init__(self, name, topic, qos, retain, optimistic, brightness, rgb, def __init__(self, name, effect_list, topic, qos, retain, optimistic,
brightness, color_temp, effect, rgb, white_value, xy,
flash_times): flash_times):
"""Initialize MQTT JSON light.""" """Initialize MQTT JSON light."""
self._name = name self._name = name
self._effect_list = effect_list
self._topic = topic self._topic = topic
self._qos = qos self._qos = qos
self._retain = retain self._retain = retain
@ -100,13 +120,45 @@ class MqttJson(Light):
else: else:
self._brightness = None self._brightness = None
if color_temp:
self._color_temp = 150
else:
self._color_temp = None
if effect:
self._effect = 'none'
else:
self._effect = None
if rgb: if rgb:
self._rgb = [0, 0, 0] self._rgb = [0, 0, 0]
else: else:
self._rgb = None self._rgb = None
if white_value:
self._white_value = 255
else:
self._white_value = None
if xy:
self._xy = [1, 1]
else:
self._xy = None
self._flash_times = flash_times self._flash_times = flash_times
self._supported_features = (SUPPORT_TRANSITION | SUPPORT_FLASH)
self._supported_features |= (rgb is not None and SUPPORT_RGB_COLOR)
self._supported_features |= (brightness is not None and
SUPPORT_BRIGHTNESS)
self._supported_features |= (color_temp is not None and
SUPPORT_COLOR_TEMP)
self._supported_features |= (effect is not None and
SUPPORT_EFFECT)
self._supported_features |= (white_value is not None and
SUPPORT_WHITE_VALUE)
self._supported_features |= (xy is not None and SUPPORT_XY_COLOR)
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Subscribe mqtt events. """Subscribe mqtt events.
@ -133,7 +185,7 @@ class MqttJson(Light):
except KeyError: except KeyError:
pass pass
except ValueError: except ValueError:
_LOGGER.warning("Invalid color value received") _LOGGER.warning("Invalid RGB color value received")
if self._brightness is not None: if self._brightness is not None:
try: try:
@ -143,6 +195,41 @@ class MqttJson(Light):
except ValueError: except ValueError:
_LOGGER.warning('Invalid brightness value received') _LOGGER.warning('Invalid brightness value received')
if self._color_temp is not None:
try:
self._color_temp = int(values['color_temp'])
except KeyError:
pass
except ValueError:
_LOGGER.warning('Invalid color temp value received')
if self._effect is not None:
try:
self._effect = values['effect']
except KeyError:
pass
except ValueError:
_LOGGER.warning('Invalid effect value received')
if self._white_value is not None:
try:
self._white_value = int(values['white_value'])
except KeyError:
pass
except ValueError:
_LOGGER.warning('Invalid white value value received')
if self._xy is not None:
try:
x_color = float(values['color']['x'])
y_color = float(values['color']['y'])
self._xy = [x_color, y_color]
except KeyError:
pass
except ValueError:
_LOGGER.warning("Invalid XY color value received")
self.hass.async_add_job(self.async_update_ha_state()) self.hass.async_add_job(self.async_update_ha_state())
if self._topic[CONF_STATE_TOPIC] is not None: if self._topic[CONF_STATE_TOPIC] is not None:
@ -155,11 +242,36 @@ class MqttJson(Light):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
return self._brightness return self._brightness
@property
def color_temp(self):
"""Return the color temperature in mired."""
return self._color_temp
@property
def effect(self):
"""Return the current effect."""
return self._effect
@property
def effect_list(self):
"""Return the list of supported effects."""
return self._effect_list
@property @property
def rgb_color(self): def rgb_color(self):
"""Return the RGB color value.""" """Return the RGB color value."""
return self._rgb return self._rgb
@property
def white_value(self):
"""Return the white property."""
return self._white_value
@property
def xy_color(self):
"""Return the XY color value."""
return self._xy
@property @property
def should_poll(self): def should_poll(self):
"""No polling needed for a MQTT light.""" """No polling needed for a MQTT light."""
@ -183,7 +295,7 @@ class MqttJson(Light):
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_MQTT_JSON return self._supported_features
@asyncio.coroutine @asyncio.coroutine
def async_turn_on(self, **kwargs): def async_turn_on(self, **kwargs):
@ -215,7 +327,7 @@ class MqttJson(Light):
message['flash'] = self._flash_times[CONF_FLASH_TIME_SHORT] message['flash'] = self._flash_times[CONF_FLASH_TIME_SHORT]
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
message['transition'] = kwargs[ATTR_TRANSITION] message['transition'] = int(kwargs[ATTR_TRANSITION])
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
message['brightness'] = int(kwargs[ATTR_BRIGHTNESS]) message['brightness'] = int(kwargs[ATTR_BRIGHTNESS])
@ -224,6 +336,37 @@ class MqttJson(Light):
self._brightness = kwargs[ATTR_BRIGHTNESS] self._brightness = kwargs[ATTR_BRIGHTNESS]
should_update = True should_update = True
if ATTR_COLOR_TEMP in kwargs:
message['color_temp'] = int(kwargs[ATTR_COLOR_TEMP])
if self._optimistic:
self._color_temp = kwargs[ATTR_COLOR_TEMP]
should_update = True
if ATTR_EFFECT in kwargs:
message['effect'] = kwargs[ATTR_EFFECT]
if self._optimistic:
self._effect = kwargs[ATTR_EFFECT]
should_update = True
if ATTR_WHITE_VALUE in kwargs:
message['white_value'] = int(kwargs[ATTR_WHITE_VALUE])
if self._optimistic:
self._white_value = kwargs[ATTR_WHITE_VALUE]
should_update = True
if ATTR_XY_COLOR in kwargs:
message['color'] = {
'x': kwargs[ATTR_XY_COLOR][0],
'y': kwargs[ATTR_XY_COLOR][1]
}
if self._optimistic:
self._xy = kwargs[ATTR_XY_COLOR]
should_update = True
mqtt.async_publish( mqtt.async_publish(
self.hass, self._topic[CONF_COMMAND_TOPIC], json.dumps(message), self.hass, self._topic[CONF_COMMAND_TOPIC], json.dumps(message),
self._qos, self._retain) self._qos, self._retain)
@ -245,7 +388,7 @@ class MqttJson(Light):
message = {'state': 'OFF'} message = {'state': 'OFF'}
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
message['transition'] = kwargs[ATTR_TRANSITION] message['transition'] = int(kwargs[ATTR_TRANSITION])
mqtt.async_publish( mqtt.async_publish(
self.hass, self._topic[CONF_COMMAND_TOPIC], json.dumps(message), self.hass, self._topic[CONF_COMMAND_TOPIC], json.dumps(message),

View File

@ -11,9 +11,10 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH,
PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, Light, PLATFORM_SCHEMA,
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light) SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH,
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE)
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
@ -28,43 +29,47 @@ DEPENDENCIES = ['mqtt']
DEFAULT_NAME = 'MQTT Template Light' DEFAULT_NAME = 'MQTT Template Light'
DEFAULT_OPTIMISTIC = False DEFAULT_OPTIMISTIC = False
CONF_EFFECT_LIST = "effect_list"
CONF_COMMAND_ON_TEMPLATE = 'command_on_template'
CONF_COMMAND_OFF_TEMPLATE = 'command_off_template'
CONF_STATE_TEMPLATE = 'state_template'
CONF_BRIGHTNESS_TEMPLATE = 'brightness_template'
CONF_RED_TEMPLATE = 'red_template'
CONF_GREEN_TEMPLATE = 'green_template'
CONF_BLUE_TEMPLATE = 'blue_template' CONF_BLUE_TEMPLATE = 'blue_template'
CONF_BRIGHTNESS_TEMPLATE = 'brightness_template'
CONF_COLOR_TEMP_TEMPLATE = 'color_temp_template'
CONF_COMMAND_OFF_TEMPLATE = 'command_off_template'
CONF_COMMAND_ON_TEMPLATE = 'command_on_template'
CONF_EFFECT_LIST = 'effect_list'
CONF_EFFECT_TEMPLATE = 'effect_template' CONF_EFFECT_TEMPLATE = 'effect_template'
CONF_GREEN_TEMPLATE = 'green_template'
SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH | CONF_RED_TEMPLATE = 'red_template'
SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) CONF_STATE_TEMPLATE = 'state_template'
CONF_WHITE_VALUE_TEMPLATE = 'white_value_template'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template,
vol.Required(CONF_COMMAND_OFF_TEMPLATE): cv.template,
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template,
vol.Optional(CONF_RED_TEMPLATE): cv.template,
vol.Optional(CONF_GREEN_TEMPLATE): cv.template,
vol.Optional(CONF_BLUE_TEMPLATE): cv.template, vol.Optional(CONF_BLUE_TEMPLATE): cv.template,
vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template,
vol.Optional(CONF_COLOR_TEMP_TEMPLATE): cv.template,
vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EFFECT_TEMPLATE): cv.template, vol.Optional(CONF_EFFECT_TEMPLATE): cv.template,
vol.Optional(CONF_GREEN_TEMPLATE): cv.template,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_RED_TEMPLATE): cv.template,
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template,
vol.Required(CONF_COMMAND_OFF_TEMPLATE): cv.template,
vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template,
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS):
vol.All(vol.Coerce(int), vol.In([0, 1, 2])), vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean
}) })
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a MQTT Template light.""" """Setup a MQTT Template light."""
yield from async_add_devices([MqttTemplate( if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
async_add_devices([MqttTemplate(
hass, hass,
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_EFFECT_LIST), config.get(CONF_EFFECT_LIST),
@ -76,14 +81,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
}, },
{ {
key: config.get(key) for key in ( key: config.get(key) for key in (
CONF_COMMAND_ON_TEMPLATE,
CONF_COMMAND_OFF_TEMPLATE,
CONF_STATE_TEMPLATE,
CONF_BRIGHTNESS_TEMPLATE,
CONF_RED_TEMPLATE,
CONF_GREEN_TEMPLATE,
CONF_BLUE_TEMPLATE, CONF_BLUE_TEMPLATE,
CONF_EFFECT_TEMPLATE CONF_BRIGHTNESS_TEMPLATE,
CONF_COLOR_TEMP_TEMPLATE,
CONF_COMMAND_OFF_TEMPLATE,
CONF_COMMAND_ON_TEMPLATE,
CONF_EFFECT_TEMPLATE,
CONF_GREEN_TEMPLATE,
CONF_RED_TEMPLATE,
CONF_STATE_TEMPLATE,
CONF_WHITE_VALUE_TEMPLATE,
) )
}, },
config.get(CONF_OPTIMISTIC), config.get(CONF_OPTIMISTIC),
@ -114,6 +121,16 @@ class MqttTemplate(Light):
else: else:
self._brightness = None self._brightness = None
if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None:
self._color_temp = 255
else:
self._color_temp = None
if self._templates[CONF_WHITE_VALUE_TEMPLATE] is not None:
self._white_value = 255
else:
self._white_value = None
if (self._templates[CONF_RED_TEMPLATE] is not None and if (self._templates[CONF_RED_TEMPLATE] is not None and
self._templates[CONF_GREEN_TEMPLATE] is not None and self._templates[CONF_GREEN_TEMPLATE] is not None and
self._templates[CONF_BLUE_TEMPLATE] is not None): self._templates[CONF_BLUE_TEMPLATE] is not None):
@ -156,6 +173,16 @@ class MqttTemplate(Light):
except ValueError: except ValueError:
_LOGGER.warning('Invalid brightness value received') _LOGGER.warning('Invalid brightness value received')
# read color temperature
if self._color_temp is not None:
try:
self._color_temp = int(
self._templates[CONF_COLOR_TEMP_TEMPLATE].
async_render_with_possible_json_value(payload)
)
except ValueError:
_LOGGER.warning('Invalid color temperature value received')
# read color # read color
if self._rgb is not None: if self._rgb is not None:
try: try:
@ -171,6 +198,16 @@ class MqttTemplate(Light):
except ValueError: except ValueError:
_LOGGER.warning('Invalid color value received') _LOGGER.warning('Invalid color value received')
# read white value
if self._white_value is not None:
try:
self._white_value = int(
self._templates[CONF_WHITE_VALUE_TEMPLATE].
async_render_with_possible_json_value(payload)
)
except ValueError:
_LOGGER.warning('Invalid white value received')
# read effect # read effect
if self._templates[CONF_EFFECT_TEMPLATE] is not None: if self._templates[CONF_EFFECT_TEMPLATE] is not None:
effect = self._templates[CONF_EFFECT_TEMPLATE].\ effect = self._templates[CONF_EFFECT_TEMPLATE].\
@ -194,11 +231,21 @@ class MqttTemplate(Light):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
return self._brightness return self._brightness
@property
def color_temp(self):
"""Return the color temperature in mired."""
return self._color_temp
@property @property
def rgb_color(self): def rgb_color(self):
"""Return the RGB color value [int, int, int].""" """Return the RGB color value [int, int, int]."""
return self._rgb return self._rgb
@property
def white_value(self):
"""Return the white property."""
return self._white_value
@property @property
def should_poll(self): def should_poll(self):
"""Return True if entity has to be polled for state. """Return True if entity has to be polled for state.
@ -250,6 +297,13 @@ class MqttTemplate(Light):
if self._optimistic: if self._optimistic:
self._brightness = kwargs[ATTR_BRIGHTNESS] self._brightness = kwargs[ATTR_BRIGHTNESS]
# color_temp
if ATTR_COLOR_TEMP in kwargs:
values['color_temp'] = int(kwargs[ATTR_COLOR_TEMP])
if self._optimistic:
self._color_temp = kwargs[ATTR_COLOR_TEMP]
# color # color
if ATTR_RGB_COLOR in kwargs: if ATTR_RGB_COLOR in kwargs:
values['red'] = kwargs[ATTR_RGB_COLOR][0] values['red'] = kwargs[ATTR_RGB_COLOR][0]
@ -259,6 +313,13 @@ class MqttTemplate(Light):
if self._optimistic: if self._optimistic:
self._rgb = kwargs[ATTR_RGB_COLOR] self._rgb = kwargs[ATTR_RGB_COLOR]
# white value
if ATTR_WHITE_VALUE in kwargs:
values['white_value'] = int(kwargs[ATTR_WHITE_VALUE])
if self._optimistic:
self._white_value = kwargs[ATTR_WHITE_VALUE]
# effect # effect
if ATTR_EFFECT in kwargs: if ATTR_EFFECT in kwargs:
values['effect'] = kwargs.get(ATTR_EFFECT) values['effect'] = kwargs.get(ATTR_EFFECT)
@ -269,7 +330,7 @@ class MqttTemplate(Light):
# transition # transition
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
values['transition'] = kwargs[ATTR_TRANSITION] values['transition'] = int(kwargs[ATTR_TRANSITION])
mqtt.async_publish( mqtt.async_publish(
self.hass, self._topics[CONF_COMMAND_TOPIC], self.hass, self._topics[CONF_COMMAND_TOPIC],
@ -293,7 +354,7 @@ class MqttTemplate(Light):
# transition # transition
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
values['transition'] = kwargs[ATTR_TRANSITION] values['transition'] = int(kwargs[ATTR_TRANSITION])
mqtt.async_publish( mqtt.async_publish(
self.hass, self._topics[CONF_COMMAND_TOPIC], self.hass, self._topics[CONF_COMMAND_TOPIC],
@ -307,12 +368,16 @@ class MqttTemplate(Light):
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
features = 0 features = (SUPPORT_FLASH | SUPPORT_TRANSITION)
if self._brightness is not None: if self._brightness is not None:
features = features | SUPPORT_BRIGHTNESS features = features | SUPPORT_BRIGHTNESS
if self._rgb is not None: if self._rgb is not None:
features = features | SUPPORT_RGB_COLOR features = features | SUPPORT_RGB_COLOR
if self._effect_list is not None: if self._effect_list is not None:
features = features | SUPPORT_EFFECT features = features | SUPPORT_EFFECT
if self._color_temp is not None:
features = features | SUPPORT_COLOR_TEMP
if self._white_value is not None:
features = features | SUPPORT_WHITE_VALUE
return features return features

View File

@ -17,6 +17,8 @@ from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR,
ATTR_TRANSITION, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, ATTR_TRANSITION, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT,
SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA)
from homeassistant.util.color import (
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['https://github.com/tfriedel/python-lightify/archive/' REQUIREMENTS = ['https://github.com/tfriedel/python-lightify/archive/'
@ -24,10 +26,6 @@ REQUIREMENTS = ['https://github.com/tfriedel/python-lightify/archive/'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TEMP_MIN = 2000 # lightify minimum temperature
TEMP_MAX = 6500 # lightify maximum temperature
TEMP_MIN_HASS = 154 # home assistant minimum temperature
TEMP_MAX_HASS = 500 # home assistant maximum temperature
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
@ -93,10 +91,10 @@ class OsramLightifyLight(Light):
self._light = light self._light = light
self._light_id = light_id self._light_id = light_id
self.update_lights = update_lights self.update_lights = update_lights
self._brightness = 0 self._brightness = None
self._rgb = (0, 0, 0) self._rgb = None
self._name = "" self._name = None
self._temperature = TEMP_MIN self._temperature = None
self._state = False self._state = False
self.update() self.update()
@ -145,7 +143,7 @@ class OsramLightifyLight(Light):
self._state = self._light.on() self._state = self._light.on()
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
transition = kwargs[ATTR_TRANSITION] * 10 transition = int(kwargs[ATTR_TRANSITION] * 10)
_LOGGER.debug("turn_on requested transition time for light:" _LOGGER.debug("turn_on requested transition time for light:"
" %s is: %s ", " %s is: %s ",
self._name, transition) self._name, transition)
@ -164,8 +162,7 @@ class OsramLightifyLight(Light):
if ATTR_COLOR_TEMP in kwargs: if ATTR_COLOR_TEMP in kwargs:
color_t = kwargs[ATTR_COLOR_TEMP] color_t = kwargs[ATTR_COLOR_TEMP]
kelvin = int(((TEMP_MAX - TEMP_MIN) * (color_t - TEMP_MIN_HASS) / kelvin = int(color_temperature_mired_to_kelvin(color_t))
(TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN)
_LOGGER.debug("turn_on requested set_temperature for light:" _LOGGER.debug("turn_on requested set_temperature for light:"
" %s: %s ", self._name, kelvin) " %s: %s ", self._name, kelvin)
self._light.set_temperature(kelvin, transition) self._light.set_temperature(kelvin, transition)
@ -196,7 +193,7 @@ class OsramLightifyLight(Light):
_LOGGER.debug("turn_off Attempting to turn off light: %s ", _LOGGER.debug("turn_off Attempting to turn off light: %s ",
self._name) self._name)
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
transition = kwargs[ATTR_TRANSITION] * 10 transition = int(kwargs[ATTR_TRANSITION] * 10)
_LOGGER.debug("turn_off requested transition time for light:" _LOGGER.debug("turn_off requested transition time for light:"
" %s is: %s ", " %s is: %s ",
self._name, transition) self._name, transition)
@ -218,6 +215,5 @@ class OsramLightifyLight(Light):
self._name = self._light.name() self._name = self._light.name()
self._rgb = self._light.rgb() self._rgb = self._light.rgb()
o_temp = self._light.temp() o_temp = self._light.temp()
self._temperature = int(TEMP_MIN_HASS + (TEMP_MAX_HASS - TEMP_MIN_HASS) self._temperature = color_temperature_kelvin_to_mired(o_temp)
* (o_temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN))
self._state = self._light.on() self._state = self._light.on()

View File

@ -117,7 +117,7 @@ def devices_from_config(domain_config, hass=None):
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Rflink light platform.""" """Set up the Rflink light platform."""
yield from async_add_devices(devices_from_config(config, hass)) async_add_devices(devices_from_config(config, hass))
# Add new (unconfigured) devices to user desired group # Add new (unconfigured) devices to user desired group
if config[CONF_NEW_DEVICES_GROUP]: if config[CONF_NEW_DEVICES_GROUP]:
@ -136,7 +136,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
device_config = config[CONF_DEVICE_DEFAULTS] device_config = config[CONF_DEVICE_DEFAULTS]
device = entity_class(device_id, hass, **device_config) device = entity_class(device_id, hass, **device_config)
yield from async_add_devices([device]) async_add_devices([device])
# Register entity to listen to incoming Rflink events # Register entity to listen to incoming Rflink events
hass.data[DATA_ENTITY_LOOKUP][ hass.data[DATA_ENTITY_LOOKUP][
@ -156,7 +156,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class RflinkLight(SwitchableRflinkDevice, Light): class RflinkLight(SwitchableRflinkDevice, Light):
"""Representation of a Rflink light.""" """Representation of a Rflink light."""
pass @property
def entity_id(self):
"""Return entity id."""
return "light.{}".format(self.name)
class DimmableRflinkLight(SwitchableRflinkDevice, Light): class DimmableRflinkLight(SwitchableRflinkDevice, Light):
@ -164,6 +167,11 @@ class DimmableRflinkLight(SwitchableRflinkDevice, Light):
_brightness = 255 _brightness = 255
@property
def entity_id(self):
"""Return entity id."""
return "light.{}".format(self.name)
@asyncio.coroutine @asyncio.coroutine
def async_turn_on(self, **kwargs): def async_turn_on(self, **kwargs):
"""Turn the device on.""" """Turn the device on."""
@ -202,6 +210,11 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light):
_brightness = 255 _brightness = 255
@property
def entity_id(self):
"""Return entity id."""
return "light.{}".format(self.name)
@asyncio.coroutine @asyncio.coroutine
def async_turn_on(self, **kwargs): def async_turn_on(self, **kwargs):
"""Turn the device on and set dim level.""" """Turn the device on and set dim level."""

View File

@ -106,7 +106,7 @@ class SCSGateLight(Light):
return return
self._toggled = message.toggled self._toggled = message.toggled
self.update_ha_state() self.schedule_update_ha_state()
command = "off" command = "off"
if self._toggled: if self._toggled:

View File

@ -7,10 +7,10 @@ https://home-assistant.io/components/light.vera/
import logging import logging
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS)
from homeassistant.const import (STATE_OFF, STATE_ON) from homeassistant.const import (STATE_OFF, STATE_ON)
from homeassistant.components.vera import ( from homeassistant.components.vera import (
VeraDevice, VERA_DEVICES, VERA_CONTROLLER) VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -33,6 +33,7 @@ class VeraLight(VeraDevice, Light):
"""Initialize the light.""" """Initialize the light."""
self._state = False self._state = False
VeraDevice.__init__(self, vera_device, controller) VeraDevice.__init__(self, vera_device, controller)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property @property
def brightness(self): def brightness(self):

View File

@ -259,7 +259,7 @@ class YeelightLight(Light):
_LOGGER.error("Flash supported currently only in RGB mode.") _LOGGER.error("Flash supported currently only in RGB mode.")
return return
transition = self.config[CONF_TRANSITION] transition = int(self.config[CONF_TRANSITION])
if flash == FLASH_LONG: if flash == FLASH_LONG:
count = 1 count = 1
duration = transition * 5 duration = transition * 5
@ -288,9 +288,9 @@ class YeelightLight(Light):
rgb = kwargs.get(ATTR_RGB_COLOR) rgb = kwargs.get(ATTR_RGB_COLOR)
flash = kwargs.get(ATTR_FLASH) flash = kwargs.get(ATTR_FLASH)
duration = self.config[CONF_TRANSITION] # in ms duration = int(self.config[CONF_TRANSITION]) # in ms
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
duration = kwargs.get(ATTR_TRANSITION) * 1000 # kwarg in s duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
self._bulb.turn_on(duration=duration) self._bulb.turn_on(duration=duration)

View File

@ -2,9 +2,7 @@
Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi). Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi).
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.yeelight-sunflower https://home-assistant.io/components/light.yeelightsunflower
Uses the yeelightsunflower library:
https://github.com/lindsaymarkward/python-yeelight-sunflower
""" """
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -17,34 +15,29 @@ from homeassistant.components.light import (Light,
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['yeelightsunflower==0.0.6'] REQUIREMENTS = ['yeelightsunflower==0.0.8']
SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Validate the user's configuration
SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string vol.Required(CONF_HOST): cv.string
}) })
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Yeelight Sunflower Light platform.""" """Set up the Yeelight Sunflower Light platform."""
import yeelightsunflower import yeelightsunflower
# Assign configuration variables.
# The configuration check takes care they are present.
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
# Setup connection with Yeelight Sunflower hub
hub = yeelightsunflower.Hub(host) hub = yeelightsunflower.Hub(host)
# Verify that hub is responsive
if not hub.available: if not hub.available:
_LOGGER.error('Could not connect to Yeelight Sunflower hub') _LOGGER.error('Could not connect to Yeelight Sunflower hub')
return False return False
# Add devices
add_devices(SunflowerBulb(light) for light in hub.get_lights()) add_devices(SunflowerBulb(light) for light in hub.get_lights())

View File

@ -13,6 +13,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \ ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \
SUPPORT_RGB_COLOR, DOMAIN, Light SUPPORT_RGB_COLOR, DOMAIN, Light
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \ from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \
color_temperature_mired_to_kelvin, color_temperature_to_rgb, \ color_temperature_mired_to_kelvin, color_temperature_to_rgb, \
@ -48,38 +49,25 @@ SUPPORT_ZWAVE_COLORTEMP = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
| SUPPORT_COLOR_TEMP) | SUPPORT_COLOR_TEMP)
def setup_platform(hass, config, add_devices, discovery_info=None): def get_device(node, value, node_config, **kwargs):
"""Find and add Z-Wave lights.""" """Create zwave entity device."""
if discovery_info is None or zwave.NETWORK is None:
return
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
name = '{}.{}'.format(DOMAIN, zwave.object_id(value)) name = '{}.{}'.format(DOMAIN, zwave.object_id(value))
node_config = hass.data[zwave.DATA_DEVICE_CONFIG].get(name)
refresh = node_config.get(zwave.CONF_REFRESH_VALUE) refresh = node_config.get(zwave.CONF_REFRESH_VALUE)
delay = node_config.get(zwave.CONF_REFRESH_DELAY) delay = node_config.get(zwave.CONF_REFRESH_DELAY)
_LOGGER.debug('name=%s node_config=%s CONF_REFRESH_VALUE=%s' _LOGGER.debug('name=%s node_config=%s CONF_REFRESH_VALUE=%s'
' CONF_REFRESH_DELAY=%s', name, node_config, ' CONF_REFRESH_DELAY=%s', name, node_config,
refresh, delay) refresh, delay)
if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL:
return
if value.type != zwave.const.TYPE_BYTE:
return
if value.genre != zwave.const.GENRE_USER:
return
value.set_change_verified(False)
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR): if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
add_devices([ZwaveColorLight(value, refresh, delay)]) return ZwaveColorLight(value, refresh, delay)
else: else:
add_devices([ZwaveDimmer(value, refresh, delay)]) return ZwaveDimmer(value, refresh, delay)
def brightness_state(value): def brightness_state(value):
"""Return the brightness and state.""" """Return the brightness and state."""
if value.data > 0: if value.data > 0:
return (value.data / 99) * 255, STATE_ON return round((value.data / 99) * 255, 0), STATE_ON
else: else:
return 0, STATE_OFF return 0, STATE_OFF
@ -119,7 +107,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
# Brightness # Brightness
self._brightness, self._state = brightness_state(self._value) self._brightness, self._state = brightness_state(self._value)
def value_changed(self, value): def value_changed(self):
"""Called when a value for this entity's node has changed.""" """Called when a value for this entity's node has changed."""
if self._refresh_value: if self._refresh_value:
if self._refreshing: if self._refreshing:
@ -136,7 +124,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
self._timer = Timer(self._delay, _refresh_value) self._timer = Timer(self._delay, _refresh_value)
self._timer.start() self._timer.start()
return return
super().value_changed(value) super().value_changed()
@property @property
def brightness(self): def brightness(self):
@ -200,6 +188,12 @@ class ZwaveColorLight(ZwaveDimmer):
self._value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED) self._value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED)
self._get_color_values() self._get_color_values()
@property
def dependent_value_ids(self):
"""List of value IDs a device depends on."""
return [val.value_id for val in [
self._value_color, self._value_color_channels] if val]
def _get_color_values(self): def _get_color_values(self):
"""Search for color values available on this node.""" """Search for color values available on this node."""
from openzwave.network import ZWaveNetwork from openzwave.network import ZWaveNetwork

View File

@ -47,7 +47,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
yield from async_add_devices([MqttLock( async_add_devices([MqttLock(
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC), config.get(CONF_STATE_TOPIC),
config.get(CONF_COMMAND_TOPIC), config.get(CONF_COMMAND_TOPIC),

View File

@ -6,10 +6,10 @@ https://home-assistant.io/components/lock.vera/
""" """
import logging import logging
from homeassistant.components.lock import LockDevice from homeassistant.components.lock import ENTITY_ID_FORMAT, LockDevice
from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED)
from homeassistant.components.vera import ( from homeassistant.components.vera import (
VeraDevice, VERA_DEVICES, VERA_CONTROLLER) VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,6 +30,7 @@ class VeraLock(VeraDevice, LockDevice):
"""Initialize the Vera device.""" """Initialize the Vera device."""
self._state = None self._state = None
VeraDevice.__init__(self, vera_device, controller) VeraDevice.__init__(self, vera_device, controller)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
def lock(self, **kwargs): def lock(self, **kwargs):
"""Lock the device.""" """Lock the device."""

View File

@ -13,6 +13,7 @@ import voluptuous as vol
from homeassistant.components.lock import DOMAIN, LockDevice from homeassistant.components.lock import DOMAIN, LockDevice
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -119,15 +120,8 @@ CLEAR_USERCODE_SCHEMA = vol.Schema({
}) })
# pylint: disable=unused-argument def get_device(hass, node, value, **kwargs):
def setup_platform(hass, config, add_devices, discovery_info=None): """Create zwave entity device."""
"""Find and return Z-Wave locks."""
if discovery_info is None or zwave.NETWORK is None:
return
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml')) path.join(path.dirname(__file__), 'services.yaml'))
@ -181,12 +175,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.info('Usercode at slot %s is cleared', value.index) _LOGGER.info('Usercode at slot %s is cleared', value.index)
break break
if value.command_class != zwave.const.COMMAND_CLASS_DOOR_LOCK:
return
if value.type != zwave.const.TYPE_BOOL:
return
if value.genre != zwave.const.GENRE_USER:
return
if node.has_command_class(zwave.const.COMMAND_CLASS_USER_CODE): if node.has_command_class(zwave.const.COMMAND_CLASS_USER_CODE):
hass.services.register(DOMAIN, hass.services.register(DOMAIN,
SERVICE_SET_USERCODE, SERVICE_SET_USERCODE,
@ -203,8 +191,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
clear_usercode, clear_usercode,
descriptions.get(SERVICE_CLEAR_USERCODE), descriptions.get(SERVICE_CLEAR_USERCODE),
schema=CLEAR_USERCODE_SCHEMA) schema=CLEAR_USERCODE_SCHEMA)
value.set_change_verified(False) return ZwaveLock(value)
add_devices([ZwaveLock(value)])
class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
@ -305,3 +292,8 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
if self._lock_status: if self._lock_status:
data[ATTR_LOCK_STATUS] = self._lock_status data[ATTR_LOCK_STATUS] = self._lock_status
return data return data
@property
def dependent_value_ids(self):
"""List of value IDs a device depends on."""
return None

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