commit
e9f273e7e0
|
@ -20,6 +20,9 @@ omit =
|
||||||
homeassistant/components/android_ip_webcam.py
|
homeassistant/components/android_ip_webcam.py
|
||||||
homeassistant/components/*/android_ip_webcam.py
|
homeassistant/components/*/android_ip_webcam.py
|
||||||
|
|
||||||
|
homeassistant/components/arlo.py
|
||||||
|
homeassistant/components/*/arlo.py
|
||||||
|
|
||||||
homeassistant/components/axis.py
|
homeassistant/components/axis.py
|
||||||
homeassistant/components/*/axis.py
|
homeassistant/components/*/axis.py
|
||||||
|
|
||||||
|
@ -89,6 +92,9 @@ omit =
|
||||||
homeassistant/components/qwikswitch.py
|
homeassistant/components/qwikswitch.py
|
||||||
homeassistant/components/*/qwikswitch.py
|
homeassistant/components/*/qwikswitch.py
|
||||||
|
|
||||||
|
homeassistant/components/rachio.py
|
||||||
|
homeassistant/components/*/rachio.py
|
||||||
|
|
||||||
homeassistant/components/raspihats.py
|
homeassistant/components/raspihats.py
|
||||||
homeassistant/components/*/raspihats.py
|
homeassistant/components/*/raspihats.py
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||||
#ENV INSTALL_TELLSTICK no
|
#ENV INSTALL_TELLSTICK no
|
||||||
#ENV INSTALL_OPENALPR no
|
#ENV INSTALL_OPENALPR no
|
||||||
#ENV INSTALL_FFMPEG no
|
#ENV INSTALL_FFMPEG no
|
||||||
#ENV INSTALL_OPENZWAVE no
|
|
||||||
#ENV INSTALL_LIBCEC no
|
#ENV INSTALL_LIBCEC no
|
||||||
#ENV INSTALL_PHANTOMJS no
|
#ENV INSTALL_PHANTOMJS no
|
||||||
#ENV INSTALL_COAP_CLIENT no
|
#ENV INSTALL_COAP_CLIENT no
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="https://community.home-assistant.io">📌 Community Forums</a></li>
|
<li><a href="https://home-assistant.io/">Homepage</a></li>
|
||||||
<li><a href="https://github.com/home-assistant/home-assistant">🚀 GitHub</a></li>
|
<li><a href="https://community.home-assistant.io">Community Forums</a></li>
|
||||||
<li><a href="https://home-assistant.io/">🏡 Homepage</a></li>
|
<li><a href="https://github.com/home-assistant/home-assistant">GitHub</a></li>
|
||||||
<li><a href="https://gitter.im/home-assistant/home-assistant">💬 Gitter</a></li>
|
<li><a href="https://gitter.im/home-assistant/home-assistant">Gitter</a></li>
|
||||||
<li><a href="https://pypi.python.org/pypi/homeassistant">💾 Download Releases</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<hr>
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import threading
|
||||||
|
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from homeassistant import monkey_patch
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
__version__,
|
__version__,
|
||||||
EVENT_HOMEASSISTANT_START,
|
EVENT_HOMEASSISTANT_START,
|
||||||
|
@ -17,7 +18,6 @@ from homeassistant.const import (
|
||||||
REQUIRED_PYTHON_VER_WIN,
|
REQUIRED_PYTHON_VER_WIN,
|
||||||
RESTART_EXIT_CODE,
|
RESTART_EXIT_CODE,
|
||||||
)
|
)
|
||||||
from homeassistant.util.async import run_callback_threadsafe
|
|
||||||
|
|
||||||
|
|
||||||
def attempt_use_uvloop():
|
def attempt_use_uvloop():
|
||||||
|
@ -310,6 +310,9 @@ def setup_and_run_hass(config_dir: str,
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if args.open_ui:
|
if args.open_ui:
|
||||||
|
# Imported here to avoid importing asyncio before monkey patch
|
||||||
|
from homeassistant.util.async import run_callback_threadsafe
|
||||||
|
|
||||||
def open_browser(event):
|
def open_browser(event):
|
||||||
"""Open the webinterface in a browser."""
|
"""Open the webinterface in a browser."""
|
||||||
if hass.config.api is not None:
|
if hass.config.api is not None:
|
||||||
|
@ -371,6 +374,13 @@ def main() -> int:
|
||||||
"""Start Home Assistant."""
|
"""Start Home Assistant."""
|
||||||
validate_python()
|
validate_python()
|
||||||
|
|
||||||
|
if os.environ.get('HASS_MONKEYPATCH_ASYNCIO') == '1':
|
||||||
|
if sys.version_info[:3] >= (3, 6):
|
||||||
|
monkey_patch.disable_c_asyncio()
|
||||||
|
monkey_patch.patch_weakref_tasks()
|
||||||
|
elif sys.version_info[:3] < (3, 5, 3):
|
||||||
|
monkey_patch.patch_weakref_tasks()
|
||||||
|
|
||||||
attempt_use_uvloop()
|
attempt_use_uvloop()
|
||||||
|
|
||||||
if sys.version_info[:3] < (3, 5, 3):
|
if sys.version_info[:3] < (3, 5, 3):
|
||||||
|
|
|
@ -83,8 +83,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||||
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
yield from hass.loop.run_in_executor(
|
yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
|
||||||
None, conf_util.process_ha_config_upgrade, hass)
|
|
||||||
|
|
||||||
if enable_log:
|
if enable_log:
|
||||||
async_enable_logging(hass, verbose, log_rotate_days)
|
async_enable_logging(hass, verbose, log_rotate_days)
|
||||||
|
@ -95,7 +94,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||||
'This may cause issues.')
|
'This may cause issues.')
|
||||||
|
|
||||||
if not loader.PREPARED:
|
if not loader.PREPARED:
|
||||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
yield from hass.async_add_job(loader.prepare, hass)
|
||||||
|
|
||||||
# Merge packages
|
# Merge packages
|
||||||
conf_util.merge_packages_config(
|
conf_util.merge_packages_config(
|
||||||
|
@ -184,14 +183,13 @@ def async_from_config_file(config_path: str,
|
||||||
# Set config dir to directory holding config file
|
# Set config dir to directory holding config file
|
||||||
config_dir = os.path.abspath(os.path.dirname(config_path))
|
config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||||
hass.config.config_dir = config_dir
|
hass.config.config_dir = config_dir
|
||||||
yield from hass.loop.run_in_executor(
|
yield from hass.async_add_job(mount_local_lib_path, config_dir)
|
||||||
None, mount_local_lib_path, config_dir)
|
|
||||||
|
|
||||||
async_enable_logging(hass, verbose, log_rotate_days)
|
async_enable_logging(hass, verbose, log_rotate_days)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config_dict = yield from hass.loop.run_in_executor(
|
config_dict = yield from hass.async_add_job(
|
||||||
None, conf_util.load_yaml_config_file, config_path)
|
conf_util.load_yaml_config_file, config_path)
|
||||||
except HomeAssistantError as err:
|
except HomeAssistantError as err:
|
||||||
_LOGGER.error('Error loading %s: %s', config_path, err)
|
_LOGGER.error('Error loading %s: %s', config_path, err)
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -123,8 +123,8 @@ def async_setup(hass, config):
|
||||||
if update_tasks:
|
if update_tasks:
|
||||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, os.path.join(
|
load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml'))
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
for service in SERVICE_TO_METHOD:
|
for service in SERVICE_TO_METHOD:
|
||||||
|
@ -158,8 +158,7 @@ class AlarmControlPanel(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.alarm_disarm, code)
|
||||||
None, self.alarm_disarm, code)
|
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
|
@ -170,8 +169,7 @@ class AlarmControlPanel(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.alarm_arm_home, code)
|
||||||
None, self.alarm_arm_home, code)
|
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
|
@ -182,8 +180,7 @@ class AlarmControlPanel(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.alarm_arm_away, code)
|
||||||
None, self.alarm_arm_away, code)
|
|
||||||
|
|
||||||
def alarm_trigger(self, code=None):
|
def alarm_trigger(self, code=None):
|
||||||
"""Send alarm trigger command."""
|
"""Send alarm trigger command."""
|
||||||
|
@ -194,8 +191,7 @@ class AlarmControlPanel(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.alarm_trigger, code)
|
||||||
None, self.alarm_trigger, code)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
|
|
|
@ -117,7 +117,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
self._alarm.arm('home')
|
self._alarm.arm('stay')
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
|
|
|
@ -70,8 +70,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
device.async_alarm_keypress(keypress)
|
device.async_alarm_keypress(keypress)
|
||||||
|
|
||||||
# Register Envisalink specific services
|
# Register Envisalink specific services
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, os.path.join(
|
load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml'))
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
|
|
|
@ -128,8 +128,8 @@ def async_setup(hass, config):
|
||||||
all_alerts[entity.entity_id] = entity
|
all_alerts[entity.entity_id] = entity
|
||||||
|
|
||||||
# Read descriptions
|
# Read descriptions
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, os.path.join(
|
load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml'))
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
descriptions = descriptions.get(DOMAIN, {})
|
descriptions = descriptions.get(DOMAIN, {})
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
REQUIREMENTS = ['apcaccess==0.0.4']
|
REQUIREMENTS = ['apcaccess==0.0.10']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@ class APIEventStream(HomeAssistantView):
|
||||||
stop_obj = object()
|
stop_obj = object()
|
||||||
to_write = asyncio.Queue(loop=hass.loop)
|
to_write = asyncio.Queue(loop=hass.loop)
|
||||||
|
|
||||||
restrict = request.GET.get('restrict')
|
restrict = request.query.get('restrict')
|
||||||
if restrict:
|
if restrict:
|
||||||
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
"""
|
||||||
|
This component provides basic support for Netgear Arlo IP cameras.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/arlo/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import voluptuous as vol
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||||
|
import homeassistant.loader as loader
|
||||||
|
|
||||||
|
from requests.exceptions import HTTPError, ConnectTimeout
|
||||||
|
|
||||||
|
REQUIREMENTS = ['pyarlo==0.0.4']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_ATTRIBUTION = 'Data provided by arlo.netgear.com'
|
||||||
|
|
||||||
|
DOMAIN = 'arlo'
|
||||||
|
|
||||||
|
DEFAULT_BRAND = 'Netgear Arlo'
|
||||||
|
|
||||||
|
NOTIFICATION_ID = 'arlo_notification'
|
||||||
|
NOTIFICATION_TITLE = 'Arlo Camera Setup'
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
}),
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Set up an Arlo component."""
|
||||||
|
conf = config[DOMAIN]
|
||||||
|
username = conf.get(CONF_USERNAME)
|
||||||
|
password = conf.get(CONF_PASSWORD)
|
||||||
|
|
||||||
|
persistent_notification = loader.get_component('persistent_notification')
|
||||||
|
try:
|
||||||
|
from pyarlo import PyArlo
|
||||||
|
|
||||||
|
arlo = PyArlo(username, password, preload=False)
|
||||||
|
if not arlo.is_connected:
|
||||||
|
return False
|
||||||
|
hass.data['arlo'] = arlo
|
||||||
|
except (ConnectTimeout, HTTPError) as ex:
|
||||||
|
_LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex))
|
||||||
|
persistent_notification.create(
|
||||||
|
hass, 'Error: {}<br />'
|
||||||
|
'You will need to restart hass after fixing.'
|
||||||
|
''.format(ex),
|
||||||
|
title=NOTIFICATION_TITLE,
|
||||||
|
notification_id=NOTIFICATION_ID)
|
||||||
|
return False
|
||||||
|
return True
|
|
@ -158,8 +158,8 @@ def async_setup(hass, config):
|
||||||
|
|
||||||
yield from _async_process_config(hass, config, component)
|
yield from _async_process_config(hass, config, component)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, conf_util.load_yaml_config_file, os.path.join(
|
conf_util.load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml')
|
os.path.dirname(__file__), 'services.yaml')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.const import MATCH_ALL, CONF_PLATFORM
|
from homeassistant.const import MATCH_ALL, CONF_PLATFORM
|
||||||
from homeassistant.helpers.event import (
|
from homeassistant.helpers.event import (
|
||||||
async_track_state_change, async_track_point_in_utc_time)
|
async_track_state_change, async_track_point_in_utc_time)
|
||||||
|
from homeassistant.helpers.deprecation import get_deprecated
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
CONF_ENTITY_ID = 'entity_id'
|
CONF_ENTITY_ID = 'entity_id'
|
||||||
|
@ -40,10 +41,11 @@ def async_trigger(hass, config, action):
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
entity_id = config.get(CONF_ENTITY_ID)
|
entity_id = config.get(CONF_ENTITY_ID)
|
||||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||||
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
|
to_state = get_deprecated(config, CONF_TO, CONF_STATE, MATCH_ALL)
|
||||||
time_delta = config.get(CONF_FOR)
|
time_delta = config.get(CONF_FOR)
|
||||||
async_remove_state_for_cancel = None
|
async_remove_state_for_cancel = None
|
||||||
async_remove_state_for_listener = None
|
async_remove_state_for_listener = None
|
||||||
|
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def clear_listener():
|
def clear_listener():
|
||||||
|
@ -75,13 +77,13 @@ def async_trigger(hass, config, action):
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if time_delta is None:
|
# Ignore changes to state attributes if from/to is in use
|
||||||
call_action()
|
if (not match_all and from_s is not None and to_s is not None and
|
||||||
|
from_s.last_changed == to_s.last_changed):
|
||||||
return
|
return
|
||||||
|
|
||||||
# If only state attributes changed, ignore this event
|
if time_delta is None:
|
||||||
if (from_s is not None and to_s is not None and
|
call_action()
|
||||||
from_s.last_changed == to_s.last_changed):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
|
@ -10,7 +10,7 @@ import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import CONF_AFTER, CONF_PLATFORM
|
from homeassistant.const import CONF_AT, CONF_PLATFORM, CONF_AFTER
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.event import async_track_time_change
|
from homeassistant.helpers.event import async_track_time_change
|
||||||
|
|
||||||
|
@ -22,20 +22,26 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||||
vol.Required(CONF_PLATFORM): 'time',
|
vol.Required(CONF_PLATFORM): 'time',
|
||||||
|
CONF_AT: cv.time,
|
||||||
CONF_AFTER: cv.time,
|
CONF_AFTER: cv.time,
|
||||||
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||||
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||||
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||||
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES,
|
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES,
|
||||||
CONF_SECONDS, CONF_AFTER))
|
CONF_SECONDS, CONF_AT, CONF_AFTER))
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_trigger(hass, config, action):
|
def async_trigger(hass, config, action):
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
if CONF_AFTER in config:
|
if CONF_AT in config:
|
||||||
after = config.get(CONF_AFTER)
|
at_time = config.get(CONF_AT)
|
||||||
hours, minutes, seconds = after.hour, after.minute, after.second
|
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
|
||||||
|
elif CONF_AFTER in config:
|
||||||
|
_LOGGER.warning("'after' is deprecated for the time trigger. Please "
|
||||||
|
"rename 'after' to 'at' in your configuration file.")
|
||||||
|
at_time = config.get(CONF_AFTER)
|
||||||
|
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
|
||||||
else:
|
else:
|
||||||
hours = config.get(CONF_HOURS)
|
hours = config.get(CONF_HOURS)
|
||||||
minutes = config.get(CONF_MINUTES)
|
minutes = config.get(CONF_MINUTES)
|
||||||
|
|
|
@ -38,7 +38,7 @@ class MyStromView(HomeAssistantView):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""The GET request received from a myStrom button."""
|
"""The GET request received from a myStrom button."""
|
||||||
res = yield from self._handle(request.app['hass'], request.GET)
|
res = yield from self._handle(request.app['hass'], request.query)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
|
@ -1,82 +1,82 @@
|
||||||
"""
|
"""
|
||||||
Demo platform that has two fake binary sensors.
|
Demo platform that has two fake binary sensors.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation
|
For more details about this platform, please refer to the documentation
|
||||||
https://home-assistant.io/components/demo/
|
https://home-assistant.io/components/demo/
|
||||||
"""
|
"""
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.components.calendar import CalendarEventDevice
|
from homeassistant.components.calendar import CalendarEventDevice
|
||||||
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
|
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the Demo Calendar platform."""
|
"""Set up the Demo Calendar platform."""
|
||||||
calendar_data_future = DemoGoogleCalendarDataFuture()
|
calendar_data_future = DemoGoogleCalendarDataFuture()
|
||||||
calendar_data_current = DemoGoogleCalendarDataCurrent()
|
calendar_data_current = DemoGoogleCalendarDataCurrent()
|
||||||
add_devices([
|
add_devices([
|
||||||
DemoGoogleCalendar(hass, calendar_data_future, {
|
DemoGoogleCalendar(hass, calendar_data_future, {
|
||||||
CONF_NAME: 'Future Event',
|
CONF_NAME: 'Future Event',
|
||||||
CONF_DEVICE_ID: 'future_event',
|
CONF_DEVICE_ID: 'future_event',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
DemoGoogleCalendar(hass, calendar_data_current, {
|
DemoGoogleCalendar(hass, calendar_data_current, {
|
||||||
CONF_NAME: 'Current Event',
|
CONF_NAME: 'Current Event',
|
||||||
CONF_DEVICE_ID: 'current_event',
|
CONF_DEVICE_ID: 'current_event',
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
class DemoGoogleCalendarData(object):
|
class DemoGoogleCalendarData(object):
|
||||||
"""Representation of a Demo Calendar element."""
|
"""Representation of a Demo Calendar element."""
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Return true so entity knows we have new data."""
|
"""Return true so entity knows we have new data."""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
|
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
|
||||||
"""Representation of a Demo Calendar for a future event."""
|
"""Representation of a Demo Calendar for a future event."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Set the event to a future event."""
|
"""Set the event to a future event."""
|
||||||
one_hour_from_now = dt_util.now() \
|
one_hour_from_now = dt_util.now() \
|
||||||
+ dt_util.dt.timedelta(minutes=30)
|
+ dt_util.dt.timedelta(minutes=30)
|
||||||
self.event = {
|
self.event = {
|
||||||
'start': {
|
'start': {
|
||||||
'dateTime': one_hour_from_now.isoformat()
|
'dateTime': one_hour_from_now.isoformat()
|
||||||
},
|
},
|
||||||
'end': {
|
'end': {
|
||||||
'dateTime': (one_hour_from_now + dt_util.dt.
|
'dateTime': (one_hour_from_now + dt_util.dt.
|
||||||
timedelta(minutes=60)).isoformat()
|
timedelta(minutes=60)).isoformat()
|
||||||
},
|
},
|
||||||
'summary': 'Future Event',
|
'summary': 'Future Event',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
|
class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
|
||||||
"""Representation of a Demo Calendar for a current event."""
|
"""Representation of a Demo Calendar for a current event."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Set the event data."""
|
"""Set the event data."""
|
||||||
middle_of_event = dt_util.now() \
|
middle_of_event = dt_util.now() \
|
||||||
- dt_util.dt.timedelta(minutes=30)
|
- dt_util.dt.timedelta(minutes=30)
|
||||||
self.event = {
|
self.event = {
|
||||||
'start': {
|
'start': {
|
||||||
'dateTime': middle_of_event.isoformat()
|
'dateTime': middle_of_event.isoformat()
|
||||||
},
|
},
|
||||||
'end': {
|
'end': {
|
||||||
'dateTime': (middle_of_event + dt_util.dt.
|
'dateTime': (middle_of_event + dt_util.dt.
|
||||||
timedelta(minutes=60)).isoformat()
|
timedelta(minutes=60)).isoformat()
|
||||||
},
|
},
|
||||||
'summary': 'Current Event',
|
'summary': 'Current Event',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DemoGoogleCalendar(CalendarEventDevice):
|
class DemoGoogleCalendar(CalendarEventDevice):
|
||||||
"""Representation of a Demo Calendar element."""
|
"""Representation of a Demo Calendar element."""
|
||||||
|
|
||||||
def __init__(self, hass, calendar_data, data):
|
def __init__(self, hass, calendar_data, data):
|
||||||
"""Initialize Google Calendar but without the API calls."""
|
"""Initialize Google Calendar but without the API calls."""
|
||||||
self.data = calendar_data
|
self.data = calendar_data
|
||||||
super().__init__(hass, data)
|
super().__init__(hass, data)
|
||||||
|
|
|
@ -1,78 +1,78 @@
|
||||||
"""
|
"""
|
||||||
Support for Google Calendar Search binary sensors.
|
Support for Google Calendar Search binary sensors.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/binary_sensor.google_calendar/
|
https://home-assistant.io/components/binary_sensor.google_calendar/
|
||||||
"""
|
"""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from homeassistant.components.calendar import CalendarEventDevice
|
from homeassistant.components.calendar import CalendarEventDevice
|
||||||
from homeassistant.components.google import (
|
from homeassistant.components.google import (
|
||||||
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
|
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
|
||||||
GoogleCalendarService)
|
GoogleCalendarService)
|
||||||
from homeassistant.util import Throttle, dt
|
from homeassistant.util import Throttle, dt
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_GOOGLE_SEARCH_PARAMS = {
|
DEFAULT_GOOGLE_SEARCH_PARAMS = {
|
||||||
'orderBy': 'startTime',
|
'orderBy': 'startTime',
|
||||||
'maxResults': 1,
|
'maxResults': 1,
|
||||||
'singleEvents': True,
|
'singleEvents': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, disc_info=None):
|
def setup_platform(hass, config, add_devices, disc_info=None):
|
||||||
"""Set up the calendar platform for event devices."""
|
"""Set up the calendar platform for event devices."""
|
||||||
if disc_info is None:
|
if disc_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
|
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
|
||||||
return
|
return
|
||||||
|
|
||||||
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
||||||
add_devices([GoogleCalendarEventDevice(hass, calendar_service,
|
add_devices([GoogleCalendarEventDevice(hass, calendar_service,
|
||||||
disc_info[CONF_CAL_ID], data)
|
disc_info[CONF_CAL_ID], data)
|
||||||
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
|
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
# pylint: disable=too-many-instance-attributes
|
||||||
class GoogleCalendarEventDevice(CalendarEventDevice):
|
class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||||
"""A calendar event device."""
|
"""A calendar event device."""
|
||||||
|
|
||||||
def __init__(self, hass, calendar_service, calendar, data):
|
def __init__(self, hass, calendar_service, calendar, data):
|
||||||
"""Create the Calendar event device."""
|
"""Create the Calendar event device."""
|
||||||
self.data = GoogleCalendarData(calendar_service, calendar,
|
self.data = GoogleCalendarData(calendar_service, calendar,
|
||||||
data.get('search', None))
|
data.get('search', None))
|
||||||
super().__init__(hass, data)
|
super().__init__(hass, data)
|
||||||
|
|
||||||
|
|
||||||
class GoogleCalendarData(object):
|
class GoogleCalendarData(object):
|
||||||
"""Class to utilize calendar service object to get next event."""
|
"""Class to utilize calendar service object to get next event."""
|
||||||
|
|
||||||
def __init__(self, calendar_service, calendar_id, search=None):
|
def __init__(self, calendar_service, calendar_id, search=None):
|
||||||
"""Set up how we are going to search the google calendar."""
|
"""Set up how we are going to search the google calendar."""
|
||||||
self.calendar_service = calendar_service
|
self.calendar_service = calendar_service
|
||||||
self.calendar_id = calendar_id
|
self.calendar_id = calendar_id
|
||||||
self.search = search
|
self.search = search
|
||||||
self.event = None
|
self.event = None
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest data."""
|
"""Get the latest data."""
|
||||||
service = self.calendar_service.get()
|
service = self.calendar_service.get()
|
||||||
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
||||||
params['timeMin'] = dt.now().isoformat('T')
|
params['timeMin'] = dt.now().isoformat('T')
|
||||||
params['calendarId'] = self.calendar_id
|
params['calendarId'] = self.calendar_id
|
||||||
if self.search:
|
if self.search:
|
||||||
params['q'] = self.search
|
params['q'] = self.search
|
||||||
|
|
||||||
events = service.events() # pylint: disable=no-member
|
events = service.events() # pylint: disable=no-member
|
||||||
result = events.list(**params).execute()
|
result = events.list(**params).execute()
|
||||||
|
|
||||||
items = result.get('items', [])
|
items = result.get('items', [])
|
||||||
self.event = items[0] if len(items) == 1 else None
|
self.event = items[0] if len(items) == 1 else None
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -138,7 +138,7 @@ class Camera(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(None, self.camera_image)
|
return self.hass.async_add_job(self.camera_image)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def handle_async_mjpeg_stream(self, request):
|
def handle_async_mjpeg_stream(self, request):
|
||||||
|
@ -241,7 +241,7 @@ class CameraView(HomeAssistantView):
|
||||||
return web.Response(status=status)
|
return web.Response(status=status)
|
||||||
|
|
||||||
authenticated = (request[KEY_AUTHENTICATED] or
|
authenticated = (request[KEY_AUTHENTICATED] or
|
||||||
request.GET.get('token') in camera.access_tokens)
|
request.query.get('token') in camera.access_tokens)
|
||||||
|
|
||||||
if not authenticated:
|
if not authenticated:
|
||||||
return web.Response(status=401)
|
return web.Response(status=401)
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
"""
|
||||||
|
This component provides basic support for Netgear Arlo IP cameras.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/camera.arlo/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.components.arlo import DEFAULT_BRAND
|
||||||
|
|
||||||
|
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||||
|
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||||
|
from homeassistant.helpers.aiohttp_client import (
|
||||||
|
async_aiohttp_proxy_stream)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['arlo', 'ffmpeg']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_FFMPEG_ARGUMENTS):
|
||||||
|
cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Set up an Arlo IP Camera."""
|
||||||
|
arlo = hass.data.get('arlo')
|
||||||
|
if not arlo:
|
||||||
|
return False
|
||||||
|
|
||||||
|
cameras = []
|
||||||
|
for camera in arlo.cameras:
|
||||||
|
cameras.append(ArloCam(hass, camera, config))
|
||||||
|
|
||||||
|
async_add_devices(cameras, True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ArloCam(Camera):
|
||||||
|
"""An implementation of a Netgear Arlo IP camera."""
|
||||||
|
|
||||||
|
def __init__(self, hass, camera, device_info):
|
||||||
|
"""Initialize an Arlo camera."""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._camera = camera
|
||||||
|
self._name = self._camera.name
|
||||||
|
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||||
|
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||||
|
|
||||||
|
def camera_image(self):
|
||||||
|
"""Return a still image reponse from the camera."""
|
||||||
|
return self._camera.last_image
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def handle_async_mjpeg_stream(self, request):
|
||||||
|
"""Generate an HTTP MJPEG stream from the camera."""
|
||||||
|
from haffmpeg import CameraMjpeg
|
||||||
|
video = self._camera.last_video
|
||||||
|
if not video:
|
||||||
|
return
|
||||||
|
|
||||||
|
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
||||||
|
yield from stream.open_camera(
|
||||||
|
video.video_url, extra_cmd=self._ffmpeg_arguments)
|
||||||
|
|
||||||
|
yield from async_aiohttp_proxy_stream(
|
||||||
|
self.hass, request, stream,
|
||||||
|
'multipart/x-mixed-replace;boundary=ffserver')
|
||||||
|
yield from stream.close()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of this camera."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self):
|
||||||
|
"""Camera model."""
|
||||||
|
return self._camera.model_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brand(self):
|
||||||
|
"""Camera brand."""
|
||||||
|
return DEFAULT_BRAND
|
|
@ -103,8 +103,8 @@ class GenericCamera(Camera):
|
||||||
_LOGGER.error("Error getting camera image: %s", error)
|
_LOGGER.error("Error getting camera image: %s", error)
|
||||||
return self._last_image
|
return self._last_image
|
||||||
|
|
||||||
self._last_image = yield from self.hass.loop.run_in_executor(
|
self._last_image = yield from self.hass.async_add_job(
|
||||||
None, fetch)
|
fetch)
|
||||||
# async
|
# async
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -88,8 +88,8 @@ class MjpegCamera(Camera):
|
||||||
# DigestAuth is not supported
|
# DigestAuth is not supported
|
||||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION or \
|
if self._authentication == HTTP_DIGEST_AUTHENTICATION or \
|
||||||
self._still_image_url is None:
|
self._still_image_url is None:
|
||||||
image = yield from self.hass.loop.run_in_executor(
|
image = yield from self.hass.async_add_job(
|
||||||
None, self.camera_image)
|
self.camera_image)
|
||||||
return image
|
return image
|
||||||
|
|
||||||
websession = async_get_clientsession(self.hass)
|
websession = async_get_clientsession(self.hass)
|
||||||
|
|
|
@ -1,250 +1,250 @@
|
||||||
"""
|
"""
|
||||||
Support for Synology Surveillance Station Cameras.
|
Support for Synology Surveillance Station Cameras.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/camera.synology/
|
https://home-assistant.io/components/camera.synology/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
|
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
|
||||||
from homeassistant.components.camera import (
|
from homeassistant.components.camera import (
|
||||||
Camera, PLATFORM_SCHEMA)
|
Camera, PLATFORM_SCHEMA)
|
||||||
from homeassistant.helpers.aiohttp_client import (
|
from homeassistant.helpers.aiohttp_client import (
|
||||||
async_get_clientsession, async_create_clientsession,
|
async_get_clientsession, async_create_clientsession,
|
||||||
async_aiohttp_proxy_web)
|
async_aiohttp_proxy_web)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util.async import run_coroutine_threadsafe
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = 'Synology Camera'
|
DEFAULT_NAME = 'Synology Camera'
|
||||||
DEFAULT_STREAM_ID = '0'
|
DEFAULT_STREAM_ID = '0'
|
||||||
DEFAULT_TIMEOUT = 5
|
DEFAULT_TIMEOUT = 5
|
||||||
CONF_CAMERA_NAME = 'camera_name'
|
CONF_CAMERA_NAME = 'camera_name'
|
||||||
CONF_STREAM_ID = 'stream_id'
|
CONF_STREAM_ID = 'stream_id'
|
||||||
|
|
||||||
QUERY_CGI = 'query.cgi'
|
QUERY_CGI = 'query.cgi'
|
||||||
QUERY_API = 'SYNO.API.Info'
|
QUERY_API = 'SYNO.API.Info'
|
||||||
AUTH_API = 'SYNO.API.Auth'
|
AUTH_API = 'SYNO.API.Auth'
|
||||||
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
|
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
|
||||||
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
|
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
|
||||||
SESSION_ID = '0'
|
SESSION_ID = '0'
|
||||||
|
|
||||||
WEBAPI_PATH = '/webapi/'
|
WEBAPI_PATH = '/webapi/'
|
||||||
AUTH_PATH = 'auth.cgi'
|
AUTH_PATH = 'auth.cgi'
|
||||||
CAMERA_PATH = 'camera.cgi'
|
CAMERA_PATH = 'camera.cgi'
|
||||||
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
|
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
|
||||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||||
|
|
||||||
SYNO_API_URL = '{0}{1}{2}'
|
SYNO_API_URL = '{0}{1}{2}'
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Required(CONF_URL): cv.string,
|
vol.Required(CONF_URL): cv.string,
|
||||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||||
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
|
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
|
||||||
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Set up a Synology IP Camera."""
|
"""Set up a Synology IP Camera."""
|
||||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||||
timeout = config.get(CONF_TIMEOUT)
|
timeout = config.get(CONF_TIMEOUT)
|
||||||
websession_init = async_get_clientsession(hass, verify_ssl)
|
websession_init = async_get_clientsession(hass, verify_ssl)
|
||||||
|
|
||||||
# Determine API to use for authentication
|
# Determine API to use for authentication
|
||||||
syno_api_url = SYNO_API_URL.format(
|
syno_api_url = SYNO_API_URL.format(
|
||||||
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
|
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
|
||||||
|
|
||||||
query_payload = {
|
query_payload = {
|
||||||
'api': QUERY_API,
|
'api': QUERY_API,
|
||||||
'method': 'Query',
|
'method': 'Query',
|
||||||
'version': '1',
|
'version': '1',
|
||||||
'query': 'SYNO.'
|
'query': 'SYNO.'
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
query_req = yield from websession_init.get(
|
query_req = yield from websession_init.get(
|
||||||
syno_api_url,
|
syno_api_url,
|
||||||
params=query_payload
|
params=query_payload
|
||||||
)
|
)
|
||||||
|
|
||||||
# Skip content type check because Synology doesn't return JSON with
|
# Skip content type check because Synology doesn't return JSON with
|
||||||
# right content type
|
# right content type
|
||||||
query_resp = yield from query_req.json(content_type=None)
|
query_resp = yield from query_req.json(content_type=None)
|
||||||
auth_path = query_resp['data'][AUTH_API]['path']
|
auth_path = query_resp['data'][AUTH_API]['path']
|
||||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.exception("Error on %s", syno_api_url)
|
_LOGGER.exception("Error on %s", syno_api_url)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Authticate to NAS to get a session id
|
# Authticate to NAS to get a session id
|
||||||
syno_auth_url = SYNO_API_URL.format(
|
syno_auth_url = SYNO_API_URL.format(
|
||||||
config.get(CONF_URL), WEBAPI_PATH, auth_path)
|
config.get(CONF_URL), WEBAPI_PATH, auth_path)
|
||||||
|
|
||||||
session_id = yield from get_session_id(
|
session_id = yield from get_session_id(
|
||||||
hass,
|
hass,
|
||||||
websession_init,
|
websession_init,
|
||||||
config.get(CONF_USERNAME),
|
config.get(CONF_USERNAME),
|
||||||
config.get(CONF_PASSWORD),
|
config.get(CONF_PASSWORD),
|
||||||
syno_auth_url,
|
syno_auth_url,
|
||||||
timeout
|
timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
# init websession
|
# init websession
|
||||||
websession = async_create_clientsession(
|
websession = async_create_clientsession(
|
||||||
hass, verify_ssl, cookies={'id': session_id})
|
hass, verify_ssl, cookies={'id': session_id})
|
||||||
|
|
||||||
# Use SessionID to get cameras in system
|
# Use SessionID to get cameras in system
|
||||||
syno_camera_url = SYNO_API_URL.format(
|
syno_camera_url = SYNO_API_URL.format(
|
||||||
config.get(CONF_URL), WEBAPI_PATH, camera_api)
|
config.get(CONF_URL), WEBAPI_PATH, camera_api)
|
||||||
|
|
||||||
camera_payload = {
|
camera_payload = {
|
||||||
'api': CAMERA_API,
|
'api': CAMERA_API,
|
||||||
'method': 'List',
|
'method': 'List',
|
||||||
'version': '1'
|
'version': '1'
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
camera_req = yield from websession.get(
|
camera_req = yield from websession.get(
|
||||||
syno_camera_url,
|
syno_camera_url,
|
||||||
params=camera_payload
|
params=camera_payload
|
||||||
)
|
)
|
||||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.exception("Error on %s", syno_camera_url)
|
_LOGGER.exception("Error on %s", syno_camera_url)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
camera_resp = yield from camera_req.json(content_type=None)
|
camera_resp = yield from camera_req.json(content_type=None)
|
||||||
cameras = camera_resp['data']['cameras']
|
cameras = camera_resp['data']['cameras']
|
||||||
|
|
||||||
# add cameras
|
# add cameras
|
||||||
devices = []
|
devices = []
|
||||||
for camera in cameras:
|
for camera in cameras:
|
||||||
if not config.get(CONF_WHITELIST):
|
if not config.get(CONF_WHITELIST):
|
||||||
camera_id = camera['id']
|
camera_id = camera['id']
|
||||||
snapshot_path = camera['snapshot_path']
|
snapshot_path = camera['snapshot_path']
|
||||||
|
|
||||||
device = SynologyCamera(
|
device = SynologyCamera(
|
||||||
hass, websession, config, camera_id, camera['name'],
|
hass, websession, config, camera_id, camera['name'],
|
||||||
snapshot_path, streaming_path, camera_path, auth_path, timeout
|
snapshot_path, streaming_path, camera_path, auth_path, timeout
|
||||||
)
|
)
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
|
|
||||||
async_add_devices(devices)
|
async_add_devices(devices)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def get_session_id(hass, websession, username, password, login_url, timeout):
|
def get_session_id(hass, websession, username, password, login_url, timeout):
|
||||||
"""Get a session id."""
|
"""Get a session id."""
|
||||||
auth_payload = {
|
auth_payload = {
|
||||||
'api': AUTH_API,
|
'api': AUTH_API,
|
||||||
'method': 'Login',
|
'method': 'Login',
|
||||||
'version': '2',
|
'version': '2',
|
||||||
'account': username,
|
'account': username,
|
||||||
'passwd': password,
|
'passwd': password,
|
||||||
'session': 'SurveillanceStation',
|
'session': 'SurveillanceStation',
|
||||||
'format': 'sid'
|
'format': 'sid'
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
auth_req = yield from websession.get(
|
auth_req = yield from websession.get(
|
||||||
login_url,
|
login_url,
|
||||||
params=auth_payload
|
params=auth_payload
|
||||||
)
|
)
|
||||||
auth_resp = yield from auth_req.json(content_type=None)
|
auth_resp = yield from auth_req.json(content_type=None)
|
||||||
return auth_resp['data']['sid']
|
return auth_resp['data']['sid']
|
||||||
|
|
||||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.exception("Error on %s", login_url)
|
_LOGGER.exception("Error on %s", login_url)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class SynologyCamera(Camera):
|
class SynologyCamera(Camera):
|
||||||
"""An implementation of a Synology NAS based IP camera."""
|
"""An implementation of a Synology NAS based IP camera."""
|
||||||
|
|
||||||
def __init__(self, hass, websession, config, camera_id,
|
def __init__(self, hass, websession, config, camera_id,
|
||||||
camera_name, snapshot_path, streaming_path, camera_path,
|
camera_name, snapshot_path, streaming_path, camera_path,
|
||||||
auth_path, timeout):
|
auth_path, timeout):
|
||||||
"""Initialize a Synology Surveillance Station camera."""
|
"""Initialize a Synology Surveillance Station camera."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._websession = websession
|
self._websession = websession
|
||||||
self._name = camera_name
|
self._name = camera_name
|
||||||
self._synology_url = config.get(CONF_URL)
|
self._synology_url = config.get(CONF_URL)
|
||||||
self._camera_name = config.get(CONF_CAMERA_NAME)
|
self._camera_name = config.get(CONF_CAMERA_NAME)
|
||||||
self._stream_id = config.get(CONF_STREAM_ID)
|
self._stream_id = config.get(CONF_STREAM_ID)
|
||||||
self._camera_id = camera_id
|
self._camera_id = camera_id
|
||||||
self._snapshot_path = snapshot_path
|
self._snapshot_path = snapshot_path
|
||||||
self._streaming_path = streaming_path
|
self._streaming_path = streaming_path
|
||||||
self._camera_path = camera_path
|
self._camera_path = camera_path
|
||||||
self._auth_path = auth_path
|
self._auth_path = auth_path
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
|
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
"""Return bytes of camera image."""
|
"""Return bytes of camera image."""
|
||||||
return run_coroutine_threadsafe(
|
return run_coroutine_threadsafe(
|
||||||
self.async_camera_image(), self.hass.loop).result()
|
self.async_camera_image(), self.hass.loop).result()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_camera_image(self):
|
def async_camera_image(self):
|
||||||
"""Return a still image response from the camera."""
|
"""Return a still image response from the camera."""
|
||||||
image_url = SYNO_API_URL.format(
|
image_url = SYNO_API_URL.format(
|
||||||
self._synology_url, WEBAPI_PATH, self._camera_path)
|
self._synology_url, WEBAPI_PATH, self._camera_path)
|
||||||
|
|
||||||
image_payload = {
|
image_payload = {
|
||||||
'api': CAMERA_API,
|
'api': CAMERA_API,
|
||||||
'method': 'GetSnapshot',
|
'method': 'GetSnapshot',
|
||||||
'version': '1',
|
'version': '1',
|
||||||
'cameraId': self._camera_id
|
'cameraId': self._camera_id
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
|
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
|
||||||
response = yield from self._websession.get(
|
response = yield from self._websession.get(
|
||||||
image_url,
|
image_url,
|
||||||
params=image_payload
|
params=image_payload
|
||||||
)
|
)
|
||||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
_LOGGER.error("Error fetching %s", image_url)
|
_LOGGER.error("Error fetching %s", image_url)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
image = yield from response.read()
|
image = yield from response.read()
|
||||||
|
|
||||||
return image
|
return image
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def handle_async_mjpeg_stream(self, request):
|
def handle_async_mjpeg_stream(self, request):
|
||||||
"""Return a MJPEG stream image response directly from the camera."""
|
"""Return a MJPEG stream image response directly from the camera."""
|
||||||
streaming_url = SYNO_API_URL.format(
|
streaming_url = SYNO_API_URL.format(
|
||||||
self._synology_url, WEBAPI_PATH, self._streaming_path)
|
self._synology_url, WEBAPI_PATH, self._streaming_path)
|
||||||
|
|
||||||
streaming_payload = {
|
streaming_payload = {
|
||||||
'api': STREAMING_API,
|
'api': STREAMING_API,
|
||||||
'method': 'Stream',
|
'method': 'Stream',
|
||||||
'version': '1',
|
'version': '1',
|
||||||
'cameraId': self._camera_id,
|
'cameraId': self._camera_id,
|
||||||
'format': 'mjpeg'
|
'format': 'mjpeg'
|
||||||
}
|
}
|
||||||
stream_coro = self._websession.get(
|
stream_coro = self._websession.get(
|
||||||
streaming_url, params=streaming_payload)
|
streaming_url, params=streaming_payload)
|
||||||
|
|
||||||
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of this device."""
|
"""Return the name of this device."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
|
@ -213,8 +213,8 @@ def async_setup(hass, config):
|
||||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||||
yield from component.async_setup(config)
|
yield from component.async_setup(config)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file,
|
load_yaml_config_file,
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -569,8 +569,8 @@ class ClimateDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(
|
||||||
None, ft.partial(self.set_temperature, **kwargs))
|
ft.partial(self.set_temperature, **kwargs))
|
||||||
|
|
||||||
def set_humidity(self, humidity):
|
def set_humidity(self, humidity):
|
||||||
"""Set new target humidity."""
|
"""Set new target humidity."""
|
||||||
|
@ -581,8 +581,7 @@ class ClimateDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.set_humidity, humidity)
|
||||||
None, self.set_humidity, humidity)
|
|
||||||
|
|
||||||
def set_fan_mode(self, fan):
|
def set_fan_mode(self, fan):
|
||||||
"""Set new target fan mode."""
|
"""Set new target fan mode."""
|
||||||
|
@ -593,8 +592,7 @@ class ClimateDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.set_fan_mode, fan)
|
||||||
None, self.set_fan_mode, fan)
|
|
||||||
|
|
||||||
def set_operation_mode(self, operation_mode):
|
def set_operation_mode(self, operation_mode):
|
||||||
"""Set new target operation mode."""
|
"""Set new target operation mode."""
|
||||||
|
@ -605,8 +603,7 @@ class ClimateDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.set_operation_mode, operation_mode)
|
||||||
None, self.set_operation_mode, operation_mode)
|
|
||||||
|
|
||||||
def set_swing_mode(self, swing_mode):
|
def set_swing_mode(self, swing_mode):
|
||||||
"""Set new target swing operation."""
|
"""Set new target swing operation."""
|
||||||
|
@ -617,8 +614,7 @@ class ClimateDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.set_swing_mode, swing_mode)
|
||||||
None, self.set_swing_mode, swing_mode)
|
|
||||||
|
|
||||||
def turn_away_mode_on(self):
|
def turn_away_mode_on(self):
|
||||||
"""Turn away mode on."""
|
"""Turn away mode on."""
|
||||||
|
@ -629,8 +625,7 @@ class ClimateDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.turn_away_mode_on)
|
||||||
None, self.turn_away_mode_on)
|
|
||||||
|
|
||||||
def turn_away_mode_off(self):
|
def turn_away_mode_off(self):
|
||||||
"""Turn away mode off."""
|
"""Turn away mode off."""
|
||||||
|
@ -641,8 +636,7 @@ class ClimateDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.turn_away_mode_off)
|
||||||
None, self.turn_away_mode_off)
|
|
||||||
|
|
||||||
def set_hold_mode(self, hold_mode):
|
def set_hold_mode(self, hold_mode):
|
||||||
"""Set new target hold mode."""
|
"""Set new target hold mode."""
|
||||||
|
@ -653,8 +647,7 @@ class ClimateDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.set_hold_mode, hold_mode)
|
||||||
None, self.set_hold_mode, hold_mode)
|
|
||||||
|
|
||||||
def turn_aux_heat_on(self):
|
def turn_aux_heat_on(self):
|
||||||
"""Turn auxillary heater on."""
|
"""Turn auxillary heater on."""
|
||||||
|
@ -665,8 +658,7 @@ class ClimateDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.turn_aux_heat_on)
|
||||||
None, self.turn_aux_heat_on)
|
|
||||||
|
|
||||||
def turn_aux_heat_off(self):
|
def turn_aux_heat_off(self):
|
||||||
"""Turn auxillary heater off."""
|
"""Turn auxillary heater off."""
|
||||||
|
@ -677,8 +669,7 @@ class ClimateDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.turn_aux_heat_off)
|
||||||
None, self.turn_aux_heat_off)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def min_temp(self):
|
def min_temp(self):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""
|
"""
|
||||||
Tado component to create a climate device for each zone.
|
Tado component to create a climate device for each zone.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
|
|
|
@ -175,8 +175,8 @@ def async_setup(hass, config):
|
||||||
if update_tasks:
|
if update_tasks:
|
||||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, os.path.join(
|
load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml'))
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
for service_name in SERVICE_TO_METHOD:
|
for service_name in SERVICE_TO_METHOD:
|
||||||
|
@ -263,8 +263,7 @@ class CoverDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(ft.partial(self.open_cover, **kwargs))
|
||||||
None, ft.partial(self.open_cover, **kwargs))
|
|
||||||
|
|
||||||
def close_cover(self, **kwargs):
|
def close_cover(self, **kwargs):
|
||||||
"""Close cover."""
|
"""Close cover."""
|
||||||
|
@ -275,8 +274,7 @@ class CoverDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(ft.partial(self.close_cover, **kwargs))
|
||||||
None, ft.partial(self.close_cover, **kwargs))
|
|
||||||
|
|
||||||
def set_cover_position(self, **kwargs):
|
def set_cover_position(self, **kwargs):
|
||||||
"""Move the cover to a specific position."""
|
"""Move the cover to a specific position."""
|
||||||
|
@ -287,8 +285,8 @@ class CoverDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(
|
||||||
None, ft.partial(self.set_cover_position, **kwargs))
|
ft.partial(self.set_cover_position, **kwargs))
|
||||||
|
|
||||||
def stop_cover(self, **kwargs):
|
def stop_cover(self, **kwargs):
|
||||||
"""Stop the cover."""
|
"""Stop the cover."""
|
||||||
|
@ -299,8 +297,7 @@ class CoverDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(ft.partial(self.stop_cover, **kwargs))
|
||||||
None, ft.partial(self.stop_cover, **kwargs))
|
|
||||||
|
|
||||||
def open_cover_tilt(self, **kwargs):
|
def open_cover_tilt(self, **kwargs):
|
||||||
"""Open the cover tilt."""
|
"""Open the cover tilt."""
|
||||||
|
@ -311,8 +308,8 @@ class CoverDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(
|
||||||
None, ft.partial(self.open_cover_tilt, **kwargs))
|
ft.partial(self.open_cover_tilt, **kwargs))
|
||||||
|
|
||||||
def close_cover_tilt(self, **kwargs):
|
def close_cover_tilt(self, **kwargs):
|
||||||
"""Close the cover tilt."""
|
"""Close the cover tilt."""
|
||||||
|
@ -323,8 +320,8 @@ class CoverDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(
|
||||||
None, ft.partial(self.close_cover_tilt, **kwargs))
|
ft.partial(self.close_cover_tilt, **kwargs))
|
||||||
|
|
||||||
def set_cover_tilt_position(self, **kwargs):
|
def set_cover_tilt_position(self, **kwargs):
|
||||||
"""Move the cover tilt to a specific position."""
|
"""Move the cover tilt to a specific position."""
|
||||||
|
@ -335,8 +332,8 @@ class CoverDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(
|
||||||
None, ft.partial(self.set_cover_tilt_position, **kwargs))
|
ft.partial(self.set_cover_tilt_position, **kwargs))
|
||||||
|
|
||||||
def stop_cover_tilt(self, **kwargs):
|
def stop_cover_tilt(self, **kwargs):
|
||||||
"""Stop the cover."""
|
"""Stop the cover."""
|
||||||
|
@ -347,5 +344,5 @@ class CoverDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(
|
||||||
None, ft.partial(self.stop_cover_tilt, **kwargs))
|
ft.partial(self.stop_cover_tilt, **kwargs))
|
||||||
|
|
|
@ -12,19 +12,25 @@ from homeassistant.components.cover import CoverDevice
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED)
|
CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import homeassistant.loader as loader
|
||||||
|
|
||||||
REQUIREMENTS = [
|
REQUIREMENTS = [
|
||||||
'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip'
|
'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip'
|
||||||
'#pymyq==0.0.8']
|
'#pymyq==0.0.8']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'myq'
|
||||||
|
|
||||||
|
NOTIFICATION_ID = 'myq_notification'
|
||||||
|
NOTIFICATION_TITLE = 'MyQ Cover Setup'
|
||||||
|
|
||||||
COVER_SCHEMA = vol.Schema({
|
COVER_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_TYPE): cv.string,
|
vol.Required(CONF_TYPE): cv.string,
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): cv.string
|
vol.Required(CONF_PASSWORD): cv.string
|
||||||
})
|
})
|
||||||
|
|
||||||
DEFAULT_NAME = 'myq'
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the MyQ component."""
|
"""Set up the MyQ component."""
|
||||||
|
@ -33,23 +39,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
username = config.get(CONF_USERNAME)
|
username = config.get(CONF_USERNAME)
|
||||||
password = config.get(CONF_PASSWORD)
|
password = config.get(CONF_PASSWORD)
|
||||||
brand = config.get(CONF_TYPE)
|
brand = config.get(CONF_TYPE)
|
||||||
|
persistent_notification = loader.get_component('persistent_notification')
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
myq = pymyq(username, password, brand)
|
myq = pymyq(username, password, brand)
|
||||||
|
|
||||||
if not myq.is_supported_brand():
|
|
||||||
logger.error("Unsupported type. See documentation")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not myq.is_login_valid():
|
|
||||||
logger.error("Username or Password is incorrect")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not myq.is_supported_brand():
|
||||||
|
raise ValueError("Unsupported type. See documentation")
|
||||||
|
|
||||||
|
if not myq.is_login_valid():
|
||||||
|
raise ValueError("Username or Password is incorrect")
|
||||||
|
|
||||||
add_devices(MyQDevice(myq, door) for door in myq.get_garage_doors())
|
add_devices(MyQDevice(myq, door) for door in myq.get_garage_doors())
|
||||||
except (TypeError, KeyError, NameError) as ex:
|
return True
|
||||||
logger.error("%s", ex)
|
|
||||||
|
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||||
|
_LOGGER.error("%s", ex)
|
||||||
|
persistent_notification.create(
|
||||||
|
hass, 'Error: {}<br />'
|
||||||
|
'You will need to restart hass after fixing.'
|
||||||
|
''.format(ex),
|
||||||
|
title=NOTIFICATION_TITLE,
|
||||||
|
notification_id=NOTIFICATION_ID)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class MyQDevice(CoverDevice):
|
class MyQDevice(CoverDevice):
|
||||||
|
|
|
@ -41,7 +41,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||||
"""Initialize the Z-Wave rollershutter."""
|
"""Initialize the Z-Wave rollershutter."""
|
||||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
self._network = hass.data[zwave.ZWAVE_NETWORK]
|
self._network = hass.data[zwave.const.DATA_NETWORK]
|
||||||
self._open_id = None
|
self._open_id = None
|
||||||
self._close_id = None
|
self._close_id = None
|
||||||
self._current_position = None
|
self._current_position = None
|
||||||
|
|
|
@ -35,7 +35,8 @@ from homeassistant.util.yaml import dump
|
||||||
from homeassistant.helpers.event import async_track_utc_time_change
|
from homeassistant.helpers.event import async_track_utc_time_change
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC,
|
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC,
|
||||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID)
|
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID,
|
||||||
|
CONF_ICON, ATTR_ICON)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -150,14 +151,14 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||||
scanner = yield from platform.async_get_scanner(
|
scanner = yield from platform.async_get_scanner(
|
||||||
hass, {DOMAIN: p_config})
|
hass, {DOMAIN: p_config})
|
||||||
elif hasattr(platform, 'get_scanner'):
|
elif hasattr(platform, 'get_scanner'):
|
||||||
scanner = yield from hass.loop.run_in_executor(
|
scanner = yield from hass.async_add_job(
|
||||||
None, platform.get_scanner, hass, {DOMAIN: p_config})
|
platform.get_scanner, hass, {DOMAIN: p_config})
|
||||||
elif hasattr(platform, 'async_setup_scanner'):
|
elif hasattr(platform, 'async_setup_scanner'):
|
||||||
setup = yield from platform.async_setup_scanner(
|
setup = yield from platform.async_setup_scanner(
|
||||||
hass, p_config, tracker.async_see, disc_info)
|
hass, p_config, tracker.async_see, disc_info)
|
||||||
elif hasattr(platform, 'setup_scanner'):
|
elif hasattr(platform, 'setup_scanner'):
|
||||||
setup = yield from hass.loop.run_in_executor(
|
setup = yield from hass.async_add_job(
|
||||||
None, platform.setup_scanner, hass, p_config, tracker.see,
|
platform.setup_scanner, hass, p_config, tracker.see,
|
||||||
disc_info)
|
disc_info)
|
||||||
else:
|
else:
|
||||||
raise HomeAssistantError("Invalid device_tracker platform.")
|
raise HomeAssistantError("Invalid device_tracker platform.")
|
||||||
|
@ -209,8 +210,8 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||||
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
|
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
|
||||||
yield from tracker.async_see(**args)
|
yield from tracker.async_see(**args)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file,
|
load_yaml_config_file,
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml')
|
os.path.join(os.path.dirname(__file__), 'services.yaml')
|
||||||
)
|
)
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
|
@ -322,8 +323,8 @@ class DeviceTracker(object):
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
with (yield from self._is_updating):
|
with (yield from self._is_updating):
|
||||||
yield from self.hass.loop.run_in_executor(
|
yield from self.hass.async_add_job(
|
||||||
None, update_config, self.hass.config.path(YAML_DEVICES),
|
update_config, self.hass.config.path(YAML_DEVICES),
|
||||||
dev_id, device)
|
dev_id, device)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -381,6 +382,7 @@ class Device(Entity):
|
||||||
battery = None # type: str
|
battery = None # type: str
|
||||||
attributes = None # type: dict
|
attributes = None # type: dict
|
||||||
vendor = None # type: str
|
vendor = None # type: str
|
||||||
|
icon = None # type: str
|
||||||
|
|
||||||
# Track if the last update of this device was HOME.
|
# Track if the last update of this device was HOME.
|
||||||
last_update_home = False
|
last_update_home = False
|
||||||
|
@ -388,7 +390,7 @@ class Device(Entity):
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||||
track: bool, dev_id: str, mac: str, name: str=None,
|
track: bool, dev_id: str, mac: str, name: str=None,
|
||||||
picture: str=None, gravatar: str=None,
|
picture: str=None, gravatar: str=None, icon: str=None,
|
||||||
hide_if_away: bool=False, vendor: str=None) -> None:
|
hide_if_away: bool=False, vendor: str=None) -> None:
|
||||||
"""Initialize a device."""
|
"""Initialize a device."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
@ -414,6 +416,8 @@ class Device(Entity):
|
||||||
else:
|
else:
|
||||||
self.config_picture = picture
|
self.config_picture = picture
|
||||||
|
|
||||||
|
self.icon = icon
|
||||||
|
|
||||||
self.away_hide = hide_if_away
|
self.away_hide = hide_if_away
|
||||||
self.vendor = vendor
|
self.vendor = vendor
|
||||||
|
|
||||||
|
@ -608,7 +612,7 @@ class DeviceScanner(object):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(None, self.scan_devices)
|
return self.hass.async_add_job(self.scan_devices)
|
||||||
|
|
||||||
def get_device_name(self, mac: str) -> str:
|
def get_device_name(self, mac: str) -> str:
|
||||||
"""Get device name from mac."""
|
"""Get device name from mac."""
|
||||||
|
@ -619,7 +623,7 @@ class DeviceScanner(object):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(None, self.get_device_name, mac)
|
return self.hass.async_add_job(self.get_device_name, mac)
|
||||||
|
|
||||||
|
|
||||||
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||||
|
@ -637,6 +641,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
|
||||||
"""
|
"""
|
||||||
dev_schema = vol.Schema({
|
dev_schema = vol.Schema({
|
||||||
vol.Required(CONF_NAME): cv.string,
|
vol.Required(CONF_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_ICON, default=False):
|
||||||
|
vol.Any(None, cv.icon),
|
||||||
vol.Optional('track', default=False): cv.boolean,
|
vol.Optional('track', default=False): cv.boolean,
|
||||||
vol.Optional(CONF_MAC, default=None):
|
vol.Optional(CONF_MAC, default=None):
|
||||||
vol.Any(None, vol.All(cv.string, vol.Upper)),
|
vol.Any(None, vol.All(cv.string, vol.Upper)),
|
||||||
|
@ -650,8 +656,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
|
||||||
try:
|
try:
|
||||||
result = []
|
result = []
|
||||||
try:
|
try:
|
||||||
devices = yield from hass.loop.run_in_executor(
|
devices = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, path)
|
load_yaml_config_file, path)
|
||||||
except HomeAssistantError as err:
|
except HomeAssistantError as err:
|
||||||
_LOGGER.error("Unable to load %s: %s", path, str(err))
|
_LOGGER.error("Unable to load %s: %s", path, str(err))
|
||||||
return []
|
return []
|
||||||
|
@ -728,6 +734,7 @@ def update_config(path: str, dev_id: str, device: Device):
|
||||||
device = {device.dev_id: {
|
device = {device.dev_id: {
|
||||||
ATTR_NAME: device.name,
|
ATTR_NAME: device.name,
|
||||||
ATTR_MAC: device.mac,
|
ATTR_MAC: device.mac,
|
||||||
|
ATTR_ICON: device.icon,
|
||||||
'picture': device.config_picture,
|
'picture': device.config_picture,
|
||||||
'track': device.track,
|
'track': device.track,
|
||||||
CONF_AWAY_HIDE: device.away_hide,
|
CONF_AWAY_HIDE: device.away_hide,
|
||||||
|
|
|
@ -118,25 +118,29 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||||
self.protocol = config[CONF_PROTOCOL]
|
self.protocol = config[CONF_PROTOCOL]
|
||||||
self.mode = config[CONF_MODE]
|
self.mode = config[CONF_MODE]
|
||||||
self.port = config[CONF_PORT]
|
self.port = config[CONF_PORT]
|
||||||
self.ssh_args = {}
|
|
||||||
|
|
||||||
if self.protocol == 'ssh':
|
if self.protocol == 'ssh':
|
||||||
|
if not (self.ssh_key or self.password):
|
||||||
self.ssh_args['port'] = self.port
|
|
||||||
if self.ssh_key:
|
|
||||||
self.ssh_args['ssh_key'] = self.ssh_key
|
|
||||||
elif self.password:
|
|
||||||
self.ssh_args['password'] = self.password
|
|
||||||
else:
|
|
||||||
_LOGGER.error("No password or private key specified")
|
_LOGGER.error("No password or private key specified")
|
||||||
self.success_init = False
|
self.success_init = False
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.connection = SshConnection(self.host, self.port,
|
||||||
|
self.username,
|
||||||
|
self.password,
|
||||||
|
self.ssh_key,
|
||||||
|
self.mode == "ap")
|
||||||
else:
|
else:
|
||||||
if not self.password:
|
if not self.password:
|
||||||
_LOGGER.error("No password specified")
|
_LOGGER.error("No password specified")
|
||||||
self.success_init = False
|
self.success_init = False
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.connection = TelnetConnection(self.host, self.port,
|
||||||
|
self.username,
|
||||||
|
self.password,
|
||||||
|
self.mode == "ap")
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
|
@ -182,105 +186,9 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||||
self.last_results = active_clients
|
self.last_results = active_clients
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def ssh_connection(self):
|
|
||||||
"""Retrieve data from ASUSWRT via the ssh protocol."""
|
|
||||||
from pexpect import pxssh, exceptions
|
|
||||||
|
|
||||||
ssh = pxssh.pxssh()
|
|
||||||
try:
|
|
||||||
ssh.login(self.host, self.username, **self.ssh_args)
|
|
||||||
except exceptions.EOF as err:
|
|
||||||
_LOGGER.error("Connection refused. SSH enabled?")
|
|
||||||
return None
|
|
||||||
except pxssh.ExceptionPxssh as err:
|
|
||||||
_LOGGER.error("Unable to connect via SSH: %s", str(err))
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
ssh.sendline(_IP_NEIGH_CMD)
|
|
||||||
ssh.prompt()
|
|
||||||
neighbors = ssh.before.split(b'\n')[1:-1]
|
|
||||||
if self.mode == 'ap':
|
|
||||||
ssh.sendline(_ARP_CMD)
|
|
||||||
ssh.prompt()
|
|
||||||
arp_result = ssh.before.split(b'\n')[1:-1]
|
|
||||||
ssh.sendline(_WL_CMD)
|
|
||||||
ssh.prompt()
|
|
||||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
|
||||||
ssh.sendline(_NVRAM_CMD)
|
|
||||||
ssh.prompt()
|
|
||||||
nvram_result = ssh.before.split(b'\n')[1].split(b'<')[1:]
|
|
||||||
else:
|
|
||||||
arp_result = ['']
|
|
||||||
nvram_result = ['']
|
|
||||||
ssh.sendline(_LEASES_CMD)
|
|
||||||
ssh.prompt()
|
|
||||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
|
||||||
ssh.logout()
|
|
||||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
|
||||||
nvram_result)
|
|
||||||
except pxssh.ExceptionPxssh as exc:
|
|
||||||
_LOGGER.error("Unexpected response from router: %s", exc)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def telnet_connection(self):
|
|
||||||
"""Retrieve data from ASUSWRT via the telnet protocol."""
|
|
||||||
try:
|
|
||||||
telnet = telnetlib.Telnet(self.host)
|
|
||||||
telnet.read_until(b'login: ')
|
|
||||||
telnet.write((self.username + '\n').encode('ascii'))
|
|
||||||
telnet.read_until(b'Password: ')
|
|
||||||
telnet.write((self.password + '\n').encode('ascii'))
|
|
||||||
prompt_string = telnet.read_until(b'#').split(b'\n')[-1]
|
|
||||||
telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
|
|
||||||
neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1]
|
|
||||||
if self.mode == 'ap':
|
|
||||||
telnet.write('{}\n'.format(_ARP_CMD).encode('ascii'))
|
|
||||||
arp_result = (telnet.read_until(prompt_string).
|
|
||||||
split(b'\n')[1:-1])
|
|
||||||
telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
|
|
||||||
leases_result = (telnet.read_until(prompt_string).
|
|
||||||
split(b'\n')[1:-1])
|
|
||||||
telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii'))
|
|
||||||
nvram_result = (telnet.read_until(prompt_string).
|
|
||||||
split(b'\n')[1].split(b'<')[1:])
|
|
||||||
else:
|
|
||||||
arp_result = ['']
|
|
||||||
nvram_result = ['']
|
|
||||||
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
|
||||||
leases_result = (telnet.read_until(prompt_string).
|
|
||||||
split(b'\n')[1:-1])
|
|
||||||
telnet.write('exit\n'.encode('ascii'))
|
|
||||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
|
||||||
nvram_result)
|
|
||||||
except EOFError:
|
|
||||||
_LOGGER.error("Unexpected response from router")
|
|
||||||
return None
|
|
||||||
except ConnectionRefusedError:
|
|
||||||
_LOGGER.error("Connection refused by router. Telnet enabled?")
|
|
||||||
return None
|
|
||||||
except socket.gaierror as exc:
|
|
||||||
_LOGGER.error("Socket exception: %s", exc)
|
|
||||||
return None
|
|
||||||
except OSError as exc:
|
|
||||||
_LOGGER.error("OSError: %s", exc)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_asuswrt_data(self):
|
def get_asuswrt_data(self):
|
||||||
"""Retrieve data from ASUSWRT and return parsed result."""
|
"""Retrieve data from ASUSWRT and return parsed result."""
|
||||||
if self.protocol == 'ssh':
|
result = self.connection.get_result()
|
||||||
result = self.ssh_connection()
|
|
||||||
elif self.protocol == 'telnet':
|
|
||||||
result = self.telnet_connection()
|
|
||||||
else:
|
|
||||||
# autodetect protocol
|
|
||||||
result = self.ssh_connection()
|
|
||||||
if result:
|
|
||||||
self.protocol = 'ssh'
|
|
||||||
else:
|
|
||||||
result = self.telnet_connection()
|
|
||||||
if result:
|
|
||||||
self.protocol = 'telnet'
|
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
return {}
|
return {}
|
||||||
|
@ -363,3 +271,193 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||||
if match.group('ip') in devices:
|
if match.group('ip') in devices:
|
||||||
devices[match.group('ip')]['status'] = match.group('status')
|
devices[match.group('ip')]['status'] = match.group('status')
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
|
|
||||||
|
class _Connection:
|
||||||
|
def __init__(self):
|
||||||
|
self._connected = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connected(self):
|
||||||
|
"""Return connection state."""
|
||||||
|
return self._connected
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""Mark currenct connection state as connected."""
|
||||||
|
self._connected = True
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Mark current connection state as disconnected."""
|
||||||
|
self._connected = False
|
||||||
|
|
||||||
|
|
||||||
|
class SshConnection(_Connection):
|
||||||
|
"""Maintains an SSH connection to an ASUS-WRT router."""
|
||||||
|
|
||||||
|
def __init__(self, host, port, username, password, ssh_key, ap):
|
||||||
|
"""Initialize the SSH connection properties."""
|
||||||
|
super(SshConnection, self).__init__()
|
||||||
|
|
||||||
|
self._ssh = None
|
||||||
|
self._host = host
|
||||||
|
self._port = port
|
||||||
|
self._username = username
|
||||||
|
self._password = password
|
||||||
|
self._ssh_key = ssh_key
|
||||||
|
self._ap = ap
|
||||||
|
|
||||||
|
def get_result(self):
|
||||||
|
"""Retrieve a single AsusWrtResult through an SSH connection.
|
||||||
|
|
||||||
|
Connect to the SSH server if not currently connected, otherwise
|
||||||
|
use the existing connection.
|
||||||
|
"""
|
||||||
|
from pexpect import pxssh, exceptions
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not self.connected:
|
||||||
|
self.connect()
|
||||||
|
self._ssh.sendline(_IP_NEIGH_CMD)
|
||||||
|
self._ssh.prompt()
|
||||||
|
neighbors = self._ssh.before.split(b'\n')[1:-1]
|
||||||
|
if self._ap:
|
||||||
|
self._ssh.sendline(_ARP_CMD)
|
||||||
|
self._ssh.prompt()
|
||||||
|
arp_result = self._ssh.before.split(b'\n')[1:-1]
|
||||||
|
self._ssh.sendline(_WL_CMD)
|
||||||
|
self._ssh.prompt()
|
||||||
|
leases_result = self._ssh.before.split(b'\n')[1:-1]
|
||||||
|
self._ssh.sendline(_NVRAM_CMD)
|
||||||
|
self._ssh.prompt()
|
||||||
|
nvram_result = self._ssh.before.split(b'\n')[1].split(b'<')[1:]
|
||||||
|
else:
|
||||||
|
arp_result = ['']
|
||||||
|
nvram_result = ['']
|
||||||
|
self._ssh.sendline(_LEASES_CMD)
|
||||||
|
self._ssh.prompt()
|
||||||
|
leases_result = self._ssh.before.split(b'\n')[1:-1]
|
||||||
|
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||||
|
nvram_result)
|
||||||
|
except exceptions.EOF as err:
|
||||||
|
_LOGGER.error("Connection refused. SSH enabled?")
|
||||||
|
self.disconnect()
|
||||||
|
return None
|
||||||
|
except pxssh.ExceptionPxssh as err:
|
||||||
|
_LOGGER.error("Unexpected SSH error: %s", str(err))
|
||||||
|
self.disconnect()
|
||||||
|
return None
|
||||||
|
except AssertionError as err:
|
||||||
|
_LOGGER.error("Connection to router unavailable: %s", str(err))
|
||||||
|
self.disconnect()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""Connect to the ASUS-WRT SSH server."""
|
||||||
|
from pexpect import pxssh
|
||||||
|
|
||||||
|
self._ssh = pxssh.pxssh()
|
||||||
|
if self._ssh_key:
|
||||||
|
self._ssh.login(self._host, self._username,
|
||||||
|
ssh_key=self._ssh_key, port=self._port)
|
||||||
|
else:
|
||||||
|
self._ssh.login(self._host, self._username,
|
||||||
|
password=self._password, port=self._port)
|
||||||
|
|
||||||
|
super(SshConnection, self).connect()
|
||||||
|
|
||||||
|
def disconnect(self): \
|
||||||
|
# pylint: disable=broad-except
|
||||||
|
"""Disconnect the current SSH connection."""
|
||||||
|
try:
|
||||||
|
self._ssh.logout()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self._ssh = None
|
||||||
|
|
||||||
|
super(SshConnection, self).disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
class TelnetConnection(_Connection):
|
||||||
|
"""Maintains a Telnet connection to an ASUS-WRT router."""
|
||||||
|
|
||||||
|
def __init__(self, host, port, username, password, ap):
|
||||||
|
"""Initialize the Telnet connection properties."""
|
||||||
|
super(TelnetConnection, self).__init__()
|
||||||
|
|
||||||
|
self._telnet = None
|
||||||
|
self._host = host
|
||||||
|
self._port = port
|
||||||
|
self._username = username
|
||||||
|
self._password = password
|
||||||
|
self._ap = ap
|
||||||
|
self._prompt_string = None
|
||||||
|
|
||||||
|
def get_result(self):
|
||||||
|
"""Retrieve a single AsusWrtResult through a Telnet connection.
|
||||||
|
|
||||||
|
Connect to the Telnet server if not currently connected, otherwise
|
||||||
|
use the existing connection.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not self.connected:
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
self._telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
|
||||||
|
neighbors = (self._telnet.read_until(self._prompt_string).
|
||||||
|
split(b'\n')[1:-1])
|
||||||
|
if self._ap:
|
||||||
|
self._telnet.write('{}\n'.format(_ARP_CMD).encode('ascii'))
|
||||||
|
arp_result = (self._telnet.read_until(self._prompt_string).
|
||||||
|
split(b'\n')[1:-1])
|
||||||
|
self._telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
|
||||||
|
leases_result = (self._telnet.read_until(self._prompt_string).
|
||||||
|
split(b'\n')[1:-1])
|
||||||
|
self._telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii'))
|
||||||
|
nvram_result = (self._telnet.read_until(self._prompt_string).
|
||||||
|
split(b'\n')[1].split(b'<')[1:])
|
||||||
|
else:
|
||||||
|
arp_result = ['']
|
||||||
|
nvram_result = ['']
|
||||||
|
self._telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||||
|
leases_result = (self._telnet.read_until(self._prompt_string).
|
||||||
|
split(b'\n')[1:-1])
|
||||||
|
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||||
|
nvram_result)
|
||||||
|
except EOFError:
|
||||||
|
_LOGGER.error("Unexpected response from router")
|
||||||
|
self.disconnect()
|
||||||
|
return None
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
_LOGGER.error("Connection refused by router. Telnet enabled?")
|
||||||
|
self.disconnect()
|
||||||
|
return None
|
||||||
|
except socket.gaierror as exc:
|
||||||
|
_LOGGER.error("Socket exception: %s", exc)
|
||||||
|
self.disconnect()
|
||||||
|
return None
|
||||||
|
except OSError as exc:
|
||||||
|
_LOGGER.error("OSError: %s", exc)
|
||||||
|
self.disconnect()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""Connect to the ASUS-WRT Telnet server."""
|
||||||
|
self._telnet = telnetlib.Telnet(self._host)
|
||||||
|
self._telnet.read_until(b'login: ')
|
||||||
|
self._telnet.write((self._username + '\n').encode('ascii'))
|
||||||
|
self._telnet.read_until(b'Password: ')
|
||||||
|
self._telnet.write((self._password + '\n').encode('ascii'))
|
||||||
|
self._prompt_string = self._telnet.read_until(b'#').split(b'\n')[-1]
|
||||||
|
|
||||||
|
super(TelnetConnection, self).connect()
|
||||||
|
|
||||||
|
def disconnect(self): \
|
||||||
|
# pylint: disable=broad-except
|
||||||
|
"""Disconnect the current Telnet connection."""
|
||||||
|
try:
|
||||||
|
self._telnet.write('exit\n'.encode('ascii'))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
super(TelnetConnection, self).disconnect()
|
||||||
|
|
|
@ -39,7 +39,7 @@ class GPSLoggerView(HomeAssistantView):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Handle for GPSLogger message received as GET."""
|
"""Handle for GPSLogger message received as GET."""
|
||||||
res = yield from self._handle(request.app['hass'], request.GET)
|
res = yield from self._handle(request.app['hass'], request.query)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -75,10 +75,10 @@ class GPSLoggerView(HomeAssistantView):
|
||||||
if 'activity' in data:
|
if 'activity' in data:
|
||||||
attrs['activity'] = data['activity']
|
attrs['activity'] = data['activity']
|
||||||
|
|
||||||
yield from hass.loop.run_in_executor(
|
yield from hass.async_add_job(
|
||||||
None, partial(self.see, dev_id=device,
|
partial(self.see, dev_id=device,
|
||||||
gps=gps_location, battery=battery,
|
gps=gps_location, battery=battery,
|
||||||
gps_accuracy=accuracy,
|
gps_accuracy=accuracy,
|
||||||
attributes=attrs))
|
attributes=attrs))
|
||||||
|
|
||||||
return 'Setting location for {}'.format(device)
|
return 'Setting location for {}'.format(device)
|
||||||
|
|
|
@ -41,7 +41,7 @@ class LocativeView(HomeAssistantView):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Locative message received as GET."""
|
"""Locative message received as GET."""
|
||||||
res = yield from self._handle(request.app['hass'], request.GET)
|
res = yield from self._handle(request.app['hass'], request.query)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -79,10 +79,9 @@ class LocativeView(HomeAssistantView):
|
||||||
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
|
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
|
||||||
|
|
||||||
if direction == 'enter':
|
if direction == 'enter':
|
||||||
yield from hass.loop.run_in_executor(
|
yield from hass.async_add_job(
|
||||||
None, partial(self.see, dev_id=device,
|
partial(self.see, dev_id=device, location_name=location_name,
|
||||||
location_name=location_name,
|
gps=gps_location))
|
||||||
gps=gps_location))
|
|
||||||
return 'Setting location to {}'.format(location_name)
|
return 'Setting location to {}'.format(location_name)
|
||||||
|
|
||||||
elif direction == 'exit':
|
elif direction == 'exit':
|
||||||
|
@ -91,10 +90,9 @@ class LocativeView(HomeAssistantView):
|
||||||
|
|
||||||
if current_state is None or current_state.state == location_name:
|
if current_state is None or current_state.state == location_name:
|
||||||
location_name = STATE_NOT_HOME
|
location_name = STATE_NOT_HOME
|
||||||
yield from hass.loop.run_in_executor(
|
yield from hass.async_add_job(
|
||||||
None, partial(self.see, dev_id=device,
|
partial(self.see, dev_id=device,
|
||||||
location_name=location_name,
|
location_name=location_name, gps=gps_location))
|
||||||
gps=gps_location))
|
|
||||||
return 'Setting location to not home'
|
return 'Setting location to not home'
|
||||||
else:
|
else:
|
||||||
# Ignore the message if it is telling us to exit a zone that we
|
# Ignore the message if it is telling us to exit a zone that we
|
||||||
|
|
|
@ -60,13 +60,20 @@ class MikrotikScanner(DeviceScanner):
|
||||||
self.success_init = False
|
self.success_init = False
|
||||||
self.client = None
|
self.client = None
|
||||||
|
|
||||||
|
self.wireless_exist = None
|
||||||
self.success_init = self.connect_to_device()
|
self.success_init = self.connect_to_device()
|
||||||
|
|
||||||
if self.success_init:
|
if self.success_init:
|
||||||
_LOGGER.info("Start polling Mikrotik router...")
|
_LOGGER.info(
|
||||||
|
"Start polling Mikrotik (%s) router...",
|
||||||
|
self.host
|
||||||
|
)
|
||||||
self._update_info()
|
self._update_info()
|
||||||
else:
|
else:
|
||||||
_LOGGER.error("Connection to Mikrotik failed")
|
_LOGGER.error(
|
||||||
|
"Connection to Mikrotik (%s) failed",
|
||||||
|
self.host
|
||||||
|
)
|
||||||
|
|
||||||
def connect_to_device(self):
|
def connect_to_device(self):
|
||||||
"""Connect to Mikrotik method."""
|
"""Connect to Mikrotik method."""
|
||||||
|
@ -87,6 +94,16 @@ class MikrotikScanner(DeviceScanner):
|
||||||
routerboard_info[0].get('model', 'Router'),
|
routerboard_info[0].get('model', 'Router'),
|
||||||
self.host)
|
self.host)
|
||||||
self.connected = True
|
self.connected = True
|
||||||
|
self.wireless_exist = self.client(
|
||||||
|
cmd='/interface/wireless/getall'
|
||||||
|
)
|
||||||
|
if not self.wireless_exist:
|
||||||
|
_LOGGER.info(
|
||||||
|
'Mikrotik %s: Wireless adapters not found. Try to '
|
||||||
|
'use DHCP lease table as presence tracker source. '
|
||||||
|
'Please decrease lease time as much as possible.',
|
||||||
|
self.host
|
||||||
|
)
|
||||||
|
|
||||||
except (librouteros.exceptions.TrapError,
|
except (librouteros.exceptions.TrapError,
|
||||||
librouteros.exceptions.ConnectionError) as api_error:
|
librouteros.exceptions.ConnectionError) as api_error:
|
||||||
|
@ -108,24 +125,39 @@ class MikrotikScanner(DeviceScanner):
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
"""Retrieve latest information from the Mikrotik box."""
|
"""Retrieve latest information from the Mikrotik box."""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
_LOGGER.info("Loading wireless device from Mikrotik...")
|
if self.wireless_exist:
|
||||||
|
devices_tracker = 'wireless'
|
||||||
|
else:
|
||||||
|
devices_tracker = 'ip'
|
||||||
|
|
||||||
wireless_clients = self.client(
|
_LOGGER.info(
|
||||||
cmd='/interface/wireless/registration-table/getall'
|
"Loading %s devices from Mikrotik (%s) ...",
|
||||||
|
devices_tracker,
|
||||||
|
self.host
|
||||||
)
|
)
|
||||||
device_names = self.client(cmd='/ip/dhcp-server/lease/getall')
|
|
||||||
|
|
||||||
if device_names is None or wireless_clients is None:
|
device_names = self.client(cmd='/ip/dhcp-server/lease/getall')
|
||||||
|
if self.wireless_exist:
|
||||||
|
devices = self.client(
|
||||||
|
cmd='/interface/wireless/registration-table/getall'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
devices = device_names
|
||||||
|
|
||||||
|
if device_names is None and devices is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
mac_names = {device.get('mac-address'): device.get('host-name')
|
mac_names = {device.get('mac-address'): device.get('host-name')
|
||||||
for device in device_names
|
for device in device_names
|
||||||
if device.get('mac-address')}
|
if device.get('mac-address')}
|
||||||
|
|
||||||
self.last_results = {
|
if self.wireless_exist:
|
||||||
device.get('mac-address'):
|
self.last_results = {
|
||||||
mac_names.get(device.get('mac-address'))
|
device.get('mac-address'):
|
||||||
for device in wireless_clients
|
mac_names.get(device.get('mac-address'))
|
||||||
}
|
for device in devices
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
self.last_results = mac_names
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.util import Throttle
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['pysnmp==4.3.5']
|
REQUIREMENTS = ['pysnmp==4.3.7']
|
||||||
|
|
||||||
CONF_COMMUNITY = 'community'
|
CONF_COMMUNITY = 'community'
|
||||||
CONF_AUTHKEY = 'authkey'
|
CONF_AUTHKEY = 'authkey'
|
||||||
|
|
|
@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
from homeassistant.helpers.discovery import async_load_platform, async_discover
|
from homeassistant.helpers.discovery import async_load_platform, async_discover
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
REQUIREMENTS = ['netdisco==1.0.0']
|
REQUIREMENTS = ['netdisco==1.0.1']
|
||||||
|
|
||||||
DOMAIN = 'discovery'
|
DOMAIN = 'discovery'
|
||||||
|
|
||||||
|
@ -115,8 +115,7 @@ def async_setup(hass, config):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def scan_devices(now):
|
def scan_devices(now):
|
||||||
"""Scan for devices."""
|
"""Scan for devices."""
|
||||||
results = yield from hass.loop.run_in_executor(
|
results = yield from hass.async_add_job(_discover, netdisco)
|
||||||
None, _discover, netdisco)
|
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
hass.async_add_job(new_service_found(*result))
|
hass.async_add_job(new_service_found(*result))
|
||||||
|
|
|
@ -24,7 +24,7 @@ from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
REQUIREMENTS = ['pyeight==0.0.5']
|
REQUIREMENTS = ['pyeight==0.0.6']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -159,8 +159,8 @@ def async_setup(hass, config):
|
||||||
CONF_BINARY_SENSORS: binary_sensors,
|
CONF_BINARY_SENSORS: binary_sensors,
|
||||||
}, config))
|
}, config))
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file,
|
load_yaml_config_file,
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.discovery import async_load_platform
|
from homeassistant.helpers.discovery import async_load_platform
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
|
||||||
REQUIREMENTS = ['pyenvisalink==2.0']
|
REQUIREMENTS = ['pyenvisalink==2.1']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -229,8 +229,8 @@ def async_setup(hass, config: dict):
|
||||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||||
|
|
||||||
# Listen for fan service calls.
|
# Listen for fan service calls.
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, os.path.join(
|
load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml'))
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
for service_name in SERVICE_TO_METHOD:
|
for service_name in SERVICE_TO_METHOD:
|
||||||
|
@ -256,7 +256,7 @@ class FanEntity(ToggleEntity):
|
||||||
"""
|
"""
|
||||||
if speed is SPEED_OFF:
|
if speed is SPEED_OFF:
|
||||||
return self.async_turn_off()
|
return self.async_turn_off()
|
||||||
return self.hass.loop.run_in_executor(None, self.set_speed, speed)
|
return self.hass.async_add_job(self.set_speed, speed)
|
||||||
|
|
||||||
def set_direction(self: ToggleEntity, direction: str) -> None:
|
def set_direction(self: ToggleEntity, direction: str) -> None:
|
||||||
"""Set the direction of the fan."""
|
"""Set the direction of the fan."""
|
||||||
|
@ -267,8 +267,7 @@ class FanEntity(ToggleEntity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.set_direction, direction)
|
||||||
None, self.set_direction, direction)
|
|
||||||
|
|
||||||
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
|
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
|
||||||
"""Turn on the fan."""
|
"""Turn on the fan."""
|
||||||
|
@ -281,8 +280,8 @@ class FanEntity(ToggleEntity):
|
||||||
"""
|
"""
|
||||||
if speed is SPEED_OFF:
|
if speed is SPEED_OFF:
|
||||||
return self.async_turn_off()
|
return self.async_turn_off()
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(
|
||||||
None, ft.partial(self.turn_on, speed, **kwargs))
|
ft.partial(self.turn_on, speed, **kwargs))
|
||||||
|
|
||||||
def oscillate(self: ToggleEntity, oscillating: bool) -> None:
|
def oscillate(self: ToggleEntity, oscillating: bool) -> None:
|
||||||
"""Oscillate the fan."""
|
"""Oscillate the fan."""
|
||||||
|
@ -293,8 +292,7 @@ class FanEntity(ToggleEntity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.oscillate, oscillating)
|
||||||
None, self.oscillate, oscillating)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
"""
|
||||||
|
Z-Wave platform that handles fans.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/fan.zwave/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
|
||||||
|
from homeassistant.components.fan import (
|
||||||
|
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||||
|
SUPPORT_SET_SPEED)
|
||||||
|
from homeassistant.components import zwave
|
||||||
|
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SPEED_LIST = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||||
|
|
||||||
|
SUPPORTED_FEATURES = SUPPORT_SET_SPEED
|
||||||
|
|
||||||
|
# Value will first be divided to an integer
|
||||||
|
VALUE_TO_SPEED = {
|
||||||
|
0: SPEED_OFF,
|
||||||
|
1: SPEED_LOW,
|
||||||
|
2: SPEED_MEDIUM,
|
||||||
|
3: SPEED_HIGH,
|
||||||
|
}
|
||||||
|
|
||||||
|
SPEED_TO_VALUE = {
|
||||||
|
SPEED_OFF: 0,
|
||||||
|
SPEED_LOW: 1,
|
||||||
|
SPEED_MEDIUM: 50,
|
||||||
|
SPEED_HIGH: 99,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_device(values, **kwargs):
|
||||||
|
"""Create zwave entity device."""
|
||||||
|
return ZwaveFan(values)
|
||||||
|
|
||||||
|
|
||||||
|
class ZwaveFan(zwave.ZWaveDeviceEntity, FanEntity):
|
||||||
|
"""Representation of a Z-Wave fan."""
|
||||||
|
|
||||||
|
def __init__(self, values):
|
||||||
|
"""Initialize the Z-Wave fan device."""
|
||||||
|
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||||
|
self.update_properties()
|
||||||
|
|
||||||
|
def update_properties(self):
|
||||||
|
"""Handle data changes for node values."""
|
||||||
|
value = math.ceil(self.values.primary.data * 3 / 100)
|
||||||
|
self._state = VALUE_TO_SPEED[value]
|
||||||
|
|
||||||
|
def set_speed(self, speed):
|
||||||
|
"""Set the speed of the fan."""
|
||||||
|
self.node.set_dimmer(
|
||||||
|
self.values.primary.value_id, SPEED_TO_VALUE[speed])
|
||||||
|
|
||||||
|
def turn_on(self, speed=None, **kwargs):
|
||||||
|
"""Turn the device on."""
|
||||||
|
if speed is None:
|
||||||
|
# Value 255 tells device to return to previous value
|
||||||
|
self.node.set_dimmer(self.values.primary.value_id, 255)
|
||||||
|
else:
|
||||||
|
self.set_speed(speed)
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Turn the device off."""
|
||||||
|
self.node.set_dimmer(self.values.primary.value_id, 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def speed(self):
|
||||||
|
"""Return the current speed."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def speed_list(self):
|
||||||
|
"""Get the list of available speeds."""
|
||||||
|
return SPEED_LIST
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag supported features."""
|
||||||
|
return SUPPORTED_FEATURES
|
|
@ -89,8 +89,8 @@ def async_setup(hass, config):
|
||||||
conf.get(CONF_RUN_TEST, DEFAULT_RUN_TEST)
|
conf.get(CONF_RUN_TEST, DEFAULT_RUN_TEST)
|
||||||
)
|
)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file,
|
load_yaml_config_file,
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
# Register service
|
# Register service
|
||||||
|
|
|
@ -268,8 +268,8 @@ class IndexView(HomeAssistantView):
|
||||||
no_auth = 'true'
|
no_auth = 'true'
|
||||||
|
|
||||||
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
|
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
|
||||||
template = yield from hass.loop.run_in_executor(
|
template = yield from hass.async_add_job(
|
||||||
None, self.templates.get_template, 'index.html')
|
self.templates.get_template, 'index.html')
|
||||||
|
|
||||||
# pylint is wrong
|
# pylint is wrong
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
FINGERPRINTS = {
|
FINGERPRINTS = {
|
||||||
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
|
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
|
||||||
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
|
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
|
||||||
"frontend.html": "fbb9d6bdd3d661db26cad9475a5e22f1",
|
"frontend.html": "ed18c05632c071eb4f7b012382d0f810",
|
||||||
"mdi.html": "f407a5a57addbe93817ee1b244d33fbe",
|
"mdi.html": "f407a5a57addbe93817ee1b244d33fbe",
|
||||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||||
"panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8",
|
"panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8",
|
||||||
|
@ -18,6 +18,6 @@ FINGERPRINTS = {
|
||||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||||
"panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163",
|
"panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163",
|
||||||
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
|
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
|
||||||
"panels/ha-panel-zwave.html": "19336d2c50c91dd6a122acc0606ff10d",
|
"panels/ha-panel-zwave.html": "780a792213e98510b475f752c40ef0f9",
|
||||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1 +1 @@
|
||||||
Subproject commit 6858555c86f18eb0ab176008e9aa2c3842fec7ce
|
Subproject commit 75679e90f2aa11bc1b42188965746217feef0ea6
|
|
@ -31,6 +31,200 @@
|
||||||
});
|
});
|
||||||
this.selectedNodeAttrs = att.sort();
|
this.selectedNodeAttrs = att.sort();
|
||||||
},
|
},
|
||||||
|
});</script><dom-module id="zwave-values" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node Values"><div class="device-picker"><paper-dropdown-menu label="Value" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedValue}}"><template is="dom-repeat" items="[[values]]" as="item"><paper-item>[[computeSelectCaption(item)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsValueSelected(selectedValue)]]"><paper-input float-label="Value Name" type="text" value="{{newValueNameInput}}" placeholder="[[computeGetValueName(selectedValue)]]"></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="rename_value" service-data="[[computeValueNameServiceData(newValueNameInput)]]">Rename Value</ha-call-service-button></template></paper-card></div></template></dom-module><script>Polymer({
|
||||||
|
is: 'zwave-values',
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
hass: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
|
||||||
|
nodes: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
|
||||||
|
values: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedNode: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedValue: {
|
||||||
|
type: Number,
|
||||||
|
value: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
listeners: {
|
||||||
|
'hass-service-called': 'serviceCalled',
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceCalled: function (ev) {
|
||||||
|
if (ev.detail.success) {
|
||||||
|
var foo = this;
|
||||||
|
setTimeout(function () {
|
||||||
|
foo.refreshValues(foo.selectedNode);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computeSelectCaption: function (item) {
|
||||||
|
return item.value.label;
|
||||||
|
},
|
||||||
|
|
||||||
|
computeGetValueName: function (selectedValue) {
|
||||||
|
return this.values[selectedValue].value.label;
|
||||||
|
},
|
||||||
|
|
||||||
|
computeIsValueSelected: function (selectedValue) {
|
||||||
|
return (!this.nodes || this.selectedNode === -1 || selectedValue === -1);
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshValues: function (selectedNode) {
|
||||||
|
var valueData = [];
|
||||||
|
this.hass.callApi('GET', 'zwave/values/' + this.nodes[selectedNode].attributes.node_id).then(function (values) {
|
||||||
|
Object.entries(values).forEach(([key, value]) => {
|
||||||
|
valueData.push({ key, value });
|
||||||
|
});
|
||||||
|
this.values = valueData;
|
||||||
|
this.selectedValueChanged(this.selectedValue);
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
computeValueNameServiceData: function (newValueNameInput) {
|
||||||
|
if (!this.selectedNode === -1 || this.selectedValue === -1) return -1;
|
||||||
|
return {
|
||||||
|
node_id: this.nodes[this.selectedNode].attributes.node_id,
|
||||||
|
value_id: this.values[this.selectedValue].key,
|
||||||
|
name: newValueNameInput,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});</script><dom-module id="zwave-groups" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node group associations"><div class="device-picker"><paper-dropdown-menu label="Node to control" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedTargetNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsTargetNodeSelected(selectedTargetNode)]]"><div class="device-picker"><paper-dropdown-menu label="Group" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedGroup}}"><template is="dom-repeat" items="[[groups]]" as="state"><paper-item>[[computeSelectCaptionGroup(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><template is="dom-if" if="[[!computeIsGroupSelected(selectedGroup)]]"><div class="help-text"><span>Other Nodes in this group:</span><template is="dom-repeat" items="[[otherGroupNodes]]" as="state"><span>[[state]]</span></template></div><div class="help-text"><span>Max Associations:</span> <span>[[maxAssociations]]</span></div><div class="card-actions"><template is="dom-if" if="[[!noAssociationsLeft]]"><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, "add")]]">Add To Group</ha-call-service-button></template><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, "remove")]]">Remove From Group</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
|
||||||
|
is: 'zwave-groups',
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
hass: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
|
||||||
|
nodes: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
|
||||||
|
groups: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedNode: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedTargetNode: {
|
||||||
|
type: Number,
|
||||||
|
value: -1
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedGroup: {
|
||||||
|
type: Number,
|
||||||
|
value: -1,
|
||||||
|
observer: 'selectedGroupChanged'
|
||||||
|
},
|
||||||
|
|
||||||
|
otherGroupNodes: {
|
||||||
|
type: Array,
|
||||||
|
value: -1,
|
||||||
|
computed: 'computeOtherGroupNodes(selectedGroup)'
|
||||||
|
},
|
||||||
|
|
||||||
|
maxAssociations: {
|
||||||
|
type: String,
|
||||||
|
value: '',
|
||||||
|
computed: 'computeMaxAssociations(selectedGroup)'
|
||||||
|
},
|
||||||
|
|
||||||
|
noAssociationsLeft: {
|
||||||
|
type: Boolean,
|
||||||
|
value: true,
|
||||||
|
computed: 'computeAssociationsLeft(selectedGroup)'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
listeners: {
|
||||||
|
'hass-service-called': 'serviceCalled',
|
||||||
|
},
|
||||||
|
|
||||||
|
serviceCalled: function (ev) {
|
||||||
|
if (ev.detail.success) {
|
||||||
|
var foo = this;
|
||||||
|
setTimeout(function () {
|
||||||
|
foo.refreshGroups(foo.selectedNode);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computeAssociationsLeft: function (selectedGroup) {
|
||||||
|
if (selectedGroup === -1) return true;
|
||||||
|
return (this.maxAssociations === this.otherGroupNodes.length);
|
||||||
|
},
|
||||||
|
|
||||||
|
computeMaxAssociations: function (selectedGroup) {
|
||||||
|
if (selectedGroup === -1) return -1;
|
||||||
|
var maxAssociations = this.groups[selectedGroup].value.max_associations;
|
||||||
|
if (!maxAssociations) return ['None'];
|
||||||
|
return maxAssociations;
|
||||||
|
},
|
||||||
|
|
||||||
|
computeOtherGroupNodes: function (selectedGroup) {
|
||||||
|
if (selectedGroup === -1) return -1;
|
||||||
|
var associations = Object.values(this.groups[selectedGroup].value.associations);
|
||||||
|
if (!associations.length) return ['None'];
|
||||||
|
return associations;
|
||||||
|
},
|
||||||
|
|
||||||
|
computeSelectCaption: function (stateObj) {
|
||||||
|
return window.hassUtil.computeStateName(stateObj) + ' (Node:' +
|
||||||
|
stateObj.attributes.node_id + ' ' +
|
||||||
|
stateObj.attributes.query_stage + ')';
|
||||||
|
},
|
||||||
|
|
||||||
|
computeSelectCaptionGroup: function (stateObj) {
|
||||||
|
return (stateObj.key + ': ' + stateObj.value.label);
|
||||||
|
},
|
||||||
|
|
||||||
|
computeIsTargetNodeSelected: function (selectedTargetNode) {
|
||||||
|
return (!this.nodes || selectedTargetNode === -1);
|
||||||
|
},
|
||||||
|
|
||||||
|
computeIsGroupSelected: function (selectedGroup) {
|
||||||
|
return (!this.nodes || this.selectedNode === -1 || selectedGroup === -1);
|
||||||
|
},
|
||||||
|
|
||||||
|
computeAssocServiceData: function (selectedGroup, type) {
|
||||||
|
if (!this.groups === -1 || selectedGroup === -1 || this.selectedNode === -1) return -1;
|
||||||
|
return { node_id: this.nodes[this.selectedNode].attributes.node_id,
|
||||||
|
association: type,
|
||||||
|
target_node_id: this.nodes[this.selectedTargetNode].attributes.node_id,
|
||||||
|
group: this.groups[selectedGroup].key };
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshGroups: function (selectedNode) {
|
||||||
|
var groupData = [];
|
||||||
|
this.hass.callApi('GET', 'zwave/groups/' + this.nodes[selectedNode].attributes.node_id).then(function (groups) {
|
||||||
|
Object.entries(groups).forEach(([key, value]) => {
|
||||||
|
groupData.push({ key, value });
|
||||||
|
});
|
||||||
|
this.groups = groupData;
|
||||||
|
this.selectedGroupChanged(this.selectedGroup);
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedGroupChanged: function (selectedGroup) {
|
||||||
|
if (this.selectedGroup === -1 || selectedGroup === -1) return;
|
||||||
|
this.maxAssociations = this.groups[selectedGroup].value.max_associations;
|
||||||
|
this.otherGroupNodes = Object.values(this.groups[selectedGroup].value.associations);
|
||||||
|
},
|
||||||
});</script><dom-module id="zwave-node-config" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node config options"><template is="dom-if" if="[[wakeupNode]]"><div class="card-actions"><paper-input float-label="Wakeup Interval" type="number" value="{{wakeupInput}}" placeholder="[[computeGetWakeupValue(selectedNode)]]"><div suffix="">seconds</div></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="set_wakeup" service-data="[[computeWakeupServiceData(wakeupInput)]]">Set Wakeup</ha-call-service-button></div></template><div class="device-picker"><paper-dropdown-menu label="Config parameter" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedConfigParameter}}"><template is="dom-repeat" items="[[config]]" as="state"><paper-item>[[computeSelectCaptionConfigParameter(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'List')]]"><div class="device-picker"><paper-dropdown-menu label="Config value" class="flex" placeholder="{{loadedConfigValue}}"><paper-listbox class="dropdown-content" selected="{{selectedConfigValue}}"><template is="dom-repeat" items="[[selectedConfigParameterValues]]" as="state"><paper-item>[[state]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Byte Short Int')]]"><div class="card-actions"><paper-input label="{{selectedConfigParameterNumValues}}" type="number" value="{{selectedConfigValue}}" max="{{configParameterMax}}" min="{{configParameterMin}}"></paper-input></div></template><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Bool Button')]]"><div class="device-picker"><paper-dropdown-menu label="Config value" class="flex" placeholder="{{loadedConfigValue}}"><paper-listbox class="dropdown-content" selected="{{selectedConfigValue}}"><template is="dom-repeat" items="[[selectedConfigParameterValues]]" as="state"><paper-item>[[state]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><div class="help-text"><span>[[configValueHelpText]]</span></div><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Bool Button Byte Short Int List')]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="set_config_parameter" service-data="[[computeSetConfigParameterServiceData(selectedConfigValue)]]">Set Config Parameter</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
|
});</script><dom-module id="zwave-node-config" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node config options"><template is="dom-if" if="[[wakeupNode]]"><div class="card-actions"><paper-input float-label="Wakeup Interval" type="number" value="{{wakeupInput}}" placeholder="[[computeGetWakeupValue(selectedNode)]]"><div suffix="">seconds</div></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="set_wakeup" service-data="[[computeWakeupServiceData(wakeupInput)]]">Set Wakeup</ha-call-service-button></div></template><div class="device-picker"><paper-dropdown-menu label="Config parameter" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedConfigParameter}}"><template is="dom-repeat" items="[[config]]" as="state"><paper-item>[[computeSelectCaptionConfigParameter(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'List')]]"><div class="device-picker"><paper-dropdown-menu label="Config value" class="flex" placeholder="{{loadedConfigValue}}"><paper-listbox class="dropdown-content" selected="{{selectedConfigValue}}"><template is="dom-repeat" items="[[selectedConfigParameterValues]]" as="state"><paper-item>[[state]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Byte Short Int')]]"><div class="card-actions"><paper-input label="{{selectedConfigParameterNumValues}}" type="number" value="{{selectedConfigValue}}" max="{{configParameterMax}}" min="{{configParameterMin}}"></paper-input></div></template><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Bool Button')]]"><div class="device-picker"><paper-dropdown-menu label="Config value" class="flex" placeholder="{{loadedConfigValue}}"><paper-listbox class="dropdown-content" selected="{{selectedConfigValue}}"><template is="dom-repeat" items="[[selectedConfigParameterValues]]" as="state"><paper-item>[[state]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><div class="help-text"><span>[[configValueHelpText]]</span></div><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Bool Button Byte Short Int List')]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="set_config_parameter" service-data="[[computeSetConfigParameterServiceData(selectedConfigValue)]]">Set Config Parameter</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
|
||||||
is: 'zwave-node-config',
|
is: 'zwave-node-config',
|
||||||
|
|
||||||
|
@ -313,131 +507,7 @@
|
||||||
this.selectedUserCodeChanged(this.selectedUserCode);
|
this.selectedUserCodeChanged(this.selectedUserCode);
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
},
|
},
|
||||||
});</script><dom-module id="zwave-groups" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node group associations"><div class="device-picker"><paper-dropdown-menu label="Node to control" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedTargetNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsTargetNodeSelected(selectedTargetNode)]]"><div class="device-picker"><paper-dropdown-menu label="Group" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedGroup}}"><template is="dom-repeat" items="[[groups]]" as="state"><paper-item>[[computeSelectCaptionGroup(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><template is="dom-if" if="[[!computeIsGroupSelected(selectedGroup)]]"><div class="help-text"><span>Other Nodes in this group:</span><template is="dom-repeat" items="[[otherGroupNodes]]" as="state"><span>[[state]]</span></template></div><div class="help-text"><span>Max Associations:</span> <span>[[maxAssociations]]</span></div><div class="card-actions"><template is="dom-if" if="[[!noAssociationsLeft]]"><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, "add")]]">Add To Group</ha-call-service-button></template><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, "remove")]]">Remove From Group</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
|
});</script></div><dom-module id="ha-panel-zwave"><template><style include="iron-flex ha-style">.content{margin-top:24px}.node-info{margin-left:16px;text-transform:capitalize}.help-text{padding-left:24px;padding-right:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">Z-Wave Manager</div></app-toolbar></app-header><div class="content"><zwave-network id="zwave-network" hass="[[hass]]"></zwave-network></div><div class="content"><paper-card heading="Z-Wave Node Management"><div class="card-content">Z-Wave Node controls.</div><div class="device-picker"><paper-dropdown-menu label="Nodes" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_node" service-data="[[computeNodeServiceData(selectedNode)]]">Refresh Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="remove_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Remove Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="replace_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Replace Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="print_node" service-data="[[computeNodeServiceData(selectedNode)]]">Print Node</ha-call-service-button></div><div class="card-actions"><paper-input float-label="New node name" type="text" value="{{newNodeNameInput}}" placeholder="[[computeGetNodeName(selectedNode)]]"></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="rename_node" service-data="[[computeNodeNameServiceData(newNodeNameInput)]]">Rename Node</ha-call-service-button></div><div class="device-picker"><paper-dropdown-menu label="Entities of this node" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedEntity}}"><template is="dom-repeat" items="[[entities]]" as="state"><paper-item>[[computeSelectCaptionEnt(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsEntitySelected(selectedEntity)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_entity" service-data="[[computeRefreshEntityServiceData(selectedEntity)]]">Refresh Entity</ha-call-service-button></div><div class="content"><div class="card-actions"><paper-button toggles="" raised="" noink="" active="{{entityInfoActive}}">Entity Attributes</paper-button></div><template is="dom-if" if="{{entityInfoActive}}"><template is="dom-repeat" items="[[selectedEntityAttrs]]" as="state"><div class="node-info"><span>[[state]]</span></div></template></template></div></template></template></paper-card></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-information id="zwave-node-information" nodes="[[nodes]]" selected-node="[[selectedNode]]"></zwave-node-information></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-values hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" values="[[values]]"></zwave-values></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-groups hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" groups="[[groups]]"></zwave-groups></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-config hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" config="[[config]]"></zwave-node-config></template><template is="dom-if" if="{{hasNodeUserCodes}}"><zwave-usercodes id="zwave-usercodes" hass="[[hass]]" nodes="[[nodes]]" user-codes="[[userCodes]]" selected-node="[[selectedNode]]"></zwave-usercodes></template><div class="content"><ozw-log id="ozw-log" hass="[[hass]]"></ozw-log></div></app-header-layout></template></dom-module><script>Polymer({
|
||||||
is: 'zwave-groups',
|
|
||||||
|
|
||||||
properties: {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
nodes: {
|
|
||||||
type: Array,
|
|
||||||
},
|
|
||||||
|
|
||||||
groups: {
|
|
||||||
type: Array,
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedNode: {
|
|
||||||
type: Number,
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedTargetNode: {
|
|
||||||
type: Number,
|
|
||||||
value: -1
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedGroup: {
|
|
||||||
type: Number,
|
|
||||||
value: -1,
|
|
||||||
observer: 'selectedGroupChanged'
|
|
||||||
},
|
|
||||||
|
|
||||||
otherGroupNodes: {
|
|
||||||
type: Array,
|
|
||||||
value: -1,
|
|
||||||
computed: 'computeOtherGroupNodes(selectedGroup)'
|
|
||||||
},
|
|
||||||
|
|
||||||
maxAssociations: {
|
|
||||||
type: String,
|
|
||||||
value: '',
|
|
||||||
computed: 'computeMaxAssociations(selectedGroup)'
|
|
||||||
},
|
|
||||||
|
|
||||||
noAssociationsLeft: {
|
|
||||||
type: Boolean,
|
|
||||||
value: true,
|
|
||||||
computed: 'computeAssociationsLeft(selectedGroup)'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
listeners: {
|
|
||||||
'hass-service-called': 'serviceCalled',
|
|
||||||
},
|
|
||||||
|
|
||||||
serviceCalled: function (ev) {
|
|
||||||
if (ev.detail.success) {
|
|
||||||
var foo = this;
|
|
||||||
setTimeout(function () {
|
|
||||||
foo.refreshGroups(foo.selectedNode);
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computeAssociationsLeft: function (selectedGroup) {
|
|
||||||
if (selectedGroup === -1) return true;
|
|
||||||
return (this.maxAssociations === this.otherGroupNodes.length);
|
|
||||||
},
|
|
||||||
|
|
||||||
computeMaxAssociations: function (selectedGroup) {
|
|
||||||
if (selectedGroup === -1) return -1;
|
|
||||||
var maxAssociations = this.groups[selectedGroup].value.max_associations;
|
|
||||||
if (!maxAssociations) return ['None'];
|
|
||||||
return maxAssociations;
|
|
||||||
},
|
|
||||||
|
|
||||||
computeOtherGroupNodes: function (selectedGroup) {
|
|
||||||
if (selectedGroup === -1) return -1;
|
|
||||||
var associations = Object.values(this.groups[selectedGroup].value.associations);
|
|
||||||
if (!associations.length) return ['None'];
|
|
||||||
return associations;
|
|
||||||
},
|
|
||||||
|
|
||||||
computeSelectCaption: function (stateObj) {
|
|
||||||
return window.hassUtil.computeStateName(stateObj) + ' (Node:' +
|
|
||||||
stateObj.attributes.node_id + ' ' +
|
|
||||||
stateObj.attributes.query_stage + ')';
|
|
||||||
},
|
|
||||||
|
|
||||||
computeSelectCaptionGroup: function (stateObj) {
|
|
||||||
return (stateObj.key + ': ' + stateObj.value.label);
|
|
||||||
},
|
|
||||||
|
|
||||||
computeIsTargetNodeSelected: function (selectedTargetNode) {
|
|
||||||
return (!this.nodes || selectedTargetNode === -1);
|
|
||||||
},
|
|
||||||
|
|
||||||
computeIsGroupSelected: function (selectedGroup) {
|
|
||||||
return (!this.nodes || this.selectedNode === -1 || selectedGroup === -1);
|
|
||||||
},
|
|
||||||
|
|
||||||
computeAssocServiceData: function (selectedGroup, type) {
|
|
||||||
if (!this.groups === -1 || selectedGroup === -1 || this.selectedNode === -1) return -1;
|
|
||||||
return { node_id: this.nodes[this.selectedNode].attributes.node_id,
|
|
||||||
association: type,
|
|
||||||
target_node_id: this.nodes[this.selectedTargetNode].attributes.node_id,
|
|
||||||
group: this.groups[selectedGroup].key };
|
|
||||||
},
|
|
||||||
|
|
||||||
refreshGroups: function (selectedNode) {
|
|
||||||
var groupData = [];
|
|
||||||
this.hass.callApi('GET', 'zwave/groups/' + this.nodes[selectedNode].attributes.node_id).then(function (groups) {
|
|
||||||
Object.entries(groups).forEach(([key, value]) => {
|
|
||||||
groupData.push({ key, value });
|
|
||||||
});
|
|
||||||
this.groups = groupData;
|
|
||||||
this.selectedGroupChanged(this.selectedGroup);
|
|
||||||
}.bind(this));
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedGroupChanged: function (selectedGroup) {
|
|
||||||
if (this.selectedGroup === -1 || selectedGroup === -1) return;
|
|
||||||
this.maxAssociations = this.groups[selectedGroup].value.max_associations;
|
|
||||||
this.otherGroupNodes = Object.values(this.groups[selectedGroup].value.associations);
|
|
||||||
},
|
|
||||||
});</script></div><dom-module id="ha-panel-zwave"><template><style include="iron-flex ha-style">.content{margin-top:24px}.node-info{margin-left:16px;text-transform:capitalize}.help-text{padding-left:24px;padding-right:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">Z-Wave Manager</div></app-toolbar></app-header><div class="content"><zwave-network id="zwave-network" hass="[[hass]]"></zwave-network></div><div class="content"><paper-card heading="Z-Wave Node Management"><div class="card-content">Z-Wave Node controls.</div><div class="device-picker"><paper-dropdown-menu label="Nodes" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_node" service-data="[[computeNodeServiceData(selectedNode)]]">Refresh Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="remove_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Remove Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="replace_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Replace Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="print_node" service-data="[[computeNodeServiceData(selectedNode)]]">Print Node</ha-call-service-button></div><div class="card-actions"><paper-input float-label="New node name" type="text" value="{{newNodeNameInput}}" placeholder="[[computeGetNodeName(selectedNode)]]"></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="rename_node" service-data="[[computeNodeNameServiceData(newNodeNameInput)]]">Rename Node</ha-call-service-button></div><div class="device-picker"><paper-dropdown-menu label="Entities of this node" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedEntity}}"><template is="dom-repeat" items="[[entities]]" as="state"><paper-item>[[computeSelectCaptionEnt(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsEntitySelected(selectedEntity)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_entity" service-data="[[computeRefreshEntityServiceData(selectedEntity)]]">Refresh Entity</ha-call-service-button></div><div class="content"><div class="card-actions"><paper-button toggles="" raised="" noink="" active="{{entityInfoActive}}">Entity Attributes</paper-button></div><template is="dom-if" if="{{entityInfoActive}}"><template is="dom-repeat" items="[[selectedEntityAttrs]]" as="state"><div class="node-info"><span>[[state]]</span></div></template></template></div></template></template></paper-card></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-information id="zwave-node-information" nodes="[[nodes]]" selected-node="[[selectedNode]]"></zwave-node-information></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-groups hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" groups="[[groups]]"></zwave-groups></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-config hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" config="[[config]]"></zwave-node-config></template><template is="dom-if" if="{{hasNodeUserCodes}}"><zwave-usercodes id="zwave-usercodes" hass="[[hass]]" nodes="[[nodes]]" user-codes="[[userCodes]]" selected-node="[[selectedNode]]"></zwave-usercodes></template><div class="content"><ozw-log id="ozw-log" hass="[[hass]]"></ozw-log></div></app-header-layout></template></dom-module><script>Polymer({
|
|
||||||
is: 'ha-panel-zwave',
|
is: 'ha-panel-zwave',
|
||||||
|
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -493,6 +563,10 @@
|
||||||
computed: 'computeSelectedEntityAttrs(selectedEntity)'
|
computed: 'computeSelectedEntityAttrs(selectedEntity)'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
values: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
|
||||||
groups: {
|
groups: {
|
||||||
type: Array,
|
type: Array,
|
||||||
},
|
},
|
||||||
|
@ -559,6 +633,8 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
selectedNodeChanged: function (selectedNode) {
|
selectedNodeChanged: function (selectedNode) {
|
||||||
|
this.newNodeNameInput = '';
|
||||||
|
|
||||||
if (selectedNode === -1) return;
|
if (selectedNode === -1) return;
|
||||||
this.selectedConfigParameter = -1;
|
this.selectedConfigParameter = -1;
|
||||||
this.selectedConfigParameterValue = -1;
|
this.selectedConfigParameterValue = -1;
|
||||||
|
@ -570,6 +646,13 @@
|
||||||
});
|
});
|
||||||
this.config = configData;
|
this.config = configData;
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
var valueData = [];
|
||||||
|
this.hass.callApi('GET', 'zwave/values/' + this.nodes[selectedNode].attributes.node_id).then(function (values) {
|
||||||
|
Object.entries(values).forEach(([key, value]) => {
|
||||||
|
valueData.push({ key, value });
|
||||||
|
});
|
||||||
|
this.values = valueData;
|
||||||
|
}.bind(this));
|
||||||
var groupData = [];
|
var groupData = [];
|
||||||
this.hass.callApi('GET', 'zwave/groups/' + this.nodes[selectedNode].attributes.node_id).then(function (groups) {
|
this.hass.callApi('GET', 'zwave/groups/' + this.nodes[selectedNode].attributes.node_id).then(function (groups) {
|
||||||
Object.entries(groups).forEach(([key, value]) => {
|
Object.entries(groups).forEach(([key, value]) => {
|
||||||
|
@ -630,9 +713,7 @@
|
||||||
computeGetNodeName: function (selectedNode) {
|
computeGetNodeName: function (selectedNode) {
|
||||||
if (this.selectedNode === -1 ||
|
if (this.selectedNode === -1 ||
|
||||||
!this.nodes[selectedNode].entity_id) return -1;
|
!this.nodes[selectedNode].entity_id) return -1;
|
||||||
var str = (this.nodes[selectedNode].entity_id);
|
return this.nodes[selectedNode].attributes.node_name;
|
||||||
var name = str.replace('zwave.', '');
|
|
||||||
return name;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computeNodeNameServiceData: function (newNodeNameInput) {
|
computeNodeNameServiceData: function (newNodeNameInput) {
|
||||||
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -173,8 +173,8 @@ def async_setup(hass, config):
|
||||||
|
|
||||||
yield from _async_process_config(hass, config, component)
|
yield from _async_process_config(hass, config, component)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, conf_util.load_yaml_config_file, os.path.join(
|
conf_util.load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml')
|
os.path.dirname(__file__), 'services.yaml')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -223,7 +223,7 @@ class HistoryPeriodView(HomeAssistantView):
|
||||||
if start_time > now:
|
if start_time > now:
|
||||||
return self.json([])
|
return self.json([])
|
||||||
|
|
||||||
end_time = request.GET.get('end_time')
|
end_time = request.query.get('end_time')
|
||||||
if end_time:
|
if end_time:
|
||||||
end_time = dt_util.as_utc(
|
end_time = dt_util.as_utc(
|
||||||
dt_util.parse_datetime(end_time))
|
dt_util.parse_datetime(end_time))
|
||||||
|
@ -231,11 +231,11 @@ class HistoryPeriodView(HomeAssistantView):
|
||||||
return self.json_message('Invalid end_time', HTTP_BAD_REQUEST)
|
return self.json_message('Invalid end_time', HTTP_BAD_REQUEST)
|
||||||
else:
|
else:
|
||||||
end_time = start_time + one_day
|
end_time = start_time + one_day
|
||||||
entity_id = request.GET.get('filter_entity_id')
|
entity_id = request.query.get('filter_entity_id')
|
||||||
|
|
||||||
result = yield from request.app['hass'].loop.run_in_executor(
|
result = yield from request.app['hass'].async_add_job(
|
||||||
None, get_significant_states, request.app['hass'], start_time,
|
get_significant_states, request.app['hass'], start_time, end_time,
|
||||||
end_time, entity_id, self.filters)
|
entity_id, self.filters)
|
||||||
result = result.values()
|
result = result.values()
|
||||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||||
elapsed = time.perf_counter() - timer_start
|
elapsed = time.perf_counter() - timer_start
|
||||||
|
|
|
@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.event import track_time_interval
|
from homeassistant.helpers.event import track_time_interval
|
||||||
from homeassistant.config import load_yaml_config_file
|
from homeassistant.config import load_yaml_config_file
|
||||||
|
|
||||||
REQUIREMENTS = ['pyhomematic==0.1.26']
|
REQUIREMENTS = ['pyhomematic==0.1.27']
|
||||||
|
|
||||||
DOMAIN = 'homematic'
|
DOMAIN = 'homematic'
|
||||||
|
|
||||||
|
|
|
@ -37,8 +37,8 @@ def auth_middleware(app, handler):
|
||||||
# A valid auth header has been set
|
# A valid auth header has been set
|
||||||
authenticated = True
|
authenticated = True
|
||||||
|
|
||||||
elif (DATA_API_PASSWORD in request.GET and
|
elif (DATA_API_PASSWORD in request.query and
|
||||||
validate_password(request, request.GET[DATA_API_PASSWORD])):
|
validate_password(request, request.query[DATA_API_PASSWORD])):
|
||||||
authenticated = True
|
authenticated = True
|
||||||
|
|
||||||
elif is_trusted_ip(request):
|
elif is_trusted_ip(request):
|
||||||
|
|
|
@ -40,8 +40,8 @@ def ban_middleware(app, handler):
|
||||||
|
|
||||||
if KEY_BANNED_IPS not in app:
|
if KEY_BANNED_IPS not in app:
|
||||||
hass = app['hass']
|
hass = app['hass']
|
||||||
app[KEY_BANNED_IPS] = yield from hass.loop.run_in_executor(
|
app[KEY_BANNED_IPS] = yield from hass.async_add_job(
|
||||||
None, load_ip_bans_config, hass.config.path(IP_BANS_FILE))
|
load_ip_bans_config, hass.config.path(IP_BANS_FILE))
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def ban_middleware_handler(request):
|
def ban_middleware_handler(request):
|
||||||
|
@ -90,9 +90,8 @@ def process_wrong_login(request):
|
||||||
request.app[KEY_BANNED_IPS].append(new_ban)
|
request.app[KEY_BANNED_IPS].append(new_ban)
|
||||||
|
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
yield from hass.loop.run_in_executor(
|
yield from hass.async_add_job(
|
||||||
None, update_ip_bans_config, hass.config.path(IP_BANS_FILE),
|
update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban)
|
||||||
new_ban)
|
|
||||||
|
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Banned IP %s for too many login attempts", remote_addr)
|
"Banned IP %s for too many login attempts", remote_addr)
|
||||||
|
|
|
@ -72,8 +72,8 @@ def async_setup(hass, config):
|
||||||
|
|
||||||
yield from component.async_setup(config)
|
yield from component.async_setup(config)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file,
|
load_yaml_config_file,
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -117,7 +117,7 @@ class ImageProcessingEntity(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(None, self.process_image, image)
|
return self.hass.async_add_job(self.process_image, image)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_update(self):
|
def async_update(self):
|
||||||
|
|
|
@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/insteon_local/
|
https://home-assistant.io/components/insteon_local/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -13,7 +14,7 @@ from homeassistant.const import (
|
||||||
CONF_PASSWORD, CONF_USERNAME, CONF_HOST, CONF_PORT, CONF_TIMEOUT)
|
CONF_PASSWORD, CONF_USERNAME, CONF_HOST, CONF_PORT, CONF_TIMEOUT)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['insteonlocal==0.48']
|
REQUIREMENTS = ['insteonlocal==0.52']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -47,7 +48,12 @@ def setup(hass, config):
|
||||||
timeout = conf.get(CONF_TIMEOUT)
|
timeout = conf.get(CONF_TIMEOUT)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
insteonhub = Hub(host, username, password, port, timeout, _LOGGER)
|
if not os.path.exists(hass.config.path('.insteon_cache')):
|
||||||
|
os.makedirs(hass.config.path('.insteon_cache'))
|
||||||
|
|
||||||
|
insteonhub = Hub(host, username, password, port, timeout, _LOGGER,
|
||||||
|
hass.config.path('.insteon_cache'))
|
||||||
|
|
||||||
# Check for successful connection
|
# Check for successful connection
|
||||||
insteonhub.get_buffer_status()
|
insteonhub.get_buffer_status()
|
||||||
except requests.exceptions.ConnectTimeout:
|
except requests.exceptions.ConnectTimeout:
|
||||||
|
|
|
@ -167,7 +167,7 @@ IDENTIFY_SCHEMA = vol.Schema({
|
||||||
vol.Optional(ATTR_PUSH_SOUNDS): list
|
vol.Optional(ATTR_PUSH_SOUNDS): list
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
CONFIGURATION_FILE = 'ios.conf'
|
CONFIGURATION_FILE = '.ios.conf'
|
||||||
|
|
||||||
CONFIG_FILE = {ATTR_DEVICES: {}}
|
CONFIG_FILE = {ATTR_DEVICES: {}}
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,8 @@ EFFECT_COLORLOOP = "colorloop"
|
||||||
EFFECT_RANDOM = "random"
|
EFFECT_RANDOM = "random"
|
||||||
EFFECT_WHITE = "white"
|
EFFECT_WHITE = "white"
|
||||||
|
|
||||||
|
COLOR_GROUP = "Color descriptors"
|
||||||
|
|
||||||
LIGHT_PROFILES_FILE = "light_profiles.csv"
|
LIGHT_PROFILES_FILE = "light_profiles.csv"
|
||||||
|
|
||||||
PROP_TO_ATTR = {
|
PROP_TO_ATTR = {
|
||||||
|
@ -98,17 +100,21 @@ VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
|
||||||
|
|
||||||
LIGHT_TURN_ON_SCHEMA = vol.Schema({
|
LIGHT_TURN_ON_SCHEMA = vol.Schema({
|
||||||
ATTR_ENTITY_ID: cv.entity_ids,
|
ATTR_ENTITY_ID: cv.entity_ids,
|
||||||
ATTR_PROFILE: cv.string,
|
vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string,
|
||||||
ATTR_TRANSITION: VALID_TRANSITION,
|
ATTR_TRANSITION: VALID_TRANSITION,
|
||||||
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
|
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
|
||||||
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
|
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
|
||||||
ATTR_COLOR_NAME: cv.string,
|
vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
|
||||||
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP):
|
||||||
vol.Coerce(tuple)),
|
vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
||||||
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
vol.Coerce(tuple)),
|
||||||
vol.Coerce(tuple)),
|
vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP):
|
||||||
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
|
vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
||||||
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
|
vol.Coerce(tuple)),
|
||||||
|
vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP):
|
||||||
|
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||||
|
vol.Exclusive(ATTR_KELVIN, COLOR_GROUP):
|
||||||
|
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||||
ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
|
ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
|
||||||
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
|
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
|
||||||
ATTR_EFFECT: cv.string,
|
ATTR_EFFECT: cv.string,
|
||||||
|
@ -285,8 +291,8 @@ def async_setup(hass, config):
|
||||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||||
|
|
||||||
# Listen for light on and light off service calls.
|
# Listen for light on and light off service calls.
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, os.path.join(
|
load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml'))
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
|
@ -341,8 +347,7 @@ class Profiles:
|
||||||
return None
|
return None
|
||||||
return profiles
|
return profiles
|
||||||
|
|
||||||
cls._all = yield from hass.loop.run_in_executor(
|
cls._all = yield from hass.async_add_job(load_profile_data, hass)
|
||||||
None, load_profile_data, hass)
|
|
||||||
return cls._all is not None
|
return cls._all is not None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -12,10 +12,9 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL
|
from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, ATTR_WHITE_VALUE,
|
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, EFFECT_COLORLOOP,
|
||||||
EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT,
|
EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT,
|
||||||
SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, Light,
|
SUPPORT_RGB_COLOR, Light, PLATFORM_SCHEMA)
|
||||||
PLATFORM_SCHEMA)
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['flux_led==0.19']
|
REQUIREMENTS = ['flux_led==0.19']
|
||||||
|
@ -27,10 +26,8 @@ ATTR_MODE = 'mode'
|
||||||
|
|
||||||
DOMAIN = 'flux_led'
|
DOMAIN = 'flux_led'
|
||||||
|
|
||||||
SUPPORT_FLUX_LED_RGB = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
|
SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
|
||||||
SUPPORT_RGB_COLOR)
|
SUPPORT_RGB_COLOR)
|
||||||
SUPPORT_FLUX_LED_RGBW = (SUPPORT_WHITE_VALUE | SUPPORT_EFFECT |
|
|
||||||
SUPPORT_RGB_COLOR)
|
|
||||||
|
|
||||||
MODE_RGB = 'rgb'
|
MODE_RGB = 'rgb'
|
||||||
MODE_RGBW = 'rgbw'
|
MODE_RGBW = 'rgbw'
|
||||||
|
@ -182,16 +179,7 @@ class FluxLight(Light):
|
||||||
@property
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
"""Return the brightness of this light between 0..255."""
|
"""Return the brightness of this light between 0..255."""
|
||||||
if self._mode == MODE_RGB:
|
return self._bulb.brightness
|
||||||
return self._bulb.brightness
|
|
||||||
return None # not used for RGBW
|
|
||||||
|
|
||||||
@property
|
|
||||||
def white_value(self):
|
|
||||||
"""Return the white value of this light between 0..255."""
|
|
||||||
if self._mode == MODE_RGBW:
|
|
||||||
return self._bulb.getRgbw()[3]
|
|
||||||
return None # not used for RGB
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rgb_color(self):
|
def rgb_color(self):
|
||||||
|
@ -201,11 +189,7 @@ class FluxLight(Light):
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Flag supported features."""
|
"""Flag supported features."""
|
||||||
if self._mode == MODE_RGBW:
|
return SUPPORT_FLUX_LED
|
||||||
return SUPPORT_FLUX_LED_RGBW
|
|
||||||
elif self._mode == MODE_RGB:
|
|
||||||
return SUPPORT_FLUX_LED_RGB
|
|
||||||
return 0
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def effect_list(self):
|
def effect_list(self):
|
||||||
|
@ -219,23 +203,17 @@ class FluxLight(Light):
|
||||||
|
|
||||||
rgb = kwargs.get(ATTR_RGB_COLOR)
|
rgb = kwargs.get(ATTR_RGB_COLOR)
|
||||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||||
white_value = kwargs.get(ATTR_WHITE_VALUE)
|
|
||||||
effect = kwargs.get(ATTR_EFFECT)
|
effect = kwargs.get(ATTR_EFFECT)
|
||||||
|
|
||||||
if rgb is not None and brightness is not None:
|
if rgb is not None and brightness is not None:
|
||||||
self._bulb.setRgb(*tuple(rgb), brightness=brightness)
|
self._bulb.setRgb(*tuple(rgb), brightness=brightness)
|
||||||
elif rgb is not None and white_value is not None:
|
|
||||||
self._bulb.setRgbw(*tuple(rgb), w=white_value)
|
|
||||||
elif rgb is not None:
|
elif rgb is not None:
|
||||||
# self.white_value and self.brightness are appropriately
|
self._bulb.setRgb(*tuple(rgb))
|
||||||
# returning None for MODE_RGB and MODE_RGBW respectively
|
|
||||||
self._bulb.setRgbw(*tuple(rgb),
|
|
||||||
w=self.white_value,
|
|
||||||
brightness=self.brightness)
|
|
||||||
elif brightness is not None:
|
elif brightness is not None:
|
||||||
self._bulb.setRgb(*self.rgb_color, brightness=brightness)
|
if self._mode == 'rgbw':
|
||||||
elif white_value is not None:
|
self._bulb.setWarmWhite255(brightness)
|
||||||
self._bulb.setRgbw(*self.rgb_color, w=white_value)
|
elif self._mode == 'rgb':
|
||||||
|
(red, green, blue) = self._bulb.getRgb()
|
||||||
|
self._bulb.setRgb(red, green, blue, brightness=brightness)
|
||||||
elif effect == EFFECT_RANDOM:
|
elif effect == EFFECT_RANDOM:
|
||||||
self._bulb.setRgb(random.randint(0, 255),
|
self._bulb.setRgb(random.randint(0, 255),
|
||||||
random.randint(0, 255),
|
random.randint(0, 255),
|
||||||
|
|
|
@ -37,7 +37,7 @@ from . import effects as lifx_effects
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['aiolifx==0.4.6']
|
REQUIREMENTS = ['aiolifx==0.4.7']
|
||||||
|
|
||||||
UDP_BROADCAST_PORT = 56700
|
UDP_BROADCAST_PORT = 56700
|
||||||
|
|
||||||
|
@ -54,9 +54,6 @@ ATTR_POWER = 'power'
|
||||||
BYTE_MAX = 255
|
BYTE_MAX = 255
|
||||||
SHORT_MAX = 65535
|
SHORT_MAX = 65535
|
||||||
|
|
||||||
SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
|
|
||||||
SUPPORT_XY_COLOR | SUPPORT_TRANSITION | SUPPORT_EFFECT)
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
|
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
|
||||||
})
|
})
|
||||||
|
@ -229,6 +226,12 @@ class LIFXLight(Light):
|
||||||
self.set_power(device.power_level)
|
self.set_power(device.power_level)
|
||||||
self.set_color(*device.color)
|
self.set_color(*device.color)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lifxwhite(self):
|
||||||
|
"""Return whether this is a white-only bulb."""
|
||||||
|
# https://lan.developer.lifx.com/docs/lifx-products
|
||||||
|
return self.product in [10, 11, 18]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
"""Return the availability of the device."""
|
"""Return the availability of the device."""
|
||||||
|
@ -273,8 +276,7 @@ class LIFXLight(Light):
|
||||||
def min_mireds(self):
|
def min_mireds(self):
|
||||||
"""Return the coldest color_temp that this light supports."""
|
"""Return the coldest color_temp that this light supports."""
|
||||||
# The 3 LIFX "White" products supported a limited temperature range
|
# The 3 LIFX "White" products supported a limited temperature range
|
||||||
# https://lan.developer.lifx.com/docs/lifx-products
|
if self.lifxwhite:
|
||||||
if self.product in [10, 11, 18]:
|
|
||||||
kelvin = 6500
|
kelvin = 6500
|
||||||
else:
|
else:
|
||||||
kelvin = 9000
|
kelvin = 9000
|
||||||
|
@ -284,8 +286,7 @@ class LIFXLight(Light):
|
||||||
def max_mireds(self):
|
def max_mireds(self):
|
||||||
"""Return the warmest color_temp that this light supports."""
|
"""Return the warmest color_temp that this light supports."""
|
||||||
# The 3 LIFX "White" products supported a limited temperature range
|
# The 3 LIFX "White" products supported a limited temperature range
|
||||||
# https://lan.developer.lifx.com/docs/lifx-products
|
if self.lifxwhite:
|
||||||
if self.product in [10, 11, 18]:
|
|
||||||
kelvin = 2700
|
kelvin = 2700
|
||||||
else:
|
else:
|
||||||
kelvin = 2500
|
kelvin = 2500
|
||||||
|
@ -305,12 +306,18 @@ class LIFXLight(Light):
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Flag supported features."""
|
"""Flag supported features."""
|
||||||
return SUPPORT_LIFX
|
features = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP |
|
||||||
|
SUPPORT_TRANSITION | SUPPORT_EFFECT)
|
||||||
|
|
||||||
|
if not self.lifxwhite:
|
||||||
|
features |= SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR
|
||||||
|
|
||||||
|
return features
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def effect_list(self):
|
def effect_list(self):
|
||||||
"""Return the list of supported effects."""
|
"""Return the list of supported effects."""
|
||||||
return lifx_effects.effect_list()
|
return lifx_effects.effect_list(self)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def update_after_transition(self, now):
|
def update_after_transition(self, now):
|
||||||
|
|
|
@ -7,7 +7,7 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME,
|
DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME,
|
||||||
ATTR_RGB_COLOR, ATTR_EFFECT, ATTR_TRANSITION,
|
ATTR_RGB_COLOR, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_EFFECT, ATTR_TRANSITION,
|
||||||
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT)
|
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT)
|
||||||
from homeassistant.const import (ATTR_ENTITY_ID)
|
from homeassistant.const import (ATTR_ENTITY_ID)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
@ -42,6 +42,8 @@ LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
|
||||||
ATTR_COLOR_NAME: cv.string,
|
ATTR_COLOR_NAME: cv.string,
|
||||||
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
||||||
vol.Coerce(tuple)),
|
vol.Coerce(tuple)),
|
||||||
|
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||||
|
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||||
vol.Optional(ATTR_PERIOD, default=1.0):
|
vol.Optional(ATTR_PERIOD, default=1.0):
|
||||||
vol.All(vol.Coerce(float), vol.Range(min=0.05)),
|
vol.All(vol.Coerce(float), vol.Range(min=0.05)),
|
||||||
vol.Optional(ATTR_CYCLES, default=1.0):
|
vol.Optional(ATTR_CYCLES, default=1.0):
|
||||||
|
@ -131,14 +133,21 @@ def default_effect(light, **kwargs):
|
||||||
yield from light.hass.services.async_call(DOMAIN, service, data)
|
yield from light.hass.services.async_call(DOMAIN, service, data)
|
||||||
|
|
||||||
|
|
||||||
def effect_list():
|
def effect_list(light):
|
||||||
"""Return the list of supported effects."""
|
"""Return the list of supported effects for this light."""
|
||||||
return [
|
if light.lifxwhite:
|
||||||
SERVICE_EFFECT_COLORLOOP,
|
return [
|
||||||
SERVICE_EFFECT_BREATHE,
|
SERVICE_EFFECT_BREATHE,
|
||||||
SERVICE_EFFECT_PULSE,
|
SERVICE_EFFECT_PULSE,
|
||||||
SERVICE_EFFECT_STOP,
|
SERVICE_EFFECT_STOP,
|
||||||
]
|
]
|
||||||
|
else:
|
||||||
|
return [
|
||||||
|
SERVICE_EFFECT_COLORLOOP,
|
||||||
|
SERVICE_EFFECT_BREATHE,
|
||||||
|
SERVICE_EFFECT_PULSE,
|
||||||
|
SERVICE_EFFECT_STOP,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class LIFXEffectData(object):
|
class LIFXEffectData(object):
|
||||||
|
@ -230,12 +239,14 @@ class LIFXEffectBreathe(LIFXEffect):
|
||||||
cycles = kwargs[ATTR_CYCLES]
|
cycles = kwargs[ATTR_CYCLES]
|
||||||
hsbk, color_changed = light.find_hsbk(**kwargs)
|
hsbk, color_changed = light.find_hsbk(**kwargs)
|
||||||
|
|
||||||
# Default color is to fully (de)saturate with full brightness
|
# Set default effect color based on current setting
|
||||||
if not color_changed:
|
if not color_changed:
|
||||||
if hsbk[1] > 65536/2:
|
if light.lifxwhite or hsbk[1] < 65536/2:
|
||||||
hsbk = [hsbk[0], 0, 65535, 4000]
|
# White: toggle brightness
|
||||||
|
hsbk[2] = 65535 if hsbk[2] < 65536/2 else 0
|
||||||
else:
|
else:
|
||||||
hsbk = [hsbk[0], 65535, 65535, hsbk[3]]
|
# Color: fully desaturate with full brightness
|
||||||
|
hsbk = [hsbk[0], 0, 65535, 4000]
|
||||||
|
|
||||||
# Start the effect
|
# Start the effect
|
||||||
args = {
|
args = {
|
||||||
|
|
|
@ -0,0 +1,236 @@
|
||||||
|
"""
|
||||||
|
Support for Template lights.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/light.template/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.components.light import (
|
||||||
|
ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, CONF_FRIENDLY_NAME, STATE_ON,
|
||||||
|
STATE_OFF, EVENT_HOMEASSISTANT_START, MATCH_ALL
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
|
||||||
|
from homeassistant.exceptions import TemplateError
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
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
|
||||||
|
from homeassistant.helpers.script import Script
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
_VALID_STATES = [STATE_ON, STATE_OFF, 'true', 'false']
|
||||||
|
|
||||||
|
CONF_LIGHTS = 'lights'
|
||||||
|
CONF_ON_ACTION = 'turn_on'
|
||||||
|
CONF_OFF_ACTION = 'turn_off'
|
||||||
|
CONF_LEVEL_ACTION = 'set_level'
|
||||||
|
CONF_LEVEL_TEMPLATE = 'level_template'
|
||||||
|
|
||||||
|
|
||||||
|
LIGHT_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||||
|
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||||
|
vol.Optional(CONF_VALUE_TEMPLATE, default=None): cv.template,
|
||||||
|
vol.Optional(CONF_LEVEL_ACTION, default=None): cv.SCRIPT_SCHEMA,
|
||||||
|
vol.Optional(CONF_LEVEL_TEMPLATE, default=None): cv.template,
|
||||||
|
vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string,
|
||||||
|
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
|
||||||
|
})
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_LIGHTS): vol.Schema({cv.slug: LIGHT_SCHEMA}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Set up Template Lights."""
|
||||||
|
lights = []
|
||||||
|
|
||||||
|
for device, device_config in config[CONF_LIGHTS].items():
|
||||||
|
friendly_name = device_config.get(CONF_FRIENDLY_NAME, device)
|
||||||
|
state_template = device_config[CONF_VALUE_TEMPLATE]
|
||||||
|
on_action = device_config[CONF_ON_ACTION]
|
||||||
|
off_action = device_config[CONF_OFF_ACTION]
|
||||||
|
level_action = device_config[CONF_LEVEL_ACTION]
|
||||||
|
level_template = device_config[CONF_LEVEL_TEMPLATE]
|
||||||
|
|
||||||
|
template_entity_ids = set()
|
||||||
|
|
||||||
|
if state_template is not None:
|
||||||
|
temp_ids = state_template.extract_entities()
|
||||||
|
if str(temp_ids) != MATCH_ALL:
|
||||||
|
template_entity_ids |= set(temp_ids)
|
||||||
|
|
||||||
|
if level_template is not None:
|
||||||
|
temp_ids = level_template.extract_entities()
|
||||||
|
if str(temp_ids) != MATCH_ALL:
|
||||||
|
template_entity_ids |= set(temp_ids)
|
||||||
|
|
||||||
|
if not template_entity_ids:
|
||||||
|
template_entity_ids = MATCH_ALL
|
||||||
|
|
||||||
|
entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids)
|
||||||
|
|
||||||
|
lights.append(
|
||||||
|
LightTemplate(
|
||||||
|
hass, device, friendly_name, state_template,
|
||||||
|
on_action, off_action, level_action, level_template,
|
||||||
|
entity_ids)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not lights:
|
||||||
|
_LOGGER.error("No lights added")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async_add_devices(lights, True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class LightTemplate(Light):
|
||||||
|
"""Representation of a templated Light, including dimmable."""
|
||||||
|
|
||||||
|
def __init__(self, hass, device_id, friendly_name, state_template,
|
||||||
|
on_action, off_action, level_action, level_template,
|
||||||
|
entity_ids):
|
||||||
|
"""Initialize the light."""
|
||||||
|
self.hass = hass
|
||||||
|
self.entity_id = async_generate_entity_id(
|
||||||
|
ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||||
|
self._name = friendly_name
|
||||||
|
self._template = state_template
|
||||||
|
self._on_script = Script(hass, on_action)
|
||||||
|
self._off_script = Script(hass, off_action)
|
||||||
|
self._level_script = Script(hass, level_action)
|
||||||
|
self._level_template = level_template
|
||||||
|
|
||||||
|
self._state = False
|
||||||
|
self._brightness = None
|
||||||
|
self._entities = entity_ids
|
||||||
|
|
||||||
|
if self._template is not None:
|
||||||
|
self._template.hass = self.hass
|
||||||
|
if self._level_template is not None:
|
||||||
|
self._level_template.hass = self.hass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Return the brightness of the light."""
|
||||||
|
return self._brightness
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag supported features."""
|
||||||
|
if self._level_script is not None:
|
||||||
|
return SUPPORT_BRIGHTNESS
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if device is on."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""Return the polling state."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@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_light_state_listener(entity, old_state, new_state):
|
||||||
|
"""Handle target device state changes."""
|
||||||
|
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def template_light_startup(event):
|
||||||
|
"""Update template on startup."""
|
||||||
|
if (self._template is not None or
|
||||||
|
self._level_template is not None):
|
||||||
|
async_track_state_change(
|
||||||
|
self.hass, self._entities, template_light_state_listener)
|
||||||
|
|
||||||
|
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||||
|
|
||||||
|
self.hass.bus.async_listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_START, template_light_startup)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_turn_on(self, **kwargs):
|
||||||
|
"""Turn the light on."""
|
||||||
|
optimistic_set = False
|
||||||
|
# set optimistic states
|
||||||
|
if self._template is None:
|
||||||
|
self._state = True
|
||||||
|
optimistic_set = True
|
||||||
|
|
||||||
|
if self._level_template is None and ATTR_BRIGHTNESS in kwargs:
|
||||||
|
_LOGGER.info("Optimistically setting brightness to %s",
|
||||||
|
kwargs[ATTR_BRIGHTNESS])
|
||||||
|
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||||
|
optimistic_set = True
|
||||||
|
|
||||||
|
if ATTR_BRIGHTNESS in kwargs and self._level_script:
|
||||||
|
self.hass.async_add_job(self._level_script.async_run(
|
||||||
|
{"brightness": kwargs[ATTR_BRIGHTNESS]}))
|
||||||
|
else:
|
||||||
|
self.hass.async_add_job(self._on_script.async_run())
|
||||||
|
|
||||||
|
if optimistic_set:
|
||||||
|
self.hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_turn_off(self, **kwargs):
|
||||||
|
"""Turn the light off."""
|
||||||
|
self.hass.async_add_job(self._off_script.async_run())
|
||||||
|
if self._template is None:
|
||||||
|
self._state = False
|
||||||
|
self.hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_update(self):
|
||||||
|
"""Update the state from the template."""
|
||||||
|
if self._template is not None:
|
||||||
|
try:
|
||||||
|
state = self._template.async_render().lower()
|
||||||
|
except TemplateError as ex:
|
||||||
|
_LOGGER.error(ex)
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
if state in _VALID_STATES:
|
||||||
|
self._state = state in ('true', STATE_ON)
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
'Received invalid light is_on state: %s. ' +
|
||||||
|
'Expected: %s',
|
||||||
|
state, ', '.join(_VALID_STATES))
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
if self._level_template is not None:
|
||||||
|
try:
|
||||||
|
brightness = self._level_template.async_render()
|
||||||
|
except TemplateError as ex:
|
||||||
|
_LOGGER.error(ex)
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
if 0 <= int(brightness) <= 255:
|
||||||
|
self._brightness = brightness
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
'Received invalid brightness : %s' +
|
||||||
|
'Expected: 0-255',
|
||||||
|
brightness)
|
||||||
|
self._brightness = None
|
|
@ -19,8 +19,6 @@ DEPENDENCIES = ['wink']
|
||||||
|
|
||||||
SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR
|
SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR
|
||||||
|
|
||||||
RGB_MODES = ['hsb', 'rgb']
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the Wink lights."""
|
"""Set up the Wink lights."""
|
||||||
|
@ -62,8 +60,6 @@ class WinkLight(WinkDevice, Light):
|
||||||
"""Define current bulb color in RGB."""
|
"""Define current bulb color in RGB."""
|
||||||
if not self.wink.supports_hue_saturation():
|
if not self.wink.supports_hue_saturation():
|
||||||
return None
|
return None
|
||||||
elif self.wink.color_model() not in RGB_MODES:
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
hue = self.wink.color_hue()
|
hue = self.wink.color_hue()
|
||||||
saturation = self.wink.color_saturation()
|
saturation = self.wink.color_saturation()
|
||||||
|
|
|
@ -108,8 +108,8 @@ def async_setup(hass, config):
|
||||||
if update_tasks:
|
if update_tasks:
|
||||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, os.path.join(
|
load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml'))
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
|
@ -150,8 +150,7 @@ class LockDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(ft.partial(self.lock, **kwargs))
|
||||||
None, ft.partial(self.lock, **kwargs))
|
|
||||||
|
|
||||||
def unlock(self, **kwargs):
|
def unlock(self, **kwargs):
|
||||||
"""Unlock the lock."""
|
"""Unlock the lock."""
|
||||||
|
@ -162,8 +161,7 @@ class LockDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(ft.partial(self.unlock, **kwargs))
|
||||||
None, ft.partial(self.unlock, **kwargs))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
|
|
|
@ -128,7 +128,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
|
||||||
descriptions = load_yaml_config_file(
|
descriptions = load_yaml_config_file(
|
||||||
path.join(path.dirname(__file__), 'services.yaml'))
|
path.join(path.dirname(__file__), 'services.yaml'))
|
||||||
network = hass.data[zwave.ZWAVE_NETWORK]
|
network = hass.data[zwave.const.DATA_NETWORK]
|
||||||
|
|
||||||
def set_usercode(service):
|
def set_usercode(service):
|
||||||
"""Set the usercode to index X on the lock."""
|
"""Set the usercode to index X on the lock."""
|
||||||
|
|
|
@ -134,8 +134,8 @@ class LogbookView(HomeAssistantView):
|
||||||
end_day = start_day + timedelta(days=1)
|
end_day = start_day + timedelta(days=1)
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
|
|
||||||
events = yield from hass.loop.run_in_executor(
|
events = yield from hass.async_add_job(
|
||||||
None, _get_events, hass, start_day, end_day)
|
_get_events, hass, start_day, end_day)
|
||||||
events = _exclude_events(events, self.config)
|
events = _exclude_events(events, self.config)
|
||||||
return self.json(humanify(events))
|
return self.json(humanify(events))
|
||||||
|
|
||||||
|
|
|
@ -123,8 +123,8 @@ def async_setup(hass, config):
|
||||||
"""Handle logger services."""
|
"""Handle logger services."""
|
||||||
set_log_levels(service.data)
|
set_log_levels(service.data)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, os.path.join(
|
load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml'))
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
|
|
|
@ -10,8 +10,7 @@ import logging
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/thecynic/pylutron/archive/v0.1.0.zip#'
|
REQUIREMENTS = ['pylutron==0.1.0']
|
||||||
'pylutron==0.1.0']
|
|
||||||
|
|
||||||
DOMAIN = 'lutron'
|
DOMAIN = 'lutron'
|
||||||
|
|
||||||
|
|
|
@ -353,8 +353,8 @@ def async_setup(hass, config):
|
||||||
|
|
||||||
yield from component.async_setup(config)
|
yield from component.async_setup(config)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, os.path.join(
|
load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml'))
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -583,8 +583,7 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.turn_on)
|
||||||
None, self.turn_on)
|
|
||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
"""Turn the media player off."""
|
"""Turn the media player off."""
|
||||||
|
@ -595,8 +594,7 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.turn_off)
|
||||||
None, self.turn_off)
|
|
||||||
|
|
||||||
def mute_volume(self, mute):
|
def mute_volume(self, mute):
|
||||||
"""Mute the volume."""
|
"""Mute the volume."""
|
||||||
|
@ -607,8 +605,7 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.mute_volume, mute)
|
||||||
None, self.mute_volume, mute)
|
|
||||||
|
|
||||||
def set_volume_level(self, volume):
|
def set_volume_level(self, volume):
|
||||||
"""Set volume level, range 0..1."""
|
"""Set volume level, range 0..1."""
|
||||||
|
@ -619,8 +616,7 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.set_volume_level, volume)
|
||||||
None, self.set_volume_level, volume)
|
|
||||||
|
|
||||||
def media_play(self):
|
def media_play(self):
|
||||||
"""Send play commmand."""
|
"""Send play commmand."""
|
||||||
|
@ -631,8 +627,7 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.media_play)
|
||||||
None, self.media_play)
|
|
||||||
|
|
||||||
def media_pause(self):
|
def media_pause(self):
|
||||||
"""Send pause command."""
|
"""Send pause command."""
|
||||||
|
@ -643,8 +638,7 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.media_pause)
|
||||||
None, self.media_pause)
|
|
||||||
|
|
||||||
def media_stop(self):
|
def media_stop(self):
|
||||||
"""Send stop command."""
|
"""Send stop command."""
|
||||||
|
@ -655,8 +649,7 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.media_stop)
|
||||||
None, self.media_stop)
|
|
||||||
|
|
||||||
def media_previous_track(self):
|
def media_previous_track(self):
|
||||||
"""Send previous track command."""
|
"""Send previous track command."""
|
||||||
|
@ -667,8 +660,7 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.media_previous_track)
|
||||||
None, self.media_previous_track)
|
|
||||||
|
|
||||||
def media_next_track(self):
|
def media_next_track(self):
|
||||||
"""Send next track command."""
|
"""Send next track command."""
|
||||||
|
@ -679,8 +671,7 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.media_next_track)
|
||||||
None, self.media_next_track)
|
|
||||||
|
|
||||||
def media_seek(self, position):
|
def media_seek(self, position):
|
||||||
"""Send seek command."""
|
"""Send seek command."""
|
||||||
|
@ -691,8 +682,7 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.media_seek, position)
|
||||||
None, self.media_seek, position)
|
|
||||||
|
|
||||||
def play_media(self, media_type, media_id, **kwargs):
|
def play_media(self, media_type, media_id, **kwargs):
|
||||||
"""Play a piece of media."""
|
"""Play a piece of media."""
|
||||||
|
@ -703,8 +693,8 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(
|
||||||
None, ft.partial(self.play_media, media_type, media_id, **kwargs))
|
ft.partial(self.play_media, media_type, media_id, **kwargs))
|
||||||
|
|
||||||
def select_source(self, source):
|
def select_source(self, source):
|
||||||
"""Select input source."""
|
"""Select input source."""
|
||||||
|
@ -715,8 +705,7 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.select_source, source)
|
||||||
None, self.select_source, source)
|
|
||||||
|
|
||||||
def clear_playlist(self):
|
def clear_playlist(self):
|
||||||
"""Clear players playlist."""
|
"""Clear players playlist."""
|
||||||
|
@ -727,8 +716,7 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.clear_playlist)
|
||||||
None, self.clear_playlist)
|
|
||||||
|
|
||||||
def set_shuffle(self, shuffle):
|
def set_shuffle(self, shuffle):
|
||||||
"""Enable/disable shuffle mode."""
|
"""Enable/disable shuffle mode."""
|
||||||
|
@ -739,8 +727,7 @@ class MediaPlayerDevice(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(self.set_shuffle, shuffle)
|
||||||
None, self.set_shuffle, shuffle)
|
|
||||||
|
|
||||||
# No need to overwrite these.
|
# No need to overwrite these.
|
||||||
@property
|
@property
|
||||||
|
@ -810,7 +797,7 @@ class MediaPlayerDevice(Entity):
|
||||||
"""
|
"""
|
||||||
if hasattr(self, 'toggle'):
|
if hasattr(self, 'toggle'):
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
return self.hass.loop.run_in_executor(None, self.toggle)
|
return self.hass.async_add_job(self.toggle)
|
||||||
|
|
||||||
if self.state in [STATE_OFF, STATE_IDLE]:
|
if self.state in [STATE_OFF, STATE_IDLE]:
|
||||||
return self.async_turn_on()
|
return self.async_turn_on()
|
||||||
|
@ -825,7 +812,7 @@ class MediaPlayerDevice(Entity):
|
||||||
"""
|
"""
|
||||||
if hasattr(self, 'volume_up'):
|
if hasattr(self, 'volume_up'):
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
yield from self.hass.loop.run_in_executor(None, self.volume_up)
|
yield from self.hass.async_add_job(self.volume_up)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.volume_level < 1:
|
if self.volume_level < 1:
|
||||||
|
@ -840,7 +827,7 @@ class MediaPlayerDevice(Entity):
|
||||||
"""
|
"""
|
||||||
if hasattr(self, 'volume_down'):
|
if hasattr(self, 'volume_down'):
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
yield from self.hass.loop.run_in_executor(None, self.volume_down)
|
yield from self.hass.async_add_job(self.volume_down)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.volume_level > 0:
|
if self.volume_level > 0:
|
||||||
|
@ -854,7 +841,7 @@ class MediaPlayerDevice(Entity):
|
||||||
"""
|
"""
|
||||||
if hasattr(self, 'media_play_pause'):
|
if hasattr(self, 'media_play_pause'):
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
return self.hass.loop.run_in_executor(None, self.media_play_pause)
|
return self.hass.async_add_job(self.media_play_pause)
|
||||||
|
|
||||||
if self.state == STATE_PLAYING:
|
if self.state == STATE_PLAYING:
|
||||||
return self.async_media_pause()
|
return self.async_media_pause()
|
||||||
|
@ -960,7 +947,7 @@ class MediaPlayerImageView(HomeAssistantView):
|
||||||
return web.Response(status=status)
|
return web.Response(status=status)
|
||||||
|
|
||||||
authenticated = (request[KEY_AUTHENTICATED] or
|
authenticated = (request[KEY_AUTHENTICATED] or
|
||||||
request.GET.get('token') == player.access_token)
|
request.query.get('token') == player.access_token)
|
||||||
|
|
||||||
if not authenticated:
|
if not authenticated:
|
||||||
return web.Response(status=401)
|
return web.Response(status=401)
|
||||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||||
CONF_NAME, STATE_ON)
|
CONF_NAME, STATE_ON)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['denonavr==0.4.1']
|
REQUIREMENTS = ['denonavr==0.4.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -175,8 +175,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA):
|
if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA):
|
||||||
return
|
return
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, os.path.join(
|
load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml'))
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
for service in SERVICE_TO_METHOD:
|
for service in SERVICE_TO_METHOD:
|
||||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF)
|
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF)
|
||||||
|
|
||||||
REQUIREMENTS = ['openhomedevice==0.2.1']
|
REQUIREMENTS = ['openhomedevice==0.4.0']
|
||||||
|
|
||||||
SUPPORT_OPENHOME = SUPPORT_SELECT_SOURCE | \
|
SUPPORT_OPENHOME = SUPPORT_SELECT_SOURCE | \
|
||||||
SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
|
SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
|
||||||
|
@ -92,7 +92,7 @@ class OpenhomeDevice(MediaPlayerDevice):
|
||||||
|
|
||||||
if self._source["type"] == "Radio":
|
if self._source["type"] == "Radio":
|
||||||
self._supported_features |= SUPPORT_STOP | SUPPORT_PLAY
|
self._supported_features |= SUPPORT_STOP | SUPPORT_PLAY
|
||||||
if self._source["type"] == "Playlist":
|
if self._source["type"] in ("Playlist", "Cloud"):
|
||||||
self._supported_features |= SUPPORT_PREVIOUS_TRACK | \
|
self._supported_features |= SUPPORT_PREVIOUS_TRACK | \
|
||||||
SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PLAY
|
SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PLAY
|
||||||
|
|
||||||
|
@ -173,17 +173,17 @@ class OpenhomeDevice(MediaPlayerDevice):
|
||||||
@property
|
@property
|
||||||
def media_image_url(self):
|
def media_image_url(self):
|
||||||
"""Image url of current playing media."""
|
"""Image url of current playing media."""
|
||||||
return self._track_information["albumArt"]
|
return self._track_information["albumArtwork"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_artist(self):
|
def media_artist(self):
|
||||||
"""Artist of current playing media, music track only."""
|
"""Artist of current playing media, music track only."""
|
||||||
return self._track_information["artist"]
|
return self._track_information["artist"][0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_album_name(self):
|
def media_album_name(self):
|
||||||
"""Album name of current playing media, music track only."""
|
"""Album name of current playing media, music track only."""
|
||||||
return self._track_information["album"]
|
return self._track_information["albumTitle"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_title(self):
|
def media_title(self):
|
||||||
|
|
|
@ -165,6 +165,22 @@ shuffle_set:
|
||||||
description: True/false for enabling/disabling shuffle
|
description: True/false for enabling/disabling shuffle
|
||||||
example: true
|
example: true
|
||||||
|
|
||||||
|
snapcast_snapshot:
|
||||||
|
description: Take a snapshot of the media player.
|
||||||
|
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name(s) of entites that will be snapshotted. Platform dependent.
|
||||||
|
example: 'media_player.living_room'
|
||||||
|
|
||||||
|
snapcast_restore:
|
||||||
|
description: Restore a snapshot of the media player.
|
||||||
|
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name(s) of entites that will be restored. Platform dependent.
|
||||||
|
example: 'media_player.living_room'
|
||||||
|
|
||||||
sonos_join:
|
sonos_join:
|
||||||
description: Group player together.
|
description: Group player together.
|
||||||
|
|
||||||
|
|
|
@ -4,62 +4,197 @@ Support for interacting with Snapcast clients.
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/media_player.snapcast/
|
https://home-assistant.io/components/media_player.snapcast/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from os import path
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE,
|
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE,
|
||||||
SUPPORT_PLAY, PLATFORM_SCHEMA, MediaPlayerDevice)
|
PLATFORM_SCHEMA, MediaPlayerDevice)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, CONF_HOST, CONF_PORT)
|
STATE_ON, STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, CONF_HOST,
|
||||||
|
CONF_PORT, ATTR_ENTITY_ID)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
|
|
||||||
REQUIREMENTS = ['snapcast==1.2.2']
|
REQUIREMENTS = ['snapcast==2.0.6']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'snapcast'
|
DOMAIN = 'snapcast'
|
||||||
|
|
||||||
SUPPORT_SNAPCAST = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
SERVICE_SNAPSHOT = 'snapcast_snapshot'
|
||||||
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY
|
SERVICE_RESTORE = 'snapcast_restore'
|
||||||
|
|
||||||
|
SUPPORT_SNAPCAST_CLIENT = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET
|
||||||
|
SUPPORT_SNAPCAST_GROUP = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET |\
|
||||||
|
SUPPORT_SELECT_SOURCE
|
||||||
|
|
||||||
|
GROUP_PREFIX = 'snapcast_group_'
|
||||||
|
GROUP_SUFFIX = 'Snapcast Group'
|
||||||
|
CLIENT_PREFIX = 'snapcast_client_'
|
||||||
|
CLIENT_SUFFIX = 'Snapcast Client'
|
||||||
|
|
||||||
|
SERVICE_SCHEMA = vol.Schema({
|
||||||
|
ATTR_ENTITY_ID: cv.entity_ids,
|
||||||
|
})
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Optional(CONF_PORT): cv.port,
|
vol.Optional(CONF_PORT): cv.port
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
@asyncio.coroutine
|
||||||
"""Set up the Snapcast platform."""
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Setup the Snapcast platform."""
|
||||||
import snapcast.control
|
import snapcast.control
|
||||||
|
from snapcast.control.server import CONTROL_PORT
|
||||||
host = config.get(CONF_HOST)
|
host = config.get(CONF_HOST)
|
||||||
port = config.get(CONF_PORT, snapcast.control.CONTROL_PORT)
|
port = config.get(CONF_PORT, CONTROL_PORT)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _handle_service(service):
|
||||||
|
"""Handle services."""
|
||||||
|
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||||
|
devices = [device for device in hass.data[DOMAIN]
|
||||||
|
if device.entity_id in entity_ids]
|
||||||
|
for device in devices:
|
||||||
|
if service.service == SERVICE_SNAPSHOT:
|
||||||
|
device.snapshot()
|
||||||
|
elif service.service == SERVICE_RESTORE:
|
||||||
|
yield from device.async_restore()
|
||||||
|
|
||||||
|
descriptions = load_yaml_config_file(
|
||||||
|
path.join(path.dirname(__file__), 'services.yaml'))
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, SERVICE_SNAPSHOT, _handle_service,
|
||||||
|
descriptions.get(SERVICE_SNAPSHOT), schema=SERVICE_SCHEMA)
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, SERVICE_RESTORE, _handle_service,
|
||||||
|
descriptions.get(SERVICE_RESTORE), schema=SERVICE_SCHEMA)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server = snapcast.control.Snapserver(host, port)
|
server = yield from snapcast.control.create_server(
|
||||||
|
hass.loop, host, port)
|
||||||
except socket.gaierror:
|
except socket.gaierror:
|
||||||
_LOGGER.error(
|
_LOGGER.error('Could not connect to Snapcast server at %s:%d',
|
||||||
"Could not connect to Snapcast server at %s:%d", host, port)
|
host, port)
|
||||||
|
return False
|
||||||
|
groups = [SnapcastGroupDevice(group) for group in server.groups]
|
||||||
|
clients = [SnapcastClientDevice(client) for client in server.clients]
|
||||||
|
devices = groups + clients
|
||||||
|
hass.data[DOMAIN] = devices
|
||||||
|
async_add_devices(devices)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class SnapcastGroupDevice(MediaPlayerDevice):
|
||||||
|
"""Representation of a Snapcast group device."""
|
||||||
|
|
||||||
|
def __init__(self, group):
|
||||||
|
"""Initialize the Snapcast group device."""
|
||||||
|
group.set_callback(self.schedule_update_ha_state)
|
||||||
|
self._group = group
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the player."""
|
||||||
|
return {
|
||||||
|
'idle': STATE_IDLE,
|
||||||
|
'playing': STATE_PLAYING,
|
||||||
|
'unknown': STATE_UNKNOWN,
|
||||||
|
}.get(self._group.stream_status, STATE_UNKNOWN)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return '{}{}'.format(GROUP_PREFIX, self._group.identifier)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source(self):
|
||||||
|
"""Return the current input source."""
|
||||||
|
return self._group.stream
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume_level(self):
|
||||||
|
"""Return the volume level."""
|
||||||
|
return self._group.volume / 100
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_volume_muted(self):
|
||||||
|
"""Volume muted."""
|
||||||
|
return self._group.muted
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag media player features that are supported."""
|
||||||
|
return SUPPORT_SNAPCAST_GROUP
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source_list(self):
|
||||||
|
"""List of available input sources."""
|
||||||
|
return list(self._group.streams_by_name().keys())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
name = '{} {}'.format(self._group.friendly_name, GROUP_SUFFIX)
|
||||||
|
return {
|
||||||
|
'friendly_name': name
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""Do not poll for state."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
add_devices([SnapcastDevice(client) for client in server.clients])
|
@asyncio.coroutine
|
||||||
|
def async_select_source(self, source):
|
||||||
|
"""Set input source."""
|
||||||
|
streams = self._group.streams_by_name()
|
||||||
|
if source in streams:
|
||||||
|
yield from self._group.set_stream(streams[source].identifier)
|
||||||
|
self.hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_mute_volume(self, mute):
|
||||||
|
"""Send the mute command."""
|
||||||
|
yield from self._group.set_muted(mute)
|
||||||
|
self.hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_set_volume_level(self, volume):
|
||||||
|
"""Set the volume level."""
|
||||||
|
yield from self._group.set_volume(round(volume * 100))
|
||||||
|
self.hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
|
def snapshot(self):
|
||||||
|
"""Snapshot the group state."""
|
||||||
|
self._group.snapshot()
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_restore(self):
|
||||||
|
"""Restore the group state."""
|
||||||
|
yield from self._group.restore()
|
||||||
|
|
||||||
|
|
||||||
class SnapcastDevice(MediaPlayerDevice):
|
class SnapcastClientDevice(MediaPlayerDevice):
|
||||||
"""Representation of a Snapcast client device."""
|
"""Representation of a Snapcast client device."""
|
||||||
|
|
||||||
def __init__(self, client):
|
def __init__(self, client):
|
||||||
"""Initialize the Snapcast device."""
|
"""Initialize the Snapcast client device."""
|
||||||
|
client.set_callback(self.schedule_update_ha_state)
|
||||||
self._client = client
|
self._client = client
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
return self._client.identifier
|
return '{}{}'.format(CLIENT_PREFIX, self._client.identifier)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self):
|
||||||
|
@ -74,39 +209,45 @@ class SnapcastDevice(MediaPlayerDevice):
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Flag media player features that are supported."""
|
"""Flag media player features that are supported."""
|
||||||
return SUPPORT_SNAPCAST
|
return SUPPORT_SNAPCAST_CLIENT
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the player."""
|
"""Return the state of the player."""
|
||||||
if not self._client.connected:
|
if self._client.connected:
|
||||||
return STATE_OFF
|
return STATE_ON
|
||||||
|
return STATE_OFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
name = '{} {}'.format(self._client.friendly_name, CLIENT_SUFFIX)
|
||||||
return {
|
return {
|
||||||
'idle': STATE_IDLE,
|
'friendly_name': name
|
||||||
'playing': STATE_PLAYING,
|
}
|
||||||
'unknown': STATE_UNKNOWN,
|
|
||||||
}.get(self._client.stream.status, STATE_UNKNOWN)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source(self):
|
def should_poll(self):
|
||||||
"""Return the current input source."""
|
"""Do not poll for state."""
|
||||||
return self._client.stream.name
|
return False
|
||||||
|
|
||||||
@property
|
@asyncio.coroutine
|
||||||
def source_list(self):
|
def async_mute_volume(self, mute):
|
||||||
"""List of available input sources."""
|
|
||||||
return list(self._client.streams_by_name().keys())
|
|
||||||
|
|
||||||
def mute_volume(self, mute):
|
|
||||||
"""Send the mute command."""
|
"""Send the mute command."""
|
||||||
self._client.muted = mute
|
yield from self._client.set_muted(mute)
|
||||||
|
self.hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
def set_volume_level(self, volume):
|
@asyncio.coroutine
|
||||||
|
def async_set_volume_level(self, volume):
|
||||||
"""Set the volume level."""
|
"""Set the volume level."""
|
||||||
self._client.volume = round(volume * 100)
|
yield from self._client.set_volume(round(volume * 100))
|
||||||
|
self.hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
def select_source(self, source):
|
def snapshot(self):
|
||||||
"""Set input source."""
|
"""Snapshot the client state."""
|
||||||
streams = self._client.streams_by_name()
|
self._client.snapshot()
|
||||||
if source in streams:
|
|
||||||
self._client.stream = streams[source].identifier
|
@asyncio.coroutine
|
||||||
|
def async_restore(self):
|
||||||
|
"""Restore the client state."""
|
||||||
|
yield from self._client.restore()
|
||||||
|
|
|
@ -37,6 +37,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
# speaker every 10 seconds. Quiet it down a bit to just actual problems.
|
# speaker every 10 seconds. Quiet it down a bit to just actual problems.
|
||||||
_SOCO_LOGGER = logging.getLogger('soco')
|
_SOCO_LOGGER = logging.getLogger('soco')
|
||||||
_SOCO_LOGGER.setLevel(logging.ERROR)
|
_SOCO_LOGGER.setLevel(logging.ERROR)
|
||||||
|
_SOCO_SERVICES_LOGGER = logging.getLogger('soco.services')
|
||||||
_REQUESTS_LOGGER = logging.getLogger('requests')
|
_REQUESTS_LOGGER = logging.getLogger('requests')
|
||||||
_REQUESTS_LOGGER.setLevel(logging.ERROR)
|
_REQUESTS_LOGGER.setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
@ -73,6 +74,8 @@ ATTR_WITH_GROUP = 'with_group'
|
||||||
|
|
||||||
ATTR_IS_COORDINATOR = 'is_coordinator'
|
ATTR_IS_COORDINATOR = 'is_coordinator'
|
||||||
|
|
||||||
|
UPNP_ERRORS_TO_IGNORE = ['701']
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_ADVERTISE_ADDR): cv.string,
|
vol.Optional(CONF_ADVERTISE_ADDR): cv.string,
|
||||||
vol.Optional(CONF_INTERFACE_ADDR): cv.string,
|
vol.Optional(CONF_INTERFACE_ADDR): cv.string,
|
||||||
|
@ -255,10 +258,36 @@ def soco_error(funct):
|
||||||
return funct(*args, **kwargs)
|
return funct(*args, **kwargs)
|
||||||
except SoCoException as err:
|
except SoCoException as err:
|
||||||
_LOGGER.error("Error on %s with %s", funct.__name__, err)
|
_LOGGER.error("Error on %s with %s", funct.__name__, err)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def soco_filter_upnperror(errorcodes=None):
|
||||||
|
"""Filter out specified UPnP errors from logs."""
|
||||||
|
def decorator(funct):
|
||||||
|
"""Decorator function."""
|
||||||
|
@ft.wraps(funct)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
"""Wrap for all soco UPnP exception."""
|
||||||
|
from soco.exceptions import SoCoUPnPException
|
||||||
|
|
||||||
|
# Temporarily disable SoCo logging because it will log the
|
||||||
|
# UPnP exception otherwise
|
||||||
|
_SOCO_SERVICES_LOGGER.disabled = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
return funct(*args, **kwargs)
|
||||||
|
except SoCoUPnPException as err:
|
||||||
|
if err.error_code in errorcodes:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
_SOCO_SERVICES_LOGGER.disabled = False
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def soco_coordinator(funct):
|
def soco_coordinator(funct):
|
||||||
"""Call function on coordinator."""
|
"""Call function on coordinator."""
|
||||||
@ft.wraps(funct)
|
@ft.wraps(funct)
|
||||||
|
@ -297,6 +326,7 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
self._media_next_title = None
|
self._media_next_title = None
|
||||||
self._support_previous_track = False
|
self._support_previous_track = False
|
||||||
self._support_next_track = False
|
self._support_next_track = False
|
||||||
|
self._support_play = False
|
||||||
self._support_stop = False
|
self._support_stop = False
|
||||||
self._support_pause = False
|
self._support_pause = False
|
||||||
self._current_track_uri = None
|
self._current_track_uri = None
|
||||||
|
@ -411,6 +441,7 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
self._current_track_is_radio_stream = False
|
self._current_track_is_radio_stream = False
|
||||||
self._support_previous_track = False
|
self._support_previous_track = False
|
||||||
self._support_next_track = False
|
self._support_next_track = False
|
||||||
|
self._support_play = False
|
||||||
self._support_stop = False
|
self._support_stop = False
|
||||||
self._support_pause = False
|
self._support_pause = False
|
||||||
self._is_playing_tv = False
|
self._is_playing_tv = False
|
||||||
|
@ -494,6 +525,7 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
|
|
||||||
support_previous_track = False
|
support_previous_track = False
|
||||||
support_next_track = False
|
support_next_track = False
|
||||||
|
support_play = False
|
||||||
support_stop = False
|
support_stop = False
|
||||||
support_pause = False
|
support_pause = False
|
||||||
|
|
||||||
|
@ -515,7 +547,8 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
)
|
)
|
||||||
support_previous_track = False
|
support_previous_track = False
|
||||||
support_next_track = False
|
support_next_track = False
|
||||||
support_stop = False
|
support_play = True
|
||||||
|
support_stop = True
|
||||||
support_pause = False
|
support_pause = False
|
||||||
|
|
||||||
source_name = 'Radio'
|
source_name = 'Radio'
|
||||||
|
@ -578,6 +611,7 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
)
|
)
|
||||||
support_previous_track = True
|
support_previous_track = True
|
||||||
support_next_track = True
|
support_next_track = True
|
||||||
|
support_play = True
|
||||||
support_stop = True
|
support_stop = True
|
||||||
support_pause = True
|
support_pause = True
|
||||||
|
|
||||||
|
@ -631,7 +665,7 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
|
|
||||||
if playlist_position is not None and playlist_size is not None:
|
if playlist_position is not None and playlist_size is not None:
|
||||||
|
|
||||||
if playlist_position == 1:
|
if playlist_position <= 1:
|
||||||
support_previous_track = False
|
support_previous_track = False
|
||||||
|
|
||||||
if playlist_position == playlist_size:
|
if playlist_position == playlist_size:
|
||||||
|
@ -651,6 +685,7 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
self._current_track_is_radio_stream = is_radio_stream
|
self._current_track_is_radio_stream = is_radio_stream
|
||||||
self._support_previous_track = support_previous_track
|
self._support_previous_track = support_previous_track
|
||||||
self._support_next_track = support_next_track
|
self._support_next_track = support_next_track
|
||||||
|
self._support_play = support_play
|
||||||
self._support_stop = support_stop
|
self._support_stop = support_stop
|
||||||
self._support_pause = support_pause
|
self._support_pause = support_pause
|
||||||
self._is_playing_tv = is_playing_tv
|
self._is_playing_tv = is_playing_tv
|
||||||
|
@ -813,6 +848,9 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
if not self._support_next_track:
|
if not self._support_next_track:
|
||||||
supported = supported ^ SUPPORT_NEXT_TRACK
|
supported = supported ^ SUPPORT_NEXT_TRACK
|
||||||
|
|
||||||
|
if not self._support_play:
|
||||||
|
supported = supported ^ SUPPORT_PLAY
|
||||||
|
|
||||||
if not self._support_stop:
|
if not self._support_stop:
|
||||||
supported = supported ^ SUPPORT_STOP
|
supported = supported ^ SUPPORT_STOP
|
||||||
|
|
||||||
|
@ -889,21 +927,25 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
@soco_error
|
@soco_error
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
"""Turn off media player."""
|
"""Turn off media player."""
|
||||||
self.media_pause()
|
if self._support_pause:
|
||||||
|
self.media_pause()
|
||||||
|
|
||||||
@soco_error
|
@soco_error
|
||||||
|
@soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE)
|
||||||
@soco_coordinator
|
@soco_coordinator
|
||||||
def media_play(self):
|
def media_play(self):
|
||||||
"""Send play command."""
|
"""Send play command."""
|
||||||
self._player.play()
|
self._player.play()
|
||||||
|
|
||||||
@soco_error
|
@soco_error
|
||||||
|
@soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE)
|
||||||
@soco_coordinator
|
@soco_coordinator
|
||||||
def media_stop(self):
|
def media_stop(self):
|
||||||
"""Send stop command."""
|
"""Send stop command."""
|
||||||
self._player.stop()
|
self._player.stop()
|
||||||
|
|
||||||
@soco_error
|
@soco_error
|
||||||
|
@soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE)
|
||||||
@soco_coordinator
|
@soco_coordinator
|
||||||
def media_pause(self):
|
def media_pause(self):
|
||||||
"""Send pause command."""
|
"""Send pause command."""
|
||||||
|
@ -936,7 +978,8 @@ class SonosDevice(MediaPlayerDevice):
|
||||||
@soco_error
|
@soco_error
|
||||||
def turn_on(self):
|
def turn_on(self):
|
||||||
"""Turn the media player on."""
|
"""Turn the media player on."""
|
||||||
self.media_play()
|
if self.support_play:
|
||||||
|
self.media_play()
|
||||||
|
|
||||||
@soco_error
|
@soco_error
|
||||||
@soco_coordinator
|
@soco_coordinator
|
||||||
|
|
|
@ -40,6 +40,7 @@ AUTH_CALLBACK_NAME = 'api:spotify'
|
||||||
ICON = 'mdi:spotify'
|
ICON = 'mdi:spotify'
|
||||||
DEFAULT_NAME = 'Spotify'
|
DEFAULT_NAME = 'Spotify'
|
||||||
DOMAIN = 'spotify'
|
DOMAIN = 'spotify'
|
||||||
|
CONF_ALIASES = 'aliases'
|
||||||
CONF_CLIENT_ID = 'client_id'
|
CONF_CLIENT_ID = 'client_id'
|
||||||
CONF_CLIENT_SECRET = 'client_secret'
|
CONF_CLIENT_SECRET = 'client_secret'
|
||||||
CONF_CACHE_PATH = 'cache_path'
|
CONF_CACHE_PATH = 'cache_path'
|
||||||
|
@ -52,7 +53,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||||
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_CACHE_PATH): cv.string
|
vol.Optional(CONF_CACHE_PATH): cv.string,
|
||||||
|
vol.Optional(CONF_ALIASES, default={}): {cv.string: cv.string}
|
||||||
})
|
})
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
@ -89,7 +91,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
configurator = get_component('configurator')
|
configurator = get_component('configurator')
|
||||||
configurator.request_done(hass.data.get(DOMAIN))
|
configurator.request_done(hass.data.get(DOMAIN))
|
||||||
del hass.data[DOMAIN]
|
del hass.data[DOMAIN]
|
||||||
player = SpotifyMediaPlayer(oauth, config.get(CONF_NAME, DEFAULT_NAME))
|
player = SpotifyMediaPlayer(oauth, config.get(CONF_NAME, DEFAULT_NAME),
|
||||||
|
config[CONF_ALIASES])
|
||||||
add_devices([player], True)
|
add_devices([player], True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -110,14 +113,14 @@ class SpotifyAuthCallbackView(HomeAssistantView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Receive authorization token."""
|
"""Receive authorization token."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
self.oauth.get_access_token(request.GET['code'])
|
self.oauth.get_access_token(request.query['code'])
|
||||||
hass.async_add_job(setup_platform, hass, self.config, self.add_devices)
|
hass.async_add_job(setup_platform, hass, self.config, self.add_devices)
|
||||||
|
|
||||||
|
|
||||||
class SpotifyMediaPlayer(MediaPlayerDevice):
|
class SpotifyMediaPlayer(MediaPlayerDevice):
|
||||||
"""Representation of a Spotify controller."""
|
"""Representation of a Spotify controller."""
|
||||||
|
|
||||||
def __init__(self, oauth, name):
|
def __init__(self, oauth, name, aliases):
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self._name = name
|
self._name = name
|
||||||
self._oauth = oauth
|
self._oauth = oauth
|
||||||
|
@ -128,10 +131,11 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
||||||
self._image_url = None
|
self._image_url = None
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
self._current_device = None
|
self._current_device = None
|
||||||
self._devices = None
|
self._devices = {}
|
||||||
self._volume = None
|
self._volume = None
|
||||||
self._shuffle = False
|
self._shuffle = False
|
||||||
self._player = None
|
self._player = None
|
||||||
|
self._aliases = aliases
|
||||||
self._token_info = self._oauth.get_cached_token()
|
self._token_info = self._oauth.get_cached_token()
|
||||||
|
|
||||||
def refresh_spotify_instance(self):
|
def refresh_spotify_instance(self):
|
||||||
|
@ -154,10 +158,19 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
||||||
"""Update state and attributes."""
|
"""Update state and attributes."""
|
||||||
self.refresh_spotify_instance()
|
self.refresh_spotify_instance()
|
||||||
# Available devices
|
# Available devices
|
||||||
devices = self._player.devices().get('devices')
|
player_devices = self._player.devices()
|
||||||
|
if player_devices is not None:
|
||||||
|
devices = player_devices.get('devices')
|
||||||
if devices is not None:
|
if devices is not None:
|
||||||
self._devices = {device.get('name'): device.get('id')
|
old_devices = self._devices
|
||||||
|
self._devices = {self._aliases.get(device.get('id'),
|
||||||
|
device.get('name')):
|
||||||
|
device.get('id')
|
||||||
for device in devices}
|
for device in devices}
|
||||||
|
device_diff = {name: id for name, id in self._devices.items()
|
||||||
|
if old_devices.get(name, None) is None}
|
||||||
|
if len(device_diff) > 0:
|
||||||
|
_LOGGER.info("New Devices: %s", str(device_diff))
|
||||||
# Current playback state
|
# Current playback state
|
||||||
current = self._player.current_playback()
|
current = self._player.current_playback()
|
||||||
if current is None:
|
if current is None:
|
||||||
|
@ -212,8 +225,9 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
||||||
|
|
||||||
def select_source(self, source):
|
def select_source(self, source):
|
||||||
"""Select playback device."""
|
"""Select playback device."""
|
||||||
self._player.transfer_playback(self._devices[source],
|
if self._devices:
|
||||||
self._state == STATE_PLAYING)
|
self._player.transfer_playback(self._devices[source],
|
||||||
|
self._state == STATE_PLAYING)
|
||||||
|
|
||||||
def play_media(self, media_type, media_id, **kwargs):
|
def play_media(self, media_type, media_id, **kwargs):
|
||||||
"""Play media."""
|
"""Play media."""
|
||||||
|
@ -258,7 +272,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
||||||
@property
|
@property
|
||||||
def source_list(self):
|
def source_list(self):
|
||||||
"""Return a list of source devices."""
|
"""Return a list of source devices."""
|
||||||
return list(self._devices.keys())
|
if self._devices:
|
||||||
|
return list(self._devices.keys())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source(self):
|
def source(self):
|
||||||
|
|
|
@ -133,8 +133,8 @@ def async_setup(hass, config):
|
||||||
|
|
||||||
hass.data[DATA_MICROSOFT_FACE] = face
|
hass.data[DATA_MICROSOFT_FACE] = face
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file,
|
load_yaml_config_file,
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
|
@ -16,8 +16,7 @@ from homeassistant.const import (
|
||||||
|
|
||||||
DOMAIN = 'modbus'
|
DOMAIN = 'modbus'
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/bashwork/pymodbus/archive/'
|
REQUIREMENTS = ['pymodbus==1.3.0rc1']
|
||||||
'd7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0']
|
|
||||||
|
|
||||||
# Type of network
|
# Type of network
|
||||||
CONF_BAUDRATE = 'baudrate'
|
CONF_BAUDRATE = 'baudrate'
|
||||||
|
|
|
@ -403,8 +403,8 @@ def async_setup(hass, config):
|
||||||
yield from hass.data[DATA_MQTT].async_publish(
|
yield from hass.data[DATA_MQTT].async_publish(
|
||||||
msg_topic, payload, qos, retain)
|
msg_topic, payload, qos, retain)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, os.path.join(
|
load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml'))
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
|
@ -477,8 +477,8 @@ class MQTT(object):
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
with (yield from self._paho_lock):
|
with (yield from self._paho_lock):
|
||||||
yield from self.hass.loop.run_in_executor(
|
yield from self.hass.async_add_job(
|
||||||
None, self._mqttc.publish, topic, payload, qos, retain)
|
self._mqttc.publish, topic, payload, qos, retain)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_connect(self):
|
def async_connect(self):
|
||||||
|
@ -486,8 +486,8 @@ class MQTT(object):
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
result = yield from self.hass.loop.run_in_executor(
|
result = yield from self.hass.async_add_job(
|
||||||
None, self._mqttc.connect, self.broker, self.port, self.keepalive)
|
self._mqttc.connect, self.broker, self.port, self.keepalive)
|
||||||
|
|
||||||
if result != 0:
|
if result != 0:
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
@ -507,7 +507,7 @@ class MQTT(object):
|
||||||
self._mqttc.disconnect()
|
self._mqttc.disconnect()
|
||||||
self._mqttc.loop_stop()
|
self._mqttc.loop_stop()
|
||||||
|
|
||||||
return self.hass.loop.run_in_executor(None, stop)
|
return self.hass.async_add_job(stop)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_subscribe(self, topic, qos):
|
def async_subscribe(self, topic, qos):
|
||||||
|
@ -522,8 +522,8 @@ class MQTT(object):
|
||||||
if topic in self.topics:
|
if topic in self.topics:
|
||||||
return
|
return
|
||||||
|
|
||||||
result, mid = yield from self.hass.loop.run_in_executor(
|
result, mid = yield from self.hass.async_add_job(
|
||||||
None, self._mqttc.subscribe, topic, qos)
|
self._mqttc.subscribe, topic, qos)
|
||||||
|
|
||||||
_raise_on_error(result)
|
_raise_on_error(result)
|
||||||
self.progress[mid] = topic
|
self.progress[mid] = topic
|
||||||
|
@ -535,8 +535,8 @@ class MQTT(object):
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
result, mid = yield from self.hass.loop.run_in_executor(
|
result, mid = yield from self.hass.async_add_job(
|
||||||
None, self._mqttc.unsubscribe, topic)
|
self._mqttc.unsubscribe, topic)
|
||||||
|
|
||||||
_raise_on_error(result)
|
_raise_on_error(result)
|
||||||
self.progress[mid] = topic
|
self.progress[mid] = topic
|
||||||
|
|
|
@ -17,7 +17,8 @@ from homeassistant.components.mqtt import CONF_STATE_TOPIC
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
TOPIC_MATCHER = re.compile(
|
TOPIC_MATCHER = re.compile(
|
||||||
r'(?P<prefix_topic>\w+)/(?P<component>\w+)/(?P<object_id>\w+)/config')
|
r'(?P<prefix_topic>\w+)/(?P<component>\w+)/(?P<object_id>[a-zA-Z0-9_-]+)'
|
||||||
|
'/config')
|
||||||
|
|
||||||
SUPPORTED_COMPONENTS = ['binary_sensor', 'light', 'sensor', 'switch']
|
SUPPORTED_COMPONENTS = ['binary_sensor', 'light', 'sensor', 'switch']
|
||||||
|
|
||||||
|
@ -28,6 +29,8 @@ ALLOWED_PLATFORMS = {
|
||||||
'switch': ['mqtt'],
|
'switch': ['mqtt'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ALREADY_DISCOVERED = 'mqtt_discovered_components'
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_start(hass, discovery_topic, hass_config):
|
def async_start(hass, discovery_topic, hass_config):
|
||||||
|
@ -65,6 +68,19 @@ def async_start(hass, discovery_topic, hass_config):
|
||||||
payload[CONF_STATE_TOPIC] = '{}/{}/{}/state'.format(
|
payload[CONF_STATE_TOPIC] = '{}/{}/{}/state'.format(
|
||||||
discovery_topic, component, object_id)
|
discovery_topic, component, object_id)
|
||||||
|
|
||||||
|
if ALREADY_DISCOVERED not in hass.data:
|
||||||
|
hass.data[ALREADY_DISCOVERED] = set()
|
||||||
|
|
||||||
|
discovery_hash = (component, object_id)
|
||||||
|
if discovery_hash in hass.data[ALREADY_DISCOVERED]:
|
||||||
|
_LOGGER.info("Component has already been discovered: %s %s",
|
||||||
|
component, object_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
hass.data[ALREADY_DISCOVERED].add(discovery_hash)
|
||||||
|
|
||||||
|
_LOGGER.info("Found new component: %s %s", component, object_id)
|
||||||
|
|
||||||
yield from async_load_platform(
|
yield from async_load_platform(
|
||||||
hass, component, platform, payload, hass_config)
|
hass, component, platform, payload, hass_config)
|
||||||
|
|
||||||
|
|
|
@ -69,8 +69,8 @@ def send_message(hass, message, title=None, data=None):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup(hass, config):
|
def async_setup(hass, config):
|
||||||
"""Set up the notify services."""
|
"""Set up the notify services."""
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file,
|
load_yaml_config_file,
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
targets = {}
|
targets = {}
|
||||||
|
@ -97,8 +97,8 @@ def async_setup(hass, config):
|
||||||
notify_service = yield from \
|
notify_service = yield from \
|
||||||
platform.async_get_service(hass, p_config, discovery_info)
|
platform.async_get_service(hass, p_config, discovery_info)
|
||||||
elif hasattr(platform, 'get_service'):
|
elif hasattr(platform, 'get_service'):
|
||||||
notify_service = yield from hass.loop.run_in_executor(
|
notify_service = yield from hass.async_add_job(
|
||||||
None, platform.get_service, hass, p_config, discovery_info)
|
platform.get_service, hass, p_config, discovery_info)
|
||||||
else:
|
else:
|
||||||
raise HomeAssistantError("Invalid notify platform.")
|
raise HomeAssistantError("Invalid notify platform.")
|
||||||
|
|
||||||
|
@ -192,5 +192,5 @@ class BaseNotificationService(object):
|
||||||
kwargs can contain ATTR_TITLE to specify a title.
|
kwargs can contain ATTR_TITLE to specify a title.
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(
|
||||||
None, partial(self.send_message, message, **kwargs))
|
partial(self.send_message, message, **kwargs))
|
||||||
|
|
|
@ -246,7 +246,7 @@ class HTML5PushCallbackView(HomeAssistantView):
|
||||||
# 2a. If decode is successful, return the payload.
|
# 2a. If decode is successful, return the payload.
|
||||||
# 2b. If decode is unsuccessful, return a 401.
|
# 2b. If decode is unsuccessful, return a 401.
|
||||||
|
|
||||||
target_check = jwt.decode(token, verify=False)
|
target_check = jwt.decode(token, options={'verify_signature': False})
|
||||||
if target_check[ATTR_TARGET] in self.registrations:
|
if target_check[ATTR_TARGET] in self.registrations:
|
||||||
possible_target = self.registrations[target_check[ATTR_TARGET]]
|
possible_target = self.registrations[target_check[ATTR_TARGET]]
|
||||||
key = possible_target[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH]
|
key = possible_target[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH]
|
||||||
|
|
|
@ -85,7 +85,7 @@ class iOSNotificationService(BaseNotificationService):
|
||||||
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
if target not in ios.enabled_push_ids():
|
if target not in ios.enabled_push_ids():
|
||||||
_LOGGER.error("The target (%s) does not exist in ios.conf.",
|
_LOGGER.error("The target (%s) does not exist in .ios.conf.",
|
||||||
targets)
|
targets)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,8 @@ from homeassistant.components.notify import (
|
||||||
from homeassistant.const import (CONF_RESOURCE, CONF_METHOD, CONF_NAME)
|
from homeassistant.const import (CONF_RESOURCE, CONF_METHOD, CONF_NAME)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
CONF_DATA = 'data'
|
||||||
|
CONF_DATA_TEMPLATE = 'data_template'
|
||||||
CONF_MESSAGE_PARAMETER_NAME = 'message_param_name'
|
CONF_MESSAGE_PARAMETER_NAME = 'message_param_name'
|
||||||
CONF_TARGET_PARAMETER_NAME = 'target_param_name'
|
CONF_TARGET_PARAMETER_NAME = 'target_param_name'
|
||||||
CONF_TITLE_PARAMETER_NAME = 'title_param_name'
|
CONF_TITLE_PARAMETER_NAME = 'title_param_name'
|
||||||
|
@ -34,6 +36,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
default=DEFAULT_TARGET_PARAM_NAME): cv.string,
|
default=DEFAULT_TARGET_PARAM_NAME): cv.string,
|
||||||
vol.Optional(CONF_TITLE_PARAMETER_NAME,
|
vol.Optional(CONF_TITLE_PARAMETER_NAME,
|
||||||
default=DEFAULT_TITLE_PARAM_NAME): cv.string,
|
default=DEFAULT_TITLE_PARAM_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_DATA,
|
||||||
|
default=None): dict,
|
||||||
|
vol.Optional(CONF_DATA_TEMPLATE,
|
||||||
|
default=None): {cv.match_all: cv.template_complex}
|
||||||
})
|
})
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -46,23 +52,28 @@ def get_service(hass, config, discovery_info=None):
|
||||||
message_param_name = config.get(CONF_MESSAGE_PARAMETER_NAME)
|
message_param_name = config.get(CONF_MESSAGE_PARAMETER_NAME)
|
||||||
title_param_name = config.get(CONF_TITLE_PARAMETER_NAME)
|
title_param_name = config.get(CONF_TITLE_PARAMETER_NAME)
|
||||||
target_param_name = config.get(CONF_TARGET_PARAMETER_NAME)
|
target_param_name = config.get(CONF_TARGET_PARAMETER_NAME)
|
||||||
|
data = config.get(CONF_DATA)
|
||||||
|
data_template = config.get(CONF_DATA_TEMPLATE)
|
||||||
|
|
||||||
return RestNotificationService(
|
return RestNotificationService(
|
||||||
resource, method, message_param_name, title_param_name,
|
hass, resource, method, message_param_name,
|
||||||
target_param_name)
|
title_param_name, target_param_name, data, data_template)
|
||||||
|
|
||||||
|
|
||||||
class RestNotificationService(BaseNotificationService):
|
class RestNotificationService(BaseNotificationService):
|
||||||
"""Implementation of a notification service for REST."""
|
"""Implementation of a notification service for REST."""
|
||||||
|
|
||||||
def __init__(self, resource, method, message_param_name, title_param_name,
|
def __init__(self, hass, resource, method, message_param_name,
|
||||||
target_param_name):
|
title_param_name, target_param_name, data, data_template):
|
||||||
"""Initialize the service."""
|
"""Initialize the service."""
|
||||||
self._resource = resource
|
self._resource = resource
|
||||||
|
self._hass = hass
|
||||||
self._method = method.upper()
|
self._method = method.upper()
|
||||||
self._message_param_name = message_param_name
|
self._message_param_name = message_param_name
|
||||||
self._title_param_name = title_param_name
|
self._title_param_name = title_param_name
|
||||||
self._target_param_name = target_param_name
|
self._target_param_name = target_param_name
|
||||||
|
self._data = data
|
||||||
|
self._data_template = data_template
|
||||||
|
|
||||||
def send_message(self, message="", **kwargs):
|
def send_message(self, message="", **kwargs):
|
||||||
"""Send a message to a user."""
|
"""Send a message to a user."""
|
||||||
|
@ -79,6 +90,20 @@ class RestNotificationService(BaseNotificationService):
|
||||||
# integrations, so just return the first target in the list.
|
# integrations, so just return the first target in the list.
|
||||||
data[self._target_param_name] = kwargs[ATTR_TARGET][0]
|
data[self._target_param_name] = kwargs[ATTR_TARGET][0]
|
||||||
|
|
||||||
|
if self._data:
|
||||||
|
data.update(self._data)
|
||||||
|
elif self._data_template:
|
||||||
|
def _data_template_creator(value):
|
||||||
|
"""Recursive template creator helper function."""
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [_data_template_creator(item) for item in value]
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
return {key: _data_template_creator(item)
|
||||||
|
for key, item in value.items()}
|
||||||
|
value.hass = self._hass
|
||||||
|
return value.async_render(kwargs)
|
||||||
|
data.update(_data_template_creator(self._data_template))
|
||||||
|
|
||||||
if self._method == 'POST':
|
if self._method == 'POST':
|
||||||
response = requests.post(self._resource, data=data, timeout=10)
|
response = requests.post(self._resource, data=data, timeout=10)
|
||||||
elif self._method == 'POST_JSON':
|
elif self._method == 'POST_JSON':
|
||||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.const import (
|
||||||
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
|
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['slacker==0.9.42']
|
REQUIREMENTS = ['slacker==0.9.50']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -12,16 +12,17 @@ from email.mime.image import MIMEImage
|
||||||
from email.mime.application import MIMEApplication
|
from email.mime.application import MIMEApplication
|
||||||
import email.utils
|
import email.utils
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.components.notify import (
|
from homeassistant.components.notify import (
|
||||||
ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA,
|
ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA,
|
||||||
BaseNotificationService)
|
BaseNotificationService)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_TIMEOUT,
|
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_TIMEOUT,
|
||||||
CONF_SENDER, CONF_RECIPIENT)
|
CONF_SENDER, CONF_RECIPIENT)
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
import homeassistant.util.dt as dt_util
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -42,10 +43,10 @@ DEFAULT_STARTTLS = False
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [vol.Email()]),
|
vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [vol.Email()]),
|
||||||
|
vol.Required(CONF_SENDER): vol.Email(),
|
||||||
vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string,
|
vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||||
vol.Optional(CONF_SENDER): vol.Email(),
|
|
||||||
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
|
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
|
||||||
vol.Optional(CONF_USERNAME): cv.string,
|
vol.Optional(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_PASSWORD): cv.string,
|
vol.Optional(CONF_PASSWORD): cv.string,
|
||||||
|
@ -75,11 +76,11 @@ def get_service(hass, config, discovery_info=None):
|
||||||
|
|
||||||
|
|
||||||
class MailNotificationService(BaseNotificationService):
|
class MailNotificationService(BaseNotificationService):
|
||||||
"""Implement the notification service for E-Mail messages."""
|
"""Implement the notification service for E-mail messages."""
|
||||||
|
|
||||||
def __init__(self, server, port, timeout, sender, starttls, username,
|
def __init__(self, server, port, timeout, sender, starttls, username,
|
||||||
password, recipients, sender_name, debug):
|
password, recipients, sender_name, debug):
|
||||||
"""Initialize the service."""
|
"""Initialize the SMTP service."""
|
||||||
self._server = server
|
self._server = server
|
||||||
self._port = port
|
self._port = port
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
|
@ -142,11 +143,11 @@ class MailNotificationService(BaseNotificationService):
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
if ATTR_HTML in data:
|
if ATTR_HTML in data:
|
||||||
msg = _build_html_msg(message, data[ATTR_HTML],
|
msg = _build_html_msg(
|
||||||
images=data.get(ATTR_IMAGES))
|
message, data[ATTR_HTML], images=data.get(ATTR_IMAGES))
|
||||||
else:
|
else:
|
||||||
msg = _build_multipart_msg(message,
|
msg = _build_multipart_msg(
|
||||||
images=data.get(ATTR_IMAGES))
|
message, images=data.get(ATTR_IMAGES))
|
||||||
else:
|
else:
|
||||||
msg = _build_text_msg(message)
|
msg = _build_text_msg(message)
|
||||||
|
|
||||||
|
@ -167,8 +168,7 @@ class MailNotificationService(BaseNotificationService):
|
||||||
mail = self.connect()
|
mail = self.connect()
|
||||||
for _ in range(self.tries):
|
for _ in range(self.tries):
|
||||||
try:
|
try:
|
||||||
mail.sendmail(self._sender, self.recipients,
|
mail.sendmail(self._sender, self.recipients, msg.as_string())
|
||||||
msg.as_string())
|
|
||||||
break
|
break
|
||||||
except smtplib.SMTPServerDisconnected:
|
except smtplib.SMTPServerDisconnected:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
|
@ -210,7 +210,7 @@ def _build_multipart_msg(message, images):
|
||||||
msg.attach(attachment)
|
msg.attach(attachment)
|
||||||
attachment.add_header('Content-ID', '<{}>'.format(cid))
|
attachment.add_header('Content-ID', '<{}>'.format(cid))
|
||||||
except TypeError:
|
except TypeError:
|
||||||
_LOGGER.warning("Attachment %s has an unkown MIME type. "
|
_LOGGER.warning("Attachment %s has an unknown MIME type. "
|
||||||
"Falling back to file", atch_name)
|
"Falling back to file", atch_name)
|
||||||
attachment = MIMEApplication(file_bytes, Name=atch_name)
|
attachment = MIMEApplication(file_bytes, Name=atch_name)
|
||||||
attachment['Content-Disposition'] = ('attachment; '
|
attachment['Content-Disposition'] = ('attachment; '
|
||||||
|
@ -226,8 +226,8 @@ def _build_multipart_msg(message, images):
|
||||||
|
|
||||||
|
|
||||||
def _build_html_msg(text, html, images):
|
def _build_html_msg(text, html, images):
|
||||||
"""Build Multipart message with in-line images and rich html (UTF-8)."""
|
"""Build Multipart message with in-line images and rich HTML (UTF-8)."""
|
||||||
_LOGGER.debug("Building html rich email")
|
_LOGGER.debug("Building HTML rich email")
|
||||||
msg = MIMEMultipart('related')
|
msg = MIMEMultipart('related')
|
||||||
alternative = MIMEMultipart('alternative')
|
alternative = MIMEMultipart('alternative')
|
||||||
alternative.attach(MIMEText(text, _charset='utf-8'))
|
alternative.attach(MIMEText(text, _charset='utf-8'))
|
||||||
|
@ -242,6 +242,6 @@ def _build_html_msg(text, html, images):
|
||||||
msg.attach(attachment)
|
msg.attach(attachment)
|
||||||
attachment.add_header('Content-ID', '<{}>'.format(name))
|
attachment.add_header('Content-ID', '<{}>'.format(name))
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
_LOGGER.warning('Attachment %s [#%s] not found. Skipping',
|
_LOGGER.warning("Attachment %s [#%s] not found. Skipping",
|
||||||
atch_name, atch_num)
|
atch_name, atch_num)
|
||||||
return msg
|
return msg
|
||||||
|
|
|
@ -92,8 +92,8 @@ def async_setup(hass, config):
|
||||||
|
|
||||||
hass.states.async_set(entity_id, message, attr)
|
hass.states.async_set(entity_id, message, attr)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, os.path.join(
|
load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml')
|
os.path.dirname(__file__), 'services.yaml')
|
||||||
)
|
)
|
||||||
hass.services.async_register(DOMAIN, SERVICE_CREATE, create_service,
|
hass.services.async_register(DOMAIN, SERVICE_CREATE, create_service,
|
||||||
|
|
|
@ -33,7 +33,7 @@ from . import purge, migration
|
||||||
from .const import DATA_INSTANCE
|
from .const import DATA_INSTANCE
|
||||||
from .util import session_scope
|
from .util import session_scope
|
||||||
|
|
||||||
REQUIREMENTS = ['sqlalchemy==1.1.9']
|
REQUIREMENTS = ['sqlalchemy==1.1.10']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ DEFAULT_DB_FILE = 'home-assistant_v2.db'
|
||||||
|
|
||||||
CONF_DB_URL = 'db_url'
|
CONF_DB_URL = 'db_url'
|
||||||
CONF_PURGE_DAYS = 'purge_days'
|
CONF_PURGE_DAYS = 'purge_days'
|
||||||
|
CONF_EVENT_TYPES = 'event_types'
|
||||||
|
|
||||||
CONNECT_RETRY_WAIT = 3
|
CONNECT_RETRY_WAIT = 3
|
||||||
|
|
||||||
|
@ -51,6 +52,8 @@ FILTER_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({
|
vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({
|
||||||
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
|
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
|
||||||
vol.Optional(CONF_DOMAINS, default=[]):
|
vol.Optional(CONF_DOMAINS, default=[]):
|
||||||
|
vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
vol.Optional(CONF_EVENT_TYPES, default=[]):
|
||||||
vol.All(cv.ensure_list, [cv.string])
|
vol.All(cv.ensure_list, [cv.string])
|
||||||
}),
|
}),
|
||||||
vol.Optional(CONF_INCLUDE, default={}): vol.Schema({
|
vol.Optional(CONF_INCLUDE, default={}): vol.Schema({
|
||||||
|
@ -142,6 +145,7 @@ class Recorder(threading.Thread):
|
||||||
self.include_d = include.get(CONF_DOMAINS, [])
|
self.include_d = include.get(CONF_DOMAINS, [])
|
||||||
self.exclude = exclude.get(CONF_ENTITIES, []) + \
|
self.exclude = exclude.get(CONF_ENTITIES, []) + \
|
||||||
exclude.get(CONF_DOMAINS, [])
|
exclude.get(CONF_DOMAINS, [])
|
||||||
|
self.exclude_t = exclude.get(CONF_EVENT_TYPES, [])
|
||||||
|
|
||||||
self.get_session = None
|
self.get_session = None
|
||||||
|
|
||||||
|
@ -245,6 +249,9 @@ class Recorder(threading.Thread):
|
||||||
elif event.event_type == EVENT_TIME_CHANGED:
|
elif event.event_type == EVENT_TIME_CHANGED:
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
continue
|
continue
|
||||||
|
elif event.event_type in self.exclude_t:
|
||||||
|
self.queue.task_done()
|
||||||
|
continue
|
||||||
|
|
||||||
entity_id = event.data.get(ATTR_ENTITY_ID)
|
entity_id = event.data.get(ATTR_ENTITY_ID)
|
||||||
if entity_id is not None:
|
if entity_id is not None:
|
||||||
|
|
|
@ -26,6 +26,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
ATTR_ACTIVITY = 'activity'
|
ATTR_ACTIVITY = 'activity'
|
||||||
ATTR_COMMAND = 'command'
|
ATTR_COMMAND = 'command'
|
||||||
ATTR_DEVICE = 'device'
|
ATTR_DEVICE = 'device'
|
||||||
|
ATTR_NUM_REPEATS = 'num_repeats'
|
||||||
|
ATTR_DELAY_SECS = 'delay_secs'
|
||||||
|
|
||||||
DOMAIN = 'remote'
|
DOMAIN = 'remote'
|
||||||
|
|
||||||
|
@ -40,6 +42,9 @@ SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
SERVICE_SEND_COMMAND = 'send_command'
|
SERVICE_SEND_COMMAND = 'send_command'
|
||||||
SERVICE_SYNC = 'sync'
|
SERVICE_SYNC = 'sync'
|
||||||
|
|
||||||
|
DEFAULT_NUM_REPEATS = '1'
|
||||||
|
DEFAULT_DELAY_SECS = '0.4'
|
||||||
|
|
||||||
REMOTE_SERVICE_SCHEMA = vol.Schema({
|
REMOTE_SERVICE_SCHEMA = vol.Schema({
|
||||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
})
|
})
|
||||||
|
@ -50,7 +55,9 @@ REMOTE_SERVICE_TURN_ON_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({
|
||||||
|
|
||||||
REMOTE_SERVICE_SEND_COMMAND_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({
|
REMOTE_SERVICE_SEND_COMMAND_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({
|
||||||
vol.Required(ATTR_DEVICE): cv.string,
|
vol.Required(ATTR_DEVICE): cv.string,
|
||||||
vol.Required(ATTR_COMMAND): cv.string,
|
vol.Required(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
vol.Optional(ATTR_NUM_REPEATS, default=DEFAULT_NUM_REPEATS): cv.string,
|
||||||
|
vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): cv.string
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,11 +81,19 @@ def turn_off(hass, entity_id=None):
|
||||||
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||||
|
|
||||||
|
|
||||||
def send_command(hass, device, command, entity_id=None):
|
def send_command(hass, device, command, entity_id=None,
|
||||||
|
num_repeats=None, delay_secs=None):
|
||||||
"""Send a command to a device."""
|
"""Send a command to a device."""
|
||||||
data = {ATTR_DEVICE: str(device), ATTR_COMMAND: command}
|
data = {ATTR_DEVICE: str(device), ATTR_COMMAND: command}
|
||||||
if entity_id:
|
if entity_id:
|
||||||
data[ATTR_ENTITY_ID] = entity_id
|
data[ATTR_ENTITY_ID] = entity_id
|
||||||
|
|
||||||
|
if num_repeats:
|
||||||
|
data[ATTR_NUM_REPEATS] = num_repeats
|
||||||
|
|
||||||
|
if delay_secs:
|
||||||
|
data[ATTR_DELAY_SECS] = delay_secs
|
||||||
|
|
||||||
hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data)
|
hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -97,13 +112,16 @@ def async_setup(hass, config):
|
||||||
activity_id = service.data.get(ATTR_ACTIVITY)
|
activity_id = service.data.get(ATTR_ACTIVITY)
|
||||||
device = service.data.get(ATTR_DEVICE)
|
device = service.data.get(ATTR_DEVICE)
|
||||||
command = service.data.get(ATTR_COMMAND)
|
command = service.data.get(ATTR_COMMAND)
|
||||||
|
num_repeats = service.data.get(ATTR_NUM_REPEATS)
|
||||||
|
delay_secs = service.data.get(ATTR_DELAY_SECS)
|
||||||
|
|
||||||
for remote in target_remotes:
|
for remote in target_remotes:
|
||||||
if service.service == SERVICE_TURN_ON:
|
if service.service == SERVICE_TURN_ON:
|
||||||
yield from remote.async_turn_on(activity=activity_id)
|
yield from remote.async_turn_on(activity=activity_id)
|
||||||
elif service.service == SERVICE_SEND_COMMAND:
|
elif service.service == SERVICE_SEND_COMMAND:
|
||||||
yield from remote.async_send_command(
|
yield from remote.async_send_command(
|
||||||
device=device, command=command)
|
device=device, command=command,
|
||||||
|
num_repeats=num_repeats, delay_secs=delay_secs)
|
||||||
else:
|
else:
|
||||||
yield from remote.async_turn_off()
|
yield from remote.async_turn_off()
|
||||||
|
|
||||||
|
@ -122,8 +140,8 @@ def async_setup(hass, config):
|
||||||
if update_tasks:
|
if update_tasks:
|
||||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||||
|
|
||||||
descriptions = yield from hass.loop.run_in_executor(
|
descriptions = yield from hass.async_add_job(
|
||||||
None, load_yaml_config_file, os.path.join(
|
load_yaml_config_file, os.path.join(
|
||||||
os.path.dirname(__file__), 'services.yaml'))
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_TURN_OFF, async_handle_remote_service,
|
DOMAIN, SERVICE_TURN_OFF, async_handle_remote_service,
|
||||||
|
@ -153,5 +171,4 @@ class RemoteDevice(ToggleEntity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(ft.partial(self.send_command, **kwargs))
|
||||||
None, ft.partial(self.send_command, **kwargs))
|
|
||||||
|
|
|
@ -1,187 +1,189 @@
|
||||||
"""
|
"""
|
||||||
Support for Harmony Hub devices.
|
Support for Harmony Hub devices.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/remote.harmony/
|
https://home-assistant.io/components/remote.harmony/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from os import path
|
from os import path
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.components.remote as remote
|
import homeassistant.components.remote as remote
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID)
|
CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID)
|
||||||
from homeassistant.components.remote import (
|
from homeassistant.components.remote import (
|
||||||
PLATFORM_SCHEMA, DOMAIN, ATTR_DEVICE, ATTR_COMMAND, ATTR_ACTIVITY)
|
PLATFORM_SCHEMA, DOMAIN, ATTR_DEVICE, ATTR_COMMAND,
|
||||||
from homeassistant.util import slugify
|
ATTR_ACTIVITY, ATTR_NUM_REPEATS, ATTR_DELAY_SECS)
|
||||||
from homeassistant.config import load_yaml_config_file
|
from homeassistant.util import slugify
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
REQUIREMENTS = ['pyharmony==1.0.12']
|
|
||||||
|
REQUIREMENTS = ['pyharmony==1.0.16']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DEFAULT_PORT = 5222
|
|
||||||
DEVICES = []
|
DEFAULT_PORT = 5222
|
||||||
|
DEVICES = []
|
||||||
SERVICE_SYNC = 'harmony_sync'
|
|
||||||
|
SERVICE_SYNC = 'harmony_sync'
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
||||||
vol.Required(CONF_NAME): cv.string,
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Required(ATTR_ACTIVITY, default=None): cv.string,
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
})
|
vol.Required(ATTR_ACTIVITY, default=None): cv.string,
|
||||||
|
})
|
||||||
HARMONY_SYNC_SCHEMA = vol.Schema({
|
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
HARMONY_SYNC_SCHEMA = vol.Schema({
|
||||||
})
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
|
})
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
|
||||||
"""Set up the Harmony platform."""
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
import pyharmony
|
"""Set up the Harmony platform."""
|
||||||
global DEVICES
|
import pyharmony
|
||||||
|
global DEVICES
|
||||||
name = config.get(CONF_NAME)
|
|
||||||
host = config.get(CONF_HOST)
|
name = config.get(CONF_NAME)
|
||||||
port = config.get(CONF_PORT)
|
host = config.get(CONF_HOST)
|
||||||
_LOGGER.debug("Loading Harmony platform: %s", name)
|
port = config.get(CONF_PORT)
|
||||||
|
_LOGGER.debug("Loading Harmony platform: %s", name)
|
||||||
harmony_conf_file = hass.config.path(
|
|
||||||
'{}{}{}'.format('harmony_', slugify(name), '.conf'))
|
harmony_conf_file = hass.config.path(
|
||||||
|
'{}{}{}'.format('harmony_', slugify(name), '.conf'))
|
||||||
try:
|
|
||||||
_LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s",
|
try:
|
||||||
host, port)
|
_LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s",
|
||||||
token = urllib.parse.quote_plus(pyharmony.ha_get_token(host, port))
|
host, port)
|
||||||
except ValueError as err:
|
token = urllib.parse.quote_plus(pyharmony.ha_get_token(host, port))
|
||||||
_LOGGER.warning("%s for remote: %s", err.args[0], name)
|
except ValueError as err:
|
||||||
return False
|
_LOGGER.warning("%s for remote: %s", err.args[0], name)
|
||||||
|
return False
|
||||||
_LOGGER.debug("Received token: %s", token)
|
|
||||||
DEVICES = [HarmonyRemote(
|
_LOGGER.debug("Received token: %s", token)
|
||||||
config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT),
|
DEVICES = [HarmonyRemote(
|
||||||
config.get(ATTR_ACTIVITY), harmony_conf_file, token)]
|
config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT),
|
||||||
add_devices(DEVICES, True)
|
config.get(ATTR_ACTIVITY), harmony_conf_file, token)]
|
||||||
register_services(hass)
|
add_devices(DEVICES, True)
|
||||||
return True
|
register_services(hass)
|
||||||
|
return True
|
||||||
|
|
||||||
def register_services(hass):
|
|
||||||
"""Register all services for harmony devices."""
|
def register_services(hass):
|
||||||
descriptions = load_yaml_config_file(
|
"""Register all services for harmony devices."""
|
||||||
path.join(path.dirname(__file__), 'services.yaml'))
|
descriptions = load_yaml_config_file(
|
||||||
|
path.join(path.dirname(__file__), 'services.yaml'))
|
||||||
hass.services.register(
|
|
||||||
DOMAIN, SERVICE_SYNC, _sync_service, descriptions.get(SERVICE_SYNC),
|
hass.services.register(
|
||||||
schema=HARMONY_SYNC_SCHEMA)
|
DOMAIN, SERVICE_SYNC, _sync_service, descriptions.get(SERVICE_SYNC),
|
||||||
|
schema=HARMONY_SYNC_SCHEMA)
|
||||||
|
|
||||||
def _apply_service(service, service_func, *service_func_args):
|
|
||||||
"""Handle services to apply."""
|
def _apply_service(service, service_func, *service_func_args):
|
||||||
entity_ids = service.data.get('entity_id')
|
"""Handle services to apply."""
|
||||||
|
entity_ids = service.data.get('entity_id')
|
||||||
if entity_ids:
|
|
||||||
_devices = [device for device in DEVICES
|
if entity_ids:
|
||||||
if device.entity_id in entity_ids]
|
_devices = [device for device in DEVICES
|
||||||
else:
|
if device.entity_id in entity_ids]
|
||||||
_devices = DEVICES
|
else:
|
||||||
|
_devices = DEVICES
|
||||||
for device in _devices:
|
|
||||||
service_func(device, *service_func_args)
|
for device in _devices:
|
||||||
device.schedule_update_ha_state(True)
|
service_func(device, *service_func_args)
|
||||||
|
device.schedule_update_ha_state(True)
|
||||||
|
|
||||||
def _sync_service(service):
|
|
||||||
_apply_service(service, HarmonyRemote.sync)
|
def _sync_service(service):
|
||||||
|
_apply_service(service, HarmonyRemote.sync)
|
||||||
|
|
||||||
class HarmonyRemote(remote.RemoteDevice):
|
|
||||||
"""Remote representation used to control a Harmony device."""
|
class HarmonyRemote(remote.RemoteDevice):
|
||||||
|
"""Remote representation used to control a Harmony device."""
|
||||||
def __init__(self, name, host, port, activity, out_path, token):
|
|
||||||
"""Initialize HarmonyRemote class."""
|
def __init__(self, name, host, port, activity, out_path, token):
|
||||||
import pyharmony
|
"""Initialize HarmonyRemote class."""
|
||||||
from pathlib import Path
|
import pyharmony
|
||||||
|
from pathlib import Path
|
||||||
_LOGGER.debug("HarmonyRemote device init started for: %s", name)
|
|
||||||
self._name = name
|
_LOGGER.debug("HarmonyRemote device init started for: %s", name)
|
||||||
self._ip = host
|
self._name = name
|
||||||
self._port = port
|
self._ip = host
|
||||||
self._state = None
|
self._port = port
|
||||||
self._current_activity = None
|
self._state = None
|
||||||
self._default_activity = activity
|
self._current_activity = None
|
||||||
self._token = token
|
self._default_activity = activity
|
||||||
self._config_path = out_path
|
self._token = token
|
||||||
_LOGGER.debug("Retrieving harmony config using token: %s", token)
|
self._config_path = out_path
|
||||||
self._config = pyharmony.ha_get_config(self._token, host, port)
|
_LOGGER.debug("Retrieving harmony config using token: %s", token)
|
||||||
if not Path(self._config_path).is_file():
|
self._config = pyharmony.ha_get_config(self._token, host, port)
|
||||||
_LOGGER.debug("Writing harmony configuration to file: %s",
|
if not Path(self._config_path).is_file():
|
||||||
out_path)
|
_LOGGER.debug("Writing harmony configuration to file: %s",
|
||||||
pyharmony.ha_write_config_file(self._config, self._config_path)
|
out_path)
|
||||||
|
pyharmony.ha_write_config_file(self._config, self._config_path)
|
||||||
@property
|
|
||||||
def name(self):
|
@property
|
||||||
"""Return the Harmony device's name."""
|
def name(self):
|
||||||
return self._name
|
"""Return the Harmony device's name."""
|
||||||
|
return self._name
|
||||||
@property
|
|
||||||
def device_state_attributes(self):
|
@property
|
||||||
"""Add platform specific attributes."""
|
def device_state_attributes(self):
|
||||||
return {'current_activity': self._current_activity}
|
"""Add platform specific attributes."""
|
||||||
|
return {'current_activity': self._current_activity}
|
||||||
@property
|
|
||||||
def is_on(self):
|
@property
|
||||||
"""Return False if PowerOff is the current activity, otherwise True."""
|
def is_on(self):
|
||||||
return self._current_activity != 'PowerOff'
|
"""Return False if PowerOff is the current activity, otherwise True."""
|
||||||
|
return self._current_activity != 'PowerOff'
|
||||||
def update(self):
|
|
||||||
"""Return current activity."""
|
def update(self):
|
||||||
import pyharmony
|
"""Return current activity."""
|
||||||
name = self._name
|
import pyharmony
|
||||||
_LOGGER.debug("Polling %s for current activity", name)
|
name = self._name
|
||||||
state = pyharmony.ha_get_current_activity(
|
_LOGGER.debug("Polling %s for current activity", name)
|
||||||
self._token, self._config, self._ip, self._port)
|
state = pyharmony.ha_get_current_activity(
|
||||||
_LOGGER.debug("%s current activity reported as: %s", name, state)
|
self._token, self._config, self._ip, self._port)
|
||||||
self._current_activity = state
|
_LOGGER.debug("%s current activity reported as: %s", name, state)
|
||||||
self._state = bool(state != 'PowerOff')
|
self._current_activity = state
|
||||||
|
self._state = bool(state != 'PowerOff')
|
||||||
def turn_on(self, **kwargs):
|
|
||||||
"""Start an activity from the Harmony device."""
|
def turn_on(self, **kwargs):
|
||||||
import pyharmony
|
"""Start an activity from the Harmony device."""
|
||||||
if kwargs[ATTR_ACTIVITY]:
|
import pyharmony
|
||||||
activity = kwargs[ATTR_ACTIVITY]
|
if kwargs[ATTR_ACTIVITY]:
|
||||||
else:
|
activity = kwargs[ATTR_ACTIVITY]
|
||||||
activity = self._default_activity
|
else:
|
||||||
|
activity = self._default_activity
|
||||||
if activity:
|
|
||||||
pyharmony.ha_start_activity(
|
if activity:
|
||||||
self._token, self._ip, self._port, self._config, activity)
|
pyharmony.ha_start_activity(
|
||||||
self._state = True
|
self._token, self._ip, self._port, self._config, activity)
|
||||||
else:
|
self._state = True
|
||||||
_LOGGER.error("No activity specified with turn_on service")
|
else:
|
||||||
|
_LOGGER.error("No activity specified with turn_on service")
|
||||||
def turn_off(self):
|
|
||||||
"""Start the PowerOff activity."""
|
def turn_off(self):
|
||||||
import pyharmony
|
"""Start the PowerOff activity."""
|
||||||
pyharmony.ha_power_off(self._token, self._ip, self._port)
|
import pyharmony
|
||||||
|
pyharmony.ha_power_off(self._token, self._ip, self._port)
|
||||||
def send_command(self, **kwargs):
|
|
||||||
"""Send a command to one device."""
|
def send_command(self, **kwargs):
|
||||||
import pyharmony
|
"""Send a set of commands to one device."""
|
||||||
pyharmony.ha_send_command(
|
import pyharmony
|
||||||
self._token, self._ip, self._port, kwargs[ATTR_DEVICE],
|
pyharmony.ha_send_commands(
|
||||||
kwargs[ATTR_COMMAND])
|
self._token, self._ip, self._port, kwargs[ATTR_DEVICE],
|
||||||
|
kwargs[ATTR_COMMAND], int(kwargs[ATTR_NUM_REPEATS]),
|
||||||
def sync(self):
|
float(kwargs[ATTR_DELAY_SECS]))
|
||||||
"""Sync the Harmony device with the web service."""
|
|
||||||
import pyharmony
|
def sync(self):
|
||||||
_LOGGER.debug("Syncing hub with Harmony servers")
|
"""Sync the Harmony device with the web service."""
|
||||||
pyharmony.ha_sync(self._token, self._ip, self._port)
|
import pyharmony
|
||||||
self._config = pyharmony.ha_get_config(
|
_LOGGER.debug("Syncing hub with Harmony servers")
|
||||||
self._token, self._ip, self._port)
|
pyharmony.ha_sync(self._token, self._ip, self._port)
|
||||||
_LOGGER.debug("Writing hub config to file: %s", self._config_path)
|
self._config = pyharmony.ha_get_config(
|
||||||
pyharmony.ha_write_config_file(self._config, self._config_path)
|
self._token, self._ip, self._port)
|
||||||
|
_LOGGER.debug("Writing hub config to file: %s", self._config_path)
|
||||||
|
pyharmony.ha_write_config_file(self._config, self._config_path)
|
||||||
|
|
|
@ -104,7 +104,8 @@ class ITachIP2IRRemote(remote.RemoteDevice):
|
||||||
|
|
||||||
def send_command(self, **kwargs):
|
def send_command(self, **kwargs):
|
||||||
"""Send a command to one device."""
|
"""Send a command to one device."""
|
||||||
self.itachip2ir.send(self._name, kwargs[ATTR_COMMAND], 1)
|
for command in kwargs[ATTR_COMMAND]:
|
||||||
|
self.itachip2ir.send(self._name, command, 1)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update the device."""
|
"""Update the device."""
|
||||||
|
|
|
@ -64,16 +64,15 @@ class KiraRemote(Entity):
|
||||||
|
|
||||||
def send_command(self, **kwargs):
|
def send_command(self, **kwargs):
|
||||||
"""Send a command to one device."""
|
"""Send a command to one device."""
|
||||||
code_tuple = (kwargs.get(remote.ATTR_COMMAND),
|
for command in kwargs.get(remote.ATTR_COMMAND):
|
||||||
kwargs.get(remote.ATTR_DEVICE))
|
code_tuple = (command,
|
||||||
_LOGGER.info("Sending Command: %s to %s", *code_tuple)
|
kwargs.get(remote.ATTR_DEVICE))
|
||||||
|
_LOGGER.info("Sending Command: %s to %s", *code_tuple)
|
||||||
self._kira.sendCode(code_tuple)
|
self._kira.sendCode(code_tuple)
|
||||||
|
|
||||||
def async_send_command(self, **kwargs):
|
def async_send_command(self, **kwargs):
|
||||||
"""Send a command to a device.
|
"""Send a command to a device.
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(
|
return self.hass.async_add_job(ft.partial(self.send_command, **kwargs))
|
||||||
None, ft.partial(self.send_command, **kwargs))
|
|
||||||
|
|
|
@ -30,8 +30,14 @@ send_command:
|
||||||
description: Device ID to send command to
|
description: Device ID to send command to
|
||||||
example: '32756745'
|
example: '32756745'
|
||||||
command:
|
command:
|
||||||
description: Command to send
|
description: A single command or a list of commands to send.
|
||||||
example: 'Play'
|
example: 'Play'
|
||||||
|
num_repeats:
|
||||||
|
description: An optional value that specifies the number of times you want to repeat the command(s). If not specified, the command(s) will not be repeated
|
||||||
|
example: '5'
|
||||||
|
delay_secs:
|
||||||
|
description: An optional value that specifies that number of seconds you want to wait in between repeated commands. If not specified, the default of 0.4 seconds will be used
|
||||||
|
example: '0.75'
|
||||||
|
|
||||||
harmony_sync:
|
harmony_sync:
|
||||||
description: Syncs the remote's configuration
|
description: Syncs the remote's configuration
|
||||||
|
|
|
@ -371,7 +371,7 @@ class RflinkCommand(RflinkDevice):
|
||||||
# Rflink protocol/transport handles asynchronous writing of buffer
|
# Rflink protocol/transport handles asynchronous writing of buffer
|
||||||
# to serial/tcp device. Does not wait for command send
|
# to serial/tcp device. Does not wait for command send
|
||||||
# confirmation.
|
# confirmation.
|
||||||
self.hass.loop.run_in_executor(None, ft.partial(
|
self.hass.async_add_job(ft.partial(
|
||||||
self._protocol.send_command, self._device_id, cmd))
|
self._protocol.send_command, self._device_id, cmd))
|
||||||
|
|
||||||
if repetitions > 1:
|
if repetitions > 1:
|
||||||
|
|
|
@ -112,4 +112,4 @@ class Scene(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.loop.run_in_executor(None, self.activate)
|
return self.hass.async_add_job(self.activate)
|
||||||
|
|
|
@ -4,76 +4,92 @@ Support for Powerview scenes from a Powerview hub.
|
||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/scene.hunterdouglas_powerview/
|
https://home-assistant.io/components/scene.hunterdouglas_powerview/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.scene import Scene, DOMAIN
|
from homeassistant.components.scene import Scene, DOMAIN
|
||||||
from homeassistant.helpers.entity import generate_entity_id
|
from homeassistant.const import CONF_PLATFORM
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.entity import async_generate_entity_id
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = [
|
REQUIREMENTS = ['aiopvapi==1.4']
|
||||||
'https://github.com/sander76/powerviewApi/archive'
|
|
||||||
'/246e782d60d5c0addcc98d7899a0186f9d5640b0.zip#powerviewApi==0.3.15'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
HUB_ADDRESS = 'address'
|
HUB_ADDRESS = 'address'
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(CONF_PLATFORM): 'hunterdouglas_powerview',
|
||||||
|
vol.Required(HUB_ADDRESS): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
SCENE_DATA = 'sceneData'
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
ROOM_DATA = 'roomData'
|
||||||
"""Set up the powerview scenes stored in a Powerview hub."""
|
SCENE_NAME = 'name'
|
||||||
from powerview_api import powerview
|
ROOM_NAME = 'name'
|
||||||
|
SCENE_ID = 'id'
|
||||||
|
ROOM_ID = 'id'
|
||||||
|
ROOM_ID_IN_SCENE = 'roomId'
|
||||||
|
STATE_ATTRIBUTE_ROOM_NAME = 'roomName'
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Set up home assistant scene entries."""
|
||||||
|
from aiopvapi.hub import Hub
|
||||||
|
|
||||||
hub_address = config.get(HUB_ADDRESS)
|
hub_address = config.get(HUB_ADDRESS)
|
||||||
|
websession = async_get_clientsession(hass)
|
||||||
|
|
||||||
_pv = powerview.PowerView(hub_address)
|
_hub = Hub(hub_address, hass.loop, websession)
|
||||||
try:
|
_scenes = yield from _hub.scenes.get_scenes()
|
||||||
_scenes = _pv.get_scenes()
|
_rooms = yield from _hub.rooms.get_rooms()
|
||||||
_rooms = _pv.get_rooms()
|
|
||||||
except ConnectionError:
|
|
||||||
_LOGGER.exception("Error connecting to powerview "
|
|
||||||
"hub with ip address: %s", hub_address)
|
|
||||||
return False
|
|
||||||
add_devices(PowerViewScene(hass, scene, _rooms, _pv)
|
|
||||||
for scene in _scenes['sceneData'])
|
|
||||||
|
|
||||||
return True
|
if not _scenes or not _rooms:
|
||||||
|
return
|
||||||
|
pvscenes = (PowerViewScene(hass, _scene, _rooms, _hub)
|
||||||
|
for _scene in _scenes[SCENE_DATA])
|
||||||
|
async_add_devices(pvscenes)
|
||||||
|
|
||||||
|
|
||||||
class PowerViewScene(Scene):
|
class PowerViewScene(Scene):
|
||||||
"""Representation of a Powerview scene."""
|
"""Representation of a Powerview scene."""
|
||||||
|
|
||||||
def __init__(self, hass, scene_data, room_data, pv_instance):
|
def __init__(self, hass, scene_data, room_data, hub):
|
||||||
"""Initialize the scene."""
|
"""Initialize the scene."""
|
||||||
self.pv_instance = pv_instance
|
self.hub = hub
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.scene_data = scene_data
|
self._sync_room_data(room_data, scene_data)
|
||||||
self._sync_room_data(room_data)
|
self._name = scene_data[SCENE_NAME]
|
||||||
self.entity_id_format = DOMAIN + '.{}'
|
self._scene_id = scene_data[SCENE_ID]
|
||||||
self.entity_id = generate_entity_id(
|
self.entity_id = async_generate_entity_id(
|
||||||
self.entity_id_format, str(self.scene_data["id"]), hass=hass)
|
ENTITY_ID_FORMAT, str(scene_data[SCENE_ID]), hass=hass)
|
||||||
|
|
||||||
def _sync_room_data(self, room_data):
|
def _sync_room_data(self, room_data, scene_data):
|
||||||
"""Sync the room data."""
|
"""Sync the room data."""
|
||||||
room = next((room for room in room_data["roomData"]
|
room = next((room for room in room_data[ROOM_DATA]
|
||||||
if room["id"] == self.scene_data["roomId"]), None)
|
if room[ROOM_ID] == scene_data[ROOM_ID_IN_SCENE]), {})
|
||||||
if room is not None:
|
|
||||||
self.scene_data["roomName"] = room["name"]
|
self._room_name = room.get(ROOM_NAME, '')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the scene."""
|
"""Return the name of the scene."""
|
||||||
return str(self.scene_data["name"])
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return {"roomName": self.scene_data["roomName"]}
|
return {STATE_ATTRIBUTE_ROOM_NAME: self._room_name}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
"""Icon to use in the frontend."""
|
"""Icon to use in the frontend."""
|
||||||
return 'mdi:blinds'
|
return 'mdi:blinds'
|
||||||
|
|
||||||
def activate(self):
|
def async_activate(self):
|
||||||
"""Activate the scene. Tries to get entities into requested state."""
|
"""Activate scene. Try to get entities into requested state."""
|
||||||
self.pv_instance.activate_scene(self.scene_data["id"])
|
yield from self.hub.scenes.activate_scene(self._scene_id)
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
"""
|
||||||
|
This component provides HA sensor for Netgear Arlo IP cameras.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.arlo/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.components.arlo import (
|
||||||
|
CONF_ATTRIBUTION, DEFAULT_BRAND)
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, STATE_UNKNOWN)
|
||||||
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
DEPENDENCIES = ['arlo']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# sensor_type [ description, unit, icon ]
|
||||||
|
SENSOR_TYPES = {
|
||||||
|
'last_capture': ['Last', None, 'run-fast'],
|
||||||
|
'total_cameras': ['Arlo Cameras', None, 'video'],
|
||||||
|
'captured_today': ['Captured Today', None, 'file-video'],
|
||||||
|
}
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
|
||||||
|
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||||
|
})
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=90)
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Set up an Arlo IP sensor."""
|
||||||
|
arlo = hass.data.get('arlo')
|
||||||
|
if not arlo:
|
||||||
|
return False
|
||||||
|
|
||||||
|
sensors = []
|
||||||
|
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
|
||||||
|
if sensor_type == 'total_cameras':
|
||||||
|
sensors.append(ArloSensor(hass,
|
||||||
|
SENSOR_TYPES[sensor_type][0],
|
||||||
|
arlo,
|
||||||
|
sensor_type))
|
||||||
|
else:
|
||||||
|
for camera in arlo.cameras:
|
||||||
|
name = '{0} {1}'.format(SENSOR_TYPES[sensor_type][0],
|
||||||
|
camera.name)
|
||||||
|
sensors.append(ArloSensor(hass, name, camera, sensor_type))
|
||||||
|
|
||||||
|
async_add_devices(sensors, True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ArloSensor(Entity):
|
||||||
|
"""An implementation of a Netgear Arlo IP sensor."""
|
||||||
|
|
||||||
|
def __init__(self, hass, name, device, sensor_type):
|
||||||
|
"""Initialize an Arlo sensor."""
|
||||||
|
super().__init__()
|
||||||
|
self._name = name
|
||||||
|
self._hass = hass
|
||||||
|
self._data = device
|
||||||
|
self._sensor_type = sensor_type
|
||||||
|
self._state = None
|
||||||
|
self._icon = 'mdi:{}'.format(SENSOR_TYPES.get(self._sensor_type)[2])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of this camera."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Icon to use in the frontend, if any."""
|
||||||
|
return self._icon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the units of measurement."""
|
||||||
|
return SENSOR_TYPES.get(self._sensor_type)[1]
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest data and updates the state."""
|
||||||
|
self._data.update()
|
||||||
|
|
||||||
|
if self._sensor_type == 'total_cameras':
|
||||||
|
self._state = len(self._data.cameras)
|
||||||
|
|
||||||
|
elif self._sensor_type == 'captured_today':
|
||||||
|
self._state = len(self._data.captured_today)
|
||||||
|
|
||||||
|
elif self._sensor_type == 'last_capture':
|
||||||
|
try:
|
||||||
|
video = self._data.videos()[0]
|
||||||
|
self._state = video.created_at_pretty("%m-%d-%Y %H:%M:%S")
|
||||||
|
except (AttributeError, IndexError):
|
||||||
|
self._state = STATE_UNKNOWN
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
attrs = {}
|
||||||
|
|
||||||
|
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
|
||||||
|
attrs['brand'] = DEFAULT_BRAND
|
||||||
|
|
||||||
|
if self._sensor_type == 'last_capture' or \
|
||||||
|
self._sensor_type == 'captured_today':
|
||||||
|
attrs['model'] = self._data.model_id
|
||||||
|
|
||||||
|
return attrs
|
|
@ -40,7 +40,8 @@ import voluptuous as vol
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['dsmr_parser==0.8']
|
REQUIREMENTS = ['dsmr_parser==0.9']
|
||||||
|
|
||||||
|
|
||||||
CONF_DSMR_VERSION = 'dsmr_version'
|
CONF_DSMR_VERSION = 'dsmr_version'
|
||||||
CONF_RECONNECT_INTERVAL = 'reconnect_interval'
|
CONF_RECONNECT_INTERVAL = 'reconnect_interval'
|
||||||
|
@ -60,7 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string,
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string,
|
||||||
vol.Optional(CONF_HOST, default=None): cv.string,
|
vol.Optional(CONF_HOST, default=None): cv.string,
|
||||||
vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All(
|
vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All(
|
||||||
cv.string, vol.In(['4', '2.2'])),
|
cv.string, vol.In(['5', '4', '2.2'])),
|
||||||
vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int,
|
vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -93,7 +94,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
devices = [DSMREntity(name, obis) for name, obis in obis_mapping]
|
devices = [DSMREntity(name, obis) for name, obis in obis_mapping]
|
||||||
|
|
||||||
# Protocol version specific obis
|
# Protocol version specific obis
|
||||||
if dsmr_version == '4':
|
if dsmr_version in ('4', '5'):
|
||||||
gas_obis = obis_ref.HOURLY_GAS_METER_READING
|
gas_obis = obis_ref.HOURLY_GAS_METER_READING
|
||||||
else:
|
else:
|
||||||
gas_obis = obis_ref.GAS_METER_READING
|
gas_obis = obis_ref.GAS_METER_READING
|
||||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.util import Throttle
|
||||||
from homeassistant.util.dt import now, parse_date
|
from homeassistant.util.dt import now, parse_date
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['fedexdeliverymanager==1.0.2']
|
REQUIREMENTS = ['fedexdeliverymanager==1.0.3']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -299,7 +299,7 @@ class FitbitAuthCallbackView(HomeAssistantView):
|
||||||
from oauthlib.oauth2.rfc6749.errors import MissingTokenError
|
from oauthlib.oauth2.rfc6749.errors import MissingTokenError
|
||||||
|
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
data = request.GET
|
data = request.query
|
||||||
|
|
||||||
response_message = """Fitbit has been successfully authorized!
|
response_message = """Fitbit has been successfully authorized!
|
||||||
You can close this window now!"""
|
You can close this window now!"""
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue