mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 04:07:08 +00:00
commit
a8b32edc8e
23
.coveragerc
23
.coveragerc
@ -14,9 +14,15 @@ omit =
|
||||
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/blink.py
|
||||
homeassistant/components/*/blink.py
|
||||
|
||||
homeassistant/components/bloomsky.py
|
||||
homeassistant/components/*/bloomsky.py
|
||||
|
||||
@ -85,6 +91,10 @@ omit =
|
||||
|
||||
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
|
||||
|
||||
@ -132,6 +142,9 @@ omit =
|
||||
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/concord232.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
@ -231,6 +244,7 @@ omit =
|
||||
homeassistant/components/media_player/dunehd.py
|
||||
homeassistant/components/media_player/emby.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/frontier_silicon.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/gstreamer.py
|
||||
homeassistant/components/media_player/hdmi_cec.py
|
||||
@ -259,6 +273,7 @@ omit =
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
@ -286,8 +301,6 @@ omit =
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.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/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
@ -303,12 +316,14 @@ omit =
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/cups.py
|
||||
homeassistant/components/sensor/currencylayer.py
|
||||
homeassistant/components/sensor/darksky.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dnsip.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
@ -333,11 +348,12 @@ omit =
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
homeassistant/components/sensor/kwb.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/mhz19.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/modem_callerid.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
@ -411,6 +427,7 @@ omit =
|
||||
homeassistant/components/upnp.py
|
||||
homeassistant/components/weather/bom.py
|
||||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/weather/zamg.py
|
||||
homeassistant/components/zeroconf.py
|
||||
|
||||
|
||||
|
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,16 +1,16 @@
|
||||
**Description:**
|
||||
## Description:
|
||||
|
||||
|
||||
**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>
|
||||
|
||||
**Example entry for `configuration.yaml` (if applicable):**
|
||||
## Example entry for `configuration.yaml` (if applicable):
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
**Checklist:**
|
||||
## Checklist:
|
||||
|
||||
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)
|
||||
|
@ -14,6 +14,8 @@ matrix:
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "3.6-dev"
|
||||
env: TOXENV=py36
|
||||
# allow_failures:
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
|
@ -4,320 +4,31 @@ import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
from time import time
|
||||
from collections import OrderedDict
|
||||
|
||||
from types import ModuleType
|
||||
from typing import Any, Optional, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
import homeassistant.components as core_components
|
||||
from homeassistant.components import persistent_notification
|
||||
import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
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.yaml import clear_secret_cache
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
event_decorators, service, config_per_platform, extract_domain_configs)
|
||||
from homeassistant.helpers import event_decorators, service
|
||||
from homeassistant.helpers.signal import async_register_signal_handling
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_COMPONENT = 'component'
|
||||
|
||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
_PERSISTENT_ERRORS = {}
|
||||
HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)'
|
||||
|
||||
|
||||
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
|
||||
FIRST_INIT_COMPONENT = set((
|
||||
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction'))
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
future = asyncio.Future(loop=hass.loop)
|
||||
hass.async_add_job(_async_init_from_config_dict(future))
|
||||
hass.loop.run_until_complete(future)
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days)
|
||||
)
|
||||
|
||||
return future.result()
|
||||
return hass
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -372,19 +74,15 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
Dynamically loads required components and its dependencies.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
start = time()
|
||||
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, {})
|
||||
|
||||
try:
|
||||
yield from conf_util.async_process_ha_core_config(hass, core_config)
|
||||
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
|
||||
|
||||
yield from hass.loop.run_in_executor(
|
||||
@ -433,20 +131,25 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
event_decorators.HASS = hass
|
||||
service.HASS = hass
|
||||
|
||||
# Setup the components
|
||||
dependency_blacklist = loader.DEPENDENCY_BLACKLIST - set(components)
|
||||
# stage 1
|
||||
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):
|
||||
if domain in dependency_blacklist:
|
||||
raise HomeAssistantError(
|
||||
'{} is not allowed to be a dependency'.format(domain))
|
||||
yield from hass.async_block_till_done()
|
||||
|
||||
yield from _async_setup_component(hass, domain, config)
|
||||
|
||||
setup_lock.release()
|
||||
# stage 2
|
||||
for component in components:
|
||||
if component in FIRST_INIT_COMPONENT:
|
||||
continue
|
||||
hass.async_add_job(async_setup_component(hass, component, config))
|
||||
|
||||
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)
|
||||
return hass
|
||||
|
||||
@ -464,22 +167,13 @@ def from_config_file(config_path: str,
|
||||
if hass is None:
|
||||
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
|
||||
future = asyncio.Future(loop=hass.loop)
|
||||
hass.loop.create_task(_async_init_from_config_file(future))
|
||||
hass.loop.run_until_complete(future)
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip, log_rotate_days)
|
||||
)
|
||||
|
||||
return future.result()
|
||||
return hass
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -504,7 +198,8 @@ def async_from_config_file(config_path: str,
|
||||
try:
|
||||
config_dict = yield from hass.loop.run_in_executor(
|
||||
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
|
||||
finally:
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
"""Add local library to Python Path.
|
||||
|
||||
|
@ -55,7 +55,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
yield from async_add_devices(devices)
|
||||
async_add_devices(devices)
|
||||
|
||||
@callback
|
||||
def alarm_keypress_handler(service):
|
||||
@ -94,10 +94,13 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||
super().__init__(alarm_name, info, controller)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
||||
self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_PARTITION_UPDATE, self._update_callback)
|
||||
self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self, partition):
|
||||
|
@ -46,7 +46,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the MQTT platform."""
|
||||
yield from async_add_devices([MqttAlarm(
|
||||
async_add_devices([MqttAlarm(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
|
@ -18,7 +18,6 @@ from homeassistant.const import (
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers import service, event
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -62,8 +61,7 @@ def is_on(hass, entity_id):
|
||||
|
||||
def turn_on(hass, entity_id):
|
||||
"""Reset the alert."""
|
||||
run_callback_threadsafe(
|
||||
hass.loop, async_turn_on, hass, entity_id).result()
|
||||
hass.add_job(async_turn_on, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@ -76,8 +74,7 @@ def async_turn_on(hass, entity_id):
|
||||
|
||||
def turn_off(hass, entity_id):
|
||||
"""Acknowledge alert."""
|
||||
run_callback_threadsafe(
|
||||
hass.loop, async_turn_off, hass, entity_id).result()
|
||||
hass.add_job(async_turn_off, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@ -90,7 +87,7 @@ def async_turn_off(hass, entity_id):
|
||||
|
||||
def toggle(hass, entity_id):
|
||||
"""Toggle acknowledgement of alert."""
|
||||
run_callback_threadsafe(hass.loop, async_toggle, hass, entity_id)
|
||||
hass.add_job(async_toggle, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
|
303
homeassistant/components/android_ip_webcam.py
Normal file
303
homeassistant/components/android_ip_webcam.py
Normal 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
|
@ -11,16 +11,17 @@ import os
|
||||
|
||||
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.const import (
|
||||
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.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import extract_domain_configs, script, condition
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
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.util.dt import utcnow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -28,8 +29,6 @@ import homeassistant.helpers.config_validation as cv
|
||||
DOMAIN = 'automation'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
DEPENDENCIES = ['group']
|
||||
|
||||
GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
|
||||
|
||||
CONF_ALIAS = 'alias'
|
||||
@ -52,7 +51,6 @@ DEFAULT_INITIAL_STATE = True
|
||||
ATTR_LAST_TRIGGERED = 'last_triggered'
|
||||
ATTR_VARIABLES = 'variables'
|
||||
SERVICE_TRIGGER = 'trigger'
|
||||
SERVICE_RELOAD = 'reload'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -226,7 +224,7 @@ class AutomationEntity(ToggleEntity):
|
||||
"""Entity to show status of entity."""
|
||||
|
||||
def __init__(self, name, async_attach_triggers, cond_func, async_action,
|
||||
hidden):
|
||||
hidden, initial_state):
|
||||
"""Initialize an automation entity."""
|
||||
self._name = name
|
||||
self._async_attach_triggers = async_attach_triggers
|
||||
@ -236,6 +234,7 @@ class AutomationEntity(ToggleEntity):
|
||||
self._enabled = False
|
||||
self._last_triggered = None
|
||||
self._hidden = hidden
|
||||
self._initial_state = initial_state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -264,6 +263,18 @@ class AutomationEntity(ToggleEntity):
|
||||
"""Return True if entity is on."""
|
||||
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
|
||||
def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn the entity on and update the state."""
|
||||
@ -322,7 +333,6 @@ def _async_process_config(hass, config, component):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
entities = []
|
||||
tasks = []
|
||||
|
||||
for config_key in extract_domain_configs(config, DOMAIN):
|
||||
conf = config[config_key]
|
||||
@ -332,6 +342,7 @@ def _async_process_config(hass, config, component):
|
||||
list_no)
|
||||
|
||||
hidden = config_block[CONF_HIDE_ENTITY]
|
||||
initial_state = config_block[CONF_INITIAL_STATE]
|
||||
|
||||
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
|
||||
name)
|
||||
@ -348,15 +359,14 @@ def _async_process_config(hass, config, component):
|
||||
|
||||
async_attach_triggers = partial(
|
||||
_async_process_trigger, hass, config,
|
||||
config_block.get(CONF_TRIGGER, []), name)
|
||||
entity = AutomationEntity(name, async_attach_triggers, cond_func,
|
||||
action, hidden)
|
||||
if config_block[CONF_INITIAL_STATE]:
|
||||
tasks.append(entity.async_enable())
|
||||
config_block.get(CONF_TRIGGER, []), name
|
||||
)
|
||||
entity = AutomationEntity(
|
||||
name, async_attach_triggers, cond_func, action, hidden,
|
||||
initial_state)
|
||||
|
||||
entities.append(entity)
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
if entities:
|
||||
yield from component.async_add_entities(entities)
|
||||
|
||||
|
62
homeassistant/components/binary_sensor/android_ip_webcam.py
Normal file
62
homeassistant/components/binary_sensor/android_ip_webcam.py
Normal 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'
|
74
homeassistant/components/binary_sensor/blink.py
Normal file
74
homeassistant/components/binary_sensor/blink.py
Normal 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
|
@ -67,7 +67,7 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||
This method is called when there is an incoming packet associated
|
||||
with this platform.
|
||||
"""
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
if value2 == 0x70:
|
||||
self.which = 0
|
||||
self.onoff = 0
|
||||
|
@ -37,7 +37,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
yield from async_add_devices(devices)
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
@ -52,8 +52,11 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
_LOGGER.debug('Setting up zone: ' + zone_name)
|
||||
super().__init__(zone_name, info, controller)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_ZONE_UPDATE, self._update_callback)
|
||||
self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
@ -57,16 +57,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
# generate sensor object
|
||||
entity = FFmpegMotion(hass, manager, config)
|
||||
|
||||
# add to system
|
||||
manager.async_register_device(entity)
|
||||
yield from async_add_devices([entity])
|
||||
async_add_devices([entity])
|
||||
|
||||
|
||||
class FFmpegBinarySensor(FFmpegBase, BinarySensorDevice):
|
||||
"""A binary sensor which use ffmpeg for noise detection."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
def __init__(self, config):
|
||||
"""Constructor for binary sensor noise detection."""
|
||||
super().__init__(config.get(CONF_INITIAL_STATE))
|
||||
|
||||
@ -98,15 +95,19 @@ class FFmpegMotion(FFmpegBinarySensor):
|
||||
"""Initialize ffmpeg motion binary sensor."""
|
||||
from haffmpeg import SensorMotion
|
||||
|
||||
super().__init__(hass, config)
|
||||
super().__init__(config)
|
||||
self.ffmpeg = SensorMotion(
|
||||
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.
|
||||
|
||||
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
|
||||
self.ffmpeg.set_options(
|
||||
time_reset=self._config.get(CONF_RESET),
|
||||
@ -116,7 +117,7 @@ class FFmpegMotion(FFmpegBinarySensor):
|
||||
)
|
||||
|
||||
# run
|
||||
return self.ffmpeg.open_sensor(
|
||||
yield from self.ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
)
|
||||
|
@ -54,10 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
# generate sensor object
|
||||
entity = FFmpegNoise(hass, manager, config)
|
||||
|
||||
# add to system
|
||||
manager.async_register_device(entity)
|
||||
yield from async_add_devices([entity])
|
||||
async_add_devices([entity])
|
||||
|
||||
|
||||
class FFmpegNoise(FFmpegBinarySensor):
|
||||
@ -67,15 +64,19 @@ class FFmpegNoise(FFmpegBinarySensor):
|
||||
"""Initialize ffmpeg noise binary sensor."""
|
||||
from haffmpeg import SensorNoise
|
||||
|
||||
super().__init__(hass, config)
|
||||
super().__init__(config)
|
||||
self.ffmpeg = SensorNoise(
|
||||
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.
|
||||
|
||||
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
|
||||
self.ffmpeg.set_options(
|
||||
time_duration=self._config.get(CONF_DURATION),
|
||||
@ -84,7 +85,7 @@ class FFmpegNoise(FFmpegBinarySensor):
|
||||
)
|
||||
|
||||
# run
|
||||
return self.ffmpeg.open_sensor(
|
||||
yield from self.ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
output_dest=self._config.get(CONF_OUTPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
|
@ -15,9 +15,10 @@ from homeassistant.components.binary_sensor import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
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__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
@ -119,30 +120,32 @@ class HikvisionData(object):
|
||||
self._password = password
|
||||
|
||||
# Establish camera
|
||||
self._cam = HikCamera(self._url, self._port,
|
||||
self._username, self._password)
|
||||
self.camdata = HikCamera(self._url, self._port,
|
||||
self._username, self._password)
|
||||
|
||||
if self._name is None:
|
||||
self._name = self._cam.get_name
|
||||
|
||||
# Start event stream
|
||||
self._cam.start_stream()
|
||||
self._name = self.camdata.get_name
|
||||
|
||||
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):
|
||||
"""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
|
||||
def sensors(self):
|
||||
"""Return list of available sensors and their states."""
|
||||
return self._cam.current_event_states
|
||||
return self.camdata.current_event_states
|
||||
|
||||
@property
|
||||
def cam_id(self):
|
||||
"""Return camera id."""
|
||||
return self._cam.get_id
|
||||
return self.camdata.get_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -155,8 +158,6 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
|
||||
def __init__(self, hass, sensor, cam, delay):
|
||||
"""Initialize the binary_sensor."""
|
||||
from pydispatch import dispatcher
|
||||
|
||||
self._hass = hass
|
||||
self._cam = cam
|
||||
self._name = self._cam.name + ' ' + sensor
|
||||
@ -170,12 +171,8 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
|
||||
self._timer = None
|
||||
|
||||
# Form signal for dispatcher
|
||||
signal = 'ValueChanged.{}'.format(self._cam.cam_id)
|
||||
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=signal,
|
||||
sender=self._sensor)
|
||||
# Register callback function with pyHik
|
||||
self._cam.camdata.add_update_callback(self._update_callback, self._id)
|
||||
|
||||
def _sensor_state(self):
|
||||
"""Extract sensor state."""
|
||||
@ -225,13 +222,9 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
||||
|
||||
return attr
|
||||
|
||||
def _update_callback(self, signal, sender):
|
||||
def _update_callback(self, msg):
|
||||
"""Update the sensor's state, if needed."""
|
||||
_LOGGER.debug('Dispatcher callback, signal: %s, sender: %s',
|
||||
signal, sender)
|
||||
|
||||
if sender is not self._sensor:
|
||||
return
|
||||
_LOGGER.debug('Callback signal from: %s', msg)
|
||||
|
||||
if self._delay > 0 and not self.is_on:
|
||||
# Set timer to wait until updating the state
|
||||
|
76
homeassistant/components/binary_sensor/maxcube.py
Normal file
76
homeassistant/components/binary_sensor/maxcube.py
Normal 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
|
@ -46,7 +46,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
yield from async_add_devices([MqttBinarySensor(
|
||||
async_add_devices([MqttBinarySensor(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS),
|
||||
|
@ -15,12 +15,14 @@ from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
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.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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__)
|
||||
|
||||
@ -66,7 +68,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
_LOGGER.error('No sensors added')
|
||||
return False
|
||||
|
||||
yield from async_add_devices(sensors, True)
|
||||
async_add_devices(sensors, True)
|
||||
return True
|
||||
|
||||
|
||||
@ -83,14 +85,30 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
self._device_class = device_class
|
||||
self._template = value_template
|
||||
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
|
||||
def template_bsensor_state_listener(entity, old_state, new_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(
|
||||
hass, entity_ids, template_bsensor_state_listener)
|
||||
@callback
|
||||
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
|
||||
def name(self):
|
||||
|
@ -52,7 +52,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
limit_type = config.get(CONF_TYPE)
|
||||
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,
|
||||
device_class)], True)
|
||||
return True
|
||||
|
@ -7,9 +7,9 @@ https://home-assistant.io/components/binary_sensor.vera/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice)
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT)
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
@ -30,6 +30,7 @@ class VeraBinarySensor(VeraDevice, BinarySensorDevice):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._state = False
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
@ -10,6 +10,7 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.components import zwave
|
||||
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 (
|
||||
DOMAIN,
|
||||
BinarySensorDevice)
|
||||
@ -18,31 +19,20 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = []
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Z-Wave platform for binary sensors."""
|
||||
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)
|
||||
|
||||
def get_device(value, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
device_mapping = workaround.get_device_mapping(value)
|
||||
if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT:
|
||||
# Default the multiplier to 4
|
||||
re_arm_multiplier = (zwave.get_config_value(value.node, 9) or 4)
|
||||
add_devices([
|
||||
ZWaveTriggerSensor(value, "motion",
|
||||
hass, re_arm_multiplier * 8)
|
||||
])
|
||||
return
|
||||
return ZWaveTriggerSensor(value, "motion", re_arm_multiplier * 8)
|
||||
|
||||
if workaround.get_device_component_mapping(value) == DOMAIN:
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
return
|
||||
return ZWaveBinarySensor(value, None)
|
||||
|
||||
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):
|
||||
@ -77,26 +67,23 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor):
|
||||
"""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."""
|
||||
super(ZWaveTriggerSensor, self).__init__(value, device_class)
|
||||
self._hass = hass
|
||||
self.re_arm_sec = re_arm_sec
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
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)
|
||||
self.invalidate_after = None
|
||||
|
||||
def update_properties(self):
|
||||
"""Called when a value for this entity's node has changed."""
|
||||
self._state = self._value.data
|
||||
# only allow this value to be true for re_arm secs
|
||||
if not self.hass:
|
||||
return
|
||||
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
@property
|
||||
|
87
homeassistant/components/blink.py
Normal file
87
homeassistant/components/blink.py
Normal 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
|
@ -3,8 +3,8 @@ Support for Google Calendar event device sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/calendar/
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
@ -27,13 +27,13 @@ DOMAIN = 'calendar'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for calendars."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, DOMAIN)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
@ -155,7 +155,7 @@ class CalendarEventDevice(Entity):
|
||||
start = _get_date(self.data.event['start'])
|
||||
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
|
||||
# time is HH:MM or MM
|
||||
|
81
homeassistant/components/camera/blink.py
Normal file
81
homeassistant/components/camera/blink.py
Normal 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
|
67
homeassistant/components/camera/dispatcher.py
Normal file
67
homeassistant/components/camera/dispatcher.py
Normal 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
|
@ -34,7 +34,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a FFmpeg Camera."""
|
||||
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
yield from async_add_devices([FFmpegCamera(hass, config)])
|
||||
async_add_devices([FFmpegCamera(hass, config)])
|
||||
|
||||
|
||||
class FFmpegCamera(Camera):
|
||||
|
@ -47,15 +47,21 @@ class FoscamCamera(Camera):
|
||||
port = device_info.get(CONF_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._password = device_info.get(CONF_PASSWORD)
|
||||
self._snap_picture_url = self._base_url \
|
||||
+ 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=' \
|
||||
+ self._username + '&pwd=' + self._password
|
||||
self._snap_picture_url = uri_template.format(
|
||||
self._username,
|
||||
self._password
|
||||
)
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
|
||||
_LOGGER.info('Using the following URL for %s: %s',
|
||||
self._name, self._snap_picture_url)
|
||||
self._name, uri_template.format('***', '***'))
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
|
@ -44,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a generic IP Camera."""
|
||||
yield from async_add_devices([GenericCamera(hass, config)])
|
||||
async_add_devices([GenericCamera(hass, config)])
|
||||
|
||||
|
||||
class GenericCamera(Camera):
|
||||
|
@ -20,7 +20,7 @@ CONF_FILE_PATH = 'file_path'
|
||||
DEFAULT_NAME = 'Local File'
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
@ -31,8 +31,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
# check filepath given is readable
|
||||
if not os.access(file_path, os.R_OK):
|
||||
_LOGGER.error("file path is not readable")
|
||||
return False
|
||||
_LOGGER.warning("Could not read camera %s image from file: %s",
|
||||
config[CONF_NAME], file_path)
|
||||
|
||||
add_devices([LocalFile(config[CONF_NAME], file_path)])
|
||||
|
||||
@ -49,8 +49,12 @@ class LocalFile(Camera):
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
with open(self._file_path, 'rb') as file:
|
||||
return file.read()
|
||||
try:
|
||||
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
|
||||
def name(self):
|
||||
|
@ -45,7 +45,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""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):
|
||||
|
@ -153,7 +153,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
yield from async_add_devices(devices)
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -75,4 +75,4 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
_LOGGER.warning('No active cameras found')
|
||||
return
|
||||
|
||||
yield from async_add_devices(cameras)
|
||||
async_add_devices(cameras)
|
||||
|
@ -25,6 +25,8 @@ ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time'
|
||||
ATTR_RESUME_ALL = 'resume_all'
|
||||
|
||||
DEFAULT_RESUME_ALL = False
|
||||
TEMPERATURE_HOLD = 'temp'
|
||||
VACATION_HOLD = 'vacation'
|
||||
|
||||
DEPENDENCIES = ['ecobee']
|
||||
|
||||
@ -112,6 +114,8 @@ class Thermostat(ClimateDevice):
|
||||
self.thermostat_index)
|
||||
self._name = self.thermostat['name']
|
||||
self.hold_temp = hold_temp
|
||||
self.vacation = None
|
||||
self._climate_list = self.climate_list
|
||||
self._operation_list = ['auto', 'auxHeatOnly', 'cool',
|
||||
'heat', 'off']
|
||||
self.update_without_throttle = False
|
||||
@ -187,29 +191,30 @@ class Thermostat(ClimateDevice):
|
||||
def current_hold_mode(self):
|
||||
"""Return current hold mode."""
|
||||
events = self.thermostat['events']
|
||||
if any((event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) <= 1)
|
||||
or event['type'] == 'autoAway'
|
||||
for event in events):
|
||||
# away hold is auto away or a temporary hold from away climate
|
||||
hold = 'away'
|
||||
elif any(event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1
|
||||
for event in events):
|
||||
# a permanent away is not considered a hold, but away_mode
|
||||
hold = None
|
||||
elif any(event['holdClimateRef'] == 'home' or
|
||||
event['type'] == 'autoHome'
|
||||
for event in events):
|
||||
# home mode is auto home or any home hold
|
||||
hold = 'home'
|
||||
elif any(event['type'] == 'hold' and event['running']
|
||||
for event in events):
|
||||
hold = 'temp'
|
||||
# temperature hold is any other hold not based on climate
|
||||
else:
|
||||
hold = None
|
||||
return hold
|
||||
for event in events:
|
||||
if event['running']:
|
||||
if event['type'] == 'hold':
|
||||
if event['holdClimateRef'] == 'away':
|
||||
if int(event['endDate'][0:4]) - \
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
# a temporary hold from away climate is a hold
|
||||
return 'away'
|
||||
else:
|
||||
# a premanent hold from away climate is away_mode
|
||||
return None
|
||||
elif event['holdClimateRef'] != "":
|
||||
# any other hold based on climate
|
||||
return event['holdClimateRef']
|
||||
else:
|
||||
# any hold not based on a climate is a temp hold
|
||||
return TEMPERATURE_HOLD
|
||||
elif event['type'].startswith('auto'):
|
||||
# all auto modes are treated as holds
|
||||
return event['type'][4:].lower()
|
||||
elif event['type'] == 'vacation':
|
||||
self.vacation = event['name']
|
||||
return VACATION_HOLD
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
@ -232,8 +237,11 @@ class Thermostat(ClimateDevice):
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
"""Return current mode ie. home, away, sleep."""
|
||||
return self.thermostat['program']['currentClimateRef']
|
||||
"""Return current mode, as the user-visible name."""
|
||||
cur = self.thermostat['program']['currentClimateRef']
|
||||
climates = self.thermostat['program']['climates']
|
||||
current = list(filter(lambda x: x['climateRef'] == cur, climates))
|
||||
return current[0]['name']
|
||||
|
||||
@property
|
||||
def fan_min_on_time(self):
|
||||
@ -261,52 +269,44 @@ class Thermostat(ClimateDevice):
|
||||
"fan": self.fan,
|
||||
"mode": self.mode,
|
||||
"operation": operation,
|
||||
"climate_list": self.climate_list,
|
||||
"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
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
events = self.thermostat['events']
|
||||
return any(event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1
|
||||
for event in events)
|
||||
return self.current_hold_mode == 'away'
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"away", 'indefinite')
|
||||
self.update_without_throttle = True
|
||||
self.set_hold_mode('away')
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
self.update_without_throttle = True
|
||||
self.set_hold_mode(None)
|
||||
|
||||
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
|
||||
|
||||
if hold == hold_mode:
|
||||
# no change, so no action required
|
||||
return
|
||||
elif hold_mode == 'away':
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"away", self.hold_preference())
|
||||
elif hold_mode == 'home':
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"home", self.hold_preference())
|
||||
elif hold_mode == 'temp':
|
||||
self.set_temp_hold(int(self.current_temperature))
|
||||
elif hold_mode == 'None' or hold_mode is None:
|
||||
if hold == VACATION_HOLD:
|
||||
self.data.ecobee.delete_vacation(self.thermostat_index,
|
||||
self.vacation)
|
||||
else:
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
else:
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
self.update_without_throttle = True
|
||||
if hold_mode == TEMPERATURE_HOLD:
|
||||
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):
|
||||
"""Set temperature hold in auto mode."""
|
||||
@ -382,3 +382,9 @@ class Thermostat(ClimateDevice):
|
||||
# as an indefinite away hold is interpreted as away_mode
|
||||
else:
|
||||
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))
|
||||
|
@ -16,7 +16,8 @@ from homeassistant.components.climate import (
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE)
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -35,6 +36,7 @@ CONF_TARGET_TEMP = 'target_temp'
|
||||
CONF_AC_MODE = 'ac_mode'
|
||||
CONF_MIN_DUR = 'min_cycle_duration'
|
||||
CONF_TOLERANCE = 'tolerance'
|
||||
CONF_KEEP_ALIVE = 'keep_alive'
|
||||
|
||||
|
||||
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_TOLERANCE, default=DEFAULT_TOLERANCE): 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)
|
||||
min_cycle_duration = config.get(CONF_MIN_DUR)
|
||||
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,
|
||||
target_temp, ac_mode, min_cycle_duration, tolerance)])
|
||||
target_temp, ac_mode, min_cycle_duration, tolerance, keep_alive)])
|
||||
|
||||
|
||||
class GenericThermostat(ClimateDevice):
|
||||
@ -73,7 +78,7 @@ class GenericThermostat(ClimateDevice):
|
||||
|
||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
||||
tolerance):
|
||||
tolerance, keep_alive):
|
||||
"""Initialize the thermostat."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
@ -81,6 +86,7 @@ class GenericThermostat(ClimateDevice):
|
||||
self.ac_mode = ac_mode
|
||||
self.min_cycle_duration = min_cycle_duration
|
||||
self._tolerance = tolerance
|
||||
self._keep_alive = keep_alive
|
||||
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
@ -94,6 +100,10 @@ class GenericThermostat(ClimateDevice):
|
||||
async_track_state_change(
|
||||
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)
|
||||
if sensor_state:
|
||||
self._async_update_temp(sensor_state)
|
||||
@ -180,6 +190,14 @@ class GenericThermostat(ClimateDevice):
|
||||
return
|
||||
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
|
||||
def _async_update_temp(self, state):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
|
@ -6,10 +6,15 @@ https://home-assistant.io/components/climate.honeywell/
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
import datetime
|
||||
|
||||
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 (
|
||||
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
ATTR_TEMPERATURE)
|
||||
@ -21,27 +26,35 @@ REQUIREMENTS = ['evohomeclient==0.2.5',
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_FAN = 'fan'
|
||||
ATTR_FANMODE = 'fanmode'
|
||||
ATTR_SYSTEM_MODE = 'system_mode'
|
||||
ATTR_CURRENT_OPERATION = 'equipment_output_status'
|
||||
|
||||
CONF_AWAY_TEMPERATURE = 'away_temperature'
|
||||
CONF_COOL_AWAY_TEMPERATURE = 'away_cool_temperature'
|
||||
CONF_HEAT_AWAY_TEMPERATURE = 'away_heat_temperature'
|
||||
CONF_REGION = 'region'
|
||||
|
||||
DEFAULT_AWAY_TEMPERATURE = 16
|
||||
DEFAULT_COOL_AWAY_TEMPERATURE = 30
|
||||
DEFAULT_HEAT_AWAY_TEMPERATURE = 16
|
||||
DEFAULT_REGION = 'eu'
|
||||
REGIONS = ['eu', 'us']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_AWAY_TEMPERATURE, default=DEFAULT_AWAY_TEMPERATURE):
|
||||
vol.Coerce(float),
|
||||
vol.Optional(CONF_AWAY_TEMPERATURE,
|
||||
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),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the HoneywelL thermostat."""
|
||||
"""Setup the Honeywell thermostat."""
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
region = config.get(CONF_REGION)
|
||||
@ -88,8 +101,11 @@ def _setup_us(username, password, config, add_devices):
|
||||
|
||||
dev_id = config.get('thermostat')
|
||||
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 device in location.devices_by_id.values()
|
||||
if ((not loc_id or location.locationid == loc_id) and
|
||||
@ -160,7 +176,7 @@ class RoundThermostat(ClimateDevice):
|
||||
def turn_away_mode_on(self):
|
||||
"""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
|
||||
it doesn't get overwritten when away mode is switched on.
|
||||
"""
|
||||
@ -199,10 +215,16 @@ class RoundThermostat(ClimateDevice):
|
||||
class HoneywellUSThermostat(ClimateDevice):
|
||||
"""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."""
|
||||
self._client = client
|
||||
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
|
||||
def is_fan_on(self):
|
||||
@ -236,7 +258,10 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self: ClimateDevice) -> str:
|
||||
"""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):
|
||||
"""Set target temperature."""
|
||||
@ -245,29 +270,84 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
return
|
||||
import somecomfort
|
||||
try:
|
||||
if self._device.system_mode == 'cool':
|
||||
self._device.setpoint_cool = temperature
|
||||
else:
|
||||
self._device.setpoint_heat = temperature
|
||||
# Get current mode
|
||||
mode = self._device.system_mode
|
||||
# Set hold if this is not the case
|
||||
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:
|
||||
_LOGGER.error('Temperature %.1f out of range', temperature)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {
|
||||
import somecomfort
|
||||
data = {
|
||||
ATTR_FAN: (self.is_fan_on and 'running' or 'idle'),
|
||||
ATTR_FANMODE: self._device.fan_mode,
|
||||
ATTR_SYSTEM_MODE: self._device.system_mode,
|
||||
ATTR_FAN_MODE: self._device.fan_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):
|
||||
"""Turn away on."""
|
||||
pass
|
||||
"""Turn away on.
|
||||
|
||||
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):
|
||||
"""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:
|
||||
"""Set the system mode (Cool, Heat, etc)."""
|
||||
@ -276,4 +356,49 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
|
||||
def update(self):
|
||||
"""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
|
||||
|
216
homeassistant/components/climate/maxcube.py
Normal file
216
homeassistant/components/climate/maxcube.py
Normal 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
|
@ -7,14 +7,14 @@ https://home-assistant.io/components/switch.vera/
|
||||
import logging
|
||||
|
||||
from homeassistant.util import convert
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS,
|
||||
ATTR_TEMPERATURE)
|
||||
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
@ -37,6 +37,7 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
def __init__(self, vera_device, controller):
|
||||
"""Initialize the Vera device."""
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
@ -11,6 +11,7 @@ from homeassistant.components.climate import DOMAIN
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
|
||||
@ -32,19 +33,10 @@ DEVICE_MAPPINGS = {
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Z-Wave Climate devices."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
return
|
||||
def get_device(hass, value, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZWaveClimate(value, temp_unit)])
|
||||
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
return ZWaveClimate(value, temp_unit)
|
||||
|
||||
|
||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
@ -224,7 +216,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT,
|
||||
index=self._index, data=temperature)
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
@ -254,3 +246,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
if self._fan_state:
|
||||
data[ATTR_FAN_STATE] = self._fan_state
|
||||
return data
|
||||
|
||||
@property
|
||||
def dependent_value_ids(self):
|
||||
"""List of value IDs a device depends on."""
|
||||
return None
|
||||
|
@ -6,7 +6,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED
|
||||
from homeassistant.bootstrap import (
|
||||
from homeassistant.setup import (
|
||||
async_prepare_setup_platform, ATTR_COMPONENT)
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
@ -54,7 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
yield from async_add_devices([MqttCover(
|
||||
async_add_devices([MqttCover(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
|
@ -14,8 +14,8 @@ from homeassistant.const import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/arraylabs/pymyq/archive/v0.0.6.zip'
|
||||
'#pymyq==0.0.6']
|
||||
'https://github.com/arraylabs/pymyq/archive/v0.0.7.zip'
|
||||
'#pymyq==0.0.7']
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TYPE): cv.string,
|
||||
|
@ -6,9 +6,9 @@ https://home-assistant.io/components/cover.vera/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
@ -28,6 +28,7 @@ class VeraCover(VeraDevice, CoverDevice):
|
||||
def __init__(self, vera_device, controller):
|
||||
"""Initialize the Vera device."""
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
|
@ -11,6 +11,7 @@ from homeassistant.components.cover import (
|
||||
DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
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.cover import CoverDevice
|
||||
|
||||
@ -19,27 +20,15 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and return Z-Wave covers."""
|
||||
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]]
|
||||
|
||||
def get_device(value, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and value.index == 0):
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveRollershutter(value)])
|
||||
return ZwaveRollershutter(value)
|
||||
elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
||||
value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
if (value.type != zwave.const.TYPE_BOOL and
|
||||
value.genre != zwave.const.GENRE_USER):
|
||||
return
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
else:
|
||||
return
|
||||
return ZwaveGarageDoor(value)
|
||||
return None
|
||||
|
||||
|
||||
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
@ -52,6 +41,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
self._node = value.node
|
||||
self._open_id = None
|
||||
self._close_id = None
|
||||
self._current_position_id = None
|
||||
self._current_position = None
|
||||
|
||||
self._workaround = workaround.get_device_mapping(value)
|
||||
@ -59,20 +49,35 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
_LOGGER.debug("Using workaround %s", self._workaround)
|
||||
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):
|
||||
"""Callback on data changes for node values."""
|
||||
# Position value
|
||||
self._current_position = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Level'], member='data')
|
||||
self._open_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Open', 'Up'], member='value_id')
|
||||
self._close_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Close', 'Down'], member='value_id')
|
||||
if self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE:
|
||||
if not self._node.is_ready:
|
||||
if self._current_position_id is None:
|
||||
self._current_position_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Level'], member='value_id')
|
||||
if self._open_id is None:
|
||||
self._open_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Open', 'Up'], member='value_id')
|
||||
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._workaround = None
|
||||
self._current_position = self._node.get_dimmer_level(
|
||||
self._current_position_id)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
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."""
|
||||
group = loader.get_component('group')
|
||||
configurator = loader.get_component('configurator')
|
||||
@ -44,7 +46,7 @@ def setup(hass, config):
|
||||
config.setdefault(DOMAIN, {})
|
||||
|
||||
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
|
||||
if not hass.config.latitude:
|
||||
@ -53,50 +55,71 @@ def setup(hass, config):
|
||||
if not hass.config.longitude:
|
||||
hass.config.longitude = 117.22743
|
||||
|
||||
bootstrap.setup_component(hass, 'sun')
|
||||
tasks = [
|
||||
bootstrap.async_setup_component(hass, 'sun')
|
||||
]
|
||||
|
||||
# Setup demo platforms
|
||||
demo_config = config.copy()
|
||||
for component in COMPONENTS_WITH_DEMO_PLATFORM:
|
||||
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
|
||||
persistent_notification.create(
|
||||
persistent_notification.async_create(
|
||||
hass, 'This is an example of a persistent notification.',
|
||||
title='Example Notification')
|
||||
|
||||
# Setup room groups
|
||||
lights = sorted(hass.states.entity_ids('light'))
|
||||
switches = sorted(hass.states.entity_ids('switch'))
|
||||
media_players = sorted(hass.states.entity_ids('media_player'))
|
||||
lights = sorted(hass.states.async_entity_ids('light'))
|
||||
switches = sorted(hass.states.async_entity_ids('switch'))
|
||||
media_players = sorted(hass.states.async_entity_ids('media_player'))
|
||||
|
||||
group.Group.create_group(hass, 'living room', [
|
||||
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)
|
||||
tasks2 = []
|
||||
|
||||
# Setup scripts
|
||||
bootstrap.setup_component(
|
||||
tasks2.append(bootstrap.async_setup_component(
|
||||
hass, 'script',
|
||||
{'script': {
|
||||
'demo': {
|
||||
@ -115,10 +138,10 @@ def setup(hass, config):
|
||||
'service': 'light.turn_off',
|
||||
'data': {ATTR_ENTITY_ID: lights[0]}
|
||||
}]
|
||||
}}})
|
||||
}}}))
|
||||
|
||||
# Setup scenes
|
||||
bootstrap.setup_component(
|
||||
tasks2.append(bootstrap.async_setup_component(
|
||||
hass, 'scene',
|
||||
{'scene': [
|
||||
{'name': 'Romantic lights',
|
||||
@ -132,41 +155,37 @@ def setup(hass, config):
|
||||
switches[0]: True,
|
||||
switches[1]: False,
|
||||
}},
|
||||
]})
|
||||
]}))
|
||||
|
||||
# Set up input select
|
||||
bootstrap.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
|
||||
bootstrap.setup_component(
|
||||
hass, 'input_boolean',
|
||||
{'input_boolean': {'notify': {'icon': 'mdi:car',
|
||||
'initial': False,
|
||||
'name': 'Notify Anne Therese is home'}}})
|
||||
tasks2.append(group.Group.async_create_group(hass, 'living room', [
|
||||
lights[1], switches[0], 'input_select.living_room_preset',
|
||||
'rollershutter.living_room_window', media_players[1],
|
||||
'scene.romantic_lights']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'bedroom', [
|
||||
lights[0], switches[1], media_players[0],
|
||||
'input_slider.noise_allowance']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'kitchen', [
|
||||
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'doors', [
|
||||
'lock.front_door', 'lock.kitchen_door',
|
||||
'garage_door.right_garage_door', 'garage_door.left_garage_door']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'automations', [
|
||||
'input_select.who_cooks', 'input_boolean.notify', ]))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'people', [
|
||||
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
|
||||
'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
|
||||
bootstrap.setup_component(
|
||||
hass, 'input_slider',
|
||||
{'input_slider': {
|
||||
'noise_allowance': {'icon': 'mdi:bell-ring',
|
||||
'min': 0,
|
||||
'max': 10,
|
||||
'name': 'Allowed Noise',
|
||||
'unit_of_measurement': 'dB'}}})
|
||||
results = yield from asyncio.gather(*tasks2, loop=hass.loop)
|
||||
|
||||
if any(not result for result in results):
|
||||
return False
|
||||
|
||||
# Set up weblink
|
||||
bootstrap.setup_component(
|
||||
hass, 'weblink',
|
||||
{'weblink': {'entities': [{'name': 'Router',
|
||||
'url': 'http://192.168.1.1'}]}})
|
||||
# Setup configurator
|
||||
configurator_ids = []
|
||||
|
||||
@ -184,14 +203,17 @@ def setup(hass, config):
|
||||
else:
|
||||
configurator.request_done(configurator_ids[0])
|
||||
|
||||
request_id = configurator.request_config(
|
||||
hass, "Philips Hue", hue_configuration_callback,
|
||||
description=("Press the button on the bridge to register Philips Hue "
|
||||
"with Home Assistant."),
|
||||
description_image="/static/images/config_philips_hue.jpg",
|
||||
submit_caption="I have pressed the button"
|
||||
)
|
||||
def setup_configurator():
|
||||
"""Setup configurator."""
|
||||
request_id = configurator.request_config(
|
||||
hass, "Philips Hue", hue_configuration_callback,
|
||||
description=("Press the button on the bridge to register Philips "
|
||||
"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
|
||||
|
@ -14,12 +14,11 @@ import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import (
|
||||
async_prepare_setup_platform, async_log_exception)
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import group, zone
|
||||
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.helpers.aiohttp_client import async_get_clientsession
|
||||
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)
|
||||
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
|
||||
def async_setup_platform(p_type, p_config, disc_info=None):
|
||||
"""Setup a device tracker platform."""
|
||||
@ -227,6 +214,8 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE))
|
||||
|
||||
# restore
|
||||
yield from tracker.async_setup_tracked_device()
|
||||
return True
|
||||
|
||||
|
||||
@ -357,6 +346,27 @@ class DeviceTracker(object):
|
||||
device.stale(now):
|
||||
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):
|
||||
"""Represent a tracked device."""
|
||||
|
@ -110,7 +110,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
_LOGGER.info("Discovered Bluetooth LE device %s", address)
|
||||
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())
|
||||
|
||||
|
@ -82,7 +82,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
see_device((mac, result))
|
||||
except bluetooth.BluetoothError:
|
||||
_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())
|
||||
|
||||
|
@ -197,13 +197,22 @@ class Icloud(DeviceScanner):
|
||||
|
||||
def icloud_trusted_device_callback(self, callback_data):
|
||||
"""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]
|
||||
|
||||
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:
|
||||
request_id = _CONFIGURING.pop(self.accountname)
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(request_id)
|
||||
|
||||
# Trigger the next step immediately
|
||||
self.icloud_need_verification_code()
|
||||
|
||||
def icloud_need_trusted_device(self):
|
||||
"""We need a trusted device."""
|
||||
configurator = get_component('configurator')
|
||||
@ -213,7 +222,10 @@ class Icloud(DeviceScanner):
|
||||
devicesstring = ''
|
||||
devices = self.api.trusted_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(
|
||||
self.hass, 'iCloud {}'.format(self.accountname),
|
||||
@ -223,12 +235,27 @@ class Icloud(DeviceScanner):
|
||||
' the index from this list: ' + devicesstring),
|
||||
entity_picture="/static/images/config_icloud.png",
|
||||
submit_caption='Confirm',
|
||||
fields=[{'id': '0'}]
|
||||
fields=[{'id': 'trusted_device', 'name': 'Trusted Device'}]
|
||||
)
|
||||
|
||||
def icloud_verification_callback(self, callback_data):
|
||||
"""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:
|
||||
request_id = _CONFIGURING.pop(self.accountname)
|
||||
configurator = get_component('configurator')
|
||||
@ -240,22 +267,17 @@ class Icloud(DeviceScanner):
|
||||
if self.accountname in _CONFIGURING:
|
||||
return
|
||||
|
||||
if self.api.send_verification_code(self._trusted_device):
|
||||
self._verification_code = 'waiting'
|
||||
|
||||
_CONFIGURING[self.accountname] = configurator.request_config(
|
||||
self.hass, 'iCloud {}'.format(self.accountname),
|
||||
self.icloud_verification_callback,
|
||||
description=('Please enter the validation code:'),
|
||||
entity_picture="/static/images/config_icloud.png",
|
||||
submit_caption='Confirm',
|
||||
fields=[{'code': '0'}]
|
||||
fields=[{'id': 'code', 'name': 'code'}]
|
||||
)
|
||||
|
||||
def keep_alive(self, now):
|
||||
"""Keep the api alive."""
|
||||
from pyicloud.exceptions import PyiCloud2FARequiredError
|
||||
|
||||
if self.api is None:
|
||||
self.reset_account_icloud()
|
||||
|
||||
@ -263,9 +285,8 @@ class Icloud(DeviceScanner):
|
||||
return
|
||||
|
||||
if self.api.requires_2fa:
|
||||
from pyicloud.exceptions import PyiCloudException
|
||||
try:
|
||||
self.api.authenticate()
|
||||
except PyiCloud2FARequiredError:
|
||||
if self._trusted_device is None:
|
||||
self.icloud_need_trusted_device()
|
||||
return
|
||||
@ -274,12 +295,14 @@ class Icloud(DeviceScanner):
|
||||
self.icloud_need_verification_code()
|
||||
return
|
||||
|
||||
if self._verification_code == 'waiting':
|
||||
return
|
||||
self.api.authenticate()
|
||||
if self.api.requires_2fa:
|
||||
raise Exception('Unknown failure')
|
||||
|
||||
if self.api.validate_verification_code(
|
||||
self._trusted_device, self._verification_code):
|
||||
self._verification_code = None
|
||||
self._trusted_device = None
|
||||
self._verification_code = None
|
||||
except PyiCloudException as error:
|
||||
_LOGGER.error("Error setting up 2fa: %s", error)
|
||||
else:
|
||||
self.api.authenticate()
|
||||
|
||||
@ -397,13 +420,13 @@ class Icloud(DeviceScanner):
|
||||
try:
|
||||
if devicename is not None:
|
||||
if devicename in self.devices:
|
||||
self.devices[devicename].update_icloud()
|
||||
self.devices[devicename].location()
|
||||
else:
|
||||
_LOGGER.error("devicename %s unknown for account %s",
|
||||
devicename, self._attrs[ATTR_ACCOUNTNAME])
|
||||
else:
|
||||
for device in self.devices:
|
||||
self.devices[device].update_icloud()
|
||||
self.devices[device].location()
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error('No iCloud Devices found!')
|
||||
|
||||
|
@ -4,11 +4,13 @@ Support for tracking MQTT enabled devices.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_DEVICES
|
||||
from homeassistant.components.mqtt import CONF_QOS
|
||||
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."""
|
||||
devices = config[CONF_DEVICES]
|
||||
qos = config[CONF_QOS]
|
||||
|
||||
dev_id_lookup = {}
|
||||
|
||||
def device_tracker_message_received(topic, payload, qos):
|
||||
@callback
|
||||
def async_tracker_message_received(topic, payload, qos):
|
||||
"""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():
|
||||
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
|
||||
|
@ -4,14 +4,15 @@ Support the OwnTracks platform.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.owntracks/
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import base64
|
||||
from collections import defaultdict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
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.device_tracker import PLATFORM_SCHEMA
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
REQUIREMENTS = ['libnacl==1.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -30,16 +32,9 @@ CONF_SECRET = 'secret'
|
||||
CONF_WAYPOINT_IMPORT = 'waypoints'
|
||||
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
EVENT_TOPIC = 'owntracks/+/+/event'
|
||||
|
||||
LOCATION_TOPIC = 'owntracks/+/+'
|
||||
LOCK = threading.Lock()
|
||||
|
||||
MOBILE_BEACONS_ACTIVE = defaultdict(list)
|
||||
|
||||
REGIONS_ENTERED = defaultdict(list)
|
||||
|
||||
VALIDATE_LOCATION = 'location'
|
||||
VALIDATE_TRANSITION = 'transition'
|
||||
@ -61,7 +56,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
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.secret import SecretBox
|
||||
|
||||
@ -71,13 +69,17 @@ def get_cipher():
|
||||
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."""
|
||||
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
||||
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
|
||||
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
|
||||
secret = config.get(CONF_SECRET)
|
||||
|
||||
mobile_beacons_active = defaultdict(list)
|
||||
regions_entered = defaultdict(list)
|
||||
|
||||
def decrypt_payload(topic, ciphertext):
|
||||
"""Decrypt encrypted payload."""
|
||||
try:
|
||||
@ -154,7 +156,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
|
||||
return data
|
||||
|
||||
def owntracks_location_update(topic, payload, qos):
|
||||
@callback
|
||||
def async_owntracks_location_update(topic, payload, qos):
|
||||
"""MQTT message received."""
|
||||
# Docs on available data:
|
||||
# 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)
|
||||
|
||||
# Block updates if we're in a region
|
||||
with LOCK:
|
||||
if REGIONS_ENTERED[dev_id]:
|
||||
_LOGGER.debug(
|
||||
"location update ignored - inside region %s",
|
||||
REGIONS_ENTERED[-1])
|
||||
return
|
||||
if regions_entered[dev_id]:
|
||||
_LOGGER.debug(
|
||||
"location update ignored - inside region %s",
|
||||
regions_entered[-1])
|
||||
return
|
||||
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
hass.async_add_job(async_see(**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."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typetransition
|
||||
@ -199,67 +201,65 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
def enter_event():
|
||||
"""Execute enter event."""
|
||||
zone = hass.states.get("zone.{}".format(slugify(location)))
|
||||
with LOCK:
|
||||
if zone is None and data.get('t') == 'b':
|
||||
# Not a HA zone, and a beacon so assume mobile
|
||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
||||
if location not in beacons:
|
||||
beacons.append(location)
|
||||
_LOGGER.info("Added beacon %s", location)
|
||||
else:
|
||||
# Normal region
|
||||
regions = REGIONS_ENTERED[dev_id]
|
||||
if location not in regions:
|
||||
regions.append(location)
|
||||
_LOGGER.info("Enter region %s", location)
|
||||
_set_gps_from_zone(kwargs, location, zone)
|
||||
if zone is None and data.get('t') == 'b':
|
||||
# Not a HA zone, and a beacon so assume mobile
|
||||
beacons = mobile_beacons_active[dev_id]
|
||||
if location not in beacons:
|
||||
beacons.append(location)
|
||||
_LOGGER.info("Added beacon %s", location)
|
||||
else:
|
||||
# Normal region
|
||||
regions = regions_entered[dev_id]
|
||||
if location not in regions:
|
||||
regions.append(location)
|
||||
_LOGGER.info("Enter region %s", location)
|
||||
_set_gps_from_zone(kwargs, location, zone)
|
||||
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
async_see_beacons(dev_id, kwargs)
|
||||
|
||||
def leave_event():
|
||||
"""Execute leave event."""
|
||||
with LOCK:
|
||||
regions = REGIONS_ENTERED[dev_id]
|
||||
if location in regions:
|
||||
regions.remove(location)
|
||||
new_region = regions[-1] if regions else None
|
||||
regions = regions_entered[dev_id]
|
||||
if location in regions:
|
||||
regions.remove(location)
|
||||
new_region = regions[-1] if regions else None
|
||||
|
||||
if new_region:
|
||||
# Exit to previous region
|
||||
zone = hass.states.get(
|
||||
"zone.{}".format(slugify(new_region)))
|
||||
_set_gps_from_zone(kwargs, new_region, zone)
|
||||
_LOGGER.info("Exit to %s", new_region)
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
if new_region:
|
||||
# Exit to previous region
|
||||
zone = hass.states.get(
|
||||
"zone.{}".format(slugify(new_region)))
|
||||
_set_gps_from_zone(kwargs, new_region, zone)
|
||||
_LOGGER.info("Exit to %s", new_region)
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
async_see_beacons(dev_id, kwargs)
|
||||
|
||||
else:
|
||||
_LOGGER.info("Exit to GPS")
|
||||
# Check for GPS accuracy
|
||||
valid_gps = True
|
||||
if 'acc' in data:
|
||||
if data['acc'] == 0.0:
|
||||
valid_gps = False
|
||||
_LOGGER.warning(
|
||||
'Ignoring GPS in region exit because accuracy'
|
||||
'is zero: %s',
|
||||
payload)
|
||||
if (max_gps_accuracy is not None and
|
||||
data['acc'] > max_gps_accuracy):
|
||||
valid_gps = False
|
||||
_LOGGER.info(
|
||||
'Ignoring GPS in region exit because expected '
|
||||
'GPS accuracy %s is not met: %s',
|
||||
max_gps_accuracy, payload)
|
||||
if valid_gps:
|
||||
see(**kwargs)
|
||||
see_beacons(dev_id, kwargs)
|
||||
else:
|
||||
_LOGGER.info("Exit to GPS")
|
||||
# Check for GPS accuracy
|
||||
valid_gps = True
|
||||
if 'acc' in data:
|
||||
if data['acc'] == 0.0:
|
||||
valid_gps = False
|
||||
_LOGGER.warning(
|
||||
'Ignoring GPS in region exit because accuracy'
|
||||
'is zero: %s',
|
||||
payload)
|
||||
if (max_gps_accuracy is not None and
|
||||
data['acc'] > max_gps_accuracy):
|
||||
valid_gps = False
|
||||
_LOGGER.info(
|
||||
'Ignoring GPS in region exit because expected '
|
||||
'GPS accuracy %s is not met: %s',
|
||||
max_gps_accuracy, payload)
|
||||
if valid_gps:
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
async_see_beacons(dev_id, kwargs)
|
||||
|
||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
||||
if location in beacons:
|
||||
beacons.remove(location)
|
||||
_LOGGER.info("Remove beacon %s", location)
|
||||
beacons = mobile_beacons_active[dev_id]
|
||||
if location in beacons:
|
||||
beacons.remove(location)
|
||||
_LOGGER.info("Remove beacon %s", location)
|
||||
|
||||
if data['event'] == 'enter':
|
||||
enter_event()
|
||||
@ -271,7 +271,8 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
data['event'])
|
||||
return
|
||||
|
||||
def owntracks_waypoint_update(topic, payload, qos):
|
||||
@callback
|
||||
def async_owntracks_waypoint_update(topic, payload, qos):
|
||||
"""List of waypoints published by a user."""
|
||||
# Docs on available data:
|
||||
# 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_comp.ICON_IMPORT, False)
|
||||
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."""
|
||||
kwargs = kwargs_param.copy()
|
||||
# the battery state applies to the tracking device, not the beacon
|
||||
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['host_name'] = beacon
|
||||
see(**kwargs)
|
||||
hass.async_add_job(async_see(**kwargs))
|
||||
|
||||
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
|
||||
mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1)
|
||||
yield from mqtt.async_subscribe(
|
||||
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_whitelist is None:
|
||||
mqtt.subscribe(hass, WAYPOINT_TOPIC.format('+', '+'),
|
||||
owntracks_waypoint_update, 1)
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, WAYPOINT_TOPIC.format('+', '+'),
|
||||
async_owntracks_waypoint_update, 1)
|
||||
else:
|
||||
for whitelist_user in waypoint_whitelist:
|
||||
mqtt.subscribe(hass, WAYPOINT_TOPIC.format(whitelist_user,
|
||||
'+'),
|
||||
owntracks_waypoint_update, 1)
|
||||
yield from mqtt.async_subscribe(
|
||||
hass, WAYPOINT_TOPIC.format(whitelist_user, '+'),
|
||||
async_owntracks_waypoint_update, 1)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
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('/')
|
||||
dev_id_format = ''
|
||||
if pretty:
|
||||
@ -340,7 +348,10 @@ def parse_topic(topic, pretty=False):
|
||||
|
||||
|
||||
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)
|
||||
kwargs = {
|
||||
'dev_id': dev_id,
|
||||
@ -355,7 +366,10 @@ def _parse_see_args(topic, data):
|
||||
|
||||
|
||||
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:
|
||||
kwargs['gps'] = (
|
||||
zone.attributes['latitude'],
|
||||
|
@ -86,7 +86,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Update all the hosts on every interval time."""
|
||||
for host in hosts:
|
||||
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 update(util.dt.utcnow())
|
||||
|
@ -19,7 +19,7 @@ from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.3.3']
|
||||
REQUIREMENTS = ['pysnmp==4.3.4']
|
||||
|
||||
CONF_COMMUNITY = 'community'
|
||||
CONF_AUTHKEY = 'authkey'
|
||||
|
@ -142,7 +142,7 @@ class TadoDeviceScanner(DeviceScanner):
|
||||
|
||||
# Find devices that have geofencing enabled, and are currently at home.
|
||||
for mobile_device in tado_json:
|
||||
if 'location' in mobile_device:
|
||||
if mobile_device.get('location'):
|
||||
if mobile_device['location']['atHome']:
|
||||
device_id = mobile_device['id']
|
||||
device_name = mobile_device['name']
|
||||
|
@ -13,13 +13,15 @@ import homeassistant.loader as loader
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
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
|
||||
REQUIREMENTS = ['urllib3', 'pyunifi==1.3']
|
||||
REQUIREMENTS = ['pyunifi==2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_PORT = 'port'
|
||||
CONF_SITE_ID = 'site_id'
|
||||
DEFAULT_VERIFY_SSL = True
|
||||
|
||||
NOTIFICATION_ID = 'unifi_notification'
|
||||
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.Required(CONF_PASSWORD): 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)
|
||||
site_id = config[DOMAIN].get(CONF_SITE_ID)
|
||||
port = config[DOMAIN].get(CONF_PORT)
|
||||
verify_ssl = config[DOMAIN].get(CONF_VERIFY_SSL)
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
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:
|
||||
_LOGGER.error('Failed to connect to Unifi: %s', ex)
|
||||
persistent_notification.create(
|
||||
|
@ -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
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['netdisco==0.9.1']
|
||||
REQUIREMENTS = ['netdisco==0.9.2']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
@ -46,6 +46,7 @@ SERVICE_HANDLERS = {
|
||||
'yeelight': ('light', 'yeelight'),
|
||||
'flux_led': ('light', 'flux_led'),
|
||||
'apple_tv': ('media_player', 'apple_tv'),
|
||||
'frontier_silicon': ('media_player', 'frontier_silicon'),
|
||||
'openhome': ('media_player', 'openhome'),
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup MQTT fan platform."""
|
||||
yield from async_add_devices([MqttFan(
|
||||
async_add_devices([MqttFan(
|
||||
config.get(CONF_NAME),
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
|
@ -14,6 +14,8 @@ from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
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
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
@ -26,6 +28,10 @@ SERVICE_START = 'start'
|
||||
SERVICE_STOP = 'stop'
|
||||
SERVICE_RESTART = 'restart'
|
||||
|
||||
SIGNAL_FFMPEG_START = 'ffmpeg.start'
|
||||
SIGNAL_FFMPEG_STOP = 'ffmpeg.stop'
|
||||
SIGNAL_FFMPEG_RESTART = 'ffmpeg.restart'
|
||||
|
||||
DATA_FFMPEG = 'ffmpeg'
|
||||
|
||||
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."""
|
||||
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."""
|
||||
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."""
|
||||
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
|
||||
@ -89,30 +98,12 @@ def async_setup(hass, config):
|
||||
"""Handle service ffmpeg process."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if entity_ids:
|
||||
devices = [device for device in manager.entities
|
||||
if device.entity_id in entity_ids]
|
||||
if service.service == SERVICE_START:
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_START, entity_ids)
|
||||
elif service.service == SERVICE_STOP:
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_STOP, entity_ids)
|
||||
else:
|
||||
devices = manager.entities
|
||||
|
||||
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)
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_RESTART, entity_ids)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_START, async_service_handle,
|
||||
@ -140,42 +131,12 @@ class FFmpegManager(object):
|
||||
self._cache = {}
|
||||
self._bin = ffmpeg_bin
|
||||
self._run_test = run_test
|
||||
self._entities = []
|
||||
|
||||
@property
|
||||
def binary(self):
|
||||
"""Return ffmpeg binary from config."""
|
||||
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
|
||||
def async_run_test(self, input_source):
|
||||
"""Run test on this input. TRUE is deactivate or run correct.
|
||||
@ -208,6 +169,22 @@ class FFmpegBase(Entity):
|
||||
self.ffmpeg = None
|
||||
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
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
@ -218,22 +195,53 @@ class FFmpegBase(Entity):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return False
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
@asyncio.coroutine
|
||||
def _async_start_ffmpeg(self, entity_ids):
|
||||
"""Start a ffmpeg process.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_stop_ffmpeg(self):
|
||||
@asyncio.coroutine
|
||||
def _async_stop_ffmpeg(self, entity_ids):
|
||||
"""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
|
||||
def async_restart_ffmpeg(self):
|
||||
"""Stop a ffmpeg process."""
|
||||
yield from self.async_stop_ffmpeg()
|
||||
yield from self.async_start_ffmpeg()
|
||||
def _async_restart_ffmpeg(self, entity_ids):
|
||||
"""Stop a ffmpeg process.
|
||||
|
||||
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)
|
||||
|
@ -3,13 +3,13 @@
|
||||
FINGERPRINTS = {
|
||||
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
|
||||
"core.js": "1f7f88d8f5dada08bce1d935cfa5f33e",
|
||||
"frontend.html": "ca9efa7e4506aa6b1a668703c8d0f800",
|
||||
"mdi.html": "c1dde43ccf5667f687c418fc8daf9668",
|
||||
"frontend.html": "418f6ef8354ce71f1b9594ee2068ebef",
|
||||
"mdi.html": "65413cdf82f822bd6480e577852f0292",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-config.html": "412b3e24515ffa1ee8074ce974cf4057",
|
||||
"panels/ha-panel-dev-event.html": "91347dedf3b4fa9b49ccf4c0a28a03c4",
|
||||
"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-template.html": "c249a4fc18a3a6994de3d6330cfe6cbb",
|
||||
"panels/ha-panel-history.html": "fdaa4d2402d49d4c8bd64a1708ab7a50",
|
||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1 +1 @@
|
||||
Subproject commit e509ed07a08d35152b9eea6e263411dfc027867b
|
||||
Subproject commit de1b20b70a16aeb7c48a1b4867c97864c88adb1c
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -18,7 +18,7 @@ from voluptuous.error import Error as VoluptuousError
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.setup import setup_component
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
@ -118,8 +118,8 @@ def do_authentication(hass, config):
|
||||
return False
|
||||
|
||||
persistent_notification.create(
|
||||
hass, 'In order to authorize Home-Assistant to view your calendars'
|
||||
'You must visit: <a href="{}" target="_blank">{}</a> and enter'
|
||||
hass, 'In order to authorize Home-Assistant to view your calendars '
|
||||
'you must visit: <a href="{}" target="_blank">{}</a> and enter '
|
||||
'code: {}'.format(dev_flow.verification_url,
|
||||
dev_flow.verification_url,
|
||||
dev_flow.user_code),
|
||||
@ -223,7 +223,7 @@ def do_setup(hass, config):
|
||||
setup_services(hass, track_new_found_calendars, calendar_service)
|
||||
|
||||
# Ensure component is loaded
|
||||
bootstrap.setup_component(hass, 'calendar', config)
|
||||
setup_component(hass, 'calendar', config)
|
||||
|
||||
for calendar in hass.data[DATA_INDEX].values():
|
||||
discovery.load_platform(hass, 'calendar', DOMAIN, calendar)
|
||||
|
@ -14,14 +14,13 @@ from homeassistant import config as conf_util, core as ha
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
|
||||
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.helpers.entity import Entity, async_generate_entity_id
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import (
|
||||
run_callback_threadsafe, run_coroutine_threadsafe)
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
DOMAIN = 'group'
|
||||
|
||||
@ -43,7 +42,6 @@ SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_VISIBLE): cv.boolean
|
||||
})
|
||||
|
||||
SERVICE_RELOAD = 'reload'
|
||||
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -98,7 +96,7 @@ def is_on(hass, entity_id):
|
||||
|
||||
def reload(hass):
|
||||
"""Reload the automation from config."""
|
||||
hass.services.call(DOMAIN, SERVICE_RELOAD)
|
||||
hass.add_job(async_reload, hass)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -365,7 +363,7 @@ class Group(Entity):
|
||||
|
||||
def start(self):
|
||||
"""Start tracking members."""
|
||||
run_callback_threadsafe(self.hass.loop, self.async_start).result()
|
||||
self.hass.add_job(self.async_start)
|
||||
|
||||
@callback
|
||||
def async_start(self):
|
||||
@ -396,17 +394,16 @@ class Group(Entity):
|
||||
self._state = STATE_UNKNOWN
|
||||
self._async_update_group_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_remove(self):
|
||||
"""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:
|
||||
self._async_unsub_state_changed()
|
||||
self._async_unsub_state_changed = None
|
||||
|
||||
yield from super().async_remove()
|
||||
return super().async_remove()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_state_changed_listener(self, entity_id, old_state, new_state):
|
||||
|
@ -20,6 +20,7 @@ from homeassistant.components import recorder, script
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import ATTR_HIDDEN
|
||||
from homeassistant.components.recorder.util import session_scope, execute
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -34,19 +35,20 @@ SIGNIFICANT_DOMAINS = ('thermostat', 'climate')
|
||||
IGNORE_DOMAINS = ('zone', 'scene',)
|
||||
|
||||
|
||||
def last_recorder_run():
|
||||
def last_recorder_run(hass):
|
||||
"""Retireve the last closed recorder run from the DB."""
|
||||
recorder.get_instance()
|
||||
rec_runs = recorder.get_model('RecorderRuns')
|
||||
with recorder.session_scope() as session:
|
||||
res = recorder.query(rec_runs).order_by(rec_runs.end.desc()).first()
|
||||
from homeassistant.components.recorder.models import RecorderRuns
|
||||
|
||||
with session_scope(hass=hass) as session:
|
||||
res = (session.query(RecorderRuns)
|
||||
.order_by(RecorderRuns.end.desc()).first())
|
||||
if res is None:
|
||||
return None
|
||||
session.expunge(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):
|
||||
"""
|
||||
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
|
||||
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
|
||||
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:
|
||||
query = query.filter(states.last_updated < end_time)
|
||||
with session_scope(hass=hass) as session:
|
||||
query = session.query(States).filter(
|
||||
(States.domain.in_(SIGNIFICANT_DOMAINS) |
|
||||
(States.last_changed == States.last_updated)) &
|
||||
(States.last_updated > start_time))
|
||||
|
||||
states = (
|
||||
state for state in recorder.execute(
|
||||
query.order_by(states.entity_id, states.last_updated))
|
||||
if (_is_significant(state) and
|
||||
not state.attributes.get(ATTR_HIDDEN, False)))
|
||||
if filters:
|
||||
query = filters.apply(query, entity_ids)
|
||||
|
||||
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."""
|
||||
states = recorder.get_model('States')
|
||||
query = recorder.query(states).filter(
|
||||
(states.last_changed == states.last_updated) &
|
||||
(states.last_changed > start_time))
|
||||
from homeassistant.components.recorder.models import States
|
||||
|
||||
if end_time is not None:
|
||||
query = query.filter(states.last_updated < end_time)
|
||||
with session_scope(hass=hass) as session:
|
||||
query = session.query(States).filter(
|
||||
(States.last_changed == States.last_updated) &
|
||||
(States.last_changed > start_time))
|
||||
|
||||
if entity_id is not None:
|
||||
query = query.filter_by(entity_id=entity_id.lower())
|
||||
if end_time is not None:
|
||||
query = query.filter(States.last_updated < end_time)
|
||||
|
||||
states = recorder.execute(
|
||||
query.order_by(states.entity_id, states.last_updated))
|
||||
if entity_id is not None:
|
||||
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."""
|
||||
from homeassistant.components.recorder.models import States
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
states = recorder.get_model('States')
|
||||
most_recent_state_ids = recorder.query(
|
||||
func.max(states.state_id).label('max_state_id')
|
||||
).filter(
|
||||
(states.created >= run.start) &
|
||||
(states.created < utc_point_in_time) &
|
||||
(~states.domain.in_(IGNORE_DOMAINS)))
|
||||
if filters:
|
||||
most_recent_state_ids = filters.apply(most_recent_state_ids,
|
||||
entity_ids)
|
||||
with session_scope(hass=hass) as session:
|
||||
most_recent_state_ids = session.query(
|
||||
func.max(States.state_id).label('max_state_id')
|
||||
).filter(
|
||||
(States.created >= run.start) &
|
||||
(States.created < utc_point_in_time) &
|
||||
(~States.domain.in_(IGNORE_DOMAINS)))
|
||||
|
||||
most_recent_state_ids = most_recent_state_ids.group_by(
|
||||
states.entity_id).subquery()
|
||||
if filters:
|
||||
most_recent_state_ids = filters.apply(most_recent_state_ids,
|
||||
entity_ids)
|
||||
|
||||
query = recorder.query(states).join(most_recent_state_ids, and_(
|
||||
states.state_id == most_recent_state_ids.c.max_state_id))
|
||||
most_recent_state_ids = most_recent_state_ids.group_by(
|
||||
States.entity_id).subquery()
|
||||
|
||||
for state in recorder.execute(query):
|
||||
if not state.attributes.get(ATTR_HIDDEN, False):
|
||||
yield state
|
||||
query = session.query(States).join(most_recent_state_ids, and_(
|
||||
States.state_id == most_recent_state_ids.c.max_state_id))
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
# 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_updated = start_time
|
||||
result[state.entity_id].append(state)
|
||||
@ -154,9 +166,9 @@ def states_to_json(states, start_time, entity_id, filters=None):
|
||||
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."""
|
||||
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
|
||||
|
||||
|
||||
@ -173,7 +185,6 @@ def setup(hass, config):
|
||||
filters.included_entities = include[CONF_ENTITIES]
|
||||
filters.included_domains = include[CONF_DOMAINS]
|
||||
|
||||
recorder.get_instance()
|
||||
hass.http.register_view(HistoryPeriodView(filters))
|
||||
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')
|
||||
|
||||
result = yield from request.app['hass'].loop.run_in_executor(
|
||||
None, get_significant_states, start_time, end_time, entity_id,
|
||||
self.filters)
|
||||
None, get_significant_states, request.app['hass'], start_time,
|
||||
end_time, entity_id, self.filters)
|
||||
result = result.values()
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
elapsed = time.perf_counter() - timer_start
|
||||
@ -254,41 +265,42 @@ class Filters(object):
|
||||
* if include and exclude is defined - select the entities specified in
|
||||
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
|
||||
if entity_ids is not None:
|
||||
return query.filter(states.entity_id.in_(entity_ids))
|
||||
query = query.filter(~states.domain.in_(IGNORE_DOMAINS))
|
||||
return query.filter(States.entity_id.in_(entity_ids))
|
||||
query = query.filter(~States.domain.in_(IGNORE_DOMAINS))
|
||||
|
||||
filter_query = None
|
||||
# filter if only excluded domain is configured
|
||||
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:
|
||||
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
|
||||
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:
|
||||
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
|
||||
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:
|
||||
filter_query &= (states.domain.in_(self.included_domains) |
|
||||
states.entity_id.in_(self.included_entities))
|
||||
filter_query &= (States.domain.in_(self.included_domains) |
|
||||
States.entity_id.in_(self.included_entities))
|
||||
else:
|
||||
filter_query &= (states.domain.in_(self.included_domains) & ~
|
||||
states.domain.in_(self.excluded_domains))
|
||||
filter_query &= (States.domain.in_(self.included_domains) & ~
|
||||
States.domain.in_(self.excluded_domains))
|
||||
# no domain filter just included entities
|
||||
elif not self.excluded_domains and not self.included_domains and \
|
||||
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:
|
||||
query = query.filter(filter_query)
|
||||
# finally apply excluded entities filter if configured
|
||||
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
|
||||
|
||||
|
||||
|
@ -4,6 +4,7 @@ from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
import os
|
||||
|
||||
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
|
||||
import voluptuous as vol
|
||||
@ -115,13 +116,14 @@ def load_ip_bans_config(path: str):
|
||||
"""Loading list of banned IPs from config file."""
|
||||
ip_list = []
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return ip_list
|
||||
|
||||
try:
|
||||
list_ = load_yaml_config_file(path)
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error('Unable to load %s: %s', path, str(err))
|
||||
return []
|
||||
return ip_list
|
||||
|
||||
for ip_ban, ip_info in list_.items():
|
||||
try:
|
||||
|
@ -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)
|
||||
))
|
||||
|
||||
yield from async_add_devices(entities)
|
||||
async_add_devices(entities)
|
||||
|
||||
|
||||
class MicrosoftFaceDetectEntity(ImageProcessingFaceEntity):
|
||||
|
@ -54,7 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
camera.get(CONF_NAME)
|
||||
))
|
||||
|
||||
yield from async_add_devices(entities)
|
||||
async_add_devices(entities)
|
||||
|
||||
|
||||
class ImageProcessingFaceEntity(ImageProcessingEntity):
|
||||
@ -108,8 +108,7 @@ class ImageProcessingFaceEntity(ImageProcessingEntity):
|
||||
def process_faces(self, faces, total):
|
||||
"""Send event with detected faces and store data."""
|
||||
run_callback_threadsafe(
|
||||
self.hass.loop, self.async_process_faces, faces, total
|
||||
).result()
|
||||
self.hass.loop, self.async_process_faces, faces, total).result()
|
||||
|
||||
@callback
|
||||
def async_process_faces(self, faces, total):
|
||||
|
@ -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)
|
||||
))
|
||||
|
||||
yield from async_add_devices(entities)
|
||||
async_add_devices(entities)
|
||||
|
||||
|
||||
class OpenAlprCloudEntity(ImageProcessingAlprEntity):
|
||||
|
@ -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)
|
||||
))
|
||||
|
||||
yield from async_add_devices(entities)
|
||||
async_add_devices(entities)
|
||||
|
||||
|
||||
class ImageProcessingAlprEntity(ImageProcessingEntity):
|
||||
|
@ -85,7 +85,7 @@ def setup(hass, config):
|
||||
|
||||
try:
|
||||
influx = InfluxDBClient(**kwargs)
|
||||
influx.query("SELECT * FROM /.*/ LIMIT 1;")
|
||||
influx.query("SHOW DIAGNOSTICS;")
|
||||
except exceptions.InfluxDBClientError as exc:
|
||||
_LOGGER.error("Database host is not accessible due to '%s', please "
|
||||
"check your entries in the configuration file and that "
|
||||
|
@ -14,6 +14,7 @@ from homeassistant.const import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
|
||||
DOMAIN = 'input_slider'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
@ -165,6 +166,18 @@ class InputSlider(Entity):
|
||||
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
|
||||
def async_select_value(self, value):
|
||||
"""Select new value."""
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, CONF_HOST, CONF_PORT, CONF_TIMEOUT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['insteonlocal==0.39']
|
||||
REQUIREMENTS = ['insteonlocal==0.48']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -231,7 +231,7 @@ class ISYDevice(Entity):
|
||||
# pylint: disable=unused-argument
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Handle the update event from the ISY994 Node."""
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def domain(self) -> str:
|
||||
|
@ -24,8 +24,6 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.restore_state import async_restore_state
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
||||
|
||||
DOMAIN = "light"
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
@ -88,7 +86,7 @@ PROP_TO_ATTR = {
|
||||
}
|
||||
|
||||
# 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))
|
||||
|
||||
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,
|
||||
profile=None, flash=None, effect=None, color_name=None):
|
||||
"""Turn all or specified light on."""
|
||||
run_callback_threadsafe(
|
||||
hass.loop, async_turn_on, hass, entity_id, transition, brightness,
|
||||
hass.add_job(
|
||||
async_turn_on, hass, entity_id, transition, brightness,
|
||||
rgb_color, xy_color, color_temp, white_value,
|
||||
profile, flash, effect, color_name).result()
|
||||
profile, flash, effect, color_name)
|
||||
|
||||
|
||||
@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):
|
||||
"""Turn all or specified light off."""
|
||||
run_callback_threadsafe(
|
||||
hass.loop, async_turn_off, hass, entity_id, transition).result()
|
||||
hass.add_job(async_turn_off, hass, entity_id, transition)
|
||||
|
||||
|
||||
@callback
|
||||
|
@ -106,4 +106,4 @@ class EnOceanLight(enocean.EnOceanDevice, Light):
|
||||
"""Update the internal state of this device."""
|
||||
self._brightness = math.floor(val / 100.0 * 256.0)
|
||||
self._on_state = bool(val != 0)
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.components.light import (
|
||||
PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['flux_led==0.13']
|
||||
REQUIREMENTS = ['flux_led==0.15']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -47,9 +47,19 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
|
||||
|
||||
PHUE_CONFIG_FILE = 'phue.conf'
|
||||
|
||||
SUPPORT_HUE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
|
||||
SUPPORT_FLASH | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION |
|
||||
SUPPORT_XY_COLOR)
|
||||
SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION | SUPPORT_FLASH)
|
||||
SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS)
|
||||
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"
|
||||
DEFAULT_ALLOW_IN_EMULATED_HUE = True
|
||||
@ -354,7 +364,7 @@ class HueLight(Light):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_HUE
|
||||
return SUPPORT_HUE.get(self.info.get('type'), SUPPORT_HUE_EXTENDED)
|
||||
|
||||
@property
|
||||
def effect_list(self):
|
||||
@ -366,15 +376,30 @@ class HueLight(Light):
|
||||
command = {'on': True}
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
command['transitiontime'] = kwargs[ATTR_TRANSITION] * 10
|
||||
command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10)
|
||||
|
||||
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:
|
||||
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 self.info.get('manufacturername') == "OSRAM":
|
||||
hsv = color_util.color_RGB_to_hsv(
|
||||
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
|
||||
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:
|
||||
command['bri'] = kwargs[ATTR_BRIGHTNESS]
|
||||
@ -401,7 +426,8 @@ class HueLight(Light):
|
||||
command['hue'] = random.randrange(0, 65535)
|
||||
command['sat'] = random.randrange(150, 254)
|
||||
elif self.bridge_type == 'hue':
|
||||
command['effect'] = 'none'
|
||||
if self.info.get('manufacturername') != "OSRAM":
|
||||
command['effect'] = 'none'
|
||||
|
||||
self._command_func(self.light_id, command)
|
||||
|
||||
@ -410,9 +436,7 @@ class HueLight(Light):
|
||||
command = {'on': False}
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
# Transition time is in 1/10th seconds and cannot exceed
|
||||
# 900 seconds.
|
||||
command['transitiontime'] = min(9000, kwargs[ATTR_TRANSITION] * 10)
|
||||
command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10)
|
||||
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
|
||||
|
@ -152,6 +152,10 @@ class InsteonLocalDimmerDevice(Light):
|
||||
def update(self):
|
||||
"""Update state of the light."""
|
||||
resp = self.node.status(0)
|
||||
|
||||
while 'error' in resp and resp['error'] is True:
|
||||
resp = self.node.status(0)
|
||||
|
||||
if 'cmd2' in resp:
|
||||
self._value = int(resp['cmd2'], 16)
|
||||
|
||||
|
@ -98,7 +98,7 @@ class LIFX(object):
|
||||
ipaddr, name, power, hue, sat, bri, kel)
|
||||
bulb.set_power(power)
|
||||
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):
|
||||
"""Initialize the light."""
|
||||
@ -106,7 +106,7 @@ class LIFX(object):
|
||||
|
||||
if bulb is not None:
|
||||
bulb.set_color(hue, sat, bri, kel)
|
||||
bulb.update_ha_state()
|
||||
bulb.schedule_update_ha_state()
|
||||
|
||||
def on_power(self, ipaddr, power):
|
||||
"""Initialize the light."""
|
||||
@ -114,7 +114,7 @@ class LIFX(object):
|
||||
|
||||
if bulb is not None:
|
||||
bulb.set_power(power)
|
||||
bulb.update_ha_state()
|
||||
bulb.schedule_update_ha_state()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def poll(self, now):
|
||||
@ -202,7 +202,7 @@ class LIFXLight(Light):
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fade = kwargs[ATTR_TRANSITION] * 1000
|
||||
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
||||
else:
|
||||
fade = 0
|
||||
|
||||
@ -230,15 +230,17 @@ class LIFXLight(Light):
|
||||
hue, saturation, brightness, kelvin, fade)
|
||||
|
||||
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_color(self._ip, hue, saturation,
|
||||
brightness, kelvin, fade)
|
||||
else:
|
||||
self._liffylights.set_color(self._ip, hue, saturation,
|
||||
brightness, kelvin, fade)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fade = kwargs[ATTR_TRANSITION] * 1000
|
||||
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
||||
else:
|
||||
fade = 0
|
||||
|
||||
|
@ -17,7 +17,7 @@ from homeassistant.components.light import (
|
||||
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['limitlessled==1.0.4']
|
||||
REQUIREMENTS = ['limitlessled==1.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -143,7 +143,7 @@ def state(new_state):
|
||||
pipeline.on()
|
||||
# Set transition time.
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
transition_time = kwargs[ATTR_TRANSITION]
|
||||
transition_time = int(kwargs[ATTR_TRANSITION])
|
||||
# Do group type-specific work.
|
||||
function(self, transition_time, pipeline, **kwargs)
|
||||
# Update state.
|
||||
|
@ -12,83 +12,122 @@ import voluptuous as vol
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_RGB_COLOR, SUPPORT_COLOR_TEMP, Light)
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR,
|
||||
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 (
|
||||
CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF,
|
||||
CONF_PAYLOAD_ON, CONF_STATE, CONF_BRIGHTNESS, CONF_RGB,
|
||||
CONF_COLOR_TEMP)
|
||||
CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME,
|
||||
CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON,
|
||||
CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY)
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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_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_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_STATE_TOPIC = 'color_temp_state_topic'
|
||||
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_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({
|
||||
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_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.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
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Add MQTT Light."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
|
||||
config.setdefault(
|
||||
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_EFFECT_LIST),
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_BRIGHTNESS_STATE_TOPIC,
|
||||
CONF_BRIGHTNESS_COMMAND_TOPIC,
|
||||
CONF_RGB_STATE_TOPIC,
|
||||
CONF_RGB_COMMAND_TOPIC,
|
||||
CONF_BRIGHTNESS_STATE_TOPIC,
|
||||
CONF_COLOR_TEMP_COMMAND_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_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_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_RETAIN),
|
||||
@ -98,16 +137,19 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
},
|
||||
config.get(CONF_OPTIMISTIC),
|
||||
config.get(CONF_BRIGHTNESS_SCALE),
|
||||
config.get(CONF_WHITE_VALUE_SCALE),
|
||||
)])
|
||||
|
||||
|
||||
class MqttLight(Light):
|
||||
"""MQTT light."""
|
||||
|
||||
def __init__(self, name, topic, templates, qos, retain, payload,
|
||||
optimistic, brightness_scale):
|
||||
def __init__(self, name, effect_list, topic, templates, qos,
|
||||
retain, payload, optimistic, brightness_scale,
|
||||
white_value_scale):
|
||||
"""Initialize MQTT light."""
|
||||
self._name = name
|
||||
self._effect_list = effect_list
|
||||
self._topic = topic
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
@ -120,11 +162,21 @@ class MqttLight(Light):
|
||||
optimistic or topic[CONF_BRIGHTNESS_STATE_TOPIC] is None)
|
||||
self._optimistic_color_temp = (
|
||||
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._white_value_scale = white_value_scale
|
||||
self._state = False
|
||||
self._brightness = None
|
||||
self._rgb = None
|
||||
self._color_temp = None
|
||||
self._effect = None
|
||||
self._white_value = None
|
||||
self._xy = None
|
||||
self._supported_features = 0
|
||||
self._supported_features |= (
|
||||
topic[CONF_RGB_COMMAND_TOPIC] is not None and SUPPORT_RGB_COLOR)
|
||||
@ -134,6 +186,14 @@ class MqttLight(Light):
|
||||
self._supported_features |= (
|
||||
topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None and
|
||||
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
|
||||
def async_added_to_hass(self):
|
||||
@ -215,6 +275,57 @@ class MqttLight(Light):
|
||||
else:
|
||||
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
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
@ -230,6 +341,16 @@ class MqttLight(Light):
|
||||
"""Return the color temperature in mired."""
|
||||
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
|
||||
def should_poll(self):
|
||||
"""No polling needed for a MQTT light."""
|
||||
@ -250,6 +371,16 @@ class MqttLight(Light):
|
||||
"""Return true if we do optimistic updates."""
|
||||
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
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
@ -297,6 +428,41 @@ class MqttLight(Light):
|
||||
self._color_temp = kwargs[ATTR_COLOR_TEMP]
|
||||
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(
|
||||
self.hass, self._topic[CONF_COMMAND_TOPIC], self._payload['on'],
|
||||
self._qos, self._retain)
|
||||
|
@ -12,11 +12,14 @@ import voluptuous as vol
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, PLATFORM_SCHEMA,
|
||||
ATTR_FLASH, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_FLASH,
|
||||
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light)
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH,
|
||||
ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR,
|
||||
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 (
|
||||
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 (
|
||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -27,42 +30,53 @@ DOMAIN = 'mqtt_json'
|
||||
|
||||
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_OPTIMISTIC = False
|
||||
DEFAULT_BRIGHTNESS = False
|
||||
DEFAULT_RGB = False
|
||||
DEFAULT_FLASH_TIME_SHORT = 2
|
||||
DEFAULT_FLASH_TIME_LONG = 10
|
||||
DEFAULT_WHITE_VALUE = False
|
||||
DEFAULT_XY = False
|
||||
|
||||
CONF_EFFECT_LIST = 'effect_list'
|
||||
|
||||
CONF_FLASH_TIME_SHORT = 'flash_time_short'
|
||||
CONF_FLASH_TIME_LONG = 'flash_time_long'
|
||||
|
||||
SUPPORT_MQTT_JSON = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH | SUPPORT_RGB_COLOR |
|
||||
SUPPORT_TRANSITION)
|
||||
CONF_FLASH_TIME_SHORT = 'flash_time_short'
|
||||
|
||||
# Stealing some of these from the base MQTT configs.
|
||||
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_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):
|
||||
cv.positive_int,
|
||||
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
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""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_EFFECT_LIST),
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
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_OPTIMISTIC),
|
||||
config.get(CONF_BRIGHTNESS),
|
||||
config.get(CONF_COLOR_TEMP),
|
||||
config.get(CONF_EFFECT),
|
||||
config.get(CONF_RGB),
|
||||
config.get(CONF_WHITE_VALUE),
|
||||
config.get(CONF_XY),
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
CONF_FLASH_TIME_SHORT,
|
||||
@ -86,10 +104,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class MqttJson(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):
|
||||
"""Initialize MQTT JSON light."""
|
||||
self._name = name
|
||||
self._effect_list = effect_list
|
||||
self._topic = topic
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
@ -100,13 +120,45 @@ class MqttJson(Light):
|
||||
else:
|
||||
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:
|
||||
self._rgb = [0, 0, 0]
|
||||
else:
|
||||
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._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
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events.
|
||||
@ -133,7 +185,7 @@ class MqttJson(Light):
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid color value received")
|
||||
_LOGGER.warning("Invalid RGB color value received")
|
||||
|
||||
if self._brightness is not None:
|
||||
try:
|
||||
@ -143,6 +195,41 @@ class MqttJson(Light):
|
||||
except ValueError:
|
||||
_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())
|
||||
|
||||
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 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
|
||||
def rgb_color(self):
|
||||
"""Return the RGB color value."""
|
||||
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
|
||||
def should_poll(self):
|
||||
"""No polling needed for a MQTT light."""
|
||||
@ -183,7 +295,7 @@ class MqttJson(Light):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_MQTT_JSON
|
||||
return self._supported_features
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
@ -215,7 +327,7 @@ class MqttJson(Light):
|
||||
message['flash'] = self._flash_times[CONF_FLASH_TIME_SHORT]
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
message['transition'] = kwargs[ATTR_TRANSITION]
|
||||
message['transition'] = int(kwargs[ATTR_TRANSITION])
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
message['brightness'] = int(kwargs[ATTR_BRIGHTNESS])
|
||||
@ -224,6 +336,37 @@ class MqttJson(Light):
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
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(
|
||||
self.hass, self._topic[CONF_COMMAND_TOPIC], json.dumps(message),
|
||||
self._qos, self._retain)
|
||||
@ -245,7 +388,7 @@ class MqttJson(Light):
|
||||
message = {'state': 'OFF'}
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
message['transition'] = kwargs[ATTR_TRANSITION]
|
||||
message['transition'] = int(kwargs[ATTR_TRANSITION])
|
||||
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_COMMAND_TOPIC], json.dumps(message),
|
||||
|
@ -11,9 +11,10 @@ import voluptuous as vol
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION,
|
||||
PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_FLASH,
|
||||
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light)
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH,
|
||||
ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, Light, PLATFORM_SCHEMA,
|
||||
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.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
|
||||
@ -28,43 +29,47 @@ DEPENDENCIES = ['mqtt']
|
||||
DEFAULT_NAME = 'MQTT Template Light'
|
||||
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_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'
|
||||
|
||||
SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH |
|
||||
SUPPORT_RGB_COLOR | SUPPORT_TRANSITION)
|
||||
CONF_GREEN_TEMPLATE = 'green_template'
|
||||
CONF_RED_TEMPLATE = 'red_template'
|
||||
CONF_STATE_TEMPLATE = 'state_template'
|
||||
CONF_WHITE_VALUE_TEMPLATE = 'white_value_template'
|
||||
|
||||
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_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_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_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.All(vol.Coerce(int), vol.In([0, 1, 2])),
|
||||
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""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,
|
||||
config.get(CONF_NAME),
|
||||
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 (
|
||||
CONF_COMMAND_ON_TEMPLATE,
|
||||
CONF_COMMAND_OFF_TEMPLATE,
|
||||
CONF_STATE_TEMPLATE,
|
||||
CONF_BRIGHTNESS_TEMPLATE,
|
||||
CONF_RED_TEMPLATE,
|
||||
CONF_GREEN_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),
|
||||
@ -114,6 +121,16 @@ class MqttTemplate(Light):
|
||||
else:
|
||||
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
|
||||
self._templates[CONF_GREEN_TEMPLATE] is not None and
|
||||
self._templates[CONF_BLUE_TEMPLATE] is not None):
|
||||
@ -156,6 +173,16 @@ class MqttTemplate(Light):
|
||||
except ValueError:
|
||||
_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
|
||||
if self._rgb is not None:
|
||||
try:
|
||||
@ -171,6 +198,16 @@ class MqttTemplate(Light):
|
||||
except ValueError:
|
||||
_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
|
||||
if self._templates[CONF_EFFECT_TEMPLATE] is not None:
|
||||
effect = self._templates[CONF_EFFECT_TEMPLATE].\
|
||||
@ -194,11 +231,21 @@ class MqttTemplate(Light):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the color temperature in mired."""
|
||||
return self._color_temp
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""Return the RGB color value [int, int, int]."""
|
||||
return self._rgb
|
||||
|
||||
@property
|
||||
def white_value(self):
|
||||
"""Return the white property."""
|
||||
return self._white_value
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state.
|
||||
@ -250,6 +297,13 @@ class MqttTemplate(Light):
|
||||
if self._optimistic:
|
||||
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
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
values['red'] = kwargs[ATTR_RGB_COLOR][0]
|
||||
@ -259,6 +313,13 @@ class MqttTemplate(Light):
|
||||
if self._optimistic:
|
||||
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
|
||||
if ATTR_EFFECT in kwargs:
|
||||
values['effect'] = kwargs.get(ATTR_EFFECT)
|
||||
@ -269,7 +330,7 @@ class MqttTemplate(Light):
|
||||
|
||||
# transition
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
values['transition'] = kwargs[ATTR_TRANSITION]
|
||||
values['transition'] = int(kwargs[ATTR_TRANSITION])
|
||||
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topics[CONF_COMMAND_TOPIC],
|
||||
@ -293,7 +354,7 @@ class MqttTemplate(Light):
|
||||
|
||||
# transition
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
values['transition'] = kwargs[ATTR_TRANSITION]
|
||||
values['transition'] = int(kwargs[ATTR_TRANSITION])
|
||||
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topics[CONF_COMMAND_TOPIC],
|
||||
@ -307,12 +368,16 @@ class MqttTemplate(Light):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
features = 0
|
||||
features = (SUPPORT_FLASH | SUPPORT_TRANSITION)
|
||||
if self._brightness is not None:
|
||||
features = features | SUPPORT_BRIGHTNESS
|
||||
if self._rgb is not None:
|
||||
features = features | SUPPORT_RGB_COLOR
|
||||
if self._effect_list is not None:
|
||||
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
|
||||
|
@ -17,6 +17,8 @@ from homeassistant.components.light import (
|
||||
Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR,
|
||||
ATTR_TRANSITION, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT,
|
||||
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
|
||||
|
||||
REQUIREMENTS = ['https://github.com/tfriedel/python-lightify/archive/'
|
||||
@ -24,10 +26,6 @@ REQUIREMENTS = ['https://github.com/tfriedel/python-lightify/archive/'
|
||||
|
||||
_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_FORCED_SCANS = timedelta(milliseconds=100)
|
||||
|
||||
@ -93,10 +91,10 @@ class OsramLightifyLight(Light):
|
||||
self._light = light
|
||||
self._light_id = light_id
|
||||
self.update_lights = update_lights
|
||||
self._brightness = 0
|
||||
self._rgb = (0, 0, 0)
|
||||
self._name = ""
|
||||
self._temperature = TEMP_MIN
|
||||
self._brightness = None
|
||||
self._rgb = None
|
||||
self._name = None
|
||||
self._temperature = None
|
||||
self._state = False
|
||||
self.update()
|
||||
|
||||
@ -145,7 +143,7 @@ class OsramLightifyLight(Light):
|
||||
self._state = self._light.on()
|
||||
|
||||
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:"
|
||||
" %s is: %s ",
|
||||
self._name, transition)
|
||||
@ -164,8 +162,7 @@ class OsramLightifyLight(Light):
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
color_t = kwargs[ATTR_COLOR_TEMP]
|
||||
kelvin = int(((TEMP_MAX - TEMP_MIN) * (color_t - TEMP_MIN_HASS) /
|
||||
(TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN)
|
||||
kelvin = int(color_temperature_mired_to_kelvin(color_t))
|
||||
_LOGGER.debug("turn_on requested set_temperature for light:"
|
||||
" %s: %s ", self._name, kelvin)
|
||||
self._light.set_temperature(kelvin, transition)
|
||||
@ -196,7 +193,7 @@ class OsramLightifyLight(Light):
|
||||
_LOGGER.debug("turn_off Attempting to turn off light: %s ",
|
||||
self._name)
|
||||
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:"
|
||||
" %s is: %s ",
|
||||
self._name, transition)
|
||||
@ -218,6 +215,5 @@ class OsramLightifyLight(Light):
|
||||
self._name = self._light.name()
|
||||
self._rgb = self._light.rgb()
|
||||
o_temp = self._light.temp()
|
||||
self._temperature = int(TEMP_MIN_HASS + (TEMP_MAX_HASS - TEMP_MIN_HASS)
|
||||
* (o_temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN))
|
||||
self._temperature = color_temperature_kelvin_to_mired(o_temp)
|
||||
self._state = self._light.on()
|
||||
|
@ -117,7 +117,7 @@ def devices_from_config(domain_config, hass=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""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
|
||||
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 = 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
|
||||
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):
|
||||
"""Representation of a Rflink light."""
|
||||
|
||||
pass
|
||||
@property
|
||||
def entity_id(self):
|
||||
"""Return entity id."""
|
||||
return "light.{}".format(self.name)
|
||||
|
||||
|
||||
class DimmableRflinkLight(SwitchableRflinkDevice, Light):
|
||||
@ -164,6 +167,11 @@ class DimmableRflinkLight(SwitchableRflinkDevice, Light):
|
||||
|
||||
_brightness = 255
|
||||
|
||||
@property
|
||||
def entity_id(self):
|
||||
"""Return entity id."""
|
||||
return "light.{}".format(self.name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
@ -202,6 +210,11 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light):
|
||||
|
||||
_brightness = 255
|
||||
|
||||
@property
|
||||
def entity_id(self):
|
||||
"""Return entity id."""
|
||||
return "light.{}".format(self.name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on and set dim level."""
|
||||
|
@ -106,7 +106,7 @@ class SCSGateLight(Light):
|
||||
return
|
||||
|
||||
self._toggled = message.toggled
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
command = "off"
|
||||
if self._toggled:
|
||||
|
@ -7,10 +7,10 @@ https://home-assistant.io/components/light.vera/
|
||||
import logging
|
||||
|
||||
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.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -33,6 +33,7 @@ class VeraLight(VeraDevice, Light):
|
||||
"""Initialize the light."""
|
||||
self._state = False
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
|
@ -259,7 +259,7 @@ class YeelightLight(Light):
|
||||
_LOGGER.error("Flash supported currently only in RGB mode.")
|
||||
return
|
||||
|
||||
transition = self.config[CONF_TRANSITION]
|
||||
transition = int(self.config[CONF_TRANSITION])
|
||||
if flash == FLASH_LONG:
|
||||
count = 1
|
||||
duration = transition * 5
|
||||
@ -288,9 +288,9 @@ class YeelightLight(Light):
|
||||
rgb = kwargs.get(ATTR_RGB_COLOR)
|
||||
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
|
||||
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)
|
||||
|
||||
|
@ -2,9 +2,7 @@
|
||||
Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.yeelight-sunflower
|
||||
Uses the yeelightsunflower library:
|
||||
https://github.com/lindsaymarkward/python-yeelight-sunflower
|
||||
https://home-assistant.io/components/light.yeelightsunflower
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
@ -17,34 +15,29 @@ from homeassistant.components.light import (Light,
|
||||
from homeassistant.const import CONF_HOST
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['yeelightsunflower==0.0.6']
|
||||
SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR)
|
||||
REQUIREMENTS = ['yeelightsunflower==0.0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Validate the user's configuration
|
||||
|
||||
SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string
|
||||
})
|
||||
|
||||
|
||||
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
|
||||
|
||||
# Assign configuration variables.
|
||||
# The configuration check takes care they are present.
|
||||
host = config.get(CONF_HOST)
|
||||
|
||||
# Setup connection with Yeelight Sunflower hub
|
||||
hub = yeelightsunflower.Hub(host)
|
||||
|
||||
# Verify that hub is responsive
|
||||
if not hub.available:
|
||||
_LOGGER.error('Could not connect to Yeelight Sunflower hub')
|
||||
return False
|
||||
|
||||
# Add devices
|
||||
add_devices(SunflowerBulb(light) for light in hub.get_lights())
|
||||
|
||||
|
||||
|
@ -13,6 +13,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
|
||||
ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \
|
||||
SUPPORT_RGB_COLOR, DOMAIN, Light
|
||||
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.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \
|
||||
color_temperature_mired_to_kelvin, color_temperature_to_rgb, \
|
||||
@ -48,38 +49,25 @@ SUPPORT_ZWAVE_COLORTEMP = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
|
||||
| SUPPORT_COLOR_TEMP)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and add Z-Wave lights."""
|
||||
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]]
|
||||
def get_device(node, value, node_config, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
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)
|
||||
delay = node_config.get(zwave.CONF_REFRESH_DELAY)
|
||||
_LOGGER.debug('name=%s node_config=%s CONF_REFRESH_VALUE=%s'
|
||||
' CONF_REFRESH_DELAY=%s', name, node_config,
|
||||
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):
|
||||
add_devices([ZwaveColorLight(value, refresh, delay)])
|
||||
return ZwaveColorLight(value, refresh, delay)
|
||||
else:
|
||||
add_devices([ZwaveDimmer(value, refresh, delay)])
|
||||
return ZwaveDimmer(value, refresh, delay)
|
||||
|
||||
|
||||
def brightness_state(value):
|
||||
"""Return the brightness and state."""
|
||||
if value.data > 0:
|
||||
return (value.data / 99) * 255, STATE_ON
|
||||
return round((value.data / 99) * 255, 0), STATE_ON
|
||||
else:
|
||||
return 0, STATE_OFF
|
||||
|
||||
@ -119,7 +107,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
# Brightness
|
||||
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."""
|
||||
if self._refresh_value:
|
||||
if self._refreshing:
|
||||
@ -136,7 +124,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
self._timer = Timer(self._delay, _refresh_value)
|
||||
self._timer.start()
|
||||
return
|
||||
super().value_changed(value)
|
||||
super().value_changed()
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
@ -200,6 +188,12 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||
self._value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED)
|
||||
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):
|
||||
"""Search for color values available on this node."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
|
@ -47,7 +47,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
yield from async_add_devices([MqttLock(
|
||||
async_add_devices([MqttLock(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
|
@ -6,10 +6,10 @@ https://home-assistant.io/components/lock.vera/
|
||||
"""
|
||||
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.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -30,6 +30,7 @@ class VeraLock(VeraDevice, LockDevice):
|
||||
"""Initialize the Vera device."""
|
||||
self._state = None
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
|
||||
|
||||
def lock(self, **kwargs):
|
||||
"""Lock the device."""
|
||||
|
@ -13,6 +13,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.lock import DOMAIN, LockDevice
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@ -119,15 +120,8 @@ CLEAR_USERCODE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""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]]
|
||||
|
||||
def get_device(hass, node, value, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
descriptions = load_yaml_config_file(
|
||||
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)
|
||||
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):
|
||||
hass.services.register(DOMAIN,
|
||||
SERVICE_SET_USERCODE,
|
||||
@ -203,8 +191,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
clear_usercode,
|
||||
descriptions.get(SERVICE_CLEAR_USERCODE),
|
||||
schema=CLEAR_USERCODE_SCHEMA)
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveLock(value)])
|
||||
return ZwaveLock(value)
|
||||
|
||||
|
||||
class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
||||
@ -305,3 +292,8 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
||||
if self._lock_status:
|
||||
data[ATTR_LOCK_STATUS] = self._lock_status
|
||||
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
Loading…
x
Reference in New Issue
Block a user