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/arlo.py
homeassistant/components/*/arlo.py
homeassistant/components/axis.py
homeassistant/components/*/axis.py
@ -89,6 +92,9 @@ omit =
homeassistant/components/qwikswitch.py
homeassistant/components/*/qwikswitch.py
homeassistant/components/rachio.py
homeassistant/components/*/rachio.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_OPENALPR no
#ENV INSTALL_FFMPEG no
#ENV INSTALL_OPENZWAVE no
#ENV INSTALL_LIBCEC no
#ENV INSTALL_PHANTOMJS no
#ENV INSTALL_COAP_CLIENT no

View File

@ -1,8 +1,6 @@
<ul>
<li><a href="https://community.home-assistant.io">📌 Community Forums</a></li>
<li><a href="https://github.com/home-assistant/home-assistant">🚀 GitHub</a></li>
<li><a href="https://home-assistant.io/">🏡 Homepage</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>
<li><a href="https://home-assistant.io/">Homepage</a></li>
<li><a href="https://community.home-assistant.io">Community Forums</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>
</ul>
<hr>

View File

@ -10,6 +10,7 @@ import threading
from typing import Optional, List
from homeassistant import monkey_patch
from homeassistant.const import (
__version__,
EVENT_HOMEASSISTANT_START,
@ -17,7 +18,6 @@ from homeassistant.const import (
REQUIRED_PYTHON_VER_WIN,
RESTART_EXIT_CODE,
)
from homeassistant.util.async import run_callback_threadsafe
def attempt_use_uvloop():
@ -310,6 +310,9 @@ def setup_and_run_hass(config_dir: str,
return None
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):
"""Open the webinterface in a browser."""
if hass.config.api is not None:
@ -371,6 +374,13 @@ def main() -> int:
"""Start Home Assistant."""
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()
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)
return None
yield from hass.loop.run_in_executor(
None, conf_util.process_ha_config_upgrade, hass)
yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
if enable_log:
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.')
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
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
config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir
yield from hass.loop.run_in_executor(
None, mount_local_lib_path, config_dir)
yield from hass.async_add_job(mount_local_lib_path, config_dir)
async_enable_logging(hass, verbose, log_rotate_days)
try:
config_dict = yield from hass.loop.run_in_executor(
None, conf_util.load_yaml_config_file, config_path)
config_dict = yield from hass.async_add_job(
conf_util.load_yaml_config_file, config_path)
except HomeAssistantError as err:
_LOGGER.error('Error loading %s: %s', config_path, err)
return None

View File

@ -123,8 +123,8 @@ def async_setup(hass, config):
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
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.
"""
return self.hass.loop.run_in_executor(
None, self.alarm_disarm, code)
return self.hass.async_add_job(self.alarm_disarm, code)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
@ -170,8 +169,7 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.alarm_arm_home, code)
return self.hass.async_add_job(self.alarm_arm_home, code)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
@ -182,8 +180,7 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.alarm_arm_away, code)
return self.hass.async_add_job(self.alarm_arm_away, code)
def alarm_trigger(self, code=None):
"""Send alarm trigger command."""
@ -194,8 +191,7 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.alarm_trigger, code)
return self.hass.async_add_job(self.alarm_trigger, code)
@property
def state_attributes(self):

View File

@ -117,7 +117,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self._alarm.arm('home')
self._alarm.arm('stay')
def alarm_arm_away(self, code=None):
"""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)
# Register Envisalink specific services
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(

View File

@ -128,8 +128,8 @@ def async_setup(hass, config):
all_alerts[entity.entity_id] = entity
# Read descriptions
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
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
from homeassistant.util import Throttle
REQUIREMENTS = ['apcaccess==0.0.4']
REQUIREMENTS = ['apcaccess==0.0.10']
_LOGGER = logging.getLogger(__name__)

View File

@ -83,7 +83,7 @@ class APIEventStream(HomeAssistantView):
stop_obj = object()
to_write = asyncio.Queue(loop=hass.loop)
restrict = request.GET.get('restrict')
restrict = request.query.get('restrict')
if restrict:
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)
descriptions = yield from hass.loop.run_in_executor(
None, conf_util.load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
conf_util.load_yaml_config_file, os.path.join(
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.helpers.event import (
async_track_state_change, async_track_point_in_utc_time)
from homeassistant.helpers.deprecation import get_deprecated
import homeassistant.helpers.config_validation as cv
CONF_ENTITY_ID = 'entity_id'
@ -40,10 +41,11 @@ def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID)
from_state = config.get(CONF_FROM, MATCH_ALL)
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
to_state = get_deprecated(config, CONF_TO, CONF_STATE, MATCH_ALL)
time_delta = config.get(CONF_FOR)
async_remove_state_for_cancel = None
async_remove_state_for_listener = None
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
@callback
def clear_listener():
@ -75,13 +77,13 @@ def async_trigger(hass, config, action):
}
})
if time_delta is None:
call_action()
# Ignore changes to state attributes if from/to is in use
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
# If only state attributes changed, ignore this event
if (from_s is not None and to_s is not None and
from_s.last_changed == to_s.last_changed):
if time_delta is None:
call_action()
return
@callback

View File

@ -10,7 +10,7 @@ import logging
import voluptuous as vol
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.event import async_track_time_change
@ -22,20 +22,26 @@ _LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'time',
CONF_AT: cv.time,
CONF_AFTER: cv.time,
CONF_HOURS: 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)),
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES,
CONF_SECONDS, CONF_AFTER))
CONF_SECONDS, CONF_AT, CONF_AFTER))
@asyncio.coroutine
def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
if CONF_AFTER in config:
after = config.get(CONF_AFTER)
hours, minutes, seconds = after.hour, after.minute, after.second
if CONF_AT in config:
at_time = config.get(CONF_AT)
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:
hours = config.get(CONF_HOURS)
minutes = config.get(CONF_MINUTES)

View File

@ -38,7 +38,7 @@ class MyStromView(HomeAssistantView):
@asyncio.coroutine
def get(self, request):
"""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
@asyncio.coroutine

View File

@ -1,82 +1,82 @@
"""
Demo platform that has two fake binary sensors.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
import homeassistant.util.dt as dt_util
from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo Calendar platform."""
calendar_data_future = DemoGoogleCalendarDataFuture()
calendar_data_current = DemoGoogleCalendarDataCurrent()
add_devices([
DemoGoogleCalendar(hass, calendar_data_future, {
CONF_NAME: 'Future Event',
CONF_DEVICE_ID: 'future_event',
}),
DemoGoogleCalendar(hass, calendar_data_current, {
CONF_NAME: 'Current Event',
CONF_DEVICE_ID: 'current_event',
}),
])
class DemoGoogleCalendarData(object):
"""Representation of a Demo Calendar element."""
# pylint: disable=no-self-use
def update(self):
"""Return true so entity knows we have new data."""
return True
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
"""Representation of a Demo Calendar for a future event."""
def __init__(self):
"""Set the event to a future event."""
one_hour_from_now = dt_util.now() \
+ dt_util.dt.timedelta(minutes=30)
self.event = {
'start': {
'dateTime': one_hour_from_now.isoformat()
},
'end': {
'dateTime': (one_hour_from_now + dt_util.dt.
timedelta(minutes=60)).isoformat()
},
'summary': 'Future Event',
}
class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
"""Representation of a Demo Calendar for a current event."""
def __init__(self):
"""Set the event data."""
middle_of_event = dt_util.now() \
- dt_util.dt.timedelta(minutes=30)
self.event = {
'start': {
'dateTime': middle_of_event.isoformat()
},
'end': {
'dateTime': (middle_of_event + dt_util.dt.
timedelta(minutes=60)).isoformat()
},
'summary': 'Current Event',
}
class DemoGoogleCalendar(CalendarEventDevice):
"""Representation of a Demo Calendar element."""
def __init__(self, hass, calendar_data, data):
"""Initialize Google Calendar but without the API calls."""
self.data = calendar_data
super().__init__(hass, data)
"""
Demo platform that has two fake binary sensors.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
import homeassistant.util.dt as dt_util
from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo Calendar platform."""
calendar_data_future = DemoGoogleCalendarDataFuture()
calendar_data_current = DemoGoogleCalendarDataCurrent()
add_devices([
DemoGoogleCalendar(hass, calendar_data_future, {
CONF_NAME: 'Future Event',
CONF_DEVICE_ID: 'future_event',
}),
DemoGoogleCalendar(hass, calendar_data_current, {
CONF_NAME: 'Current Event',
CONF_DEVICE_ID: 'current_event',
}),
])
class DemoGoogleCalendarData(object):
"""Representation of a Demo Calendar element."""
# pylint: disable=no-self-use
def update(self):
"""Return true so entity knows we have new data."""
return True
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
"""Representation of a Demo Calendar for a future event."""
def __init__(self):
"""Set the event to a future event."""
one_hour_from_now = dt_util.now() \
+ dt_util.dt.timedelta(minutes=30)
self.event = {
'start': {
'dateTime': one_hour_from_now.isoformat()
},
'end': {
'dateTime': (one_hour_from_now + dt_util.dt.
timedelta(minutes=60)).isoformat()
},
'summary': 'Future Event',
}
class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
"""Representation of a Demo Calendar for a current event."""
def __init__(self):
"""Set the event data."""
middle_of_event = dt_util.now() \
- dt_util.dt.timedelta(minutes=30)
self.event = {
'start': {
'dateTime': middle_of_event.isoformat()
},
'end': {
'dateTime': (middle_of_event + dt_util.dt.
timedelta(minutes=60)).isoformat()
},
'summary': 'Current Event',
}
class DemoGoogleCalendar(CalendarEventDevice):
"""Representation of a Demo Calendar element."""
def __init__(self, hass, calendar_data, data):
"""Initialize Google Calendar but without the API calls."""
self.data = calendar_data
super().__init__(hass, data)

View File

@ -1,78 +1,78 @@
"""
Support for Google Calendar Search binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.google_calendar/
"""
# pylint: disable=import-error
import logging
from datetime import timedelta
from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import (
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
GoogleCalendarService)
from homeassistant.util import Throttle, dt
_LOGGER = logging.getLogger(__name__)
DEFAULT_GOOGLE_SEARCH_PARAMS = {
'orderBy': 'startTime',
'maxResults': 1,
'singleEvents': True,
}
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
def setup_platform(hass, config, add_devices, disc_info=None):
"""Set up the calendar platform for event devices."""
if disc_info is None:
return
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
return
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
add_devices([GoogleCalendarEventDevice(hass, calendar_service,
disc_info[CONF_CAL_ID], data)
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
# pylint: disable=too-many-instance-attributes
class GoogleCalendarEventDevice(CalendarEventDevice):
"""A calendar event device."""
def __init__(self, hass, calendar_service, calendar, data):
"""Create the Calendar event device."""
self.data = GoogleCalendarData(calendar_service, calendar,
data.get('search', None))
super().__init__(hass, data)
class GoogleCalendarData(object):
"""Class to utilize calendar service object to get next event."""
def __init__(self, calendar_service, calendar_id, search=None):
"""Set up how we are going to search the google calendar."""
self.calendar_service = calendar_service
self.calendar_id = calendar_id
self.search = search
self.event = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
service = self.calendar_service.get()
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
params['timeMin'] = dt.now().isoformat('T')
params['calendarId'] = self.calendar_id
if self.search:
params['q'] = self.search
events = service.events() # pylint: disable=no-member
result = events.list(**params).execute()
items = result.get('items', [])
self.event = items[0] if len(items) == 1 else None
return True
"""
Support for Google Calendar Search binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.google_calendar/
"""
# pylint: disable=import-error
import logging
from datetime import timedelta
from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import (
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
GoogleCalendarService)
from homeassistant.util import Throttle, dt
_LOGGER = logging.getLogger(__name__)
DEFAULT_GOOGLE_SEARCH_PARAMS = {
'orderBy': 'startTime',
'maxResults': 1,
'singleEvents': True,
}
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
def setup_platform(hass, config, add_devices, disc_info=None):
"""Set up the calendar platform for event devices."""
if disc_info is None:
return
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
return
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
add_devices([GoogleCalendarEventDevice(hass, calendar_service,
disc_info[CONF_CAL_ID], data)
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
# pylint: disable=too-many-instance-attributes
class GoogleCalendarEventDevice(CalendarEventDevice):
"""A calendar event device."""
def __init__(self, hass, calendar_service, calendar, data):
"""Create the Calendar event device."""
self.data = GoogleCalendarData(calendar_service, calendar,
data.get('search', None))
super().__init__(hass, data)
class GoogleCalendarData(object):
"""Class to utilize calendar service object to get next event."""
def __init__(self, calendar_service, calendar_id, search=None):
"""Set up how we are going to search the google calendar."""
self.calendar_service = calendar_service
self.calendar_id = calendar_id
self.search = search
self.event = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
service = self.calendar_service.get()
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
params['timeMin'] = dt.now().isoformat('T')
params['calendarId'] = self.calendar_id
if self.search:
params['q'] = self.search
events = service.events() # pylint: disable=no-member
result = events.list(**params).execute()
items = result.get('items', [])
self.event = items[0] if len(items) == 1 else None
return True

View File

@ -138,7 +138,7 @@ class Camera(Entity):
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
def handle_async_mjpeg_stream(self, request):
@ -241,7 +241,7 @@ class CameraView(HomeAssistantView):
return web.Response(status=status)
authenticated = (request[KEY_AUTHENTICATED] or
request.GET.get('token') in camera.access_tokens)
request.query.get('token') in camera.access_tokens)
if not authenticated:
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)
return self._last_image
self._last_image = yield from self.hass.loop.run_in_executor(
None, fetch)
self._last_image = yield from self.hass.async_add_job(
fetch)
# async
else:
try:

View File

@ -88,8 +88,8 @@ class MjpegCamera(Camera):
# DigestAuth is not supported
if self._authentication == HTTP_DIGEST_AUTHENTICATION or \
self._still_image_url is None:
image = yield from self.hass.loop.run_in_executor(
None, self.camera_image)
image = yield from self.hass.async_add_job(
self.camera_image)
return image
websession = async_get_clientsession(self.hass)

View File

@ -1,250 +1,250 @@
"""
Support for Synology Surveillance Station Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.synology/
"""
import asyncio
import logging
import voluptuous as vol
import aiohttp
import async_timeout
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA)
from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_create_clientsession,
async_aiohttp_proxy_web)
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Synology Camera'
DEFAULT_STREAM_ID = '0'
DEFAULT_TIMEOUT = 5
CONF_CAMERA_NAME = 'camera_name'
CONF_STREAM_ID = 'stream_id'
QUERY_CGI = 'query.cgi'
QUERY_API = 'SYNO.API.Info'
AUTH_API = 'SYNO.API.Auth'
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
SESSION_ID = '0'
WEBAPI_PATH = '/webapi/'
AUTH_PATH = 'auth.cgi'
CAMERA_PATH = 'camera.cgi'
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
CONTENT_TYPE_HEADER = 'Content-Type'
SYNO_API_URL = '{0}{1}{2}'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up a Synology IP Camera."""
verify_ssl = config.get(CONF_VERIFY_SSL)
timeout = config.get(CONF_TIMEOUT)
websession_init = async_get_clientsession(hass, verify_ssl)
# Determine API to use for authentication
syno_api_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
query_payload = {
'api': QUERY_API,
'method': 'Query',
'version': '1',
'query': 'SYNO.'
}
try:
with async_timeout.timeout(timeout, loop=hass.loop):
query_req = yield from websession_init.get(
syno_api_url,
params=query_payload
)
# Skip content type check because Synology doesn't return JSON with
# right content type
query_resp = yield from query_req.json(content_type=None)
auth_path = query_resp['data'][AUTH_API]['path']
camera_api = query_resp['data'][CAMERA_API]['path']
camera_path = query_resp['data'][CAMERA_API]['path']
streaming_path = query_resp['data'][STREAMING_API]['path']
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", syno_api_url)
return False
# Authticate to NAS to get a session id
syno_auth_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, auth_path)
session_id = yield from get_session_id(
hass,
websession_init,
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
syno_auth_url,
timeout
)
# init websession
websession = async_create_clientsession(
hass, verify_ssl, cookies={'id': session_id})
# Use SessionID to get cameras in system
syno_camera_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, camera_api)
camera_payload = {
'api': CAMERA_API,
'method': 'List',
'version': '1'
}
try:
with async_timeout.timeout(timeout, loop=hass.loop):
camera_req = yield from websession.get(
syno_camera_url,
params=camera_payload
)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", syno_camera_url)
return False
camera_resp = yield from camera_req.json(content_type=None)
cameras = camera_resp['data']['cameras']
# add cameras
devices = []
for camera in cameras:
if not config.get(CONF_WHITELIST):
camera_id = camera['id']
snapshot_path = camera['snapshot_path']
device = SynologyCamera(
hass, websession, config, camera_id, camera['name'],
snapshot_path, streaming_path, camera_path, auth_path, timeout
)
devices.append(device)
async_add_devices(devices)
@asyncio.coroutine
def get_session_id(hass, websession, username, password, login_url, timeout):
"""Get a session id."""
auth_payload = {
'api': AUTH_API,
'method': 'Login',
'version': '2',
'account': username,
'passwd': password,
'session': 'SurveillanceStation',
'format': 'sid'
}
try:
with async_timeout.timeout(timeout, loop=hass.loop):
auth_req = yield from websession.get(
login_url,
params=auth_payload
)
auth_resp = yield from auth_req.json(content_type=None)
return auth_resp['data']['sid']
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", login_url)
return False
class SynologyCamera(Camera):
"""An implementation of a Synology NAS based IP camera."""
def __init__(self, hass, websession, config, camera_id,
camera_name, snapshot_path, streaming_path, camera_path,
auth_path, timeout):
"""Initialize a Synology Surveillance Station camera."""
super().__init__()
self.hass = hass
self._websession = websession
self._name = camera_name
self._synology_url = config.get(CONF_URL)
self._camera_name = config.get(CONF_CAMERA_NAME)
self._stream_id = config.get(CONF_STREAM_ID)
self._camera_id = camera_id
self._snapshot_path = snapshot_path
self._streaming_path = streaming_path
self._camera_path = camera_path
self._auth_path = auth_path
self._timeout = timeout
def camera_image(self):
"""Return bytes of camera image."""
return run_coroutine_threadsafe(
self.async_camera_image(), self.hass.loop).result()
@asyncio.coroutine
def async_camera_image(self):
"""Return a still image response from the camera."""
image_url = SYNO_API_URL.format(
self._synology_url, WEBAPI_PATH, self._camera_path)
image_payload = {
'api': CAMERA_API,
'method': 'GetSnapshot',
'version': '1',
'cameraId': self._camera_id
}
try:
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
response = yield from self._websession.get(
image_url,
params=image_payload
)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Error fetching %s", image_url)
return None
image = yield from response.read()
return image
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Return a MJPEG stream image response directly from the camera."""
streaming_url = SYNO_API_URL.format(
self._synology_url, WEBAPI_PATH, self._streaming_path)
streaming_payload = {
'api': STREAMING_API,
'method': 'Stream',
'version': '1',
'cameraId': self._camera_id,
'format': 'mjpeg'
}
stream_coro = self._websession.get(
streaming_url, params=streaming_payload)
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
@property
def name(self):
"""Return the name of this device."""
return self._name
"""
Support for Synology Surveillance Station Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.synology/
"""
import asyncio
import logging
import voluptuous as vol
import aiohttp
import async_timeout
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA)
from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_create_clientsession,
async_aiohttp_proxy_web)
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Synology Camera'
DEFAULT_STREAM_ID = '0'
DEFAULT_TIMEOUT = 5
CONF_CAMERA_NAME = 'camera_name'
CONF_STREAM_ID = 'stream_id'
QUERY_CGI = 'query.cgi'
QUERY_API = 'SYNO.API.Info'
AUTH_API = 'SYNO.API.Auth'
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
SESSION_ID = '0'
WEBAPI_PATH = '/webapi/'
AUTH_PATH = 'auth.cgi'
CAMERA_PATH = 'camera.cgi'
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
CONTENT_TYPE_HEADER = 'Content-Type'
SYNO_API_URL = '{0}{1}{2}'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up a Synology IP Camera."""
verify_ssl = config.get(CONF_VERIFY_SSL)
timeout = config.get(CONF_TIMEOUT)
websession_init = async_get_clientsession(hass, verify_ssl)
# Determine API to use for authentication
syno_api_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
query_payload = {
'api': QUERY_API,
'method': 'Query',
'version': '1',
'query': 'SYNO.'
}
try:
with async_timeout.timeout(timeout, loop=hass.loop):
query_req = yield from websession_init.get(
syno_api_url,
params=query_payload
)
# Skip content type check because Synology doesn't return JSON with
# right content type
query_resp = yield from query_req.json(content_type=None)
auth_path = query_resp['data'][AUTH_API]['path']
camera_api = query_resp['data'][CAMERA_API]['path']
camera_path = query_resp['data'][CAMERA_API]['path']
streaming_path = query_resp['data'][STREAMING_API]['path']
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", syno_api_url)
return False
# Authticate to NAS to get a session id
syno_auth_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, auth_path)
session_id = yield from get_session_id(
hass,
websession_init,
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
syno_auth_url,
timeout
)
# init websession
websession = async_create_clientsession(
hass, verify_ssl, cookies={'id': session_id})
# Use SessionID to get cameras in system
syno_camera_url = SYNO_API_URL.format(
config.get(CONF_URL), WEBAPI_PATH, camera_api)
camera_payload = {
'api': CAMERA_API,
'method': 'List',
'version': '1'
}
try:
with async_timeout.timeout(timeout, loop=hass.loop):
camera_req = yield from websession.get(
syno_camera_url,
params=camera_payload
)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", syno_camera_url)
return False
camera_resp = yield from camera_req.json(content_type=None)
cameras = camera_resp['data']['cameras']
# add cameras
devices = []
for camera in cameras:
if not config.get(CONF_WHITELIST):
camera_id = camera['id']
snapshot_path = camera['snapshot_path']
device = SynologyCamera(
hass, websession, config, camera_id, camera['name'],
snapshot_path, streaming_path, camera_path, auth_path, timeout
)
devices.append(device)
async_add_devices(devices)
@asyncio.coroutine
def get_session_id(hass, websession, username, password, login_url, timeout):
"""Get a session id."""
auth_payload = {
'api': AUTH_API,
'method': 'Login',
'version': '2',
'account': username,
'passwd': password,
'session': 'SurveillanceStation',
'format': 'sid'
}
try:
with async_timeout.timeout(timeout, loop=hass.loop):
auth_req = yield from websession.get(
login_url,
params=auth_payload
)
auth_resp = yield from auth_req.json(content_type=None)
return auth_resp['data']['sid']
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.exception("Error on %s", login_url)
return False
class SynologyCamera(Camera):
"""An implementation of a Synology NAS based IP camera."""
def __init__(self, hass, websession, config, camera_id,
camera_name, snapshot_path, streaming_path, camera_path,
auth_path, timeout):
"""Initialize a Synology Surveillance Station camera."""
super().__init__()
self.hass = hass
self._websession = websession
self._name = camera_name
self._synology_url = config.get(CONF_URL)
self._camera_name = config.get(CONF_CAMERA_NAME)
self._stream_id = config.get(CONF_STREAM_ID)
self._camera_id = camera_id
self._snapshot_path = snapshot_path
self._streaming_path = streaming_path
self._camera_path = camera_path
self._auth_path = auth_path
self._timeout = timeout
def camera_image(self):
"""Return bytes of camera image."""
return run_coroutine_threadsafe(
self.async_camera_image(), self.hass.loop).result()
@asyncio.coroutine
def async_camera_image(self):
"""Return a still image response from the camera."""
image_url = SYNO_API_URL.format(
self._synology_url, WEBAPI_PATH, self._camera_path)
image_payload = {
'api': CAMERA_API,
'method': 'GetSnapshot',
'version': '1',
'cameraId': self._camera_id
}
try:
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
response = yield from self._websession.get(
image_url,
params=image_payload
)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Error fetching %s", image_url)
return None
image = yield from response.read()
return image
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Return a MJPEG stream image response directly from the camera."""
streaming_url = SYNO_API_URL.format(
self._synology_url, WEBAPI_PATH, self._streaming_path)
streaming_payload = {
'api': STREAMING_API,
'method': 'Stream',
'version': '1',
'cameraId': self._camera_id,
'format': 'mjpeg'
}
stream_coro = self._websession.get(
streaming_url, params=streaming_payload)
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
@property
def name(self):
"""Return the name of this device."""
return self._name

View File

@ -213,8 +213,8 @@ def async_setup(hass, config):
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_setup(config)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
@ -569,8 +569,8 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.set_temperature, **kwargs))
return self.hass.async_add_job(
ft.partial(self.set_temperature, **kwargs))
def set_humidity(self, 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.
"""
return self.hass.loop.run_in_executor(
None, self.set_humidity, humidity)
return self.hass.async_add_job(self.set_humidity, humidity)
def set_fan_mode(self, fan):
"""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.
"""
return self.hass.loop.run_in_executor(
None, self.set_fan_mode, fan)
return self.hass.async_add_job(self.set_fan_mode, fan)
def set_operation_mode(self, 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.
"""
return self.hass.loop.run_in_executor(
None, self.set_operation_mode, operation_mode)
return self.hass.async_add_job(self.set_operation_mode, operation_mode)
def set_swing_mode(self, swing_mode):
"""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.
"""
return self.hass.loop.run_in_executor(
None, self.set_swing_mode, swing_mode)
return self.hass.async_add_job(self.set_swing_mode, swing_mode)
def turn_away_mode_on(self):
"""Turn away mode on."""
@ -629,8 +625,7 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_away_mode_on)
return self.hass.async_add_job(self.turn_away_mode_on)
def turn_away_mode_off(self):
"""Turn away mode off."""
@ -641,8 +636,7 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_away_mode_off)
return self.hass.async_add_job(self.turn_away_mode_off)
def set_hold_mode(self, 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.
"""
return self.hass.loop.run_in_executor(
None, self.set_hold_mode, hold_mode)
return self.hass.async_add_job(self.set_hold_mode, hold_mode)
def turn_aux_heat_on(self):
"""Turn auxillary heater on."""
@ -665,8 +658,7 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_aux_heat_on)
return self.hass.async_add_job(self.turn_aux_heat_on)
def turn_aux_heat_off(self):
"""Turn auxillary heater off."""
@ -677,8 +669,7 @@ class ClimateDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_aux_heat_off)
return self.hass.async_add_job(self.turn_aux_heat_off)
@property
def min_temp(self):

View File

@ -1,4 +1,4 @@
"""
"""
Tado component to create a climate device for each zone.
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:
yield from asyncio.wait(update_tasks, loop=hass.loop)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
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.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.open_cover, **kwargs))
return self.hass.async_add_job(ft.partial(self.open_cover, **kwargs))
def close_cover(self, **kwargs):
"""Close cover."""
@ -275,8 +274,7 @@ class CoverDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.close_cover, **kwargs))
return self.hass.async_add_job(ft.partial(self.close_cover, **kwargs))
def set_cover_position(self, **kwargs):
"""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.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.set_cover_position, **kwargs))
return self.hass.async_add_job(
ft.partial(self.set_cover_position, **kwargs))
def stop_cover(self, **kwargs):
"""Stop the cover."""
@ -299,8 +297,7 @@ class CoverDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.stop_cover, **kwargs))
return self.hass.async_add_job(ft.partial(self.stop_cover, **kwargs))
def open_cover_tilt(self, **kwargs):
"""Open the cover tilt."""
@ -311,8 +308,8 @@ class CoverDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.open_cover_tilt, **kwargs))
return self.hass.async_add_job(
ft.partial(self.open_cover_tilt, **kwargs))
def close_cover_tilt(self, **kwargs):
"""Close the cover tilt."""
@ -323,8 +320,8 @@ class CoverDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.close_cover_tilt, **kwargs))
return self.hass.async_add_job(
ft.partial(self.close_cover_tilt, **kwargs))
def set_cover_tilt_position(self, **kwargs):
"""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.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.set_cover_tilt_position, **kwargs))
return self.hass.async_add_job(
ft.partial(self.set_cover_tilt_position, **kwargs))
def stop_cover_tilt(self, **kwargs):
"""Stop the cover."""
@ -347,5 +344,5 @@ class CoverDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.stop_cover_tilt, **kwargs))
return self.hass.async_add_job(
ft.partial(self.stop_cover_tilt, **kwargs))

View File

@ -12,19 +12,25 @@ from homeassistant.components.cover import CoverDevice
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED)
import homeassistant.helpers.config_validation as cv
import homeassistant.loader as loader
REQUIREMENTS = [
'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip'
'#pymyq==0.0.8']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'myq'
NOTIFICATION_ID = 'myq_notification'
NOTIFICATION_TITLE = 'MyQ Cover Setup'
COVER_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
DEFAULT_NAME = 'myq'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MyQ component."""
@ -33,23 +39,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
brand = config.get(CONF_TYPE)
logger = logging.getLogger(__name__)
persistent_notification = loader.get_component('persistent_notification')
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:
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())
except (TypeError, KeyError, NameError) as ex:
logger.error("%s", ex)
return True
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):

View File

@ -41,7 +41,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
"""Initialize the Z-Wave rollershutter."""
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
# pylint: disable=no-member
self._network = hass.data[zwave.ZWAVE_NETWORK]
self._network = hass.data[zwave.const.DATA_NETWORK]
self._open_id = None
self._close_id = 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.const import (
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__)
@ -150,14 +151,14 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
scanner = yield from platform.async_get_scanner(
hass, {DOMAIN: p_config})
elif hasattr(platform, 'get_scanner'):
scanner = yield from hass.loop.run_in_executor(
None, platform.get_scanner, hass, {DOMAIN: p_config})
scanner = yield from hass.async_add_job(
platform.get_scanner, hass, {DOMAIN: p_config})
elif hasattr(platform, 'async_setup_scanner'):
setup = yield from platform.async_setup_scanner(
hass, p_config, tracker.async_see, disc_info)
elif hasattr(platform, 'setup_scanner'):
setup = yield from hass.loop.run_in_executor(
None, platform.setup_scanner, hass, p_config, tracker.see,
setup = yield from hass.async_add_job(
platform.setup_scanner, hass, p_config, tracker.see,
disc_info)
else:
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)}
yield from tracker.async_see(**args)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml')
)
hass.services.async_register(
@ -322,8 +323,8 @@ class DeviceTracker(object):
This method is a coroutine.
"""
with (yield from self._is_updating):
yield from self.hass.loop.run_in_executor(
None, update_config, self.hass.config.path(YAML_DEVICES),
yield from self.hass.async_add_job(
update_config, self.hass.config.path(YAML_DEVICES),
dev_id, device)
@asyncio.coroutine
@ -381,6 +382,7 @@ class Device(Entity):
battery = None # type: str
attributes = None # type: dict
vendor = None # type: str
icon = None # type: str
# Track if the last update of this device was HOME.
last_update_home = False
@ -388,7 +390,7 @@ class Device(Entity):
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str=None,
picture: str=None, gravatar: str=None,
picture: str=None, gravatar: str=None, icon: str=None,
hide_if_away: bool=False, vendor: str=None) -> None:
"""Initialize a device."""
self.hass = hass
@ -414,6 +416,8 @@ class Device(Entity):
else:
self.config_picture = picture
self.icon = icon
self.away_hide = hide_if_away
self.vendor = vendor
@ -608,7 +612,7 @@ class DeviceScanner(object):
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:
"""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.
"""
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):
@ -637,6 +641,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
"""
dev_schema = vol.Schema({
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(CONF_MAC, default=None):
vol.Any(None, vol.All(cv.string, vol.Upper)),
@ -650,8 +656,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
try:
result = []
try:
devices = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, path)
devices = yield from hass.async_add_job(
load_yaml_config_file, path)
except HomeAssistantError as err:
_LOGGER.error("Unable to load %s: %s", path, str(err))
return []
@ -728,6 +734,7 @@ def update_config(path: str, dev_id: str, device: Device):
device = {device.dev_id: {
ATTR_NAME: device.name,
ATTR_MAC: device.mac,
ATTR_ICON: device.icon,
'picture': device.config_picture,
'track': device.track,
CONF_AWAY_HIDE: device.away_hide,

View File

@ -118,25 +118,29 @@ class AsusWrtDeviceScanner(DeviceScanner):
self.protocol = config[CONF_PROTOCOL]
self.mode = config[CONF_MODE]
self.port = config[CONF_PORT]
self.ssh_args = {}
if self.protocol == 'ssh':
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:
if not (self.ssh_key or self.password):
_LOGGER.error("No password or private key specified")
self.success_init = False
return
self.connection = SshConnection(self.host, self.port,
self.username,
self.password,
self.ssh_key,
self.mode == "ap")
else:
if not self.password:
_LOGGER.error("No password specified")
self.success_init = False
return
self.connection = TelnetConnection(self.host, self.port,
self.username,
self.password,
self.mode == "ap")
self.lock = threading.Lock()
self.last_results = {}
@ -182,105 +186,9 @@ class AsusWrtDeviceScanner(DeviceScanner):
self.last_results = active_clients
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):
"""Retrieve data from ASUSWRT and return parsed result."""
if self.protocol == 'ssh':
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'
result = self.connection.get_result()
if not result:
return {}
@ -363,3 +271,193 @@ class AsusWrtDeviceScanner(DeviceScanner):
if match.group('ip') in devices:
devices[match.group('ip')]['status'] = match.group('status')
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
def get(self, request):
"""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
@asyncio.coroutine
@ -75,10 +75,10 @@ class GPSLoggerView(HomeAssistantView):
if 'activity' in data:
attrs['activity'] = data['activity']
yield from hass.loop.run_in_executor(
None, partial(self.see, dev_id=device,
gps=gps_location, battery=battery,
gps_accuracy=accuracy,
attributes=attrs))
yield from hass.async_add_job(
partial(self.see, dev_id=device,
gps=gps_location, battery=battery,
gps_accuracy=accuracy,
attributes=attrs))
return 'Setting location for {}'.format(device)

View File

@ -41,7 +41,7 @@ class LocativeView(HomeAssistantView):
@asyncio.coroutine
def get(self, request):
"""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
@asyncio.coroutine
@ -79,10 +79,9 @@ class LocativeView(HomeAssistantView):
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
if direction == 'enter':
yield from hass.loop.run_in_executor(
None, partial(self.see, dev_id=device,
location_name=location_name,
gps=gps_location))
yield from hass.async_add_job(
partial(self.see, dev_id=device, location_name=location_name,
gps=gps_location))
return 'Setting location to {}'.format(location_name)
elif direction == 'exit':
@ -91,10 +90,9 @@ class LocativeView(HomeAssistantView):
if current_state is None or current_state.state == location_name:
location_name = STATE_NOT_HOME
yield from hass.loop.run_in_executor(
None, partial(self.see, dev_id=device,
location_name=location_name,
gps=gps_location))
yield from hass.async_add_job(
partial(self.see, dev_id=device,
location_name=location_name, gps=gps_location))
return 'Setting location to not home'
else:
# 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.client = None
self.wireless_exist = None
self.success_init = self.connect_to_device()
if self.success_init:
_LOGGER.info("Start polling Mikrotik router...")
_LOGGER.info(
"Start polling Mikrotik (%s) router...",
self.host
)
self._update_info()
else:
_LOGGER.error("Connection to Mikrotik failed")
_LOGGER.error(
"Connection to Mikrotik (%s) failed",
self.host
)
def connect_to_device(self):
"""Connect to Mikrotik method."""
@ -87,6 +94,16 @@ class MikrotikScanner(DeviceScanner):
routerboard_info[0].get('model', 'Router'),
self.host)
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,
librouteros.exceptions.ConnectionError) as api_error:
@ -108,24 +125,39 @@ class MikrotikScanner(DeviceScanner):
def _update_info(self):
"""Retrieve latest information from the Mikrotik box."""
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(
cmd='/interface/wireless/registration-table/getall'
_LOGGER.info(
"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
mac_names = {device.get('mac-address'): device.get('host-name')
for device in device_names
if device.get('mac-address')}
self.last_results = {
device.get('mac-address'):
mac_names.get(device.get('mac-address'))
for device in wireless_clients
}
if self.wireless_exist:
self.last_results = {
device.get('mac-address'):
mac_names.get(device.get('mac-address'))
for device in devices
}
else:
self.last_results = mac_names
return True

View File

@ -19,7 +19,7 @@ from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pysnmp==4.3.5']
REQUIREMENTS = ['pysnmp==4.3.7']
CONF_COMMUNITY = 'community'
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
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==1.0.0']
REQUIREMENTS = ['netdisco==1.0.1']
DOMAIN = 'discovery'
@ -115,8 +115,7 @@ def async_setup(hass, config):
@asyncio.coroutine
def scan_devices(now):
"""Scan for devices."""
results = yield from hass.loop.run_in_executor(
None, _discover, netdisco)
results = yield from hass.async_add_job(_discover, netdisco)
for result in results:
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.util.dt import utcnow
REQUIREMENTS = ['pyeight==0.0.5']
REQUIREMENTS = ['pyeight==0.0.6']
_LOGGER = logging.getLogger(__name__)
@ -159,8 +159,8 @@ def async_setup(hass, config):
CONF_BINARY_SENSORS: binary_sensors,
}, config))
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@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.dispatcher import async_dispatcher_send
REQUIREMENTS = ['pyenvisalink==2.0']
REQUIREMENTS = ['pyenvisalink==2.1']
_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)
# Listen for fan service calls.
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service_name in SERVICE_TO_METHOD:
@ -256,7 +256,7 @@ class FanEntity(ToggleEntity):
"""
if speed is SPEED_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:
"""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.
"""
return self.hass.loop.run_in_executor(
None, self.set_direction, direction)
return self.hass.async_add_job(self.set_direction, direction)
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
"""Turn on the fan."""
@ -281,8 +280,8 @@ class FanEntity(ToggleEntity):
"""
if speed is SPEED_OFF:
return self.async_turn_off()
return self.hass.loop.run_in_executor(
None, ft.partial(self.turn_on, speed, **kwargs))
return self.hass.async_add_job(
ft.partial(self.turn_on, speed, **kwargs))
def oscillate(self: ToggleEntity, oscillating: bool) -> None:
"""Oscillate the fan."""
@ -293,8 +292,7 @@ class FanEntity(ToggleEntity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.oscillate, oscillating)
return self.hass.async_add_job(self.oscillate, oscillating)
@property
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)
)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
# Register service

View File

@ -268,8 +268,8 @@ class IndexView(HomeAssistantView):
no_auth = 'true'
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
template = yield from hass.loop.run_in_executor(
None, self.templates.get_template, 'index.html')
template = yield from hass.async_add_job(
self.templates.get_template, 'index.html')
# pylint is wrong
# pylint: disable=no-member

View File

@ -3,7 +3,7 @@
FINGERPRINTS = {
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
"frontend.html": "fbb9d6bdd3d661db26cad9475a5e22f1",
"frontend.html": "ed18c05632c071eb4f7b012382d0f810",
"mdi.html": "f407a5a57addbe93817ee1b244d33fbe",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8",
@ -18,6 +18,6 @@ FINGERPRINTS = {
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163",
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
"panels/ha-panel-zwave.html": "19336d2c50c91dd6a122acc0606ff10d",
"panels/ha-panel-zwave.html": "780a792213e98510b475f752c40ef0f9",
"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();
},
});</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({
is: 'zwave-node-config',
@ -313,131 +507,7 @@
this.selectedUserCodeChanged(this.selectedUserCode);
}.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({
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({
});</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: 'ha-panel-zwave',
properties: {
@ -493,6 +563,10 @@
computed: 'computeSelectedEntityAttrs(selectedEntity)'
},
values: {
type: Array,
},
groups: {
type: Array,
},
@ -559,6 +633,8 @@
},
selectedNodeChanged: function (selectedNode) {
this.newNodeNameInput = '';
if (selectedNode === -1) return;
this.selectedConfigParameter = -1;
this.selectedConfigParameterValue = -1;
@ -570,6 +646,13 @@
});
this.config = configData;
}.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 = [];
this.hass.callApi('GET', 'zwave/groups/' + this.nodes[selectedNode].attributes.node_id).then(function (groups) {
Object.entries(groups).forEach(([key, value]) => {
@ -630,9 +713,7 @@
computeGetNodeName: function (selectedNode) {
if (this.selectedNode === -1 ||
!this.nodes[selectedNode].entity_id) return -1;
var str = (this.nodes[selectedNode].entity_id);
var name = str.replace('zwave.', '');
return name;
return this.nodes[selectedNode].attributes.node_name;
},
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)
descriptions = yield from hass.loop.run_in_executor(
None, conf_util.load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
conf_util.load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)

View File

@ -223,7 +223,7 @@ class HistoryPeriodView(HomeAssistantView):
if start_time > now:
return self.json([])
end_time = request.GET.get('end_time')
end_time = request.query.get('end_time')
if end_time:
end_time = dt_util.as_utc(
dt_util.parse_datetime(end_time))
@ -231,11 +231,11 @@ class HistoryPeriodView(HomeAssistantView):
return self.json_message('Invalid end_time', HTTP_BAD_REQUEST)
else:
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(
None, get_significant_states, request.app['hass'], start_time,
end_time, entity_id, self.filters)
result = yield from request.app['hass'].async_add_job(
get_significant_states, request.app['hass'], start_time, end_time,
entity_id, self.filters)
result = result.values()
if _LOGGER.isEnabledFor(logging.DEBUG):
elapsed = time.perf_counter() - timer_start

View File

@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_interval
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['pyhomematic==0.1.26']
REQUIREMENTS = ['pyhomematic==0.1.27']
DOMAIN = 'homematic'

View File

@ -37,8 +37,8 @@ def auth_middleware(app, handler):
# A valid auth header has been set
authenticated = True
elif (DATA_API_PASSWORD in request.GET and
validate_password(request, request.GET[DATA_API_PASSWORD])):
elif (DATA_API_PASSWORD in request.query and
validate_password(request, request.query[DATA_API_PASSWORD])):
authenticated = True
elif is_trusted_ip(request):

View File

@ -40,8 +40,8 @@ def ban_middleware(app, handler):
if KEY_BANNED_IPS not in app:
hass = app['hass']
app[KEY_BANNED_IPS] = yield from hass.loop.run_in_executor(
None, load_ip_bans_config, hass.config.path(IP_BANS_FILE))
app[KEY_BANNED_IPS] = yield from hass.async_add_job(
load_ip_bans_config, hass.config.path(IP_BANS_FILE))
@asyncio.coroutine
def ban_middleware_handler(request):
@ -90,9 +90,8 @@ def process_wrong_login(request):
request.app[KEY_BANNED_IPS].append(new_ban)
hass = request.app['hass']
yield from hass.loop.run_in_executor(
None, update_ip_bans_config, hass.config.path(IP_BANS_FILE),
new_ban)
yield from hass.async_add_job(
update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban)
_LOGGER.warning(
"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)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
@ -117,7 +117,7 @@ class ImageProcessingEntity(Entity):
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
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/
"""
import logging
import os
import requests
import voluptuous as vol
@ -13,7 +14,7 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_HOST, CONF_PORT, CONF_TIMEOUT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['insteonlocal==0.48']
REQUIREMENTS = ['insteonlocal==0.52']
_LOGGER = logging.getLogger(__name__)
@ -47,7 +48,12 @@ def setup(hass, config):
timeout = conf.get(CONF_TIMEOUT)
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
insteonhub.get_buffer_status()
except requests.exceptions.ConnectTimeout:

View File

@ -167,7 +167,7 @@ IDENTIFY_SCHEMA = vol.Schema({
vol.Optional(ATTR_PUSH_SOUNDS): list
}, extra=vol.ALLOW_EXTRA)
CONFIGURATION_FILE = 'ios.conf'
CONFIGURATION_FILE = '.ios.conf'
CONFIG_FILE = {ATTR_DEVICES: {}}

View File

@ -77,6 +77,8 @@ EFFECT_COLORLOOP = "colorloop"
EFFECT_RANDOM = "random"
EFFECT_WHITE = "white"
COLOR_GROUP = "Color descriptors"
LIGHT_PROFILES_FILE = "light_profiles.csv"
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({
ATTR_ENTITY_ID: cv.entity_ids,
ATTR_PROFILE: cv.string,
vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string,
ATTR_TRANSITION: VALID_TRANSITION,
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
ATTR_COLOR_NAME: cv.string,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
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.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP):
vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP):
vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
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_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
ATTR_EFFECT: cv.string,
@ -285,8 +291,8 @@ def async_setup(hass, config):
yield from asyncio.wait(update_tasks, loop=hass.loop)
# Listen for light on and light off service calls.
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(
@ -341,8 +347,7 @@ class Profiles:
return None
return profiles
cls._all = yield from hass.loop.run_in_executor(
None, load_profile_data, hass)
cls._all = yield from hass.async_add_job(load_profile_data, hass)
return cls._all is not None
@classmethod

View File

@ -12,10 +12,9 @@ import voluptuous as vol
from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, ATTR_WHITE_VALUE,
EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT,
SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, Light,
PLATFORM_SCHEMA)
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, EFFECT_COLORLOOP,
EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT,
SUPPORT_RGB_COLOR, Light, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['flux_led==0.19']
@ -27,10 +26,8 @@ ATTR_MODE = 'mode'
DOMAIN = 'flux_led'
SUPPORT_FLUX_LED_RGB = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
SUPPORT_RGB_COLOR)
SUPPORT_FLUX_LED_RGBW = (SUPPORT_WHITE_VALUE | SUPPORT_EFFECT |
SUPPORT_RGB_COLOR)
SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
SUPPORT_RGB_COLOR)
MODE_RGB = 'rgb'
MODE_RGBW = 'rgbw'
@ -182,16 +179,7 @@ class FluxLight(Light):
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
if self._mode == MODE_RGB:
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
return self._bulb.brightness
@property
def rgb_color(self):
@ -201,11 +189,7 @@ class FluxLight(Light):
@property
def supported_features(self):
"""Flag supported features."""
if self._mode == MODE_RGBW:
return SUPPORT_FLUX_LED_RGBW
elif self._mode == MODE_RGB:
return SUPPORT_FLUX_LED_RGB
return 0
return SUPPORT_FLUX_LED
@property
def effect_list(self):
@ -219,23 +203,17 @@ class FluxLight(Light):
rgb = kwargs.get(ATTR_RGB_COLOR)
brightness = kwargs.get(ATTR_BRIGHTNESS)
white_value = kwargs.get(ATTR_WHITE_VALUE)
effect = kwargs.get(ATTR_EFFECT)
if rgb is not None and brightness is not None:
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:
# self.white_value and self.brightness are appropriately
# returning None for MODE_RGB and MODE_RGBW respectively
self._bulb.setRgbw(*tuple(rgb),
w=self.white_value,
brightness=self.brightness)
self._bulb.setRgb(*tuple(rgb))
elif brightness is not None:
self._bulb.setRgb(*self.rgb_color, brightness=brightness)
elif white_value is not None:
self._bulb.setRgbw(*self.rgb_color, w=white_value)
if self._mode == 'rgbw':
self._bulb.setWarmWhite255(brightness)
elif self._mode == 'rgb':
(red, green, blue) = self._bulb.getRgb()
self._bulb.setRgb(red, green, blue, brightness=brightness)
elif effect == EFFECT_RANDOM:
self._bulb.setRgb(random.randint(0, 255),
random.randint(0, 255),

View File

@ -37,7 +37,7 @@ from . import effects as lifx_effects
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['aiolifx==0.4.6']
REQUIREMENTS = ['aiolifx==0.4.7']
UDP_BROADCAST_PORT = 56700
@ -54,9 +54,6 @@ ATTR_POWER = 'power'
BYTE_MAX = 255
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({
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_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
def available(self):
"""Return the availability of the device."""
@ -273,8 +276,7 @@ class LIFXLight(Light):
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
# The 3 LIFX "White" products supported a limited temperature range
# https://lan.developer.lifx.com/docs/lifx-products
if self.product in [10, 11, 18]:
if self.lifxwhite:
kelvin = 6500
else:
kelvin = 9000
@ -284,8 +286,7 @@ class LIFXLight(Light):
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
# The 3 LIFX "White" products supported a limited temperature range
# https://lan.developer.lifx.com/docs/lifx-products
if self.product in [10, 11, 18]:
if self.lifxwhite:
kelvin = 2700
else:
kelvin = 2500
@ -305,12 +306,18 @@ class LIFXLight(Light):
@property
def supported_features(self):
"""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
def effect_list(self):
"""Return the list of supported effects."""
return lifx_effects.effect_list()
return lifx_effects.effect_list(self)
@asyncio.coroutine
def update_after_transition(self, now):

View File

@ -7,7 +7,7 @@ import voluptuous as vol
from homeassistant.components.light import (
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)
from homeassistant.const import (ATTR_ENTITY_ID)
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_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
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.All(vol.Coerce(float), vol.Range(min=0.05)),
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)
def effect_list():
"""Return the list of supported effects."""
return [
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_BREATHE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
def effect_list(light):
"""Return the list of supported effects for this light."""
if light.lifxwhite:
return [
SERVICE_EFFECT_BREATHE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
else:
return [
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_BREATHE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
class LIFXEffectData(object):
@ -230,12 +239,14 @@ class LIFXEffectBreathe(LIFXEffect):
cycles = kwargs[ATTR_CYCLES]
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 hsbk[1] > 65536/2:
hsbk = [hsbk[0], 0, 65535, 4000]
if light.lifxwhite or hsbk[1] < 65536/2:
# White: toggle brightness
hsbk[2] = 65535 if hsbk[2] < 65536/2 else 0
else:
hsbk = [hsbk[0], 65535, 65535, hsbk[3]]
# Color: fully desaturate with full brightness
hsbk = [hsbk[0], 0, 65535, 4000]
# Start the effect
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
RGB_MODES = ['hsb', 'rgb']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Wink lights."""
@ -62,8 +60,6 @@ class WinkLight(WinkDevice, Light):
"""Define current bulb color in RGB."""
if not self.wink.supports_hue_saturation():
return None
elif self.wink.color_model() not in RGB_MODES:
return False
else:
hue = self.wink.color_hue()
saturation = self.wink.color_saturation()

View File

@ -108,8 +108,8 @@ def async_setup(hass, config):
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(
@ -150,8 +150,7 @@ class LockDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.lock, **kwargs))
return self.hass.async_add_job(ft.partial(self.lock, **kwargs))
def unlock(self, **kwargs):
"""Unlock the lock."""
@ -162,8 +161,7 @@ class LockDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.unlock, **kwargs))
return self.hass.async_add_job(ft.partial(self.unlock, **kwargs))
@property
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(
path.join(path.dirname(__file__), 'services.yaml'))
network = hass.data[zwave.ZWAVE_NETWORK]
network = hass.data[zwave.const.DATA_NETWORK]
def set_usercode(service):
"""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)
hass = request.app['hass']
events = yield from hass.loop.run_in_executor(
None, _get_events, hass, start_day, end_day)
events = yield from hass.async_add_job(
_get_events, hass, start_day, end_day)
events = _exclude_events(events, self.config)
return self.json(humanify(events))

View File

@ -123,8 +123,8 @@ def async_setup(hass, config):
"""Handle logger services."""
set_log_levels(service.data)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(

View File

@ -10,8 +10,7 @@ import logging
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['https://github.com/thecynic/pylutron/archive/v0.1.0.zip#'
'pylutron==0.1.0']
REQUIREMENTS = ['pylutron==0.1.0']
DOMAIN = 'lutron'

View File

@ -353,8 +353,8 @@ def async_setup(hass, config):
yield from component.async_setup(config)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
@ -583,8 +583,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.turn_on)
return self.hass.async_add_job(self.turn_on)
def turn_off(self):
"""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.
"""
return self.hass.loop.run_in_executor(
None, self.turn_off)
return self.hass.async_add_job(self.turn_off)
def mute_volume(self, mute):
"""Mute the volume."""
@ -607,8 +605,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.mute_volume, mute)
return self.hass.async_add_job(self.mute_volume, mute)
def set_volume_level(self, volume):
"""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.
"""
return self.hass.loop.run_in_executor(
None, self.set_volume_level, volume)
return self.hass.async_add_job(self.set_volume_level, volume)
def media_play(self):
"""Send play commmand."""
@ -631,8 +627,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.media_play)
return self.hass.async_add_job(self.media_play)
def media_pause(self):
"""Send pause command."""
@ -643,8 +638,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.media_pause)
return self.hass.async_add_job(self.media_pause)
def media_stop(self):
"""Send stop command."""
@ -655,8 +649,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.media_stop)
return self.hass.async_add_job(self.media_stop)
def media_previous_track(self):
"""Send previous track command."""
@ -667,8 +660,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.media_previous_track)
return self.hass.async_add_job(self.media_previous_track)
def media_next_track(self):
"""Send next track command."""
@ -679,8 +671,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.media_next_track)
return self.hass.async_add_job(self.media_next_track)
def media_seek(self, position):
"""Send seek command."""
@ -691,8 +682,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.media_seek, position)
return self.hass.async_add_job(self.media_seek, position)
def play_media(self, media_type, media_id, **kwargs):
"""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.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.play_media, media_type, media_id, **kwargs))
return self.hass.async_add_job(
ft.partial(self.play_media, media_type, media_id, **kwargs))
def select_source(self, source):
"""Select input source."""
@ -715,8 +705,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.select_source, source)
return self.hass.async_add_job(self.select_source, source)
def clear_playlist(self):
"""Clear players playlist."""
@ -727,8 +716,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.clear_playlist)
return self.hass.async_add_job(self.clear_playlist)
def set_shuffle(self, shuffle):
"""Enable/disable shuffle mode."""
@ -739,8 +727,7 @@ class MediaPlayerDevice(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, self.set_shuffle, shuffle)
return self.hass.async_add_job(self.set_shuffle, shuffle)
# No need to overwrite these.
@property
@ -810,7 +797,7 @@ class MediaPlayerDevice(Entity):
"""
if hasattr(self, 'toggle'):
# 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]:
return self.async_turn_on()
@ -825,7 +812,7 @@ class MediaPlayerDevice(Entity):
"""
if hasattr(self, 'volume_up'):
# 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
if self.volume_level < 1:
@ -840,7 +827,7 @@ class MediaPlayerDevice(Entity):
"""
if hasattr(self, 'volume_down'):
# 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
if self.volume_level > 0:
@ -854,7 +841,7 @@ class MediaPlayerDevice(Entity):
"""
if hasattr(self, 'media_play_pause'):
# 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:
return self.async_media_pause()
@ -960,7 +947,7 @@ class MediaPlayerImageView(HomeAssistantView):
return web.Response(status=status)
authenticated = (request[KEY_AUTHENTICATED] or
request.GET.get('token') == player.access_token)
request.query.get('token') == player.access_token)
if not authenticated:
return web.Response(status=401)

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
CONF_NAME, STATE_ON)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['denonavr==0.4.1']
REQUIREMENTS = ['denonavr==0.4.2']
_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):
return
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service in SERVICE_TO_METHOD:

View File

@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF)
REQUIREMENTS = ['openhomedevice==0.2.1']
REQUIREMENTS = ['openhomedevice==0.4.0']
SUPPORT_OPENHOME = SUPPORT_SELECT_SOURCE | \
SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
@ -92,7 +92,7 @@ class OpenhomeDevice(MediaPlayerDevice):
if self._source["type"] == "Radio":
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 | \
SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PLAY
@ -173,17 +173,17 @@ class OpenhomeDevice(MediaPlayerDevice):
@property
def media_image_url(self):
"""Image url of current playing media."""
return self._track_information["albumArt"]
return self._track_information["albumArtwork"]
@property
def media_artist(self):
"""Artist of current playing media, music track only."""
return self._track_information["artist"]
return self._track_information["artist"][0]
@property
def media_album_name(self):
"""Album name of current playing media, music track only."""
return self._track_information["album"]
return self._track_information["albumTitle"]
@property
def media_title(self):

View File

@ -165,6 +165,22 @@ shuffle_set:
description: True/false for enabling/disabling shuffle
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:
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
https://home-assistant.io/components/media_player.snapcast/
"""
import asyncio
import logging
from os import path
import socket
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE,
SUPPORT_PLAY, PLATFORM_SCHEMA, MediaPlayerDevice)
PLATFORM_SCHEMA, MediaPlayerDevice)
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
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['snapcast==1.2.2']
REQUIREMENTS = ['snapcast==2.0.6']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'snapcast'
SUPPORT_SNAPCAST = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY
SERVICE_SNAPSHOT = 'snapcast_snapshot'
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({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_PORT): cv.port
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Snapcast platform."""
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the Snapcast platform."""
import snapcast.control
from snapcast.control.server import CONTROL_PORT
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:
server = snapcast.control.Snapserver(host, port)
server = yield from snapcast.control.create_server(
hass.loop, host, port)
except socket.gaierror:
_LOGGER.error(
"Could not connect to Snapcast server at %s:%d", host, port)
_LOGGER.error('Could not connect to Snapcast server at %s:%d',
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
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."""
def __init__(self, client):
"""Initialize the Snapcast device."""
"""Initialize the Snapcast client device."""
client.set_callback(self.schedule_update_ha_state)
self._client = client
@property
def name(self):
"""Return the name of the device."""
return self._client.identifier
return '{}{}'.format(CLIENT_PREFIX, self._client.identifier)
@property
def volume_level(self):
@ -74,39 +209,45 @@ class SnapcastDevice(MediaPlayerDevice):
@property
def supported_features(self):
"""Flag media player features that are supported."""
return SUPPORT_SNAPCAST
return SUPPORT_SNAPCAST_CLIENT
@property
def state(self):
"""Return the state of the player."""
if not self._client.connected:
return STATE_OFF
if self._client.connected:
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 {
'idle': STATE_IDLE,
'playing': STATE_PLAYING,
'unknown': STATE_UNKNOWN,
}.get(self._client.stream.status, STATE_UNKNOWN)
'friendly_name': name
}
@property
def source(self):
"""Return the current input source."""
return self._client.stream.name
def should_poll(self):
"""Do not poll for state."""
return False
@property
def source_list(self):
"""List of available input sources."""
return list(self._client.streams_by_name().keys())
def mute_volume(self, mute):
@asyncio.coroutine
def async_mute_volume(self, mute):
"""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."""
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):
"""Set input source."""
streams = self._client.streams_by_name()
if source in streams:
self._client.stream = streams[source].identifier
def snapshot(self):
"""Snapshot the client state."""
self._client.snapshot()
@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.
_SOCO_LOGGER = logging.getLogger('soco')
_SOCO_LOGGER.setLevel(logging.ERROR)
_SOCO_SERVICES_LOGGER = logging.getLogger('soco.services')
_REQUESTS_LOGGER = logging.getLogger('requests')
_REQUESTS_LOGGER.setLevel(logging.ERROR)
@ -73,6 +74,8 @@ ATTR_WITH_GROUP = 'with_group'
ATTR_IS_COORDINATOR = 'is_coordinator'
UPNP_ERRORS_TO_IGNORE = ['701']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ADVERTISE_ADDR): cv.string,
vol.Optional(CONF_INTERFACE_ADDR): cv.string,
@ -255,10 +258,36 @@ def soco_error(funct):
return funct(*args, **kwargs)
except SoCoException as err:
_LOGGER.error("Error on %s with %s", funct.__name__, err)
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):
"""Call function on coordinator."""
@ft.wraps(funct)
@ -297,6 +326,7 @@ class SonosDevice(MediaPlayerDevice):
self._media_next_title = None
self._support_previous_track = False
self._support_next_track = False
self._support_play = False
self._support_stop = False
self._support_pause = False
self._current_track_uri = None
@ -411,6 +441,7 @@ class SonosDevice(MediaPlayerDevice):
self._current_track_is_radio_stream = False
self._support_previous_track = False
self._support_next_track = False
self._support_play = False
self._support_stop = False
self._support_pause = False
self._is_playing_tv = False
@ -494,6 +525,7 @@ class SonosDevice(MediaPlayerDevice):
support_previous_track = False
support_next_track = False
support_play = False
support_stop = False
support_pause = False
@ -515,7 +547,8 @@ class SonosDevice(MediaPlayerDevice):
)
support_previous_track = False
support_next_track = False
support_stop = False
support_play = True
support_stop = True
support_pause = False
source_name = 'Radio'
@ -578,6 +611,7 @@ class SonosDevice(MediaPlayerDevice):
)
support_previous_track = True
support_next_track = True
support_play = True
support_stop = 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 == 1:
if playlist_position <= 1:
support_previous_track = False
if playlist_position == playlist_size:
@ -651,6 +685,7 @@ class SonosDevice(MediaPlayerDevice):
self._current_track_is_radio_stream = is_radio_stream
self._support_previous_track = support_previous_track
self._support_next_track = support_next_track
self._support_play = support_play
self._support_stop = support_stop
self._support_pause = support_pause
self._is_playing_tv = is_playing_tv
@ -813,6 +848,9 @@ class SonosDevice(MediaPlayerDevice):
if not self._support_next_track:
supported = supported ^ SUPPORT_NEXT_TRACK
if not self._support_play:
supported = supported ^ SUPPORT_PLAY
if not self._support_stop:
supported = supported ^ SUPPORT_STOP
@ -889,21 +927,25 @@ class SonosDevice(MediaPlayerDevice):
@soco_error
def turn_off(self):
"""Turn off media player."""
self.media_pause()
if self._support_pause:
self.media_pause()
@soco_error
@soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
def media_play(self):
"""Send play command."""
self._player.play()
@soco_error
@soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
def media_stop(self):
"""Send stop command."""
self._player.stop()
@soco_error
@soco_filter_upnperror(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
def media_pause(self):
"""Send pause command."""
@ -936,7 +978,8 @@ class SonosDevice(MediaPlayerDevice):
@soco_error
def turn_on(self):
"""Turn the media player on."""
self.media_play()
if self.support_play:
self.media_play()
@soco_error
@soco_coordinator

View File

@ -40,6 +40,7 @@ AUTH_CALLBACK_NAME = 'api:spotify'
ICON = 'mdi:spotify'
DEFAULT_NAME = 'Spotify'
DOMAIN = 'spotify'
CONF_ALIASES = 'aliases'
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
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_SECRET): 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)
@ -89,7 +91,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
configurator = get_component('configurator')
configurator.request_done(hass.data.get(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)
@ -110,14 +113,14 @@ class SpotifyAuthCallbackView(HomeAssistantView):
def get(self, request):
"""Receive authorization token."""
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)
class SpotifyMediaPlayer(MediaPlayerDevice):
"""Representation of a Spotify controller."""
def __init__(self, oauth, name):
def __init__(self, oauth, name, aliases):
"""Initialize."""
self._name = name
self._oauth = oauth
@ -128,10 +131,11 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
self._image_url = None
self._state = STATE_UNKNOWN
self._current_device = None
self._devices = None
self._devices = {}
self._volume = None
self._shuffle = False
self._player = None
self._aliases = aliases
self._token_info = self._oauth.get_cached_token()
def refresh_spotify_instance(self):
@ -154,10 +158,19 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
"""Update state and attributes."""
self.refresh_spotify_instance()
# 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:
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}
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 = self._player.current_playback()
if current is None:
@ -212,8 +225,9 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
def select_source(self, source):
"""Select playback device."""
self._player.transfer_playback(self._devices[source],
self._state == STATE_PLAYING)
if self._devices:
self._player.transfer_playback(self._devices[source],
self._state == STATE_PLAYING)
def play_media(self, media_type, media_id, **kwargs):
"""Play media."""
@ -258,7 +272,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
@property
def source_list(self):
"""Return a list of source devices."""
return list(self._devices.keys())
if self._devices:
return list(self._devices.keys())
@property
def source(self):

View File

@ -133,8 +133,8 @@ def async_setup(hass, config):
hass.data[DATA_MICROSOFT_FACE] = face
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine

View File

@ -16,8 +16,7 @@ from homeassistant.const import (
DOMAIN = 'modbus'
REQUIREMENTS = ['https://github.com/bashwork/pymodbus/archive/'
'd7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0']
REQUIREMENTS = ['pymodbus==1.3.0rc1']
# Type of network
CONF_BAUDRATE = 'baudrate'

View File

@ -403,8 +403,8 @@ def async_setup(hass, config):
yield from hass.data[DATA_MQTT].async_publish(
msg_topic, payload, qos, retain)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(
@ -477,8 +477,8 @@ class MQTT(object):
This method must be run in the event loop and returns a coroutine.
"""
with (yield from self._paho_lock):
yield from self.hass.loop.run_in_executor(
None, self._mqttc.publish, topic, payload, qos, retain)
yield from self.hass.async_add_job(
self._mqttc.publish, topic, payload, qos, retain)
@asyncio.coroutine
def async_connect(self):
@ -486,8 +486,8 @@ class MQTT(object):
This method is a coroutine.
"""
result = yield from self.hass.loop.run_in_executor(
None, self._mqttc.connect, self.broker, self.port, self.keepalive)
result = yield from self.hass.async_add_job(
self._mqttc.connect, self.broker, self.port, self.keepalive)
if result != 0:
import paho.mqtt.client as mqtt
@ -507,7 +507,7 @@ class MQTT(object):
self._mqttc.disconnect()
self._mqttc.loop_stop()
return self.hass.loop.run_in_executor(None, stop)
return self.hass.async_add_job(stop)
@asyncio.coroutine
def async_subscribe(self, topic, qos):
@ -522,8 +522,8 @@ class MQTT(object):
if topic in self.topics:
return
result, mid = yield from self.hass.loop.run_in_executor(
None, self._mqttc.subscribe, topic, qos)
result, mid = yield from self.hass.async_add_job(
self._mqttc.subscribe, topic, qos)
_raise_on_error(result)
self.progress[mid] = topic
@ -535,8 +535,8 @@ class MQTT(object):
This method is a coroutine.
"""
result, mid = yield from self.hass.loop.run_in_executor(
None, self._mqttc.unsubscribe, topic)
result, mid = yield from self.hass.async_add_job(
self._mqttc.unsubscribe, topic)
_raise_on_error(result)
self.progress[mid] = topic

View File

@ -17,7 +17,8 @@ from homeassistant.components.mqtt import CONF_STATE_TOPIC
_LOGGER = logging.getLogger(__name__)
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']
@ -28,6 +29,8 @@ ALLOWED_PLATFORMS = {
'switch': ['mqtt'],
}
ALREADY_DISCOVERED = 'mqtt_discovered_components'
@asyncio.coroutine
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(
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(
hass, component, platform, payload, hass_config)

View File

@ -69,8 +69,8 @@ def send_message(hass, message, title=None, data=None):
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the notify services."""
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file,
descriptions = yield from hass.async_add_job(
load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml'))
targets = {}
@ -97,8 +97,8 @@ def async_setup(hass, config):
notify_service = yield from \
platform.async_get_service(hass, p_config, discovery_info)
elif hasattr(platform, 'get_service'):
notify_service = yield from hass.loop.run_in_executor(
None, platform.get_service, hass, p_config, discovery_info)
notify_service = yield from hass.async_add_job(
platform.get_service, hass, p_config, discovery_info)
else:
raise HomeAssistantError("Invalid notify platform.")
@ -192,5 +192,5 @@ class BaseNotificationService(object):
kwargs can contain ATTR_TITLE to specify a title.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, partial(self.send_message, message, **kwargs))
return self.hass.async_add_job(
partial(self.send_message, message, **kwargs))

View File

@ -246,7 +246,7 @@ class HTML5PushCallbackView(HomeAssistantView):
# 2a. If decode is successful, return the payload.
# 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:
possible_target = self.registrations[target_check[ATTR_TARGET]]
key = possible_target[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH]

View File

@ -85,7 +85,7 @@ class iOSNotificationService(BaseNotificationService):
for target in targets:
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)
return

View File

@ -15,6 +15,8 @@ from homeassistant.components.notify import (
from homeassistant.const import (CONF_RESOURCE, CONF_METHOD, CONF_NAME)
import homeassistant.helpers.config_validation as cv
CONF_DATA = 'data'
CONF_DATA_TEMPLATE = 'data_template'
CONF_MESSAGE_PARAMETER_NAME = 'message_param_name'
CONF_TARGET_PARAMETER_NAME = 'target_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,
vol.Optional(CONF_TITLE_PARAMETER_NAME,
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__)
@ -46,23 +52,28 @@ def get_service(hass, config, discovery_info=None):
message_param_name = config.get(CONF_MESSAGE_PARAMETER_NAME)
title_param_name = config.get(CONF_TITLE_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(
resource, method, message_param_name, title_param_name,
target_param_name)
hass, resource, method, message_param_name,
title_param_name, target_param_name, data, data_template)
class RestNotificationService(BaseNotificationService):
"""Implementation of a notification service for REST."""
def __init__(self, resource, method, message_param_name, title_param_name,
target_param_name):
def __init__(self, hass, resource, method, message_param_name,
title_param_name, target_param_name, data, data_template):
"""Initialize the service."""
self._resource = resource
self._hass = hass
self._method = method.upper()
self._message_param_name = message_param_name
self._title_param_name = title_param_name
self._target_param_name = target_param_name
self._data = data
self._data_template = data_template
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
@ -79,6 +90,20 @@ class RestNotificationService(BaseNotificationService):
# integrations, so just return the first target in the list.
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':
response = requests.post(self._resource, data=data, timeout=10)
elif self._method == 'POST_JSON':

View File

@ -14,7 +14,7 @@ from homeassistant.const import (
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['slacker==0.9.42']
REQUIREMENTS = ['slacker==0.9.50']
_LOGGER = logging.getLogger(__name__)

View File

@ -12,16 +12,17 @@ from email.mime.image import MIMEImage
from email.mime.application import MIMEApplication
import email.utils
import os
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components.notify import (
ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA,
BaseNotificationService)
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_TIMEOUT,
CONF_SENDER, CONF_RECIPIENT)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@ -42,10 +43,10 @@ DEFAULT_STARTTLS = False
# pylint: disable=no-value-for-parameter
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
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_PORT, default=DEFAULT_PORT): cv.port,
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_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
@ -75,11 +76,11 @@ def get_service(hass, config, discovery_info=None):
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,
password, recipients, sender_name, debug):
"""Initialize the service."""
"""Initialize the SMTP service."""
self._server = server
self._port = port
self._timeout = timeout
@ -142,11 +143,11 @@ class MailNotificationService(BaseNotificationService):
if data:
if ATTR_HTML in data:
msg = _build_html_msg(message, data[ATTR_HTML],
images=data.get(ATTR_IMAGES))
msg = _build_html_msg(
message, data[ATTR_HTML], images=data.get(ATTR_IMAGES))
else:
msg = _build_multipart_msg(message,
images=data.get(ATTR_IMAGES))
msg = _build_multipart_msg(
message, images=data.get(ATTR_IMAGES))
else:
msg = _build_text_msg(message)
@ -167,8 +168,7 @@ class MailNotificationService(BaseNotificationService):
mail = self.connect()
for _ in range(self.tries):
try:
mail.sendmail(self._sender, self.recipients,
msg.as_string())
mail.sendmail(self._sender, self.recipients, msg.as_string())
break
except smtplib.SMTPServerDisconnected:
_LOGGER.warning(
@ -210,7 +210,7 @@ def _build_multipart_msg(message, images):
msg.attach(attachment)
attachment.add_header('Content-ID', '<{}>'.format(cid))
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)
attachment = MIMEApplication(file_bytes, Name=atch_name)
attachment['Content-Disposition'] = ('attachment; '
@ -226,8 +226,8 @@ def _build_multipart_msg(message, images):
def _build_html_msg(text, html, images):
"""Build Multipart message with in-line images and rich html (UTF-8)."""
_LOGGER.debug("Building html rich email")
"""Build Multipart message with in-line images and rich HTML (UTF-8)."""
_LOGGER.debug("Building HTML rich email")
msg = MIMEMultipart('related')
alternative = MIMEMultipart('alternative')
alternative.attach(MIMEText(text, _charset='utf-8'))
@ -242,6 +242,6 @@ def _build_html_msg(text, html, images):
msg.attach(attachment)
attachment.add_header('Content-ID', '<{}>'.format(name))
except FileNotFoundError:
_LOGGER.warning('Attachment %s [#%s] not found. Skipping',
_LOGGER.warning("Attachment %s [#%s] not found. Skipping",
atch_name, atch_num)
return msg

View File

@ -92,8 +92,8 @@ def async_setup(hass, config):
hass.states.async_set(entity_id, message, attr)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
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 .util import session_scope
REQUIREMENTS = ['sqlalchemy==1.1.9']
REQUIREMENTS = ['sqlalchemy==1.1.10']
_LOGGER = logging.getLogger(__name__)
@ -44,6 +44,7 @@ DEFAULT_DB_FILE = 'home-assistant_v2.db'
CONF_DB_URL = 'db_url'
CONF_PURGE_DAYS = 'purge_days'
CONF_EVENT_TYPES = 'event_types'
CONNECT_RETRY_WAIT = 3
@ -51,6 +52,8 @@ FILTER_SCHEMA = vol.Schema({
vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
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.Optional(CONF_INCLUDE, default={}): vol.Schema({
@ -142,6 +145,7 @@ class Recorder(threading.Thread):
self.include_d = include.get(CONF_DOMAINS, [])
self.exclude = exclude.get(CONF_ENTITIES, []) + \
exclude.get(CONF_DOMAINS, [])
self.exclude_t = exclude.get(CONF_EVENT_TYPES, [])
self.get_session = None
@ -245,6 +249,9 @@ class Recorder(threading.Thread):
elif event.event_type == EVENT_TIME_CHANGED:
self.queue.task_done()
continue
elif event.event_type in self.exclude_t:
self.queue.task_done()
continue
entity_id = event.data.get(ATTR_ENTITY_ID)
if entity_id is not None:

View File

@ -26,6 +26,8 @@ _LOGGER = logging.getLogger(__name__)
ATTR_ACTIVITY = 'activity'
ATTR_COMMAND = 'command'
ATTR_DEVICE = 'device'
ATTR_NUM_REPEATS = 'num_repeats'
ATTR_DELAY_SECS = 'delay_secs'
DOMAIN = 'remote'
@ -40,6 +42,9 @@ SCAN_INTERVAL = timedelta(seconds=30)
SERVICE_SEND_COMMAND = 'send_command'
SERVICE_SYNC = 'sync'
DEFAULT_NUM_REPEATS = '1'
DEFAULT_DELAY_SECS = '0.4'
REMOTE_SERVICE_SCHEMA = vol.Schema({
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({
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)
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."""
data = {ATTR_DEVICE: str(device), ATTR_COMMAND: command}
if 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)
@ -97,13 +112,16 @@ def async_setup(hass, config):
activity_id = service.data.get(ATTR_ACTIVITY)
device = service.data.get(ATTR_DEVICE)
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:
if service.service == SERVICE_TURN_ON:
yield from remote.async_turn_on(activity=activity_id)
elif service.service == SERVICE_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:
yield from remote.async_turn_off()
@ -122,8 +140,8 @@ def async_setup(hass, config):
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(
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.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.send_command, **kwargs))
return self.hass.async_add_job(ft.partial(self.send_command, **kwargs))

View File

@ -1,187 +1,189 @@
"""
Support for Harmony Hub devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/remote.harmony/
"""
import logging
from os import path
import urllib.parse
import voluptuous as vol
import homeassistant.components.remote as remote
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID)
from homeassistant.components.remote import (
PLATFORM_SCHEMA, DOMAIN, ATTR_DEVICE, ATTR_COMMAND, ATTR_ACTIVITY)
from homeassistant.util import slugify
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['pyharmony==1.0.12']
_LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 5222
DEVICES = []
SERVICE_SYNC = 'harmony_sync'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_HOST): 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,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Harmony platform."""
import pyharmony
global DEVICES
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
_LOGGER.debug("Loading Harmony platform: %s", name)
harmony_conf_file = hass.config.path(
'{}{}{}'.format('harmony_', slugify(name), '.conf'))
try:
_LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s",
host, port)
token = urllib.parse.quote_plus(pyharmony.ha_get_token(host, port))
except ValueError as err:
_LOGGER.warning("%s for remote: %s", err.args[0], name)
return False
_LOGGER.debug("Received token: %s", token)
DEVICES = [HarmonyRemote(
config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT),
config.get(ATTR_ACTIVITY), harmony_conf_file, token)]
add_devices(DEVICES, True)
register_services(hass)
return True
def register_services(hass):
"""Register all services for harmony devices."""
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(
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."""
entity_ids = service.data.get('entity_id')
if entity_ids:
_devices = [device for device in DEVICES
if device.entity_id in entity_ids]
else:
_devices = DEVICES
for device in _devices:
service_func(device, *service_func_args)
device.schedule_update_ha_state(True)
def _sync_service(service):
_apply_service(service, HarmonyRemote.sync)
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."""
import pyharmony
from pathlib import Path
_LOGGER.debug("HarmonyRemote device init started for: %s", name)
self._name = name
self._ip = host
self._port = port
self._state = None
self._current_activity = None
self._default_activity = activity
self._token = token
self._config_path = out_path
_LOGGER.debug("Retrieving harmony config using token: %s", token)
self._config = pyharmony.ha_get_config(self._token, host, port)
if not Path(self._config_path).is_file():
_LOGGER.debug("Writing harmony configuration to file: %s",
out_path)
pyharmony.ha_write_config_file(self._config, self._config_path)
@property
def name(self):
"""Return the Harmony device's name."""
return self._name
@property
def device_state_attributes(self):
"""Add platform specific attributes."""
return {'current_activity': self._current_activity}
@property
def is_on(self):
"""Return False if PowerOff is the current activity, otherwise True."""
return self._current_activity != 'PowerOff'
def update(self):
"""Return current activity."""
import pyharmony
name = self._name
_LOGGER.debug("Polling %s for current activity", name)
state = pyharmony.ha_get_current_activity(
self._token, self._config, self._ip, self._port)
_LOGGER.debug("%s current activity reported as: %s", name, state)
self._current_activity = state
self._state = bool(state != 'PowerOff')
def turn_on(self, **kwargs):
"""Start an activity from the Harmony device."""
import pyharmony
if kwargs[ATTR_ACTIVITY]:
activity = kwargs[ATTR_ACTIVITY]
else:
activity = self._default_activity
if activity:
pyharmony.ha_start_activity(
self._token, self._ip, self._port, self._config, activity)
self._state = True
else:
_LOGGER.error("No activity specified with turn_on service")
def turn_off(self):
"""Start the PowerOff activity."""
import pyharmony
pyharmony.ha_power_off(self._token, self._ip, self._port)
def send_command(self, **kwargs):
"""Send a command to one device."""
import pyharmony
pyharmony.ha_send_command(
self._token, self._ip, self._port, kwargs[ATTR_DEVICE],
kwargs[ATTR_COMMAND])
def sync(self):
"""Sync the Harmony device with the web service."""
import pyharmony
_LOGGER.debug("Syncing hub with Harmony servers")
pyharmony.ha_sync(self._token, self._ip, self._port)
self._config = pyharmony.ha_get_config(
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)
"""
Support for Harmony Hub devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/remote.harmony/
"""
import logging
from os import path
import urllib.parse
import voluptuous as vol
import homeassistant.components.remote as remote
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID)
from homeassistant.components.remote import (
PLATFORM_SCHEMA, DOMAIN, ATTR_DEVICE, ATTR_COMMAND,
ATTR_ACTIVITY, ATTR_NUM_REPEATS, ATTR_DELAY_SECS)
from homeassistant.util import slugify
from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['pyharmony==1.0.16']
_LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 5222
DEVICES = []
SERVICE_SYNC = 'harmony_sync'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_HOST): 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,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Harmony platform."""
import pyharmony
global DEVICES
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
_LOGGER.debug("Loading Harmony platform: %s", name)
harmony_conf_file = hass.config.path(
'{}{}{}'.format('harmony_', slugify(name), '.conf'))
try:
_LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s",
host, port)
token = urllib.parse.quote_plus(pyharmony.ha_get_token(host, port))
except ValueError as err:
_LOGGER.warning("%s for remote: %s", err.args[0], name)
return False
_LOGGER.debug("Received token: %s", token)
DEVICES = [HarmonyRemote(
config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT),
config.get(ATTR_ACTIVITY), harmony_conf_file, token)]
add_devices(DEVICES, True)
register_services(hass)
return True
def register_services(hass):
"""Register all services for harmony devices."""
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(
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."""
entity_ids = service.data.get('entity_id')
if entity_ids:
_devices = [device for device in DEVICES
if device.entity_id in entity_ids]
else:
_devices = DEVICES
for device in _devices:
service_func(device, *service_func_args)
device.schedule_update_ha_state(True)
def _sync_service(service):
_apply_service(service, HarmonyRemote.sync)
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."""
import pyharmony
from pathlib import Path
_LOGGER.debug("HarmonyRemote device init started for: %s", name)
self._name = name
self._ip = host
self._port = port
self._state = None
self._current_activity = None
self._default_activity = activity
self._token = token
self._config_path = out_path
_LOGGER.debug("Retrieving harmony config using token: %s", token)
self._config = pyharmony.ha_get_config(self._token, host, port)
if not Path(self._config_path).is_file():
_LOGGER.debug("Writing harmony configuration to file: %s",
out_path)
pyharmony.ha_write_config_file(self._config, self._config_path)
@property
def name(self):
"""Return the Harmony device's name."""
return self._name
@property
def device_state_attributes(self):
"""Add platform specific attributes."""
return {'current_activity': self._current_activity}
@property
def is_on(self):
"""Return False if PowerOff is the current activity, otherwise True."""
return self._current_activity != 'PowerOff'
def update(self):
"""Return current activity."""
import pyharmony
name = self._name
_LOGGER.debug("Polling %s for current activity", name)
state = pyharmony.ha_get_current_activity(
self._token, self._config, self._ip, self._port)
_LOGGER.debug("%s current activity reported as: %s", name, state)
self._current_activity = state
self._state = bool(state != 'PowerOff')
def turn_on(self, **kwargs):
"""Start an activity from the Harmony device."""
import pyharmony
if kwargs[ATTR_ACTIVITY]:
activity = kwargs[ATTR_ACTIVITY]
else:
activity = self._default_activity
if activity:
pyharmony.ha_start_activity(
self._token, self._ip, self._port, self._config, activity)
self._state = True
else:
_LOGGER.error("No activity specified with turn_on service")
def turn_off(self):
"""Start the PowerOff activity."""
import pyharmony
pyharmony.ha_power_off(self._token, self._ip, self._port)
def send_command(self, **kwargs):
"""Send a set of commands to one device."""
import pyharmony
pyharmony.ha_send_commands(
self._token, self._ip, self._port, kwargs[ATTR_DEVICE],
kwargs[ATTR_COMMAND], int(kwargs[ATTR_NUM_REPEATS]),
float(kwargs[ATTR_DELAY_SECS]))
def sync(self):
"""Sync the Harmony device with the web service."""
import pyharmony
_LOGGER.debug("Syncing hub with Harmony servers")
pyharmony.ha_sync(self._token, self._ip, self._port)
self._config = pyharmony.ha_get_config(
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):
"""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):
"""Update the device."""

View File

@ -64,16 +64,15 @@ class KiraRemote(Entity):
def send_command(self, **kwargs):
"""Send a command to one device."""
code_tuple = (kwargs.get(remote.ATTR_COMMAND),
kwargs.get(remote.ATTR_DEVICE))
_LOGGER.info("Sending Command: %s to %s", *code_tuple)
self._kira.sendCode(code_tuple)
for command in kwargs.get(remote.ATTR_COMMAND):
code_tuple = (command,
kwargs.get(remote.ATTR_DEVICE))
_LOGGER.info("Sending Command: %s to %s", *code_tuple)
self._kira.sendCode(code_tuple)
def async_send_command(self, **kwargs):
"""Send a command to a device.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.loop.run_in_executor(
None, ft.partial(self.send_command, **kwargs))
return self.hass.async_add_job(ft.partial(self.send_command, **kwargs))

View File

@ -30,8 +30,14 @@ send_command:
description: Device ID to send command to
example: '32756745'
command:
description: Command to send
description: A single command or a list of commands to send.
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:
description: Syncs the remote's configuration

View File

@ -371,7 +371,7 @@ class RflinkCommand(RflinkDevice):
# Rflink protocol/transport handles asynchronous writing of buffer
# to serial/tcp device. Does not wait for command send
# 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))
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.
"""
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
https://home-assistant.io/components/scene.hunterdouglas_powerview/
"""
import asyncio
import logging
import voluptuous as vol
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__)
REQUIREMENTS = [
'https://github.com/sander76/powerviewApi/archive'
'/246e782d60d5c0addcc98d7899a0186f9d5640b0.zip#powerviewApi==0.3.15'
]
REQUIREMENTS = ['aiopvapi==1.4']
ENTITY_ID_FORMAT = DOMAIN + '.{}'
HUB_ADDRESS = 'address'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'hunterdouglas_powerview',
vol.Required(HUB_ADDRESS): cv.string,
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the powerview scenes stored in a Powerview hub."""
from powerview_api import powerview
SCENE_DATA = 'sceneData'
ROOM_DATA = 'roomData'
SCENE_NAME = 'name'
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)
websession = async_get_clientsession(hass)
_pv = powerview.PowerView(hub_address)
try:
_scenes = _pv.get_scenes()
_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'])
_hub = Hub(hub_address, hass.loop, websession)
_scenes = yield from _hub.scenes.get_scenes()
_rooms = yield from _hub.rooms.get_rooms()
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):
"""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."""
self.pv_instance = pv_instance
self.hub = hub
self.hass = hass
self.scene_data = scene_data
self._sync_room_data(room_data)
self.entity_id_format = DOMAIN + '.{}'
self.entity_id = generate_entity_id(
self.entity_id_format, str(self.scene_data["id"]), hass=hass)
self._sync_room_data(room_data, scene_data)
self._name = scene_data[SCENE_NAME]
self._scene_id = scene_data[SCENE_ID]
self.entity_id = async_generate_entity_id(
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."""
room = next((room for room in room_data["roomData"]
if room["id"] == self.scene_data["roomId"]), None)
if room is not None:
self.scene_data["roomName"] = room["name"]
room = next((room for room in room_data[ROOM_DATA]
if room[ROOM_ID] == scene_data[ROOM_ID_IN_SCENE]), {})
self._room_name = room.get(ROOM_NAME, '')
@property
def name(self):
"""Return the name of the scene."""
return str(self.scene_data["name"])
return self._name
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {"roomName": self.scene_data["roomName"]}
return {STATE_ATTRIBUTE_ROOM_NAME: self._room_name}
@property
def icon(self):
"""Icon to use in the frontend."""
return 'mdi:blinds'
def activate(self):
"""Activate the scene. Tries to get entities into requested state."""
self.pv_instance.activate_scene(self.scene_data["id"])
def async_activate(self):
"""Activate scene. Try to get entities into requested state."""
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__)
REQUIREMENTS = ['dsmr_parser==0.8']
REQUIREMENTS = ['dsmr_parser==0.9']
CONF_DSMR_VERSION = 'dsmr_version'
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_HOST, default=None): cv.string,
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,
})
@ -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]
# Protocol version specific obis
if dsmr_version == '4':
if dsmr_version in ('4', '5'):
gas_obis = obis_ref.HOURLY_GAS_METER_READING
else:
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
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['fedexdeliverymanager==1.0.2']
REQUIREMENTS = ['fedexdeliverymanager==1.0.3']
_LOGGER = logging.getLogger(__name__)

View File

@ -299,7 +299,7 @@ class FitbitAuthCallbackView(HomeAssistantView):
from oauthlib.oauth2.rfc6749.errors import MissingTokenError
hass = request.app['hass']
data = request.GET
data = request.query
response_message = """Fitbit has been successfully authorized!
You can close this window now!"""

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