Merge pull request #7866 from home-assistant/release-0-46

0.46
pull/5393/merge 0.46
Paulus Schoutsen 2017-06-03 19:16:35 -07:00 committed by GitHub
commit e9f273e7e0
203 changed files with 5911 additions and 2867 deletions

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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."""

View File

@ -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(

View File

@ -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, {})

View File

@ -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__)

View File

@ -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]

View File

@ -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

View File

@ -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')
) )

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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))

View File

@ -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):

View File

@ -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

View File

@ -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,

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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))

View File

@ -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

View File

@ -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__)

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

@ -1 +1 @@
Subproject commit 6858555c86f18eb0ab176008e9aa2c3842fec7ce Subproject commit 75679e90f2aa11bc1b42188965746217feef0ea6

View File

@ -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, &quot;add&quot;)]]">Add To Group</ha-call-service-button></template><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, &quot;remove&quot;)]]">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, &quot;add&quot;)]]">Add To Group</ha-call-service-button></template><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, &quot;remove&quot;)]]">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) {

File diff suppressed because one or more lines are too long

View File

@ -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')
) )

View File

@ -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

View File

@ -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'

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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:

View File

@ -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: {}}

View File

@ -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

View File

@ -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),

View File

@ -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):

View File

@ -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 = {

View File

@ -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

View File

@ -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()

View File

@ -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):

View File

@ -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."""

View File

@ -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))

View File

@ -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(

View File

@ -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'

View File

@ -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)

View File

@ -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__)

View File

@ -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:

View File

@ -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):

View File

@ -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.

View File

@ -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()

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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]

View File

@ -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

View File

@ -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':

View File

@ -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__)

View File

@ -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

View File

@ -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,

View File

@ -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:

View File

@ -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))

View File

@ -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)

View File

@ -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."""

View File

@ -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))

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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__)

View File

@ -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