Merge pull request #12995 from home-assistant/release-0-65

0.65
This commit is contained in:
Paulus Schoutsen 2018-03-09 09:47:11 -08:00 committed by GitHub
commit ca973b68e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
295 changed files with 11428 additions and 4121 deletions

View File

@ -62,6 +62,9 @@ omit =
homeassistant/components/comfoconnect.py homeassistant/components/comfoconnect.py
homeassistant/components/*/comfoconnect.py homeassistant/components/*/comfoconnect.py
homeassistant/components/daikin.py
homeassistant/components/*/daikin.py
homeassistant/components/deconz/* homeassistant/components/deconz/*
homeassistant/components/*/deconz.py homeassistant/components/*/deconz.py
@ -82,6 +85,9 @@ omit =
homeassistant/components/ecobee.py homeassistant/components/ecobee.py
homeassistant/components/*/ecobee.py homeassistant/components/*/ecobee.py
homeassistant/components/egardia.py
homeassistant/components/*/egardia.py
homeassistant/components/enocean.py homeassistant/components/enocean.py
homeassistant/components/*/enocean.py homeassistant/components/*/enocean.py
@ -181,6 +187,9 @@ omit =
homeassistant/components/opencv.py homeassistant/components/opencv.py
homeassistant/components/*/opencv.py homeassistant/components/*/opencv.py
homeassistant/components/pilight.py
homeassistant/components/*/pilight.py
homeassistant/components/qwikswitch.py homeassistant/components/qwikswitch.py
homeassistant/components/*/qwikswitch.py homeassistant/components/*/qwikswitch.py
@ -244,6 +253,9 @@ omit =
homeassistant/components/notify/twilio_sms.py homeassistant/components/notify/twilio_sms.py
homeassistant/components/notify/twilio_call.py homeassistant/components/notify/twilio_call.py
homeassistant/components/upcloud.py
homeassistant/components/*/upcloud.py
homeassistant/components/usps.py homeassistant/components/usps.py
homeassistant/components/*/usps.py homeassistant/components/*/usps.py
@ -293,13 +305,9 @@ omit =
homeassistant/components/zoneminder.py homeassistant/components/zoneminder.py
homeassistant/components/*/zoneminder.py homeassistant/components/*/zoneminder.py
homeassistant/components/daikin.py
homeassistant/components/*/daikin.py
homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/egardia.py
homeassistant/components/alarm_control_panel/ialarm.py homeassistant/components/alarm_control_panel/ialarm.py
homeassistant/components/alarm_control_panel/manual_mqtt.py homeassistant/components/alarm_control_panel/manual_mqtt.py
homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/nx584.py
@ -312,7 +320,6 @@ omit =
homeassistant/components/binary_sensor/hikvision.py homeassistant/components/binary_sensor/hikvision.py
homeassistant/components/binary_sensor/iss.py homeassistant/components/binary_sensor/iss.py
homeassistant/components/binary_sensor/mystrom.py homeassistant/components/binary_sensor/mystrom.py
homeassistant/components/binary_sensor/pilight.py
homeassistant/components/binary_sensor/ping.py homeassistant/components/binary_sensor/ping.py
homeassistant/components/binary_sensor/rest.py homeassistant/components/binary_sensor/rest.py
homeassistant/components/binary_sensor/tapsaff.py homeassistant/components/binary_sensor/tapsaff.py
@ -353,12 +360,14 @@ omit =
homeassistant/components/cover/scsgate.py homeassistant/components/cover/scsgate.py
homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/automatic.py homeassistant/components/device_tracker/automatic.py
homeassistant/components/device_tracker/bbox.py homeassistant/components/device_tracker/bbox.py
homeassistant/components/device_tracker/bluetooth_le_tracker.py homeassistant/components/device_tracker/bluetooth_le_tracker.py
homeassistant/components/device_tracker/bluetooth_tracker.py homeassistant/components/device_tracker/bluetooth_tracker.py
homeassistant/components/device_tracker/bt_home_hub_5.py homeassistant/components/device_tracker/bt_home_hub_5.py
homeassistant/components/device_tracker/cisco_ios.py homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/gpslogger.py homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/hitron_coda.py homeassistant/components/device_tracker/hitron_coda.py
@ -464,6 +473,7 @@ omit =
homeassistant/components/media_player/russound_rio.py homeassistant/components/media_player/russound_rio.py
homeassistant/components/media_player/russound_rnet.py homeassistant/components/media_player/russound_rnet.py
homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/songpal.py
homeassistant/components/media_player/sonos.py homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/spotify.py homeassistant/components/media_player/spotify.py
homeassistant/components/media_player/squeezebox.py homeassistant/components/media_player/squeezebox.py
@ -508,6 +518,7 @@ omit =
homeassistant/components/notify/simplepush.py homeassistant/components/notify/simplepush.py
homeassistant/components/notify/slack.py homeassistant/components/notify/slack.py
homeassistant/components/notify/smtp.py homeassistant/components/notify/smtp.py
homeassistant/components/notify/synology_chat.py
homeassistant/components/notify/syslog.py homeassistant/components/notify/syslog.py
homeassistant/components/notify/telegram.py homeassistant/components/notify/telegram.py
homeassistant/components/notify/telstra.py homeassistant/components/notify/telstra.py
@ -619,10 +630,12 @@ omit =
homeassistant/components/sensor/ripple.py homeassistant/components/sensor/ripple.py
homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/sense.py
homeassistant/components/sensor/sensehat.py homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial.py homeassistant/components/sensor/serial.py
homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/shodan.py homeassistant/components/sensor/shodan.py
homeassistant/components/sensor/simulated.py
homeassistant/components/sensor/skybeacon.py homeassistant/components/sensor/skybeacon.py
homeassistant/components/sensor/sma.py homeassistant/components/sensor/sma.py
homeassistant/components/sensor/snmp.py homeassistant/components/sensor/snmp.py
@ -639,7 +652,6 @@ omit =
homeassistant/components/sensor/sytadin.py homeassistant/components/sensor/sytadin.py
homeassistant/components/sensor/tank_utility.py homeassistant/components/sensor/tank_utility.py
homeassistant/components/sensor/ted5000.py homeassistant/components/sensor/ted5000.py
homeassistant/components/sensor/teksavvy.py
homeassistant/components/sensor/temper.py homeassistant/components/sensor/temper.py
homeassistant/components/sensor/tibber.py homeassistant/components/sensor/tibber.py
homeassistant/components/sensor/time_date.py homeassistant/components/sensor/time_date.py
@ -658,6 +670,7 @@ omit =
homeassistant/components/sensor/worxlandroid.py homeassistant/components/sensor/worxlandroid.py
homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/zamg.py homeassistant/components/sensor/zamg.py
homeassistant/components/sensor/zestimate.py
homeassistant/components/shiftr.py homeassistant/components/shiftr.py
homeassistant/components/spc.py homeassistant/components/spc.py
homeassistant/components/switch/acer_projector.py homeassistant/components/switch/acer_projector.py
@ -675,7 +688,6 @@ omit =
homeassistant/components/switch/mystrom.py homeassistant/components/switch/mystrom.py
homeassistant/components/switch/netio.py homeassistant/components/switch/netio.py
homeassistant/components/switch/orvibo.py homeassistant/components/switch/orvibo.py
homeassistant/components/switch/pilight.py
homeassistant/components/switch/pulseaudio_loopback.py homeassistant/components/switch/pulseaudio_loopback.py
homeassistant/components/switch/rainbird.py homeassistant/components/switch/rainbird.py
homeassistant/components/switch/rainmachine.py homeassistant/components/switch/rainmachine.py

3
.gitignore vendored
View File

@ -103,3 +103,6 @@ desktop.ini
# mypy # mypy
/.mypy_cache/* /.mypy_cache/*
# Secrets
.lokalise_token

View File

@ -6,12 +6,10 @@ addons:
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- python: "3.4.2" - python: "3.5.3"
env: TOXENV=lint env: TOXENV=lint
- python: "3.4.2" - python: "3.5.3"
env: TOXENV=pylint env: TOXENV=pylint
- python: "3.4.2"
env: TOXENV=py34
# - python: "3.5" # - python: "3.5"
# env: TOXENV=typing # env: TOXENV=typing
- python: "3.5.3" - python: "3.5.3"
@ -30,4 +28,15 @@ cache:
install: pip install -U tox coveralls install: pip install -U tox coveralls
language: python language: python
script: travis_wait 30 tox --develop script: travis_wait 30 tox --develop
services:
- docker
before_deploy:
- docker pull lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7
deploy:
skip_cleanup: true
provider: script
script: script/travis_deploy
on:
branch: dev
condition: $TOXENV = lint
after_success: coveralls after_success: coveralls

4
CODEOWNERS Executable file → Normal file
View File

@ -43,6 +43,7 @@ homeassistant/components/hassio.py @home-assistant/hassio
# Individual components # Individual components
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
homeassistant/components/binary_sensor/hikvision.py @mezz64
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/camera/yi.py @bachya homeassistant/components/camera/yi.py @bachya
homeassistant/components/climate/ephember.py @ttroy50 homeassistant/components/climate/ephember.py @ttroy50
@ -54,6 +55,7 @@ homeassistant/components/device_tracker/tile.py @bachya
homeassistant/components/history_graph.py @andrey-git homeassistant/components/history_graph.py @andrey-git
homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/tplink.py @rytilahti
homeassistant/components/light/yeelight.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti
homeassistant/components/media_player/emby.py @mezz64
homeassistant/components/media_player/kodi.py @armills homeassistant/components/media_player/kodi.py @armills
homeassistant/components/media_player/mediaroom.py @dgomes homeassistant/components/media_player/mediaroom.py @dgomes
homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/media_player/monoprice.py @etsinko
@ -77,6 +79,8 @@ homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/axis.py @kane610 homeassistant/components/*/axis.py @kane610
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/eight_sleep.py @mezz64
homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p homeassistant/components/homekit/* @cdce8p

View File

@ -15,7 +15,6 @@ from homeassistant.const import (
__version__, __version__,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
REQUIRED_PYTHON_VER, REQUIRED_PYTHON_VER,
REQUIRED_PYTHON_VER_WIN,
RESTART_EXIT_CODE, RESTART_EXIT_CODE,
) )
@ -33,12 +32,7 @@ def attempt_use_uvloop():
def validate_python() -> None: def validate_python() -> None:
"""Validate that the right Python version is running.""" """Validate that the right Python version is running."""
if sys.platform == "win32" and \ if sys.version_info[:3] < REQUIRED_PYTHON_VER:
sys.version_info[:3] < REQUIRED_PYTHON_VER_WIN:
print("Home Assistant requires at least Python {}.{}.{}".format(
*REQUIRED_PYTHON_VER_WIN))
sys.exit(1)
elif sys.version_info[:3] < REQUIRED_PYTHON_VER:
print("Home Assistant requires at least Python {}.{}.{}".format( print("Home Assistant requires at least Python {}.{}.{}".format(
*REQUIRED_PYTHON_VER)) *REQUIRED_PYTHON_VER))
sys.exit(1) sys.exit(1)

View File

@ -112,18 +112,13 @@ def async_from_config_dict(config: Dict[str, Any],
if not loader.PREPARED: if not loader.PREPARED:
yield from hass.async_add_job(loader.prepare, hass) yield from hass.async_add_job(loader.prepare, hass)
# Make a copy because we are mutating it.
config = OrderedDict(config)
# Merge packages # Merge packages
conf_util.merge_packages_config( conf_util.merge_packages_config(
config, core_config.get(conf_util.CONF_PACKAGES, {})) config, core_config.get(conf_util.CONF_PACKAGES, {}))
# Make a copy because we are mutating it.
# Use OrderedDict in case original one was one.
# Convert values to dictionaries if they are None
new_config = OrderedDict()
for key, value in config.items():
new_config[key] = value or {}
config = new_config
hass.config_entries = config_entries.ConfigEntries(hass, config) hass.config_entries = config_entries.ConfigEntries(hass, config)
yield from hass.config_entries.async_load() yield from hass.config_entries.async_load()

View File

@ -156,9 +156,10 @@ def async_setup(hass, config):
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service) ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
hass.helpers.intent.async_register(intent.ServiceIntentHandler( hass.helpers.intent.async_register(intent.ServiceIntentHandler(
intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned on {}")) intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on"))
hass.helpers.intent.async_register(intent.ServiceIntentHandler( hass.helpers.intent.async_register(intent.ServiceIntentHandler(
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF, "Turned off {}")) intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF,
"Turned {} off"))
hass.helpers.intent.async_register(intent.ServiceIntentHandler( hass.helpers.intent.async_register(intent.ServiceIntentHandler(
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}")) intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"))

View File

@ -17,7 +17,7 @@ from homeassistant.const import (
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyalarmdotcom==0.3.0'] REQUIREMENTS = ['pyalarmdotcom==0.3.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -26,7 +26,7 @@ DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'CONCORD232' DEFAULT_NAME = 'CONCORD232'
DEFAULT_PORT = 5007 DEFAULT_PORT = 5007
SCAN_INTERVAL = timedelta(seconds=1) SCAN_INTERVAL = timedelta(seconds=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,

View File

@ -4,130 +4,65 @@ Interfaces with Egardia/Woonveilig alarm control panel.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.egardia/ https://home-assistant.io/components/alarm_control_panel.egardia/
""" """
import asyncio
import logging import logging
import requests import requests
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
EVENT_HOMEASSISTANT_STOP, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_UNKNOWN) from homeassistant.components.egardia import (
import homeassistant.exceptions as exc EGARDIA_DEVICE, EGARDIA_SERVER,
import homeassistant.helpers.config_validation as cv REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES,
CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT
REQUIREMENTS = ['pythonegardia==1.0.26'] )
REQUIREMENTS = ['pythonegardia==1.0.38']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_REPORT_SERVER_CODES = 'report_server_codes'
CONF_REPORT_SERVER_ENABLED = 'report_server_enabled'
CONF_REPORT_SERVER_PORT = 'report_server_port'
CONF_REPORT_SERVER_CODES_IGNORE = 'ignore'
CONF_VERSION = 'version'
DEFAULT_NAME = 'Egardia'
DEFAULT_PORT = 80
DEFAULT_REPORT_SERVER_ENABLED = False
DEFAULT_REPORT_SERVER_PORT = 52010
DEFAULT_VERSION = 'GATE-01'
DOMAIN = 'egardia'
D_EGARDIASRV = 'egardiaserver'
NOTIFICATION_ID = 'egardia_notification'
NOTIFICATION_TITLE = 'Egardia'
STATES = { STATES = {
'ARM': STATE_ALARM_ARMED_AWAY, 'ARM': STATE_ALARM_ARMED_AWAY,
'DAY HOME': STATE_ALARM_ARMED_HOME, 'DAY HOME': STATE_ALARM_ARMED_HOME,
'DISARM': STATE_ALARM_DISARMED, 'DISARM': STATE_ALARM_DISARMED,
'HOME': STATE_ALARM_ARMED_HOME, 'ARMHOME': STATE_ALARM_ARMED_HOME,
'TRIGGERED': STATE_ALARM_TRIGGERED, 'TRIGGERED': STATE_ALARM_TRIGGERED
'UNKNOWN': STATE_UNKNOWN,
} }
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_REPORT_SERVER_CODES): vol.All(cv.ensure_list),
vol.Optional(CONF_REPORT_SERVER_ENABLED,
default=DEFAULT_REPORT_SERVER_ENABLED): cv.boolean,
vol.Optional(CONF_REPORT_SERVER_PORT, default=DEFAULT_REPORT_SERVER_PORT):
cv.port,
})
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Egardia platform.""" """Set up the Egardia platform."""
from pythonegardia import egardiadevice device = EgardiaAlarm(
from pythonegardia import egardiaserver discovery_info['name'],
hass.data[EGARDIA_DEVICE],
name = config.get(CONF_NAME) discovery_info[CONF_REPORT_SERVER_ENABLED],
username = config.get(CONF_USERNAME) discovery_info.get(CONF_REPORT_SERVER_CODES),
password = config.get(CONF_PASSWORD) discovery_info[CONF_REPORT_SERVER_PORT])
host = config.get(CONF_HOST) # add egardia alarm device
port = config.get(CONF_PORT) add_devices([device], True)
rs_enabled = config.get(CONF_REPORT_SERVER_ENABLED)
rs_port = config.get(CONF_REPORT_SERVER_PORT)
rs_codes = config.get(CONF_REPORT_SERVER_CODES)
version = config.get(CONF_VERSION)
try:
egardiasystem = egardiadevice.EgardiaDevice(
host, port, username, password, '', version)
except requests.exceptions.RequestException:
raise exc.PlatformNotReady()
except egardiadevice.UnauthorizedError:
_LOGGER.error("Unable to authorize. Wrong password or username")
return
eg_dev = EgardiaAlarm(
name, egardiasystem, rs_enabled, rs_codes)
if rs_enabled:
# Set up the egardia server
_LOGGER.info("Setting up EgardiaServer")
try:
if D_EGARDIASRV not in hass.data:
server = egardiaserver.EgardiaServer('', rs_port)
bound = server.bind()
if not bound:
raise IOError(
"Binding error occurred while starting EgardiaServer")
hass.data[D_EGARDIASRV] = server
server.start()
except IOError:
return
hass.data[D_EGARDIASRV].register_callback(eg_dev.handle_status_event)
def handle_stop_event(event):
"""Call function for Home Assistant stop event."""
hass.data[D_EGARDIASRV].stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event)
add_devices([eg_dev], True)
class EgardiaAlarm(alarm.AlarmControlPanel): class EgardiaAlarm(alarm.AlarmControlPanel):
"""Representation of a Egardia alarm.""" """Representation of a Egardia alarm."""
def __init__(self, name, egardiasystem, rs_enabled=False, rs_codes=None): def __init__(self, name, egardiasystem,
rs_enabled=False, rs_codes=None, rs_port=52010):
"""Initialize the Egardia alarm.""" """Initialize the Egardia alarm."""
self._name = name self._name = name
self._egardiasystem = egardiasystem self._egardiasystem = egardiasystem
self._status = None self._status = None
self._rs_enabled = rs_enabled self._rs_enabled = rs_enabled
if rs_codes is not None:
self._rs_codes = rs_codes[0]
else:
self._rs_codes = rs_codes self._rs_codes = rs_codes
self._rs_port = rs_port
@asyncio.coroutine
def async_added_to_hass(self):
"""Add Egardiaserver callback if enabled."""
if self._rs_enabled:
_LOGGER.debug("Registering callback to Egardiaserver")
self.hass.data[EGARDIA_SERVER].register_callback(
self.handle_status_event)
@property @property
def name(self): def name(self):
@ -156,31 +91,20 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
def lookupstatusfromcode(self, statuscode): def lookupstatusfromcode(self, statuscode):
"""Look at the rs_codes and returns the status from the code.""" """Look at the rs_codes and returns the status from the code."""
status = 'UNKNOWN' status = next((
if self._rs_codes is not None: status_group.upper() for status_group, codes
statuscode = str(statuscode).strip() in self._rs_codes.items() for code in codes
for i in self._rs_codes: if statuscode == code), 'UNKNOWN')
val = str(self._rs_codes[i]).strip()
if ',' in val:
splitted = val.split(',')
for code in splitted:
code = str(code).strip()
if statuscode == code:
status = i.upper()
break
elif statuscode == val:
status = i.upper()
break
return status return status
def parsestatus(self, status): def parsestatus(self, status):
"""Parse the status.""" """Parse the status."""
_LOGGER.debug("Parsing status %s", status) _LOGGER.debug("Parsing status %s", status)
# Ignore the statuscode if it is IGNORE # Ignore the statuscode if it is IGNORE
if status.lower().strip() != CONF_REPORT_SERVER_CODES_IGNORE: if status.lower().strip() != REPORT_SERVER_CODES_IGNORE:
_LOGGER.debug("Not ignoring status") _LOGGER.debug("Not ignoring status %s", status)
newstatus = ([v for k, v in STATES.items() newstatus = STATES.get(status.upper())
if status.upper() == k][0]) _LOGGER.debug("newstatus %s", newstatus)
self._status = newstatus self._status = newstatus
else: else:
_LOGGER.error("Ignoring status") _LOGGER.error("Ignoring status")

View File

@ -131,8 +131,7 @@ class APIEventStream(HomeAssistantView):
msg = "data: {}\n\n".format(payload) msg = "data: {}\n\n".format(payload)
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
msg.strip()) msg.strip())
response.write(msg.encode("UTF-8")) yield from response.write(msg.encode("UTF-8"))
yield from response.drain()
except asyncio.TimeoutError: except asyncio.TimeoutError:
yield from to_write.put(STREAM_PING_PAYLOAD) yield from to_write.put(STREAM_PING_PAYLOAD)

View File

@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
_CONFIGURING = {} _CONFIGURING = {}
REQUIREMENTS = ['py-august==0.3.0'] REQUIREMENTS = ['py-august==0.4.0']
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
ACTIVITY_FETCH_LIMIT = 10 ACTIVITY_FETCH_LIMIT = 10
@ -159,7 +159,7 @@ class AugustData:
self._api = api self._api = api
self._access_token = access_token self._access_token = access_token
self._doorbells = self._api.get_doorbells(self._access_token) or [] self._doorbells = self._api.get_doorbells(self._access_token) or []
self._locks = self._api.get_locks(self._access_token) or [] self._locks = self._api.get_operable_locks(self._access_token) or []
self._house_ids = [d.house_id for d in self._doorbells + self._locks] self._house_ids = [d.house_id for d in self._doorbells + self._locks]
self._doorbell_detail_by_id = {} self._doorbell_detail_by_id = {}

View File

@ -4,7 +4,7 @@ Component to interface with binary sensors.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor/ https://home-assistant.io/components/binary_sensor/
""" """
import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
@ -28,6 +28,7 @@ DEVICE_CLASSES = [
'gas', # On means gas detected, Off means no gas (clear) 'gas', # On means gas detected, Off means no gas (clear)
'heat', # On means hot, Off means normal 'heat', # On means hot, Off means normal
'light', # On means light detected, Off means no light 'light', # On means light detected, Off means no light
'lock', # On means open (unlocked), Off means closed (locked)
'moisture', # On means wet, Off means dry 'moisture', # On means wet, Off means dry
'motion', # On means motion detected, Off means no motion (clear) 'motion', # On means motion detected, Off means no motion (clear)
'moving', # On means moving, Off means not moving (stopped) 'moving', # On means moving, Off means not moving (stopped)
@ -47,13 +48,12 @@ DEVICE_CLASSES = [
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Track states and offer events for binary sensors.""" """Track states and offer events for binary sensors."""
component = EntityComponent( component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_setup(config) await component.async_setup(config)
return True return True

View File

@ -27,7 +27,7 @@ DEFAULT_NAME = 'Alarm'
DEFAULT_PORT = '5007' DEFAULT_PORT = '5007'
DEFAULT_SSL = False DEFAULT_SSL = False
SCAN_INTERVAL = datetime.timedelta(seconds=1) SCAN_INTERVAL = datetime.timedelta(seconds=10)
ZONE_TYPES_SCHEMA = vol.Schema({ ZONE_TYPES_SCHEMA = vol.Schema({
cv.positive_int: vol.In(DEVICE_CLASSES), cv.positive_int: vol.In(DEVICE_CLASSES),

View File

@ -0,0 +1,78 @@
"""
Interfaces with Egardia/Woonveilig alarm control panel.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.egardia/
"""
import asyncio
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.components.egardia import (
EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES)
_LOGGER = logging.getLogger(__name__)
EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion',
'Door Contact': 'opening',
'IR': 'motion'}
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Initialize the platform."""
if (discovery_info is None or
discovery_info[ATTR_DISCOVER_DEVICES] is None):
return
disc_info = discovery_info[ATTR_DISCOVER_DEVICES]
# multiple devices here!
async_add_devices(
(
EgardiaBinarySensor(
sensor_id=disc_info[sensor]['id'],
name=disc_info[sensor]['name'],
egardia_system=hass.data[EGARDIA_DEVICE],
device_class=EGARDIA_TYPE_TO_DEVICE_CLASS.get(
disc_info[sensor]['type'], None)
)
for sensor in disc_info
), True)
class EgardiaBinarySensor(BinarySensorDevice):
"""Represents a sensor based on an Egardia sensor (IR, Door Contact)."""
def __init__(self, sensor_id, name, egardia_system, device_class):
"""Initialize the sensor device."""
self._id = sensor_id
self._name = name
self._state = None
self._device_class = device_class
self._egardia_system = egardia_system
def update(self):
"""Update the status."""
egardia_input = self._egardia_system.getsensorstate(self._id)
self._state = STATE_ON if egardia_input else STATE_OFF
@property
def name(self):
"""The name of the device."""
return self._name
@property
def is_on(self):
"""Whether the device is switched on."""
return self._state == STATE_ON
@property
def hidden(self):
"""Whether the device is hidden by default."""
# these type of sensors are probably mainly used for automations
return True
@property
def device_class(self):
"""The device class."""
return self._device_class

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE) ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
REQUIREMENTS = ['pyhik==0.1.4'] REQUIREMENTS = ['pyhik==0.1.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_IGNORED = 'ignored' CONF_IGNORED = 'ignored'
@ -48,6 +48,9 @@ DEVICE_CLASS_MAP = {
'Face Detection': 'motion', 'Face Detection': 'motion',
'Scene Change Detection': 'motion', 'Scene Change Detection': 'motion',
'I/O': None, 'I/O': None,
'Unattended Baggage': 'motion',
'Attended Baggage': 'motion',
'Recording Failure': None,
} }
CUSTOMIZE_SCHEMA = vol.Schema({ CUSTOMIZE_SCHEMA = vol.Schema({
@ -211,7 +214,7 @@ class HikvisionBinarySensor(BinarySensorDevice):
@property @property
def unique_id(self): def unique_id(self):
"""Return an unique ID.""" """Return a unique ID."""
return self._id return self._id
@property @property

View File

@ -2,86 +2,56 @@
Support for INSTEON dimmers via PowerLinc Modem. Support for INSTEON dimmers via PowerLinc Modem.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/insteon_plm/ https://home-assistant.io/components/binary_sensor.insteon_plm/
""" """
import logging
import asyncio import asyncio
import logging
from homeassistant.core import callback
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.loader import get_component from homeassistant.components.insteon_plm import InsteonPLMEntity
DEPENDENCIES = ['insteon_plm'] DEPENDENCIES = ['insteon_plm']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {'openClosedSensor': 'opening',
'motionSensor': 'motion',
'doorSensor': 'door',
'leakSensor': 'moisture'}
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the INSTEON PLM device class for the hass platform.""" """Set up the INSTEON PLM device class for the hass platform."""
plm = hass.data['insteon_plm'] plm = hass.data['insteon_plm']
device_list = [] address = discovery_info['address']
for device in discovery_info: device = plm.devices[address]
name = device.get('address') state_key = discovery_info['state_key']
address = device.get('address_hex')
_LOGGER.info('Registered %s with binary_sensor platform.', name) _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform',
device.address.hex, device.states[state_key].name)
device_list.append( new_entity = InsteonPLMBinarySensor(device, state_key)
InsteonPLMBinarySensorDevice(hass, plm, address, name)
)
async_add_devices(device_list) async_add_devices([new_entity])
class InsteonPLMBinarySensorDevice(BinarySensorDevice): class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
"""A Class for an Insteon device.""" """A Class for an Insteon device entity."""
def __init__(self, hass, plm, address, name): def __init__(self, device, state_key):
"""Initialize the binarysensor.""" """Initialize the INSTEON PLM binary sensor."""
self._hass = hass super().__init__(device, state_key)
self._plm = plm.protocol self._sensor_type = SENSOR_TYPES.get(self._insteon_device_state.name)
self._address = address
self._name = name
self._plm.add_update_callback(
self.async_binarysensor_update, {'address': self._address})
@property @property
def should_poll(self): def device_class(self):
"""No polling needed.""" """Return the class of this sensor."""
return False return self._sensor_type
@property
def address(self):
"""Return the address of the node."""
return self._address
@property
def name(self):
"""Return the name of the node."""
return self._name
@property @property
def is_on(self): def is_on(self):
"""Return the boolean response if the node is on.""" """Return the boolean response if the node is on."""
sensorstate = self._plm.get_device_attr(self._address, 'sensorstate') sensorstate = self._insteon_device_state.value
_LOGGER.info("Sensor state for %s is %s", self._address, sensorstate)
return bool(sensorstate) return bool(sensorstate)
@property
def device_state_attributes(self):
"""Provide attributes for display on device card."""
insteon_plm = get_component('insteon_plm')
return insteon_plm.common_attributes(self)
def get_attr(self, key):
"""Return specified attribute for this device."""
return self._plm.get_device_attr(self.address, key)
@callback
def async_binarysensor_update(self, message):
"""Receive notification from transport that new data exists."""
_LOGGER.info("Received update callback from PLM for %s", self._address)
self._hass.async_add_job(self.async_update_ha_state())

View File

@ -56,24 +56,17 @@ def setup_platform(hass, config: ConfigType,
else: else:
device_type = _detect_device_type(node) device_type = _detect_device_type(node)
subnode_id = int(node.nid[-1]) subnode_id = int(node.nid[-1])
if device_type == 'opening': if (device_type == 'opening' or device_type == 'moisture'):
# Door/window sensors use an optional "negative" node # These sensors use an optional "negative" subnode 2 to snag
if subnode_id == 4: # all state changes
if subnode_id == 2:
parent_device.add_negative_node(node)
elif subnode_id == 4:
# Subnode 4 is the heartbeat node, which we will represent # Subnode 4 is the heartbeat node, which we will represent
# as a separate binary_sensor # as a separate binary_sensor
device = ISYBinarySensorHeartbeat(node, parent_device) device = ISYBinarySensorHeartbeat(node, parent_device)
parent_device.add_heartbeat_device(device) parent_device.add_heartbeat_device(device)
devices.append(device) devices.append(device)
elif subnode_id == 2:
parent_device.add_negative_node(node)
elif device_type == 'moisture':
# Moisture nodes have a subnode 2, but we ignore it because
# it's just the inverse of the primary node.
if subnode_id == 4:
# Heartbeat node
device = ISYBinarySensorHeartbeat(node, parent_device)
parent_device.add_heartbeat_device(device)
devices.append(device)
else: else:
# We don't yet have any special logic for other sensor types, # We don't yet have any special logic for other sensor types,
# so add the nodes as individual devices # so add the nodes as individual devices

View File

@ -4,7 +4,6 @@ Support for KNX/IP binary sensors.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.knx/ https://home-assistant.io/components/binary_sensor.knx/
""" """
import asyncio
import voluptuous as vol import voluptuous as vol
@ -26,6 +25,7 @@ CONF_DEFAULT_HOOK = 'on'
CONF_COUNTER = 'counter' CONF_COUNTER = 'counter'
CONF_DEFAULT_COUNTER = 1 CONF_DEFAULT_COUNTER = 1
CONF_ACTION = 'action' CONF_ACTION = 'action'
CONF_RESET_AFTER = 'reset_after'
CONF__ACTION = 'turn_off_action' CONF__ACTION = 'turn_off_action'
@ -49,12 +49,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICE_CLASS): cv.string, vol.Optional(CONF_DEVICE_CLASS): cv.string,
vol.Optional(CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT): vol.Optional(CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT):
cv.positive_int, cv.positive_int,
vol.Optional(CONF_RESET_AFTER): cv.positive_int,
vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA, vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA,
}) })
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Set up binary sensor(s) for KNX platform.""" """Set up binary sensor(s) for KNX platform."""
if discovery_info is not None: if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices) async_add_devices_discovery(hass, discovery_info, async_add_devices)
@ -82,7 +83,8 @@ def async_add_devices_config(hass, config, async_add_devices):
name=name, name=name,
group_address=config.get(CONF_ADDRESS), group_address=config.get(CONF_ADDRESS),
device_class=config.get(CONF_DEVICE_CLASS), device_class=config.get(CONF_DEVICE_CLASS),
significant_bit=config.get(CONF_SIGNIFICANT_BIT)) significant_bit=config.get(CONF_SIGNIFICANT_BIT),
reset_after=config.get(CONF_RESET_AFTER))
hass.data[DATA_KNX].xknx.devices.add(binary_sensor) hass.data[DATA_KNX].xknx.devices.add(binary_sensor)
entity = KNXBinarySensor(hass, binary_sensor) entity = KNXBinarySensor(hass, binary_sensor)
@ -111,11 +113,10 @@ class KNXBinarySensor(BinarySensorDevice):
@callback @callback
def async_register_callbacks(self): def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed.""" """Register callbacks to update hass after device was changed."""
@asyncio.coroutine async def after_update_callback(device):
def after_update_callback(device):
"""Call after device was updated.""" """Call after device was updated."""
# pylint: disable=unused-argument # pylint: disable=unused-argument
yield from self.async_update_ha_state() await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback) self.device.register_device_updated_cb(after_update_callback)
@property @property

View File

@ -0,0 +1,38 @@
"""
Support for monitoring the state of UpCloud servers.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.upcloud/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.upcloud import (
UpCloudServerEntity, CONF_SERVERS, DATA_UPCLOUD)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['upcloud']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SERVERS): vol.All(cv.ensure_list, [cv.string]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the UpCloud server binary sensor."""
upcloud = hass.data[DATA_UPCLOUD]
servers = config.get(CONF_SERVERS)
devices = [UpCloudBinarySensor(upcloud, uuid) for uuid in servers]
add_devices(devices, True)
class UpCloudBinarySensor(UpCloudServerEntity, BinarySensorDevice):
"""Representation of an UpCloud server sensor."""

View File

@ -319,7 +319,10 @@ class XiaomiButton(XiaomiBinarySensor):
click_type = 'double' click_type = 'double'
elif value == 'both_click': elif value == 'both_click':
click_type = 'both' click_type = 'both'
elif value == 'shake':
click_type = 'shake'
else: else:
_LOGGER.warning("Unsupported click_type detected: %s", value)
return False return False
self._hass.bus.fire('click', { self._hass.bus.fire('click', {

View File

@ -4,7 +4,6 @@ Binary sensors on Zigbee Home Automation networks.
For more details on this platform, please refer to the documentation For more details on this platform, please refer to the documentation
at https://home-assistant.io/components/binary_sensor.zha/ at https://home-assistant.io/components/binary_sensor.zha/
""" """
import asyncio
import logging import logging
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
@ -25,8 +24,8 @@ CLASS_MAPPING = {
} }
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Set up the Zigbee Home Automation binary sensors.""" """Set up the Zigbee Home Automation binary sensors."""
discovery_info = zha.get_discovery_info(hass, discovery_info) discovery_info = zha.get_discovery_info(hass, discovery_info)
if discovery_info is None: if discovery_info is None:
@ -39,19 +38,19 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
device_class = None device_class = None
cluster = in_clusters[IasZone.cluster_id] cluster = in_clusters[IasZone.cluster_id]
if discovery_info['new_join']: if discovery_info['new_join']:
yield from cluster.bind() await cluster.bind()
ieee = cluster.endpoint.device.application.ieee ieee = cluster.endpoint.device.application.ieee
yield from cluster.write_attributes({'cie_addr': ieee}) await cluster.write_attributes({'cie_addr': ieee})
try: try:
zone_type = yield from cluster['zone_type'] zone_type = await cluster['zone_type']
device_class = CLASS_MAPPING.get(zone_type, None) device_class = CLASS_MAPPING.get(zone_type, None)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
# If we fail to read from the device, use a non-specific class # If we fail to read from the device, use a non-specific class
pass pass
sensor = BinarySensor(device_class, **discovery_info) sensor = BinarySensor(device_class, **discovery_info)
async_add_devices([sensor]) async_add_devices([sensor], update_before_add=True)
class BinarySensor(zha.Entity, BinarySensorDevice): class BinarySensor(zha.Entity, BinarySensorDevice):
@ -66,6 +65,11 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
from zigpy.zcl.clusters.security import IasZone from zigpy.zcl.clusters.security import IasZone
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id] self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
@property
def should_poll(self) -> bool:
"""Let zha handle polling."""
return False
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return True if entity is on.""" """Return True if entity is on."""
@ -83,7 +87,18 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
if command_id == 0: if command_id == 0:
self._state = args[0] & 3 self._state = args[0] & 3
_LOGGER.debug("Updated alarm state: %s", self._state) _LOGGER.debug("Updated alarm state: %s", self._state)
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
elif command_id == 1: elif command_id == 1:
_LOGGER.debug("Enroll requested") _LOGGER.debug("Enroll requested")
self.hass.add_job(self._ias_zone_cluster.enroll_response(0, 0)) res = self._ias_zone_cluster.enroll_response(0, 0)
self.hass.async_add_job(res)
async def async_update(self):
"""Retrieve latest state."""
from bellows.types.basic import uint16_t
result = await zha.safe_read(self._endpoint.ias_zone,
['zone_status'])
state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3

View File

@ -264,9 +264,9 @@ class Camera(Entity):
'boundary=--frameboundary') 'boundary=--frameboundary')
yield from response.prepare(request) yield from response.prepare(request)
def write(img_bytes): async def write(img_bytes):
"""Write image to stream.""" """Write image to stream."""
response.write(bytes( await response.write(bytes(
'--frameboundary\r\n' '--frameboundary\r\n'
'Content-Type: {}\r\n' 'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format( 'Content-Length: {}\r\n\r\n'.format(
@ -282,15 +282,14 @@ class Camera(Entity):
break break
if img_bytes and img_bytes != last_image: if img_bytes and img_bytes != last_image:
write(img_bytes) yield from write(img_bytes)
# Chrome seems to always ignore first picture, # Chrome seems to always ignore first picture,
# print it twice. # print it twice.
if last_image is None: if last_image is None:
write(img_bytes) yield from write(img_bytes)
last_image = img_bytes last_image = img_bytes
yield from response.drain()
yield from asyncio.sleep(.5) yield from asyncio.sleep(.5)

View File

@ -33,6 +33,9 @@ DEFAULT_PORT = 5000
DEFAULT_USERNAME = 'admin' DEFAULT_USERNAME = 'admin'
DEFAULT_PASSWORD = '888888' DEFAULT_PASSWORD = '888888'
DEFAULT_ARGUMENTS = '-q:v 2' DEFAULT_ARGUMENTS = '-q:v 2'
DEFAULT_PROFILE = 0
CONF_PROFILE = "profile"
ATTR_PAN = "pan" ATTR_PAN = "pan"
ATTR_TILT = "tilt" ATTR_TILT = "tilt"
@ -57,6 +60,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string,
vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE):
vol.All(vol.Coerce(int), vol.Range(min=0)),
}) })
SERVICE_PTZ_SCHEMA = vol.Schema({ SERVICE_PTZ_SCHEMA = vol.Schema({
@ -67,8 +72,7 @@ SERVICE_PTZ_SCHEMA = vol.Schema({
}) })
@asyncio.coroutine def setup_platform(hass, config, add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up a ONVIF camera.""" """Set up a ONVIF camera."""
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_HOST)): if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_HOST)):
return return
@ -91,7 +95,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
hass.services.async_register(DOMAIN, SERVICE_PTZ, handle_ptz, hass.services.async_register(DOMAIN, SERVICE_PTZ, handle_ptz,
schema=SERVICE_PTZ_SCHEMA) schema=SERVICE_PTZ_SCHEMA)
async_add_devices([ONVIFHassCamera(hass, config)]) add_devices([ONVIFHassCamera(hass, config)])
class ONVIFHassCamera(Camera): class ONVIFHassCamera(Camera):
@ -114,10 +118,17 @@ class ONVIFHassCamera(Camera):
config.get(CONF_USERNAME), config.get(CONF_PASSWORD) config.get(CONF_USERNAME), config.get(CONF_PASSWORD)
) )
media_service = camera.create_media_service() media_service = camera.create_media_service()
stream_uri = media_service.GetStreamUri( self._profiles = media_service.GetProfiles()
{'StreamSetup': {'Stream': 'RTP-Unicast', 'Transport': 'RTSP'}} self._profile_index = config.get(CONF_PROFILE)
) if self._profile_index >= len(self._profiles):
self._input = stream_uri.Uri.replace( _LOGGER.warning("ONVIF Camera '%s' doesn't provide profile %d."
" Using the last profile.",
self._name, self._profile_index)
self._profile_index = -1
req = media_service.create_type('GetStreamUri')
# pylint: disable=protected-access
req.ProfileToken = self._profiles[self._profile_index]._token
self._input = media_service.GetStreamUri(req).Uri.replace(
'rtsp://', 'rtsp://{}:{}@'.format( 'rtsp://', 'rtsp://{}:{}@'.format(
config.get(CONF_USERNAME), config.get(CONF_USERNAME),
config.get(CONF_PASSWORD)), 1) config.get(CONF_PASSWORD)), 1)

View File

@ -0,0 +1,262 @@
"""
Proxy camera platform that enables image processing of camera data.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/proxy
"""
import logging
import asyncio
import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant.util.async import run_coroutine_threadsafe
from homeassistant.helpers import config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.const import (
CONF_NAME, CONF_ENTITY_ID, HTTP_HEADER_HA_AUTH)
from homeassistant.components.camera import (
PLATFORM_SCHEMA, Camera)
from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_aiohttp_proxy_web)
REQUIREMENTS = ['pillow==5.0.0']
_LOGGER = logging.getLogger(__name__)
CONF_MAX_IMAGE_WIDTH = "max_image_width"
CONF_IMAGE_QUALITY = "image_quality"
CONF_IMAGE_REFRESH_RATE = "image_refresh_rate"
CONF_FORCE_RESIZE = "force_resize"
CONF_MAX_STREAM_WIDTH = "max_stream_width"
CONF_STREAM_QUALITY = "stream_quality"
CONF_CACHE_IMAGES = "cache_images"
DEFAULT_BASENAME = "Camera Proxy"
DEFAULT_QUALITY = 75
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MAX_IMAGE_WIDTH): int,
vol.Optional(CONF_IMAGE_QUALITY): int,
vol.Optional(CONF_IMAGE_REFRESH_RATE): float,
vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean,
vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean,
vol.Optional(CONF_MAX_STREAM_WIDTH): int,
vol.Optional(CONF_STREAM_QUALITY): int,
})
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the Proxy camera platform."""
async_add_devices([ProxyCamera(hass, config)])
async def _read_frame(req):
"""Read a single frame from an MJPEG stream."""
# based on https://gist.github.com/russss/1143799
import cgi
# Read in HTTP headers:
stream = req.content
# multipart/x-mixed-replace; boundary=--frameboundary
_mimetype, options = cgi.parse_header(req.headers['content-type'])
boundary = options.get('boundary').encode('utf-8')
if not boundary:
_LOGGER.error("Malformed MJPEG missing boundary")
raise Exception("Can't find content-type")
line = await stream.readline()
# Seek ahead to the first chunk
while line.strip() != boundary:
line = await stream.readline()
# Read in chunk headers
while line.strip() != b'':
parts = line.split(b':')
if len(parts) > 1 and parts[0].lower() == b'content-length':
# Grab chunk length
length = int(parts[1].strip())
line = await stream.readline()
image = await stream.read(length)
return image
def _resize_image(image, opts):
"""Resize image."""
from PIL import Image
import io
if not opts:
return image
quality = opts.quality or DEFAULT_QUALITY
new_width = opts.max_width
img = Image.open(io.BytesIO(image))
imgfmt = str(img.format)
if imgfmt != 'PNG' and imgfmt != 'JPEG':
_LOGGER.debug("Image is of unsupported type: %s", imgfmt)
return image
(old_width, old_height) = img.size
old_size = len(image)
if old_width <= new_width:
if opts.quality is None:
_LOGGER.debug("Image is smaller-than / equal-to requested width")
return image
new_width = old_width
scale = new_width / float(old_width)
new_height = int((float(old_height)*float(scale)))
img = img.resize((new_width, new_height), Image.ANTIALIAS)
imgbuf = io.BytesIO()
img.save(imgbuf, "JPEG", optimize=True, quality=quality)
newimage = imgbuf.getvalue()
if not opts.force_resize and len(newimage) >= old_size:
_LOGGER.debug("Using original image(%d bytes) "
"because resized image (%d bytes) is not smaller",
old_size, len(newimage))
return image
_LOGGER.debug("Resized image "
"from (%dx%d - %d bytes) "
"to (%dx%d - %d bytes)",
old_width, old_height, old_size,
new_width, new_height, len(newimage))
return newimage
class ImageOpts():
"""The representation of image options."""
def __init__(self, max_width, quality, force_resize):
"""Initialize image options."""
self.max_width = max_width
self.quality = quality
self.force_resize = force_resize
def __bool__(self):
"""Bool evalution rules."""
return bool(self.max_width or self.quality)
class ProxyCamera(Camera):
"""The representation of a Proxy camera."""
def __init__(self, hass, config):
"""Initialize a proxy camera component."""
super().__init__()
self.hass = hass
self._proxied_camera = config.get(CONF_ENTITY_ID)
self._name = (
config.get(CONF_NAME) or
"{} - {}".format(DEFAULT_BASENAME, self._proxied_camera))
self._image_opts = ImageOpts(
config.get(CONF_MAX_IMAGE_WIDTH),
config.get(CONF_IMAGE_QUALITY),
config.get(CONF_FORCE_RESIZE))
self._stream_opts = ImageOpts(
config.get(CONF_MAX_STREAM_WIDTH),
config.get(CONF_STREAM_QUALITY),
True)
self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE)
self._cache_images = bool(
config.get(CONF_IMAGE_REFRESH_RATE)
or config.get(CONF_CACHE_IMAGES))
self._last_image_time = 0
self._last_image = None
self._headers = (
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
if self.hass.config.api.api_password is not None
else None)
def camera_image(self):
"""Return camera image."""
return run_coroutine_threadsafe(
self.async_camera_image(), self.hass.loop).result()
async def async_camera_image(self):
"""Return a still image response from the camera."""
now = dt_util.utcnow()
if (self._image_refresh_rate and
now < self._last_image_time + self._image_refresh_rate):
return self._last_image
self._last_image_time = now
url = "{}/api/camera_proxy/{}".format(
self.hass.config.api.base_url, self._proxied_camera)
try:
websession = async_get_clientsession(self.hass)
with async_timeout.timeout(10, loop=self.hass.loop):
response = await websession.get(url, headers=self._headers)
image = await response.read()
except asyncio.TimeoutError:
_LOGGER.error("Timeout getting camera image")
return self._last_image
except aiohttp.ClientError as err:
_LOGGER.error("Error getting new camera image: %s", err)
return self._last_image
image = await self.hass.async_add_job(
_resize_image, image, self._image_opts)
if self._cache_images:
self._last_image = image
return image
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from camera images."""
websession = async_get_clientsession(self.hass)
url = "{}/api/camera_proxy_stream/{}".format(
self.hass.config.api.base_url, self._proxied_camera)
stream_coro = websession.get(url, headers=self._headers)
if not self._stream_opts:
await async_aiohttp_proxy_web(self.hass, request, stream_coro)
return
response = aiohttp.web.StreamResponse()
response.content_type = ('multipart/x-mixed-replace; '
'boundary=--frameboundary')
await response.prepare(request)
def write(img_bytes):
"""Write image to stream."""
response.write(bytes(
'--frameboundary\r\n'
'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format(
self.content_type, len(img_bytes)),
'utf-8') + img_bytes + b'\r\n')
with async_timeout.timeout(10, loop=self.hass.loop):
req = await stream_coro
try:
while True:
image = await _read_frame(req)
if not image:
break
image = await self.hass.async_add_job(
_resize_image, image, self._stream_opts)
write(image)
except asyncio.CancelledError:
_LOGGER.debug("Stream closed by frontend.")
req.close()
response = None
finally:
if response is not None:
await response.write_eof()
@property
def name(self):
"""Return the name of this camera."""
return self._name

View File

@ -106,6 +106,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error("'%s' is not a whitelisted directory", file_path) _LOGGER.error("'%s' is not a whitelisted directory", file_path)
return False return False
add_devices([RaspberryCamera(setup_config)])
class RaspberryCamera(Camera): class RaspberryCamera(Camera):
"""Representation of a Raspberry Pi camera.""" """Representation of a Raspberry Pi camera."""

View File

@ -38,8 +38,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine async def async_setup_platform(hass,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config,
async_add_devices,
discovery_info=None):
"""Set up a Yi Camera.""" """Set up a Yi Camera."""
_LOGGER.debug('Received configuration: %s', config) _LOGGER.debug('Received configuration: %s', config)
async_add_devices([YiCamera(hass, config)], True) async_add_devices([YiCamera(hass, config)], True)
@ -107,31 +109,29 @@ class YiCamera(Camera):
self.user, self.passwd, self.host, self.port, self.path, self.user, self.passwd, self.host, self.port, self.path,
latest_dir, videos[-1]) latest_dir, videos[-1])
@asyncio.coroutine async def async_camera_image(self):
def async_camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
from haffmpeg import ImageFrame, IMAGE_JPEG from haffmpeg import ImageFrame, IMAGE_JPEG
url = yield from self.hass.async_add_job(self.get_latest_video_url) url = await self.hass.async_add_job(self.get_latest_video_url)
if url != self._last_url: if url != self._last_url:
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
self._last_image = yield from asyncio.shield(ffmpeg.get_image( self._last_image = await asyncio.shield(ffmpeg.get_image(
url, output_format=IMAGE_JPEG, url, output_format=IMAGE_JPEG,
extra_cmd=self._extra_arguments), loop=self.hass.loop) extra_cmd=self._extra_arguments), loop=self.hass.loop)
self._last_url = url self._last_url = url
return self._last_image return self._last_image
@asyncio.coroutine async def handle_async_mjpeg_stream(self, request):
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera.""" """Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg from haffmpeg import CameraMjpeg
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
yield from stream.open_camera( await stream.open_camera(
self._last_url, extra_cmd=self._extra_arguments) self._last_url, extra_cmd=self._extra_arguments)
yield from async_aiohttp_proxy_stream( await async_aiohttp_proxy_stream(
self.hass, request, stream, self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver') 'multipart/x-mixed-replace;boundary=ffserver')
yield from stream.close() await stream.close()

View File

@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['py-canary==0.4.0'] REQUIREMENTS = ['py-canary==0.4.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -237,14 +237,12 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Set up climate devices.""" """Set up climate devices."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_setup(config) await component.async_setup(config)
@asyncio.coroutine async def async_away_mode_set_service(service):
def async_away_mode_set_service(service):
"""Set away mode on target climate devices.""" """Set away mode on target climate devices."""
target_climate = component.async_extract_from_service(service) target_climate = component.async_extract_from_service(service)
@ -253,23 +251,22 @@ def async_setup(hass, config):
update_tasks = [] update_tasks = []
for climate in target_climate: for climate in target_climate:
if away_mode: if away_mode:
yield from climate.async_turn_away_mode_on() await climate.async_turn_away_mode_on()
else: else:
yield from climate.async_turn_away_mode_off() await climate.async_turn_away_mode_off()
if not climate.should_poll: if not climate.should_poll:
continue continue
update_tasks.append(climate.async_update_ha_state(True)) update_tasks.append(climate.async_update_ha_state(True))
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service, DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service,
schema=SET_AWAY_MODE_SCHEMA) schema=SET_AWAY_MODE_SCHEMA)
@asyncio.coroutine async def async_hold_mode_set_service(service):
def async_hold_mode_set_service(service):
"""Set hold mode on target climate devices.""" """Set hold mode on target climate devices."""
target_climate = component.async_extract_from_service(service) target_climate = component.async_extract_from_service(service)
@ -277,21 +274,20 @@ def async_setup(hass, config):
update_tasks = [] update_tasks = []
for climate in target_climate: for climate in target_climate:
yield from climate.async_set_hold_mode(hold_mode) await climate.async_set_hold_mode(hold_mode)
if not climate.should_poll: if not climate.should_poll:
continue continue
update_tasks.append(climate.async_update_ha_state(True)) update_tasks.append(climate.async_update_ha_state(True))
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service, DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service,
schema=SET_HOLD_MODE_SCHEMA) schema=SET_HOLD_MODE_SCHEMA)
@asyncio.coroutine async def async_aux_heat_set_service(service):
def async_aux_heat_set_service(service):
"""Set auxiliary heater on target climate devices.""" """Set auxiliary heater on target climate devices."""
target_climate = component.async_extract_from_service(service) target_climate = component.async_extract_from_service(service)
@ -300,23 +296,22 @@ def async_setup(hass, config):
update_tasks = [] update_tasks = []
for climate in target_climate: for climate in target_climate:
if aux_heat: if aux_heat:
yield from climate.async_turn_aux_heat_on() await climate.async_turn_aux_heat_on()
else: else:
yield from climate.async_turn_aux_heat_off() await climate.async_turn_aux_heat_off()
if not climate.should_poll: if not climate.should_poll:
continue continue
update_tasks.append(climate.async_update_ha_state(True)) update_tasks.append(climate.async_update_ha_state(True))
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service, DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service,
schema=SET_AUX_HEAT_SCHEMA) schema=SET_AUX_HEAT_SCHEMA)
@asyncio.coroutine async def async_temperature_set_service(service):
def async_temperature_set_service(service):
"""Set temperature on the target climate devices.""" """Set temperature on the target climate devices."""
target_climate = component.async_extract_from_service(service) target_climate = component.async_extract_from_service(service)
@ -333,21 +328,20 @@ def async_setup(hass, config):
else: else:
kwargs[value] = temp kwargs[value] = temp
yield from climate.async_set_temperature(**kwargs) await climate.async_set_temperature(**kwargs)
if not climate.should_poll: if not climate.should_poll:
continue continue
update_tasks.append(climate.async_update_ha_state(True)) update_tasks.append(climate.async_update_ha_state(True))
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service, DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service,
schema=SET_TEMPERATURE_SCHEMA) schema=SET_TEMPERATURE_SCHEMA)
@asyncio.coroutine async def async_humidity_set_service(service):
def async_humidity_set_service(service):
"""Set humidity on the target climate devices.""" """Set humidity on the target climate devices."""
target_climate = component.async_extract_from_service(service) target_climate = component.async_extract_from_service(service)
@ -355,20 +349,19 @@ def async_setup(hass, config):
update_tasks = [] update_tasks = []
for climate in target_climate: for climate in target_climate:
yield from climate.async_set_humidity(humidity) await climate.async_set_humidity(humidity)
if not climate.should_poll: if not climate.should_poll:
continue continue
update_tasks.append(climate.async_update_ha_state(True)) update_tasks.append(climate.async_update_ha_state(True))
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service, DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service,
schema=SET_HUMIDITY_SCHEMA) schema=SET_HUMIDITY_SCHEMA)
@asyncio.coroutine async def async_fan_mode_set_service(service):
def async_fan_mode_set_service(service):
"""Set fan mode on target climate devices.""" """Set fan mode on target climate devices."""
target_climate = component.async_extract_from_service(service) target_climate = component.async_extract_from_service(service)
@ -376,20 +369,19 @@ def async_setup(hass, config):
update_tasks = [] update_tasks = []
for climate in target_climate: for climate in target_climate:
yield from climate.async_set_fan_mode(fan) await climate.async_set_fan_mode(fan)
if not climate.should_poll: if not climate.should_poll:
continue continue
update_tasks.append(climate.async_update_ha_state(True)) update_tasks.append(climate.async_update_ha_state(True))
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service, DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service,
schema=SET_FAN_MODE_SCHEMA) schema=SET_FAN_MODE_SCHEMA)
@asyncio.coroutine async def async_operation_set_service(service):
def async_operation_set_service(service):
"""Set operating mode on the target climate devices.""" """Set operating mode on the target climate devices."""
target_climate = component.async_extract_from_service(service) target_climate = component.async_extract_from_service(service)
@ -397,20 +389,19 @@ def async_setup(hass, config):
update_tasks = [] update_tasks = []
for climate in target_climate: for climate in target_climate:
yield from climate.async_set_operation_mode(operation_mode) await climate.async_set_operation_mode(operation_mode)
if not climate.should_poll: if not climate.should_poll:
continue continue
update_tasks.append(climate.async_update_ha_state(True)) update_tasks.append(climate.async_update_ha_state(True))
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service, DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service,
schema=SET_OPERATION_MODE_SCHEMA) schema=SET_OPERATION_MODE_SCHEMA)
@asyncio.coroutine async def async_swing_set_service(service):
def async_swing_set_service(service):
"""Set swing mode on the target climate devices.""" """Set swing mode on the target climate devices."""
target_climate = component.async_extract_from_service(service) target_climate = component.async_extract_from_service(service)
@ -418,36 +409,35 @@ def async_setup(hass, config):
update_tasks = [] update_tasks = []
for climate in target_climate: for climate in target_climate:
yield from climate.async_set_swing_mode(swing_mode) await climate.async_set_swing_mode(swing_mode)
if not climate.should_poll: if not climate.should_poll:
continue continue
update_tasks.append(climate.async_update_ha_state(True)) update_tasks.append(climate.async_update_ha_state(True))
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service, DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service,
schema=SET_SWING_MODE_SCHEMA) schema=SET_SWING_MODE_SCHEMA)
@asyncio.coroutine async def async_on_off_service(service):
def async_on_off_service(service):
"""Handle on/off calls.""" """Handle on/off calls."""
target_climate = component.async_extract_from_service(service) target_climate = component.async_extract_from_service(service)
update_tasks = [] update_tasks = []
for climate in target_climate: for climate in target_climate:
if service.service == SERVICE_TURN_ON: if service.service == SERVICE_TURN_ON:
yield from climate.async_turn_on() await climate.async_turn_on()
elif service.service == SERVICE_TURN_OFF: elif service.service == SERVICE_TURN_OFF:
yield from climate.async_turn_off() await climate.async_turn_off()
if not climate.should_poll: if not climate.should_poll:
continue continue
update_tasks.append(climate.async_update_ha_state(True)) update_tasks.append(climate.async_update_ha_state(True))
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_TURN_OFF, async_on_off_service, DOMAIN, SERVICE_TURN_OFF, async_on_off_service,

View File

@ -4,7 +4,6 @@ Support for KNX/IP climate devices.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.knx/ https://home-assistant.io/components/climate.knx/
""" """
import asyncio
import voluptuous as vol import voluptuous as vol
@ -61,8 +60,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Set up climate(s) for KNX platform.""" """Set up climate(s) for KNX platform."""
if discovery_info is not None: if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices) async_add_devices_discovery(hass, discovery_info, async_add_devices)
@ -135,11 +134,10 @@ class KNXClimate(ClimateDevice):
def async_register_callbacks(self): def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed.""" """Register callbacks to update hass after device was changed."""
@asyncio.coroutine async def after_update_callback(device):
def after_update_callback(device):
"""Call after device was updated.""" """Call after device was updated."""
# pylint: disable=unused-argument # pylint: disable=unused-argument
yield from self.async_update_ha_state() await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback) self.device.register_device_updated_cb(after_update_callback)
@property @property
@ -187,14 +185,13 @@ class KNXClimate(ClimateDevice):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return self.device.target_temperature_max return self.device.target_temperature_max
@asyncio.coroutine async def async_set_temperature(self, **kwargs):
def async_set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None: if temperature is None:
return return
yield from self.device.set_target_temperature(temperature) await self.device.set_target_temperature(temperature)
yield from self.async_update_ha_state() await self.async_update_ha_state()
@property @property
def current_operation(self): def current_operation(self):
@ -210,10 +207,9 @@ class KNXClimate(ClimateDevice):
operation_mode in operation_mode in
self.device.get_supported_operation_modes()] self.device.get_supported_operation_modes()]
@asyncio.coroutine async def async_set_operation_mode(self, operation_mode):
def async_set_operation_mode(self, operation_mode):
"""Set operation mode.""" """Set operation mode."""
if self.device.supports_operation_mode: if self.device.supports_operation_mode:
from xknx.knx import HVACOperationMode from xknx.knx import HVACOperationMode
knx_operation_mode = HVACOperationMode(operation_mode) knx_operation_mode = HVACOperationMode(operation_mode)
yield from self.device.set_operation_mode(knx_operation_mode) await self.device.set_operation_mode(knx_operation_mode)

View File

@ -29,10 +29,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
NEST_MODE_HEAT_COOL = 'heat-cool' NEST_MODE_HEAT_COOL = 'heat-cool'
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_OPERATION_MODE |
SUPPORT_AWAY_MODE | SUPPORT_FAN_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Nest thermostat.""" """Set up the Nest thermostat."""
@ -58,6 +54,10 @@ class NestThermostat(ClimateDevice):
self.device = device self.device = device
self._fan_list = [STATE_ON, STATE_AUTO] self._fan_list = [STATE_ON, STATE_AUTO]
# Set the default supported features
self._support_flags = (SUPPORT_TARGET_TEMPERATURE |
SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE)
# Not all nest devices support cooling and heating remove unused # Not all nest devices support cooling and heating remove unused
self._operation_list = [STATE_OFF] self._operation_list = [STATE_OFF]
@ -70,11 +70,16 @@ class NestThermostat(ClimateDevice):
if self.device.can_heat and self.device.can_cool: if self.device.can_heat and self.device.can_cool:
self._operation_list.append(STATE_AUTO) self._operation_list.append(STATE_AUTO)
self._support_flags = (self._support_flags |
SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW)
self._operation_list.append(STATE_ECO) self._operation_list.append(STATE_ECO)
# feature of device # feature of device
self._has_fan = self.device.has_fan self._has_fan = self.device.has_fan
if self._has_fan:
self._support_flags = (self._support_flags | SUPPORT_FAN_MODE)
# data attributes # data attributes
self._away = None self._away = None
@ -95,7 +100,7 @@ class NestThermostat(ClimateDevice):
@property @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
return SUPPORT_FLAGS return self._support_flags
@property @property
def unique_id(self): def unique_id(self):
@ -162,6 +167,7 @@ class NestThermostat(ClimateDevice):
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
import nest
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if self._mode == NEST_MODE_HEAT_COOL: if self._mode == NEST_MODE_HEAT_COOL:
@ -170,7 +176,10 @@ class NestThermostat(ClimateDevice):
else: else:
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
_LOGGER.debug("Nest set_temperature-output-value=%s", temp) _LOGGER.debug("Nest set_temperature-output-value=%s", temp)
try:
self.device.target = temp self.device.target = temp
except nest.nest.APIError:
_LOGGER.error("An error occured while setting the temperature")
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set operation mode.""" """Set operation mode."""
@ -205,10 +214,13 @@ class NestThermostat(ClimateDevice):
@property @property
def fan_list(self): def fan_list(self):
"""List of available fan modes.""" """List of available fan modes."""
if self._has_fan:
return self._fan_list return self._fan_list
return None
def set_fan_mode(self, fan_mode): def set_fan_mode(self, fan_mode):
"""Turn fan on/off.""" """Turn fan on/off."""
if self._has_fan:
self.device.fan = fan_mode.lower() self.device.fan = fan_mode.lower()
@property @property

View File

@ -15,12 +15,12 @@ import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE) EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME)
from homeassistant.helpers import entityfilter, config_validation as cv from homeassistant.helpers import entityfilter, config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import smart_home as ga_sh from homeassistant.components.google_assistant import helpers as ga_h
from . import http_api, iot from . import http_api, iot
from .const import CONFIG_DIR, DOMAIN, SERVERS from .const import CONFIG_DIR, DOMAIN, SERVERS
@ -51,7 +51,6 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({
GOOGLE_ENTITY_SCHEMA = vol.Schema({ GOOGLE_ENTITY_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_TYPE): vol.In(ga_sh.MAPPING_COMPONENT),
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]) vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string])
}) })
@ -175,7 +174,7 @@ class Cloud:
"""If an entity should be exposed.""" """If an entity should be exposed."""
return conf['filter'](entity.entity_id) return conf['filter'](entity.entity_id)
self._gactions_config = ga_sh.Config( self._gactions_config = ga_h.Config(
should_expose=should_expose, should_expose=should_expose,
agent_user_id=self.claims['cognito:username'], agent_user_id=self.claims['cognito:username'],
entity_config=conf.get(CONF_ENTITY_CONFIG), entity_config=conf.get(CONF_ENTITY_CONFIG),

View File

@ -17,14 +17,6 @@ class UserNotConfirmed(CloudError):
"""Raised when a user has not confirmed email yet.""" """Raised when a user has not confirmed email yet."""
class ExpiredCode(CloudError):
"""Raised when an expired code is encountered."""
class InvalidCode(CloudError):
"""Raised when an invalid code is submitted."""
class PasswordChangeRequired(CloudError): class PasswordChangeRequired(CloudError):
"""Raised when a password change is required.""" """Raised when a password change is required."""
@ -42,10 +34,8 @@ class UnknownError(CloudError):
AWS_EXCEPTIONS = { AWS_EXCEPTIONS = {
'UserNotFoundException': UserNotFound, 'UserNotFoundException': UserNotFound,
'NotAuthorizedException': Unauthenticated, 'NotAuthorizedException': Unauthenticated,
'ExpiredCodeException': ExpiredCode,
'UserNotConfirmedException': UserNotConfirmed, 'UserNotConfirmedException': UserNotConfirmed,
'PasswordResetRequiredException': PasswordChangeRequired, 'PasswordResetRequiredException': PasswordChangeRequired,
'CodeMismatchException': InvalidCode,
} }
@ -69,17 +59,6 @@ def register(cloud, email, password):
raise _map_aws_exception(err) raise _map_aws_exception(err)
def confirm_register(cloud, confirmation_code, email):
"""Confirm confirmation code after registration."""
from botocore.exceptions import ClientError
cognito = _cognito(cloud)
try:
cognito.confirm_sign_up(confirmation_code, email)
except ClientError as err:
raise _map_aws_exception(err)
def resend_email_confirm(cloud, email): def resend_email_confirm(cloud, email):
"""Resend email confirmation.""" """Resend email confirmation."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
@ -107,18 +86,6 @@ def forgot_password(cloud, email):
raise _map_aws_exception(err) raise _map_aws_exception(err)
def confirm_forgot_password(cloud, confirmation_code, email, new_password):
"""Confirm forgotten password code and change password."""
from botocore.exceptions import ClientError
cognito = _cognito(cloud, username=email)
try:
cognito.confirm_forgot_password(confirmation_code, new_password)
except ClientError as err:
raise _map_aws_exception(err)
def login(cloud, email, password): def login(cloud, email, password):
"""Log user in and fetch certificate.""" """Log user in and fetch certificate."""
cognito = _authenticate(cloud, email, password) cognito = _authenticate(cloud, email, password)

View File

@ -23,10 +23,8 @@ def async_setup(hass):
hass.http.register_view(CloudLogoutView) hass.http.register_view(CloudLogoutView)
hass.http.register_view(CloudAccountView) hass.http.register_view(CloudAccountView)
hass.http.register_view(CloudRegisterView) hass.http.register_view(CloudRegisterView)
hass.http.register_view(CloudConfirmRegisterView)
hass.http.register_view(CloudResendConfirmView) hass.http.register_view(CloudResendConfirmView)
hass.http.register_view(CloudForgotPasswordView) hass.http.register_view(CloudForgotPasswordView)
hass.http.register_view(CloudConfirmForgotPasswordView)
_CLOUD_ERRORS = { _CLOUD_ERRORS = {
@ -34,8 +32,6 @@ _CLOUD_ERRORS = {
auth_api.UserNotConfirmed: (400, 'Email not confirmed.'), auth_api.UserNotConfirmed: (400, 'Email not confirmed.'),
auth_api.Unauthenticated: (401, 'Authentication failed.'), auth_api.Unauthenticated: (401, 'Authentication failed.'),
auth_api.PasswordChangeRequired: (400, 'Password change required.'), auth_api.PasswordChangeRequired: (400, 'Password change required.'),
auth_api.ExpiredCode: (400, 'Confirmation code has expired.'),
auth_api.InvalidCode: (400, 'Invalid confirmation code.'),
asyncio.TimeoutError: (502, 'Unable to reach the Home Assistant cloud.') asyncio.TimeoutError: (502, 'Unable to reach the Home Assistant cloud.')
} }
@ -149,31 +145,6 @@ class CloudRegisterView(HomeAssistantView):
return self.json_message('ok') return self.json_message('ok')
class CloudConfirmRegisterView(HomeAssistantView):
"""Confirm registration on the Home Assistant cloud."""
url = '/api/cloud/confirm_register'
name = 'api:cloud:confirm_register'
@_handle_cloud_errors
@RequestDataValidator(vol.Schema({
vol.Required('confirmation_code'): str,
vol.Required('email'): str,
}))
@asyncio.coroutine
def post(self, request, data):
"""Handle registration confirmation request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(
auth_api.confirm_register, cloud, data['confirmation_code'],
data['email'])
return self.json_message('ok')
class CloudResendConfirmView(HomeAssistantView): class CloudResendConfirmView(HomeAssistantView):
"""Resend email confirmation code.""" """Resend email confirmation code."""
@ -220,33 +191,6 @@ class CloudForgotPasswordView(HomeAssistantView):
return self.json_message('ok') return self.json_message('ok')
class CloudConfirmForgotPasswordView(HomeAssistantView):
"""View to finish Forgot Password flow.."""
url = '/api/cloud/confirm_forgot_password'
name = 'api:cloud:confirm_forgot_password'
@_handle_cloud_errors
@RequestDataValidator(vol.Schema({
vol.Required('confirmation_code'): str,
vol.Required('email'): str,
vol.Required('new_password'): vol.All(str, vol.Length(min=6))
}))
@asyncio.coroutine
def post(self, request, data):
"""Handle forgot password confirm request."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(
auth_api.confirm_forgot_password, cloud,
data['confirmation_code'], data['email'],
data['new_password'])
return self.json_message('ok')
def _account_data(cloud): def _account_data(cloud):
"""Generate the auth data JSON response.""" """Generate the auth data JSON response."""
claims = cloud.claims claims = cloud.claims

View File

@ -1,6 +1,7 @@
"""Module to handle messages from Home Assistant cloud.""" """Module to handle messages from Home Assistant cloud."""
import asyncio import asyncio
import logging import logging
import pprint
from aiohttp import hdrs, client_exceptions, WSMsgType from aiohttp import hdrs, client_exceptions, WSMsgType
@ -154,7 +155,9 @@ class CloudIoT:
disconnect_warn = 'Received invalid JSON.' disconnect_warn = 'Received invalid JSON.'
break break
_LOGGER.debug("Received message: %s", msg) if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("Received message:\n%s\n",
pprint.pformat(msg))
response = { response = {
'msgid': msg['msgid'], 'msgid': msg['msgid'],
@ -176,7 +179,9 @@ class CloudIoT:
_LOGGER.exception("Error handling message") _LOGGER.exception("Error handling message")
response['error'] = 'exception' response['error'] = 'exception'
_LOGGER.debug("Publishing message: %s", response) if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("Publishing message:\n%s\n",
pprint.pformat(response))
yield from client.send_json(response) yield from client.send_json(response)
except client_exceptions.WSServerHandshakeError as err: except client_exceptions.WSServerHandshakeError as err:

View File

@ -14,9 +14,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.discovery import load_platform
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = [ REQUIREMENTS = ['coinbase==2.1.0']
'https://github.com/balloob/coinbase-python/archive/'
'3a35efe13ef728a1cc18204b4f25be1fcb1c6006.zip#coinbase==2.0.8a1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -13,7 +13,8 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config' DOMAIN = 'config'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script') SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
'entity_registry')
ON_DEMAND = ('zwave',) ON_DEMAND = ('zwave',)
FEATURE_FLAGS = ('config_entries',) FEATURE_FLAGS = ('config_entries',)

View File

@ -97,10 +97,10 @@ class ConfigManagerFlowIndexView(HomeAssistantView):
flow for flow in hass.config_entries.flow.async_progress() flow for flow in hass.config_entries.flow.async_progress()
if flow['source'] != config_entries.SOURCE_USER]) if flow['source'] != config_entries.SOURCE_USER])
@asyncio.coroutine
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('domain'): str, vol.Required('domain'): str,
})) }))
@asyncio.coroutine
def post(self, request, data): def post(self, request, data):
"""Handle a POST request.""" """Handle a POST request."""
hass = request.app['hass'] hass = request.app['hass']
@ -139,8 +139,8 @@ class ConfigManagerFlowResourceView(HomeAssistantView):
return self.json(result) return self.json(result)
@asyncio.coroutine
@RequestDataValidator(vol.Schema(dict), allow_empty=True) @RequestDataValidator(vol.Schema(dict), allow_empty=True)
@asyncio.coroutine
def post(self, request, flow_id, data): def post(self, request, flow_id, data):
"""Handle a POST request.""" """Handle a POST request."""
hass = request.app['hass'] hass = request.app['hass']
@ -163,7 +163,7 @@ class ConfigManagerFlowResourceView(HomeAssistantView):
hass = request.app['hass'] hass = request.app['hass']
try: try:
hass.config_entries.async_abort(flow_id) hass.config_entries.flow.async_abort(flow_id)
except config_entries.UnknownFlow: except config_entries.UnknownFlow:
return self.json_message('Invalid flow specified', 404) return self.json_message('Invalid flow specified', 404)

View File

@ -0,0 +1,55 @@
"""HTTP views to interact with the entity registry."""
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.helpers.entity_registry import async_get_registry
async def async_setup(hass):
"""Enable the Entity Registry views."""
hass.http.register_view(ConfigManagerEntityView)
return True
class ConfigManagerEntityView(HomeAssistantView):
"""View to interact with an entity registry entry."""
url = '/api/config/entity_registry/{entity_id}'
name = 'api:config:entity_registry:entity'
async def get(self, request, entity_id):
"""Get the entity registry settings for an entity."""
hass = request.app['hass']
registry = await async_get_registry(hass)
entry = registry.entities.get(entity_id)
if entry is None:
return self.json_message('Entry not found', 404)
return self.json(_entry_dict(entry))
@RequestDataValidator(vol.Schema({
# If passed in, we update value. Passing None will remove old value.
vol.Optional('name'): vol.Any(str, None),
}))
async def post(self, request, entity_id, data):
"""Update the entity registry settings for an entity."""
hass = request.app['hass']
registry = await async_get_registry(hass)
if entity_id not in registry.entities:
return self.json_message('Entry not found', 404)
entry = registry.async_update_entity(entity_id, **data)
return self.json(_entry_dict(entry))
@callback
def _entry_dict(entry):
"""Helper to convert entry to API format."""
return {
'entity_id': entry.entity_id,
'name': entry.name
}

View File

@ -4,7 +4,6 @@ Support for functionality to have conversations with Home Assistant.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/conversation/ https://home-assistant.io/components/conversation/
""" """
import asyncio
import logging import logging
import re import re
@ -67,8 +66,7 @@ def async_register(hass, intent_type, utterances):
conf.append(_create_matcher(utterance)) conf.append(_create_matcher(utterance))
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Register the process service.""" """Register the process service."""
config = config.get(DOMAIN, {}) config = config.get(DOMAIN, {})
intents = hass.data.get(DOMAIN) intents = hass.data.get(DOMAIN)
@ -84,49 +82,73 @@ def async_setup(hass, config):
conf.extend(_create_matcher(utterance) for utterance in utterances) conf.extend(_create_matcher(utterance) for utterance in utterances)
@asyncio.coroutine async def process(service):
def process(service):
"""Parse text into commands.""" """Parse text into commands."""
text = service.data[ATTR_TEXT] text = service.data[ATTR_TEXT]
yield from _process(hass, text) try:
await _process(hass, text)
except intent.IntentHandleError as err:
_LOGGER.error('Error processing %s: %s', text, err)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA) DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA)
hass.http.register_view(ConversationProcessView) hass.http.register_view(ConversationProcessView)
async_register(hass, intent.INTENT_TURN_ON, # We strip trailing 's' from name because our state matcher will fail
['Turn {name} on', 'Turn on {name}']) # if a letter is not there. By removing 's' we can match singular and
async_register(hass, intent.INTENT_TURN_OFF, # plural names.
['Turn {name} off', 'Turn off {name}'])
async_register(hass, intent.INTENT_TOGGLE, async_register(hass, intent.INTENT_TURN_ON, [
['Toggle {name}', '{name} toggle']) 'Turn [the] [a] {name}[s] on',
'Turn on [the] [a] [an] {name}[s]',
])
async_register(hass, intent.INTENT_TURN_OFF, [
'Turn [the] [a] [an] {name}[s] off',
'Turn off [the] [a] [an] {name}[s]',
])
async_register(hass, intent.INTENT_TOGGLE, [
'Toggle [the] [a] [an] {name}[s]',
'[the] [a] [an] {name}[s] toggle',
])
return True return True
def _create_matcher(utterance): def _create_matcher(utterance):
"""Create a regex that matches the utterance.""" """Create a regex that matches the utterance."""
parts = re.split(r'({\w+})', utterance) # Split utterance into parts that are type: NORMAL, GROUP or OPTIONAL
# Pattern matches (GROUP|OPTIONAL): Change light to [the color] {name}
parts = re.split(r'({\w+}|\[[\w\s]+\] *)', utterance)
# Pattern to extract name from GROUP part. Matches {name}
group_matcher = re.compile(r'{(\w+)}') group_matcher = re.compile(r'{(\w+)}')
# Pattern to extract text from OPTIONAL part. Matches [the color]
optional_matcher = re.compile(r'\[([\w ]+)\] *')
pattern = ['^'] pattern = ['^']
for part in parts: for part in parts:
match = group_matcher.match(part) group_match = group_matcher.match(part)
optional_match = optional_matcher.match(part)
if match is None: # Normal part
if group_match is None and optional_match is None:
pattern.append(part) pattern.append(part)
continue continue
pattern.append('(?P<{}>{})'.format(match.groups()[0], r'[\w ]+')) # Group part
if group_match is not None:
pattern.append(
r'(?P<{}>[\w ]+?)\s*'.format(group_match.groups()[0]))
# Optional part
elif optional_match is not None:
pattern.append(r'(?:{} *)?'.format(optional_match.groups()[0]))
pattern.append('$') pattern.append('$')
return re.compile(''.join(pattern), re.I) return re.compile(''.join(pattern), re.I)
@asyncio.coroutine async def _process(hass, text):
def _process(hass, text):
"""Process a line of text.""" """Process a line of text."""
intents = hass.data.get(DOMAIN, {}) intents = hass.data.get(DOMAIN, {})
@ -137,7 +159,7 @@ def _process(hass, text):
if not match: if not match:
continue continue
response = yield from hass.helpers.intent.async_handle( response = await hass.helpers.intent.async_handle(
DOMAIN, intent_type, DOMAIN, intent_type,
{key: {'value': value} for key, value {key: {'value': value} for key, value
in match.groupdict().items()}, text) in match.groupdict().items()}, text)
@ -153,12 +175,15 @@ class ConversationProcessView(http.HomeAssistantView):
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('text'): str, vol.Required('text'): str,
})) }))
@asyncio.coroutine async def post(self, request, data):
def post(self, request, data):
"""Send a request for processing.""" """Send a request for processing."""
hass = request.app['hass'] hass = request.app['hass']
intent_result = yield from _process(hass, data['text']) try:
intent_result = await _process(hass, data['text'])
except intent.IntentHandleError as err:
intent_result = intent.IntentResponse()
intent_result.async_set_speech(str(err))
if intent_result is None: if intent_result is None:
intent_result = intent.IntentResponse() intent_result = intent.IntentResponse()

View File

@ -150,16 +150,14 @@ def stop_cover_tilt(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_STOP_COVER_TILT, data) hass.services.call(DOMAIN, SERVICE_STOP_COVER_TILT, data)
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Track states and offer events for covers.""" """Track states and offer events for covers."""
component = EntityComponent( component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS) _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS)
yield from component.async_setup(config) await component.async_setup(config)
@asyncio.coroutine async def async_handle_cover_service(service):
def async_handle_cover_service(service):
"""Handle calls to the cover services.""" """Handle calls to the cover services."""
covers = component.async_extract_from_service(service) covers = component.async_extract_from_service(service)
method = SERVICE_TO_METHOD.get(service.service) method = SERVICE_TO_METHOD.get(service.service)
@ -169,13 +167,13 @@ def async_setup(hass, config):
# call method # call method
update_tasks = [] update_tasks = []
for cover in covers: for cover in covers:
yield from getattr(cover, method['method'])(**params) await getattr(cover, method['method'])(**params)
if not cover.should_poll: if not cover.should_poll:
continue continue
update_tasks.append(cover.async_update_ha_state(True)) update_tasks.append(cover.async_update_ha_state(True))
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) await asyncio.wait(update_tasks, loop=hass.loop)
for service_name in SERVICE_TO_METHOD: for service_name in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service_name].get( schema = SERVICE_TO_METHOD[service_name].get(

View File

@ -4,7 +4,6 @@ Support for KNX/IP covers.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.knx/ https://home-assistant.io/components/cover.knx/
""" """
import asyncio
import voluptuous as vol import voluptuous as vol
@ -50,8 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Set up cover(s) for KNX platform.""" """Set up cover(s) for KNX platform."""
if discovery_info is not None: if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices) async_add_devices_discovery(hass, discovery_info, async_add_devices)
@ -106,11 +105,10 @@ class KNXCover(CoverDevice):
@callback @callback
def async_register_callbacks(self): def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed.""" """Register callbacks to update hass after device was changed."""
@asyncio.coroutine async def after_update_callback(device):
def after_update_callback(device):
"""Call after device was updated.""" """Call after device was updated."""
# pylint: disable=unused-argument # pylint: disable=unused-argument
yield from self.async_update_ha_state() await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback) self.device.register_device_updated_cb(after_update_callback)
@property @property
@ -147,32 +145,28 @@ class KNXCover(CoverDevice):
"""Return if the cover is closed.""" """Return if the cover is closed."""
return self.device.is_closed() return self.device.is_closed()
@asyncio.coroutine async def async_close_cover(self, **kwargs):
def async_close_cover(self, **kwargs):
"""Close the cover.""" """Close the cover."""
if not self.device.is_closed(): if not self.device.is_closed():
yield from self.device.set_down() await self.device.set_down()
self.start_auto_updater() self.start_auto_updater()
@asyncio.coroutine async def async_open_cover(self, **kwargs):
def async_open_cover(self, **kwargs):
"""Open the cover.""" """Open the cover."""
if not self.device.is_open(): if not self.device.is_open():
yield from self.device.set_up() await self.device.set_up()
self.start_auto_updater() self.start_auto_updater()
@asyncio.coroutine async def async_set_cover_position(self, **kwargs):
def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
if ATTR_POSITION in kwargs: if ATTR_POSITION in kwargs:
position = kwargs[ATTR_POSITION] position = kwargs[ATTR_POSITION]
yield from self.device.set_position(position) await self.device.set_position(position)
self.start_auto_updater() self.start_auto_updater()
@asyncio.coroutine async def async_stop_cover(self, **kwargs):
def async_stop_cover(self, **kwargs):
"""Stop the cover.""" """Stop the cover."""
yield from self.device.stop() await self.device.stop()
self.stop_auto_updater() self.stop_auto_updater()
@property @property
@ -182,12 +176,11 @@ class KNXCover(CoverDevice):
return None return None
return self.device.current_angle() return self.device.current_angle()
@asyncio.coroutine async def async_set_cover_tilt_position(self, **kwargs):
def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position.""" """Move the cover tilt to a specific position."""
if ATTR_TILT_POSITION in kwargs: if ATTR_TILT_POSITION in kwargs:
tilt_position = kwargs[ATTR_TILT_POSITION] tilt_position = kwargs[ATTR_TILT_POSITION]
yield from self.device.set_angle(tilt_position) await self.device.set_angle(tilt_position)
def start_auto_updater(self): def start_auto_updater(self):
"""Start the autoupdater to update HASS while cover is moving.""" """Start the autoupdater to update HASS while cover is moving."""

View File

@ -118,6 +118,17 @@ def async_setup(hass, config):
tasks2 = [] tasks2 = []
# Set up history graph
tasks2.append(bootstrap.async_setup_component(
hass, 'history_graph',
{'history_graph': {'switches': {
'name': 'Recent Switches',
'entities': switches,
'hours_to_show': 1,
'refresh': 60
}}}
))
# Set up scripts # Set up scripts
tasks2.append(bootstrap.async_setup_component( tasks2.append(bootstrap.async_setup_component(
hass, 'script', hass, 'script',

View File

@ -77,11 +77,14 @@ ATTR_MAC = 'mac'
ATTR_NAME = 'name' ATTR_NAME = 'name'
ATTR_SOURCE_TYPE = 'source_type' ATTR_SOURCE_TYPE = 'source_type'
ATTR_VENDOR = 'vendor' ATTR_VENDOR = 'vendor'
ATTR_CONSIDER_HOME = 'consider_home'
SOURCE_TYPE_GPS = 'gps' SOURCE_TYPE_GPS = 'gps'
SOURCE_TYPE_ROUTER = 'router' SOURCE_TYPE_ROUTER = 'router'
SOURCE_TYPE_BLUETOOTH = 'bluetooth' SOURCE_TYPE_BLUETOOTH = 'bluetooth'
SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le' SOURCE_TYPE_BLUETOOTH_LE = 'bluetooth_le'
SOURCE_TYPES = (SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER,
SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE)
NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({ NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(None, vol.Schema({
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
@ -96,6 +99,19 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NEW_DEVICE_DEFAULTS, vol.Optional(CONF_NEW_DEVICE_DEFAULTS,
default={}): NEW_DEVICE_DEFAULTS_SCHEMA default={}): NEW_DEVICE_DEFAULTS_SCHEMA
}) })
SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema(vol.All(
cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID), {
ATTR_MAC: cv.string,
ATTR_DEV_ID: cv.string,
ATTR_HOST_NAME: cv.string,
ATTR_LOCATION_NAME: cv.string,
ATTR_GPS: cv.gps,
ATTR_GPS_ACCURACY: cv.positive_int,
ATTR_BATTERY: cv.positive_int,
ATTR_ATTRIBUTES: dict,
ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES),
ATTR_CONSIDER_HOME: cv.time_period,
}))
@bind_hass @bind_hass
@ -109,7 +125,7 @@ def is_on(hass: HomeAssistantType, entity_id: str = None):
def see(hass: HomeAssistantType, mac: str = None, dev_id: str = None, def see(hass: HomeAssistantType, mac: str = None, dev_id: str = None,
host_name: str = None, location_name: str = None, host_name: str = None, location_name: str = None,
gps: GPSType = None, gps_accuracy=None, gps: GPSType = None, gps_accuracy=None,
battery=None, attributes: dict = None): battery: int = None, attributes: dict = None):
"""Call service to notify you see device.""" """Call service to notify you see device."""
data = {key: value for key, value in data = {key: value for key, value in
((ATTR_MAC, mac), ((ATTR_MAC, mac),
@ -203,12 +219,10 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
@asyncio.coroutine @asyncio.coroutine
def async_see_service(call): def async_see_service(call):
"""Service to see a device.""" """Service to see a device."""
args = {key: value for key, value in call.data.items() if key in yield from tracker.async_see(**call.data)
(ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME,
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
yield from tracker.async_see(**args)
hass.services.async_register(DOMAIN, SERVICE_SEE, async_see_service) hass.services.async_register(
DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA)
# restore # restore
yield from tracker.async_setup_tracked_device() yield from tracker.async_setup_tracked_device()
@ -240,23 +254,26 @@ class DeviceTracker(object):
dev.mac) dev.mac)
def see(self, mac: str = None, dev_id: str = None, host_name: str = None, def see(self, mac: str = None, dev_id: str = None, host_name: str = None,
location_name: str = None, gps: GPSType = None, gps_accuracy=None, location_name: str = None, gps: GPSType = None,
battery: str = None, attributes: dict = None, gps_accuracy: int = None, battery: int = None,
source_type: str = SOURCE_TYPE_GPS, picture: str = None, attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
icon: str = None): picture: str = None, icon: str = None,
consider_home: timedelta = None):
"""Notify the device tracker that you see a device.""" """Notify the device tracker that you see a device."""
self.hass.add_job( self.hass.add_job(
self.async_see(mac, dev_id, host_name, location_name, gps, self.async_see(mac, dev_id, host_name, location_name, gps,
gps_accuracy, battery, attributes, source_type, gps_accuracy, battery, attributes, source_type,
picture, icon) picture, icon, consider_home)
) )
@asyncio.coroutine @asyncio.coroutine
def async_see(self, mac: str = None, dev_id: str = None, def async_see(
host_name: str = None, location_name: str = None, self, mac: str = None, dev_id: str = None, host_name: str = None,
gps: GPSType = None, gps_accuracy=None, battery: str = None, location_name: str = None, gps: GPSType = None,
gps_accuracy: int = None, battery: int = None,
attributes: dict = None, source_type: str = SOURCE_TYPE_GPS, attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
picture: str = None, icon: str = None): picture: str = None, icon: str = None,
consider_home: timedelta = None):
"""Notify the device tracker that you see a device. """Notify the device tracker that you see a device.
This method is a coroutine. This method is a coroutine.
@ -275,7 +292,7 @@ class DeviceTracker(object):
if device: if device:
yield from device.async_seen( yield from device.async_seen(
host_name, location_name, gps, gps_accuracy, battery, host_name, location_name, gps, gps_accuracy, battery,
attributes, source_type) attributes, source_type, consider_home)
if device.track: if device.track:
yield from device.async_update_ha_state() yield from device.async_update_ha_state()
return return
@ -283,7 +300,7 @@ class DeviceTracker(object):
# If no device can be found, create it # If no device can be found, create it
dev_id = util.ensure_unique_string(dev_id, self.devices.keys()) dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
device = Device( device = Device(
self.hass, self.consider_home, self.track_new, self.hass, consider_home or self.consider_home, self.track_new,
dev_id, mac, (host_name or dev_id).replace('_', ' '), dev_id, mac, (host_name or dev_id).replace('_', ' '),
picture=picture, icon=icon, picture=picture, icon=icon,
hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE)) hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
@ -384,9 +401,10 @@ class Device(Entity):
host_name = None # type: str host_name = None # type: str
location_name = None # type: str location_name = None # type: str
gps = None # type: GPSType gps = None # type: GPSType
gps_accuracy = 0 gps_accuracy = 0 # type: int
last_seen = None # type: dt_util.dt.datetime last_seen = None # type: dt_util.dt.datetime
battery = None # type: str consider_home = None # type: dt_util.dt.timedelta
battery = None # type: int
attributes = None # type: dict attributes = None # type: dict
vendor = None # type: str vendor = None # type: str
icon = None # type: str icon = None # type: str
@ -476,14 +494,16 @@ class Device(Entity):
@asyncio.coroutine @asyncio.coroutine
def async_seen(self, host_name: str = None, location_name: str = None, def async_seen(self, host_name: str = None, location_name: str = None,
gps: GPSType = None, gps_accuracy=0, battery: str = None, gps: GPSType = None, gps_accuracy=0, battery: int = None,
attributes: dict = None, attributes: dict = None,
source_type: str = SOURCE_TYPE_GPS): source_type: str = SOURCE_TYPE_GPS,
consider_home: timedelta = None):
"""Mark the device as seen.""" """Mark the device as seen."""
self.source_type = source_type self.source_type = source_type
self.last_seen = dt_util.utcnow() self.last_seen = dt_util.utcnow()
self.host_name = host_name self.host_name = host_name
self.location_name = location_name self.location_name = location_name
self.consider_home = consider_home or self.consider_home
if battery: if battery:
self.battery = battery self.battery = battery

View File

@ -283,15 +283,15 @@ class SshConnection(_Connection):
lines = self._ssh.before.split(b'\n')[1:-1] lines = self._ssh.before.split(b'\n')[1:-1]
return [line.decode('utf-8') for line in lines] return [line.decode('utf-8') for line in lines]
except exceptions.EOF as err: except exceptions.EOF as err:
_LOGGER.error("Connection refused. SSH enabled?") _LOGGER.error("Connection refused. %s", self._ssh.before)
self.disconnect() self.disconnect()
return None return None
except pxssh.ExceptionPxssh as err: except pxssh.ExceptionPxssh as err:
_LOGGER.error("Unexpected SSH error: %s", str(err)) _LOGGER.error("Unexpected SSH error: %s", err)
self.disconnect() self.disconnect()
return None return None
except AssertionError as err: except AssertionError as err:
_LOGGER.error("Connection to router unavailable: %s", str(err)) _LOGGER.error("Connection to router unavailable: %s", err)
self.disconnect() self.disconnect()
return None return None
@ -301,10 +301,10 @@ class SshConnection(_Connection):
self._ssh = pxssh.pxssh() self._ssh = pxssh.pxssh()
if self._ssh_key: if self._ssh_key:
self._ssh.login(self._host, self._username, self._ssh.login(self._host, self._username, quiet=False,
ssh_key=self._ssh_key, port=self._port) ssh_key=self._ssh_key, port=self._port)
else: else:
self._ssh.login(self._host, self._username, self._ssh.login(self._host, self._username, quiet=False,
password=self._password, port=self._port) password=self._password, port=self._port)
super().connect() super().connect()

View File

@ -189,7 +189,9 @@ class Icloud(DeviceScanner):
for device in self.api.devices: for device in self.api.devices:
status = device.status(DEVICESTATUSSET) status = device.status(DEVICESTATUSSET)
devicename = slugify(status['name'].replace(' ', '', 99)) devicename = slugify(status['name'].replace(' ', '', 99))
if devicename not in self.devices: if devicename in self.devices:
_LOGGER.error('Multiple devices with name: %s', devicename)
continue
self.devices[devicename] = device self.devices[devicename] = device
self._intervals[devicename] = 1 self._intervals[devicename] = 1
self._overridestates[devicename] = None self._overridestates[devicename] = None
@ -319,14 +321,6 @@ class Icloud(DeviceScanner):
def determine_interval(self, devicename, latitude, longitude, battery): def determine_interval(self, devicename, latitude, longitude, battery):
"""Calculate new interval.""" """Calculate new interval."""
distancefromhome = None
zone_state = self.hass.states.get('zone.home')
zone_state_lat = zone_state.attributes['latitude']
zone_state_long = zone_state.attributes['longitude']
distancefromhome = distance(
latitude, longitude, zone_state_lat, zone_state_long)
distancefromhome = round(distancefromhome / 1000, 1)
currentzone = active_zone(self.hass, latitude, longitude) currentzone = active_zone(self.hass, latitude, longitude)
if ((currentzone is not None and if ((currentzone is not None and
@ -335,22 +329,48 @@ class Icloud(DeviceScanner):
self._overridestates.get(devicename) == 'away')): self._overridestates.get(devicename) == 'away')):
return return
zones = (self.hass.states.get(entity_id) for entity_id
in sorted(self.hass.states.entity_ids('zone')))
distances = []
for zone_state in zones:
zone_state_lat = zone_state.attributes['latitude']
zone_state_long = zone_state.attributes['longitude']
zone_distance = distance(
latitude, longitude, zone_state_lat, zone_state_long)
distances.append(round(zone_distance / 1000, 1))
if distances:
mindistance = min(distances)
else:
mindistance = None
self._overridestates[devicename] = None self._overridestates[devicename] = None
if currentzone is not None: if currentzone is not None:
self._intervals[devicename] = 30 self._intervals[devicename] = 30
return return
if distancefromhome is None: if mindistance is None:
return return
if distancefromhome > 25:
self._intervals[devicename] = round(distancefromhome / 2, 0) # Calculate out how long it would take for the device to drive to the
elif distancefromhome > 10: # nearest zone at 120 km/h:
self._intervals[devicename] = 5 interval = round(mindistance / 2, 0)
else:
self._intervals[devicename] = 1 # Never poll more than once per minute
if battery is not None and battery <= 33 and distancefromhome > 3: interval = max(interval, 1)
self._intervals[devicename] = self._intervals[devicename] * 2
if interval > 180:
# Three hour drive? This is far enough that they might be flying
# home - check every half hour
interval = 30
if battery is not None and battery <= 33 and mindistance > 3:
# Low battery - let's check half as often
interval = interval * 2
self._intervals[devicename] = interval
def update_device(self, devicename): def update_device(self, devicename):
"""Update the device_tracker entity.""" """Update the device_tracker entity."""

View File

@ -44,6 +44,7 @@ class TeslaDeviceTracker(object):
_LOGGER.debug("Updating device position: %s", name) _LOGGER.debug("Updating device position: %s", name)
dev_id = slugify(device.uniq_name) dev_id = slugify(device.uniq_name)
location = device.get_location() location = device.get_location()
if location:
lat = location['latitude'] lat = location['latitude']
lon = location['longitude'] lon = location['longitude']
attrs = { attrs = {

View File

@ -23,7 +23,8 @@ CONF_DHCP_SOFTWARE = 'dhcp_software'
DEFAULT_DHCP_SOFTWARE = 'dnsmasq' DEFAULT_DHCP_SOFTWARE = 'dnsmasq'
DHCP_SOFTWARES = [ DHCP_SOFTWARES = [
'dnsmasq', 'dnsmasq',
'odhcpd' 'odhcpd',
'none'
] ]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -40,8 +41,10 @@ def get_scanner(hass, config):
dhcp_sw = config[DOMAIN][CONF_DHCP_SOFTWARE] dhcp_sw = config[DOMAIN][CONF_DHCP_SOFTWARE]
if dhcp_sw == 'dnsmasq': if dhcp_sw == 'dnsmasq':
scanner = DnsmasqUbusDeviceScanner(config[DOMAIN]) scanner = DnsmasqUbusDeviceScanner(config[DOMAIN])
else: elif dhcp_sw == 'odhcpd':
scanner = OdhcpdUbusDeviceScanner(config[DOMAIN]) scanner = OdhcpdUbusDeviceScanner(config[DOMAIN])
else:
scanner = UbusDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
@ -92,8 +95,8 @@ class UbusDeviceScanner(DeviceScanner):
return self.last_results return self.last_results
def _generate_mac2name(self): def _generate_mac2name(self):
"""Must be implemented depending on the software.""" """Return empty MAC to name dict. Overriden if DHCP server is set."""
raise NotImplementedError self.mac2name = dict()
@_refresh_on_access_denied @_refresh_on_access_denied
def get_device_name(self, device): def get_device_name(self, device):

View File

@ -71,6 +71,7 @@ SERVICE_HANDLERS = {
'sabnzbd': ('sensor', 'sabnzbd'), 'sabnzbd': ('sensor', 'sabnzbd'),
'bose_soundtouch': ('media_player', 'soundtouch'), 'bose_soundtouch': ('media_player', 'soundtouch'),
'bluesound': ('media_player', 'bluesound'), 'bluesound': ('media_player', 'bluesound'),
'songpal': ('media_player', 'songpal'),
} }
CONF_IGNORE = 'ignore' CONF_IGNORE = 'ignore'

View File

@ -0,0 +1,123 @@
"""
Interfaces with Egardia/Woonveilig alarm control panel.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/egardia/
"""
import logging
import requests
import voluptuous as vol
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME,
EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['pythonegardia==1.0.38']
_LOGGER = logging.getLogger(__name__)
CONF_REPORT_SERVER_CODES = 'report_server_codes'
CONF_REPORT_SERVER_ENABLED = 'report_server_enabled'
CONF_REPORT_SERVER_PORT = 'report_server_port'
REPORT_SERVER_CODES_IGNORE = 'ignore'
CONF_VERSION = 'version'
DEFAULT_NAME = 'Egardia'
DEFAULT_PORT = 80
DEFAULT_REPORT_SERVER_ENABLED = False
DEFAULT_REPORT_SERVER_PORT = 52010
DEFAULT_VERSION = 'GATE-01'
DOMAIN = 'egardia'
EGARDIA_SERVER = 'egardia_server'
EGARDIA_DEVICE = 'egardiadevice'
EGARDIA_NAME = 'egardianame'
EGARDIA_REPORT_SERVER_ENABLED = 'egardia_rs_enabled'
EGARDIA_REPORT_SERVER_CODES = 'egardia_rs_codes'
NOTIFICATION_ID = 'egardia_notification'
NOTIFICATION_TITLE = 'Egardia'
ATTR_DISCOVER_DEVICES = 'egardia_sensor'
SERVER_CODE_SCHEMA = vol.Schema({
vol.Optional('arm'): vol.All(cv.ensure_list_csv, [cv.string]),
vol.Optional('disarm'): vol.All(cv.ensure_list_csv, [cv.string]),
vol.Optional('armhome'): vol.All(cv.ensure_list_csv, [cv.string]),
vol.Optional('triggered'): vol.All(cv.ensure_list_csv, [cv.string]),
vol.Optional('ignore'): vol.All(cv.ensure_list_csv, [cv.string])
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_REPORT_SERVER_CODES, default={}): SERVER_CODE_SCHEMA,
vol.Optional(CONF_REPORT_SERVER_ENABLED,
default=DEFAULT_REPORT_SERVER_ENABLED): cv.boolean,
vol.Optional(CONF_REPORT_SERVER_PORT,
default=DEFAULT_REPORT_SERVER_PORT): cv.port,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Egardia platform."""
from pythonegardia import egardiadevice
from pythonegardia import egardiaserver
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
host = conf.get(CONF_HOST)
port = conf.get(CONF_PORT)
version = conf.get(CONF_VERSION)
rs_enabled = conf.get(CONF_REPORT_SERVER_ENABLED)
rs_port = conf.get(CONF_REPORT_SERVER_PORT)
try:
device = hass.data[EGARDIA_DEVICE] = egardiadevice.EgardiaDevice(
host, port, username, password, '', version)
except requests.exceptions.RequestException:
_LOGGER.error("An error occurred accessing your Egardia device. " +
"Please check config.")
return False
except egardiadevice.UnauthorizedError:
_LOGGER.error("Unable to authorize. Wrong password or username.")
return False
# Set up the egardia server if enabled
if rs_enabled:
_LOGGER.debug("Setting up EgardiaServer")
try:
if EGARDIA_SERVER not in hass.data:
server = egardiaserver.EgardiaServer('', rs_port)
bound = server.bind()
if not bound:
raise IOError("Binding error occurred while " +
"starting EgardiaServer.")
hass.data[EGARDIA_SERVER] = server
server.start()
def handle_stop_event(event):
"""Callback function for HA stop event."""
server.stop()
# listen to home assistant stop event
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event)
except IOError:
_LOGGER.error("Binding error occurred while starting " +
"EgardiaServer.")
return False
discovery.load_platform(hass, 'alarm_control_panel', DOMAIN,
discovered=conf, hass_config=config)
# get the sensors from the device and add those
sensors = device.getsensors()
discovery.load_platform(hass, 'binary_sensor', DOMAIN,
{ATTR_DISCOVER_DEVICES: sensors}, config)
return True

View File

@ -4,7 +4,6 @@ Support for local control of entities by emulating the Phillips Hue bridge.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/emulated_hue/ https://home-assistant.io/components/emulated_hue/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -111,17 +110,15 @@ def setup(hass, yaml_config):
config.upnp_bind_multicast, config.advertise_ip, config.upnp_bind_multicast, config.advertise_ip,
config.advertise_port) config.advertise_port)
@asyncio.coroutine async def stop_emulated_hue_bridge(event):
def stop_emulated_hue_bridge(event):
"""Stop the emulated hue bridge.""" """Stop the emulated hue bridge."""
upnp_listener.stop() upnp_listener.stop()
yield from server.stop() await server.stop()
@asyncio.coroutine async def start_emulated_hue_bridge(event):
def start_emulated_hue_bridge(event):
"""Start the emulated hue bridge.""" """Start the emulated hue bridge."""
upnp_listener.start() upnp_listener.start()
yield from server.start() await server.start()
hass.bus.async_listen_once( hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge) EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge)

View File

@ -0,0 +1,96 @@
"""
Support for INSTEON fans via PowerLinc Modem.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/fan.insteon_plm/
"""
import asyncio
import logging
from homeassistant.components.fan import (SPEED_OFF,
SPEED_LOW,
SPEED_MEDIUM,
SPEED_HIGH,
FanEntity,
SUPPORT_SET_SPEED)
from homeassistant.const import STATE_OFF
from homeassistant.components.insteon_plm import InsteonPLMEntity
DEPENDENCIES = ['insteon_plm']
SPEED_TO_HEX = {SPEED_OFF: 0x00,
SPEED_LOW: 0x3f,
SPEED_MEDIUM: 0xbe,
SPEED_HIGH: 0xff}
FAN_SPEEDS = [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the INSTEON PLM device class for the hass platform."""
plm = hass.data['insteon_plm']
address = discovery_info['address']
device = plm.devices[address]
state_key = discovery_info['state_key']
_LOGGER.debug('Adding device %s entity %s to Fan platform',
device.address.hex, device.states[state_key].name)
new_entity = InsteonPLMFan(device, state_key)
async_add_devices([new_entity])
class InsteonPLMFan(InsteonPLMEntity, FanEntity):
"""An INSTEON fan component."""
@property
def speed(self) -> str:
"""Return the current speed."""
return self._hex_to_speed(self._insteon_device_state.value)
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
return FAN_SPEEDS
@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_SET_SPEED
@asyncio.coroutine
def async_turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn on the entity."""
if speed is None:
speed = SPEED_MEDIUM
yield from self.async_set_speed(speed)
@asyncio.coroutine
def async_turn_off(self, **kwargs) -> None:
"""Turn off the entity."""
yield from self.async_set_speed(SPEED_OFF)
@asyncio.coroutine
def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
fan_speed = SPEED_TO_HEX[speed]
if fan_speed == 0x00:
self._insteon_device_state.off()
else:
self._insteon_device_state.set_level(fan_speed)
@staticmethod
def _hex_to_speed(speed: int):
hex_speed = SPEED_OFF
if speed > 0xfe:
hex_speed = SPEED_HIGH
elif speed > 0x7f:
hex_speed = SPEED_MEDIUM
elif speed > 0:
hex_speed = SPEED_LOW
return hex_speed

View File

@ -21,9 +21,10 @@ from homeassistant.components.http.const import KEY_AUTHENTICATED
from homeassistant.config import find_config_file, load_yaml_config_file from homeassistant.config import find_config_file, load_yaml_config_file
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20180227.0', 'user-agents==1.1.0'] REQUIREMENTS = ['home-assistant-frontend==20180309.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
@ -379,6 +380,8 @@ def async_setup(hass, config):
async_setup_themes(hass, conf.get(CONF_THEMES)) async_setup_themes(hass, conf.get(CONF_THEMES))
hass.http.register_view(TranslationsView)
return True return True
@ -541,6 +544,23 @@ class ThemesView(HomeAssistantView):
}) })
class TranslationsView(HomeAssistantView):
"""View to return backend defined translations."""
url = '/api/translations/{language}'
name = 'api:translations'
@asyncio.coroutine
def get(self, request, language):
"""Return translations."""
hass = request.app['hass']
resources = yield from async_get_translations(hass, language)
return self.json({
'resources': resources,
})
def _fingerprint(path): def _fingerprint(path):
"""Fingerprint a file.""" """Fingerprint a file."""
with open(path) as fil: with open(path) as fil:
@ -553,6 +573,8 @@ def _is_latest(js_option, request):
Set according to user's preference and URL override. Set according to user's preference and URL override.
""" """
import hass_frontend
if request is None: if request is None:
return js_option == 'latest' return js_option == 'latest'
@ -573,25 +595,5 @@ def _is_latest(js_option, request):
return js_option == 'latest' return js_option == 'latest'
useragent = request.headers.get('User-Agent') useragent = request.headers.get('User-Agent')
if not useragent:
return False
from user_agents import parse return useragent and hass_frontend.version(useragent)
useragent = parse(useragent)
# on iOS every browser is a Safari which we support from version 11.
if useragent.os.family == 'iOS':
# Was >= 10, temp setting it to 12 to work around issue #11387
return useragent.os.version[0] >= 12
family_min_version = {
'Chrome': 54, # Object.values
'Chrome Mobile': 54,
'Firefox': 47, # Object.values
'Firefox Mobile': 47,
'Opera': 41, # Object.values
'Edge': 14, # Array.prototype.includes added in 14
'Safari': 10, # Many features not supported by 9
}
version = family_min_version.get(useragent.browser.family)
return version and useragent.browser.version[0] >= version

View File

@ -17,7 +17,7 @@ import voluptuous as vol
from homeassistant.core import HomeAssistant # NOQA from homeassistant.core import HomeAssistant # NOQA
from typing import Dict, Any # NOQA from typing import Dict, Any # NOQA
from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.const import CONF_NAME
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
@ -31,7 +31,6 @@ from .const import (
) )
from .auth import GoogleAssistantAuthView from .auth import GoogleAssistantAuthView
from .http import async_register_http from .http import async_register_http
from .smart_home import MAPPING_COMPONENT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -41,7 +40,6 @@ DEFAULT_AGENT_USER_ID = 'home-assistant'
ENTITY_SCHEMA = vol.Schema({ ENTITY_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_TYPE): vol.In(MAPPING_COMPONENT),
vol.Optional(CONF_EXPOSE): cv.boolean, vol.Optional(CONF_EXPOSE): cv.boolean,
vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_ROOM_HINT): cv.string vol.Optional(CONF_ROOM_HINT): cv.string

View File

@ -22,25 +22,6 @@ DEFAULT_EXPOSED_DOMAINS = [
CLIMATE_MODE_HEATCOOL = 'heatcool' CLIMATE_MODE_HEATCOOL = 'heatcool'
CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', CLIMATE_MODE_HEATCOOL} CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', CLIMATE_MODE_HEATCOOL}
PREFIX_TRAITS = 'action.devices.traits.'
TRAIT_ONOFF = PREFIX_TRAITS + 'OnOff'
TRAIT_BRIGHTNESS = PREFIX_TRAITS + 'Brightness'
TRAIT_RGB_COLOR = PREFIX_TRAITS + 'ColorSpectrum'
TRAIT_COLOR_TEMP = PREFIX_TRAITS + 'ColorTemperature'
TRAIT_SCENE = PREFIX_TRAITS + 'Scene'
TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting'
PREFIX_COMMANDS = 'action.devices.commands.'
COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff'
COMMAND_BRIGHTNESS = PREFIX_COMMANDS + 'BrightnessAbsolute'
COMMAND_COLOR = PREFIX_COMMANDS + 'ColorAbsolute'
COMMAND_ACTIVATESCENE = PREFIX_COMMANDS + 'ActivateScene'
COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT = (
PREFIX_COMMANDS + 'ThermostatTemperatureSetpoint')
COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
PREFIX_COMMANDS + 'ThermostatTemperatureSetRange')
COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode'
PREFIX_TYPES = 'action.devices.types.' PREFIX_TYPES = 'action.devices.types.'
TYPE_LIGHT = PREFIX_TYPES + 'LIGHT' TYPE_LIGHT = PREFIX_TYPES + 'LIGHT'
TYPE_SWITCH = PREFIX_TYPES + 'SWITCH' TYPE_SWITCH = PREFIX_TYPES + 'SWITCH'
@ -50,3 +31,12 @@ TYPE_THERMOSTAT = PREFIX_TYPES + 'THERMOSTAT'
SERVICE_REQUEST_SYNC = 'request_sync' SERVICE_REQUEST_SYNC = 'request_sync'
HOMEGRAPH_URL = 'https://homegraph.googleapis.com/' HOMEGRAPH_URL = 'https://homegraph.googleapis.com/'
REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + 'v1/devices:requestSync' REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + 'v1/devices:requestSync'
# Error codes used for SmartHomeError class
# https://developers.google.com/actions/smarthome/create-app#error_responses
ERR_DEVICE_OFFLINE = "deviceOffline"
ERR_DEVICE_NOT_FOUND = "deviceNotFound"
ERR_VALUE_OUT_OF_RANGE = "valueOutOfRange"
ERR_NOT_SUPPORTED = "notSupported"
ERR_PROTOCOL_ERROR = 'protocolError'
ERR_UNKNOWN_ERROR = 'unknownError'

View File

@ -0,0 +1,23 @@
"""Helper classes for Google Assistant integration."""
class SmartHomeError(Exception):
"""Google Assistant Smart Home errors.
https://developers.google.com/actions/smarthome/create-app#error_responses
"""
def __init__(self, code, msg):
"""Log error code."""
super().__init__(msg)
self.code = code
class Config:
"""Hold the configuration for Google Assistant."""
def __init__(self, should_expose, agent_user_id, entity_config=None):
"""Initialize the configuration."""
self.should_expose = should_expose
self.agent_user_id = agent_user_id
self.entity_config = entity_config or {}

View File

@ -10,8 +10,6 @@ import logging
from aiohttp.hdrs import AUTHORIZATION from aiohttp.hdrs import AUTHORIZATION
from aiohttp.web import Request, Response # NOQA from aiohttp.web import Request, Response # NOQA
from homeassistant.const import HTTP_UNAUTHORIZED
# Typing imports # Typing imports
# pylint: disable=using-constant-test,unused-import,ungrouped-imports # pylint: disable=using-constant-test,unused-import,ungrouped-imports
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -27,7 +25,8 @@ from .const import (
CONF_ENTITY_CONFIG, CONF_ENTITY_CONFIG,
CONF_EXPOSE, CONF_EXPOSE,
) )
from .smart_home import async_handle_message, Config from .smart_home import async_handle_message
from .helpers import Config
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -83,8 +82,7 @@ class GoogleAssistantView(HomeAssistantView):
"""Handle Google Assistant requests.""" """Handle Google Assistant requests."""
auth = request.headers.get(AUTHORIZATION, None) auth = request.headers.get(AUTHORIZATION, None)
if 'Bearer {}'.format(self.access_token) != auth: if 'Bearer {}'.format(self.access_token) != auth:
return self.json_message( return self.json_message("missing authorization", status_code=401)
"missing authorization", status_code=HTTP_UNAUTHORIZED)
message = yield from request.json() # type: dict message = yield from request.json() # type: dict
result = yield from async_handle_message( result = yield from async_handle_message(

View File

@ -1,5 +1,6 @@
"""Support for Google Assistant Smart Home API.""" """Support for Google Assistant Smart Home API."""
import asyncio import collections
from itertools import product
import logging import logging
# Typing imports # Typing imports
@ -9,116 +10,99 @@ from aiohttp.web import Request, Response # NOQA
from typing import Dict, Tuple, Any, Optional # NOQA from typing import Dict, Tuple, Any, Optional # NOQA
from homeassistant.helpers.entity import Entity # NOQA from homeassistant.helpers.entity import Entity # NOQA
from homeassistant.core import HomeAssistant # NOQA from homeassistant.core import HomeAssistant # NOQA
from homeassistant.util import color
from homeassistant.util.unit_system import UnitSystem # NOQA from homeassistant.util.unit_system import UnitSystem # NOQA
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from homeassistant.core import callback
from homeassistant.const import ( from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES)
STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON,
TEMP_FAHRENHEIT, TEMP_CELSIUS,
CONF_NAME, CONF_TYPE
)
from homeassistant.components import ( from homeassistant.components import (
switch, light, cover, media_player, group, fan, scene, script, climate, switch, light, cover, media_player, group, fan, scene, script, climate,
sensor
) )
from homeassistant.util.unit_system import METRIC_SYSTEM
from . import trait
from .const import ( from .const import (
COMMAND_COLOR,
COMMAND_BRIGHTNESS, COMMAND_ONOFF, COMMAND_ACTIVATESCENE,
COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT,
COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, COMMAND_THERMOSTAT_SET_MODE,
TRAIT_ONOFF, TRAIT_BRIGHTNESS, TRAIT_COLOR_TEMP,
TRAIT_RGB_COLOR, TRAIT_SCENE, TRAIT_TEMPERATURE_SETTING,
TYPE_LIGHT, TYPE_SCENE, TYPE_SWITCH, TYPE_THERMOSTAT, TYPE_LIGHT, TYPE_SCENE, TYPE_SWITCH, TYPE_THERMOSTAT,
CONF_ALIASES, CONF_ROOM_HINT, CLIMATE_SUPPORTED_MODES, CONF_ALIASES, CONF_ROOM_HINT,
CLIMATE_MODE_HEATCOOL ERR_NOT_SUPPORTED, ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE,
ERR_UNKNOWN_ERROR
) )
from .helpers import SmartHomeError
HANDLERS = Registry() HANDLERS = Registry()
QUERY_HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Mapping is [actions schema, primary trait, optional features] DOMAIN_TO_GOOGLE_TYPES = {
# optional is SUPPORT_* = (trait, command) group.DOMAIN: TYPE_SWITCH,
MAPPING_COMPONENT = { scene.DOMAIN: TYPE_SCENE,
group.DOMAIN: [TYPE_SWITCH, TRAIT_ONOFF, None], script.DOMAIN: TYPE_SCENE,
scene.DOMAIN: [TYPE_SCENE, TRAIT_SCENE, None], switch.DOMAIN: TYPE_SWITCH,
script.DOMAIN: [TYPE_SCENE, TRAIT_SCENE, None], fan.DOMAIN: TYPE_SWITCH,
switch.DOMAIN: [TYPE_SWITCH, TRAIT_ONOFF, None], light.DOMAIN: TYPE_LIGHT,
fan.DOMAIN: [TYPE_SWITCH, TRAIT_ONOFF, None], cover.DOMAIN: TYPE_SWITCH,
light.DOMAIN: [ media_player.DOMAIN: TYPE_SWITCH,
TYPE_LIGHT, TRAIT_ONOFF, { climate.DOMAIN: TYPE_THERMOSTAT,
light.SUPPORT_BRIGHTNESS: TRAIT_BRIGHTNESS,
light.SUPPORT_RGB_COLOR: TRAIT_RGB_COLOR,
light.SUPPORT_COLOR_TEMP: TRAIT_COLOR_TEMP,
} }
],
cover.DOMAIN: [
TYPE_SWITCH, TRAIT_ONOFF, {
cover.SUPPORT_SET_POSITION: TRAIT_BRIGHTNESS
}
],
media_player.DOMAIN: [
TYPE_SWITCH, TRAIT_ONOFF, {
media_player.SUPPORT_VOLUME_SET: TRAIT_BRIGHTNESS
}
],
climate.DOMAIN: [TYPE_THERMOSTAT, TRAIT_TEMPERATURE_SETTING, None],
} # type: Dict[str, list]
"""Error code used for SmartHomeError class.""" def deep_update(target, source):
ERROR_NOT_SUPPORTED = "notSupported" """Update a nested dictionary with another nested dictionary."""
for key, value in source.items():
if isinstance(value, collections.Mapping):
target[key] = deep_update(target.get(key, {}), value)
else:
target[key] = value
return target
class SmartHomeError(Exception): class _GoogleEntity:
"""Google Assistant Smart Home errors.""" """Adaptation of Entity expressed in Google's terms."""
def __init__(self, code, msg): def __init__(self, hass, config, state):
"""Log error code.""" self.hass = hass
super(SmartHomeError, self).__init__(msg) self.config = config
_LOGGER.error( self.state = state
"An error has occurred in Google SmartHome: %s."
"Error code: %s", msg, code
)
self.code = code
@property
def entity_id(self):
"""Return entity ID."""
return self.state.entity_id
class Config: @callback
"""Hold the configuration for Google Assistant.""" def traits(self):
"""Return traits for entity."""
state = self.state
domain = state.domain
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
def __init__(self, should_expose, agent_user_id, entity_config=None): return [Trait(state) for Trait in trait.TRAITS
"""Initialize the configuration.""" if Trait.supported(domain, features)]
self.should_expose = should_expose
self.agent_user_id = agent_user_id
self.entity_config = entity_config or {}
@callback
def sync_serialize(self):
"""Serialize entity for a SYNC response.
def entity_to_device(entity: Entity, config: Config, units: UnitSystem): https://developers.google.com/actions/smarthome/create-app#actiondevicessync
"""Convert a hass entity into a google actions device.""" """
entity_config = config.entity_config.get(entity.entity_id, {}) traits = self.traits()
google_domain = entity_config.get(CONF_TYPE) state = self.state
class_data = MAPPING_COMPONENT.get(
google_domain or entity.domain)
if class_data is None: # Found no supported traits for this entity
if not traits:
return None return None
device = { entity_config = self.config.entity_config.get(state.entity_id, {})
'id': entity.entity_id,
'name': {},
'attributes': {},
'traits': [],
'willReportState': False,
}
device['type'] = class_data[0]
device['traits'].append(class_data[1])
# handle custom names device = {
device['name']['name'] = entity_config.get(CONF_NAME) or entity.name 'id': state.entity_id,
'name': {
'name': entity_config.get(CONF_NAME) or state.name
},
'attributes': {},
'traits': [trait.name for trait in traits],
'willReportState': False,
'type': DOMAIN_TO_GOOGLE_TYPES[state.domain],
}
# use aliases # use aliases
aliases = entity_config.get(CONF_ALIASES) aliases = entity_config.get(CONF_ALIASES)
@ -130,326 +114,118 @@ def entity_to_device(entity: Entity, config: Config, units: UnitSystem):
if room: if room:
device['roomHint'] = room device['roomHint'] = room
# add trait if entity supports feature for trt in traits:
if class_data[2]: device['attributes'].update(trt.sync_attributes())
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
for feature, trait in class_data[2].items():
if feature & supported > 0:
device['traits'].append(trait)
# Actions require this attributes for a device
# supporting temperature
# For IKEA trådfri, these attributes only seem to
# be set only if the device is on?
if trait == TRAIT_COLOR_TEMP:
if entity.attributes.get(
light.ATTR_MAX_MIREDS) is not None:
device['attributes']['temperatureMinK'] = \
int(round(color.color_temperature_mired_to_kelvin(
entity.attributes.get(light.ATTR_MAX_MIREDS))))
if entity.attributes.get(
light.ATTR_MIN_MIREDS) is not None:
device['attributes']['temperatureMaxK'] = \
int(round(color.color_temperature_mired_to_kelvin(
entity.attributes.get(light.ATTR_MIN_MIREDS))))
if entity.domain == climate.DOMAIN:
modes = []
for mode in entity.attributes.get(climate.ATTR_OPERATION_LIST, []):
if mode in CLIMATE_SUPPORTED_MODES:
modes.append(mode)
elif mode == climate.STATE_AUTO:
modes.append(CLIMATE_MODE_HEATCOOL)
device['attributes'] = {
'availableThermostatModes': ','.join(modes),
'thermostatTemperatureUnit':
'F' if units.temperature_unit == TEMP_FAHRENHEIT else 'C',
}
_LOGGER.debug('Thermostat attributes %s', device['attributes'])
if entity.domain == sensor.DOMAIN:
if google_domain == climate.DOMAIN:
unit_of_measurement = entity.attributes.get(
ATTR_UNIT_OF_MEASUREMENT,
units.temperature_unit
)
device['attributes'] = {
'thermostatTemperatureUnit':
'F' if unit_of_measurement == TEMP_FAHRENHEIT else 'C',
}
_LOGGER.debug('Sensor attributes %s', device['attributes'])
return device return device
@callback
def query_serialize(self):
"""Serialize entity for a QUERY response.
def celsius(deg: Optional[float], units: UnitSystem) -> Optional[float]: https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
"""Convert a float to Celsius and rounds to one decimal place.""" """
if deg is None: state = self.state
return None
return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1)
if state.state == STATE_UNAVAILABLE:
return {'online': False}
@QUERY_HANDLERS.register(sensor.DOMAIN) attrs = {'online': True}
def query_response_sensor(
entity: Entity, config: Config, units: UnitSystem) -> dict:
"""Convert a sensor entity to a QUERY response."""
entity_config = config.entity_config.get(entity.entity_id, {})
google_domain = entity_config.get(CONF_TYPE)
if google_domain != climate.DOMAIN: for trt in self.traits():
deep_update(attrs, trt.query_attributes())
return attrs
async def execute(self, command, params):
"""Execute a command.
https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
"""
executed = False
for trt in self.traits():
if trt.can_execute(command, params):
await trt.execute(self.hass, command, params)
executed = True
break
if not executed:
raise SmartHomeError( raise SmartHomeError(
ERROR_NOT_SUPPORTED, ERR_NOT_SUPPORTED,
"Sensor type {} is not supported".format(google_domain) 'Unable to execute {} for {}'.format(command,
) self.state.entity_id))
# check if we have a string value to convert it to number @callback
value = entity.state def async_update(self):
if isinstance(entity.state, str): """Update the entity with latest info from Home Assistant."""
try: self.state = self.hass.states.get(self.entity_id)
value = float(value)
except ValueError:
value = None
if value is None:
raise SmartHomeError(
ERROR_NOT_SUPPORTED,
"Invalid value {} for the climate sensor"
.format(entity.state)
)
# detect if we report temperature or humidity
unit_of_measurement = entity.attributes.get(
ATTR_UNIT_OF_MEASUREMENT,
units.temperature_unit
)
if unit_of_measurement in [TEMP_FAHRENHEIT, TEMP_CELSIUS]:
value = celsius(value, units)
attr = 'thermostatTemperatureAmbient'
elif unit_of_measurement == '%':
attr = 'thermostatHumidityAmbient'
else:
raise SmartHomeError(
ERROR_NOT_SUPPORTED,
"Unit {} is not supported by the climate sensor"
.format(unit_of_measurement)
)
return {attr: value}
@QUERY_HANDLERS.register(climate.DOMAIN) async def async_handle_message(hass, config, message):
def query_response_climate( """Handle incoming API messages."""
entity: Entity, config: Config, units: UnitSystem) -> dict: response = await _process(hass, config, message)
"""Convert a climate entity to a QUERY response."""
mode = entity.attributes.get(climate.ATTR_OPERATION_MODE)
if mode is None:
mode = entity.state
mode = mode.lower()
if mode not in CLIMATE_SUPPORTED_MODES:
mode = 'heat'
attrs = entity.attributes
response = {
'thermostatMode': mode,
'thermostatTemperatureSetpoint':
celsius(attrs.get(climate.ATTR_TEMPERATURE), units),
'thermostatTemperatureAmbient':
celsius(attrs.get(climate.ATTR_CURRENT_TEMPERATURE), units),
'thermostatTemperatureSetpointHigh':
celsius(attrs.get(climate.ATTR_TARGET_TEMP_HIGH), units),
'thermostatTemperatureSetpointLow':
celsius(attrs.get(climate.ATTR_TARGET_TEMP_LOW), units),
'thermostatHumidityAmbient':
attrs.get(climate.ATTR_CURRENT_HUMIDITY),
}
return {k: v for k, v in response.items() if v is not None}
if 'errorCode' in response['payload']:
@QUERY_HANDLERS.register(media_player.DOMAIN) _LOGGER.error('Error handling message %s: %s',
def query_response_media_player( message, response['payload'])
entity: Entity, config: Config, units: UnitSystem) -> dict:
"""Convert a media_player entity to a QUERY response."""
level = entity.attributes.get(
media_player.ATTR_MEDIA_VOLUME_LEVEL,
1.0 if entity.state != STATE_OFF else 0.0)
# Convert 0.0-1.0 to 0-255
brightness = int(level * 100)
return {'brightness': brightness}
@QUERY_HANDLERS.register(light.DOMAIN)
def query_response_light(
entity: Entity, config: Config, units: UnitSystem) -> dict:
"""Convert a light entity to a QUERY response."""
response = {} # type: Dict[str, Any]
brightness = entity.attributes.get(light.ATTR_BRIGHTNESS)
if brightness is not None:
response['brightness'] = int(100 * (brightness / 255))
supported_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported_features & \
(light.SUPPORT_COLOR_TEMP | light.SUPPORT_RGB_COLOR):
response['color'] = {}
if entity.attributes.get(light.ATTR_COLOR_TEMP) is not None:
response['color']['temperature'] = \
int(round(color.color_temperature_mired_to_kelvin(
entity.attributes.get(light.ATTR_COLOR_TEMP))))
if entity.attributes.get(light.ATTR_COLOR_NAME) is not None:
response['color']['name'] = \
entity.attributes.get(light.ATTR_COLOR_NAME)
if entity.attributes.get(light.ATTR_RGB_COLOR) is not None:
color_rgb = entity.attributes.get(light.ATTR_RGB_COLOR)
if color_rgb is not None:
response['color']['spectrumRGB'] = \
int(color.color_rgb_to_hex(
color_rgb[0], color_rgb[1], color_rgb[2]), 16)
return response return response
def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict: async def _process(hass, config, message):
"""Take an entity and return a properly formatted device object.""" """Process a message."""
state = entity.state != STATE_OFF
defaults = {
'on': state,
'online': True
}
handler = QUERY_HANDLERS.get(entity.domain)
if callable(handler):
defaults.update(handler(entity, config, units))
return defaults
# erroneous bug on old pythons and pylint
# https://github.com/PyCQA/pylint/issues/1212
# pylint: disable=invalid-sequence-index
def determine_service(
entity_id: str, command: str, params: dict,
units: UnitSystem) -> Tuple[str, dict]:
"""
Determine service and service_data.
Attempt to return a tuple of service and service_data based on the entity
and action requested.
"""
_LOGGER.debug("Handling command %s with data %s", command, params)
domain = entity_id.split('.')[0]
service_data = {ATTR_ENTITY_ID: entity_id} # type: Dict[str, Any]
# special media_player handling
if domain == media_player.DOMAIN and command == COMMAND_BRIGHTNESS:
brightness = params.get('brightness', 0)
service_data[media_player.ATTR_MEDIA_VOLUME_LEVEL] = brightness / 100
return (media_player.SERVICE_VOLUME_SET, service_data)
# special cover handling
if domain == cover.DOMAIN:
if command == COMMAND_BRIGHTNESS:
service_data['position'] = params.get('brightness', 0)
return (cover.SERVICE_SET_COVER_POSITION, service_data)
if command == COMMAND_ONOFF and params.get('on') is True:
return (cover.SERVICE_OPEN_COVER, service_data)
return (cover.SERVICE_CLOSE_COVER, service_data)
# special climate handling
if domain == climate.DOMAIN:
if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT:
service_data['temperature'] = \
units.temperature(
params['thermostatTemperatureSetpoint'], TEMP_CELSIUS)
return (climate.SERVICE_SET_TEMPERATURE, service_data)
if command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE:
service_data['target_temp_high'] = units.temperature(
params.get('thermostatTemperatureSetpointHigh', 25),
TEMP_CELSIUS)
service_data['target_temp_low'] = units.temperature(
params.get('thermostatTemperatureSetpointLow', 18),
TEMP_CELSIUS)
return (climate.SERVICE_SET_TEMPERATURE, service_data)
if command == COMMAND_THERMOSTAT_SET_MODE:
mode = params['thermostatMode']
if mode == CLIMATE_MODE_HEATCOOL:
mode = climate.STATE_AUTO
service_data['operation_mode'] = mode
return (climate.SERVICE_SET_OPERATION_MODE, service_data)
if command == COMMAND_BRIGHTNESS:
brightness = params.get('brightness')
service_data['brightness'] = int(brightness / 100 * 255)
return (SERVICE_TURN_ON, service_data)
if command == COMMAND_COLOR:
color_data = params.get('color')
if color_data is not None:
if color_data.get('temperature', 0) > 0:
service_data[light.ATTR_KELVIN] = color_data.get('temperature')
return (SERVICE_TURN_ON, service_data)
if color_data.get('spectrumRGB', 0) > 0:
# blue is 255 so pad up to 6 chars
hex_value = \
('%0x' % int(color_data.get('spectrumRGB'))).zfill(6)
service_data[light.ATTR_RGB_COLOR] = \
color.rgb_hex_to_rgb_list(hex_value)
return (SERVICE_TURN_ON, service_data)
if command == COMMAND_ACTIVATESCENE:
return (SERVICE_TURN_ON, service_data)
if COMMAND_ONOFF == command:
if params.get('on') is True:
return (SERVICE_TURN_ON, service_data)
return (SERVICE_TURN_OFF, service_data)
return (None, service_data)
@asyncio.coroutine
def async_handle_message(hass, config, message):
"""Handle incoming API messages."""
request_id = message.get('requestId') # type: str request_id = message.get('requestId') # type: str
inputs = message.get('inputs') # type: list inputs = message.get('inputs') # type: list
if len(inputs) > 1: if len(inputs) != 1:
_LOGGER.warning('Got unexpected more than 1 input. %s', message) return {
'requestId': request_id,
'payload': {'errorCode': ERR_PROTOCOL_ERROR}
}
# Only use first input handler = HANDLERS.get(inputs[0].get('intent'))
intent = inputs[0].get('intent')
payload = inputs[0].get('payload')
handler = HANDLERS.get(intent) if handler is None:
return {
if handler: 'requestId': request_id,
result = yield from handler(hass, config, payload) 'payload': {'errorCode': ERR_PROTOCOL_ERROR}
else: }
result = {'errorCode': 'protocolError'}
try:
result = await handler(hass, config, inputs[0].get('payload'))
return {'requestId': request_id, 'payload': result} return {'requestId': request_id, 'payload': result}
except SmartHomeError as err:
return {
'requestId': request_id,
'payload': {'errorCode': err.code}
}
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception('Unexpected error')
return {
'requestId': request_id,
'payload': {'errorCode': ERR_UNKNOWN_ERROR}
}
@HANDLERS.register('action.devices.SYNC') @HANDLERS.register('action.devices.SYNC')
@asyncio.coroutine async def async_devices_sync(hass, config, payload):
def async_devices_sync(hass, config: Config, payload): """Handle action.devices.SYNC request.
"""Handle action.devices.SYNC request."""
https://developers.google.com/actions/smarthome/create-app#actiondevicessync
"""
devices = [] devices = []
for entity in hass.states.async_all(): for state in hass.states.async_all():
if not config.should_expose(entity): if not config.should_expose(state):
continue continue
device = entity_to_device(entity, config, hass.config.units) entity = _GoogleEntity(hass, config, state)
if device is None: serialized = entity.sync_serialize()
_LOGGER.warning("No mapping for %s domain", entity.domain)
if serialized is None:
_LOGGER.debug("No mapping for %s domain", entity.state)
continue continue
devices.append(device) devices.append(serialized)
return { return {
'agentUserId': config.agent_user_id, 'agentUserId': config.agent_user_id,
@ -458,53 +234,79 @@ def async_devices_sync(hass, config: Config, payload):
@HANDLERS.register('action.devices.QUERY') @HANDLERS.register('action.devices.QUERY')
@asyncio.coroutine async def async_devices_query(hass, config, payload):
def async_devices_query(hass, config, payload): """Handle action.devices.QUERY request.
"""Handle action.devices.QUERY request."""
https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
"""
devices = {} devices = {}
for device in payload.get('devices', []): for device in payload.get('devices', []):
devid = device.get('id') devid = device['id']
# In theory this should never happen
if not devid:
_LOGGER.error('Device missing ID: %s', device)
continue
state = hass.states.get(devid) state = hass.states.get(devid)
if not state: if not state:
# If we can't find a state, the device is offline # If we can't find a state, the device is offline
devices[devid] = {'online': False} devices[devid] = {'online': False}
else: continue
try:
devices[devid] = query_device(state, config, hass.config.units) devices[devid] = _GoogleEntity(hass, config, state).query_serialize()
except SmartHomeError as error:
devices[devid] = {'errorCode': error.code}
return {'devices': devices} return {'devices': devices}
@HANDLERS.register('action.devices.EXECUTE') @HANDLERS.register('action.devices.EXECUTE')
@asyncio.coroutine async def handle_devices_execute(hass, config, payload):
def handle_devices_execute(hass, config, payload): """Handle action.devices.EXECUTE request.
"""Handle action.devices.EXECUTE request."""
commands = []
for command in payload.get('commands', []):
ent_ids = [ent.get('id') for ent in command.get('devices', [])]
for execution in command.get('execution'):
for eid in ent_ids:
success = False
domain = eid.split('.')[0]
(service, service_data) = determine_service(
eid, execution.get('command'), execution.get('params'),
hass.config.units)
if domain == "group":
domain = "homeassistant"
success = yield from hass.services.async_call(
domain, service, service_data, blocking=True)
result = {"ids": [eid], "states": {}}
if success:
result['status'] = 'SUCCESS'
else:
result['status'] = 'ERROR'
commands.append(result)
return {'commands': commands} https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
"""
entities = {}
results = {}
for command in payload['commands']:
for device, execution in product(command['devices'],
command['execution']):
entity_id = device['id']
# Happens if error occurred. Skip entity for further processing
if entity_id in results:
continue
if entity_id not in entities:
state = hass.states.get(entity_id)
if state is None:
results[entity_id] = {
'ids': [entity_id],
'status': 'ERROR',
'errorCode': ERR_DEVICE_OFFLINE
}
continue
entities[entity_id] = _GoogleEntity(hass, config, state)
try:
await entities[entity_id].execute(execution['command'],
execution.get('params', {}))
except SmartHomeError as err:
results[entity_id] = {
'ids': [entity_id],
'status': 'ERROR',
'errorCode': err.code
}
final_results = list(results.values())
for entity in entities.values():
if entity.entity_id in results:
continue
entity.async_update()
final_results.append({
'ids': [entity.entity_id],
'status': 'SUCCESS',
'states': entity.query_serialize(),
})
return {'commands': final_results}

View File

@ -0,0 +1,521 @@
"""Implement the Smart Home traits."""
from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.components import (
climate,
cover,
group,
fan,
media_player,
light,
scene,
script,
switch,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.util import color as color_util, temperature as temp_util
from .const import ERR_VALUE_OUT_OF_RANGE
from .helpers import SmartHomeError
PREFIX_TRAITS = 'action.devices.traits.'
TRAIT_ONOFF = PREFIX_TRAITS + 'OnOff'
TRAIT_BRIGHTNESS = PREFIX_TRAITS + 'Brightness'
TRAIT_COLOR_SPECTRUM = PREFIX_TRAITS + 'ColorSpectrum'
TRAIT_COLOR_TEMP = PREFIX_TRAITS + 'ColorTemperature'
TRAIT_SCENE = PREFIX_TRAITS + 'Scene'
TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting'
PREFIX_COMMANDS = 'action.devices.commands.'
COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff'
COMMAND_BRIGHTNESS_ABSOLUTE = PREFIX_COMMANDS + 'BrightnessAbsolute'
COMMAND_COLOR_ABSOLUTE = PREFIX_COMMANDS + 'ColorAbsolute'
COMMAND_ACTIVATE_SCENE = PREFIX_COMMANDS + 'ActivateScene'
COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT = (
PREFIX_COMMANDS + 'ThermostatTemperatureSetpoint')
COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
PREFIX_COMMANDS + 'ThermostatTemperatureSetRange')
COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode'
TRAITS = []
def register_trait(trait):
"""Decorator to register a trait."""
TRAITS.append(trait)
return trait
def _google_temp_unit(state):
"""Return Google temperature unit."""
if (state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) ==
TEMP_FAHRENHEIT):
return 'F'
return 'C'
class _Trait:
"""Represents a Trait inside Google Assistant skill."""
commands = []
def __init__(self, state):
"""Initialize a trait for a state."""
self.state = state
def sync_attributes(self):
"""Return attributes for a sync request."""
raise NotImplementedError
def query_attributes(self):
"""Return the attributes of this trait for this entity."""
raise NotImplementedError
def can_execute(self, command, params):
"""Test if command can be executed."""
return command in self.commands
async def execute(self, hass, command, params):
"""Execute a trait command."""
raise NotImplementedError
@register_trait
class BrightnessTrait(_Trait):
"""Trait to control brightness of a device.
https://developers.google.com/actions/smarthome/traits/brightness
"""
name = TRAIT_BRIGHTNESS
commands = [
COMMAND_BRIGHTNESS_ABSOLUTE
]
@staticmethod
def supported(domain, features):
"""Test if state is supported."""
if domain == light.DOMAIN:
return features & light.SUPPORT_BRIGHTNESS
elif domain == cover.DOMAIN:
return features & cover.SUPPORT_SET_POSITION
elif domain == media_player.DOMAIN:
return features & media_player.SUPPORT_VOLUME_SET
return False
def sync_attributes(self):
"""Return brightness attributes for a sync request."""
return {}
def query_attributes(self):
"""Return brightness query attributes."""
domain = self.state.domain
response = {}
if domain == light.DOMAIN:
brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS)
if brightness is not None:
response['brightness'] = int(100 * (brightness / 255))
elif domain == cover.DOMAIN:
position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION)
if position is not None:
response['brightness'] = position
elif domain == media_player.DOMAIN:
level = self.state.attributes.get(
media_player.ATTR_MEDIA_VOLUME_LEVEL)
if level is not None:
# Convert 0.0-1.0 to 0-255
response['brightness'] = int(level * 100)
return response
async def execute(self, hass, command, params):
"""Execute a brightness command."""
domain = self.state.domain
if domain == light.DOMAIN:
await hass.services.async_call(
light.DOMAIN, light.SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id,
light.ATTR_BRIGHTNESS_PCT: params['brightness']
}, blocking=True)
elif domain == cover.DOMAIN:
await hass.services.async_call(
cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION, {
ATTR_ENTITY_ID: self.state.entity_id,
cover.ATTR_POSITION: params['brightness']
}, blocking=True)
elif domain == media_player.DOMAIN:
await hass.services.async_call(
media_player.DOMAIN, media_player.SERVICE_VOLUME_SET, {
ATTR_ENTITY_ID: self.state.entity_id,
media_player.ATTR_MEDIA_VOLUME_LEVEL:
params['brightness'] / 100
}, blocking=True)
@register_trait
class OnOffTrait(_Trait):
"""Trait to offer basic on and off functionality.
https://developers.google.com/actions/smarthome/traits/onoff
"""
name = TRAIT_ONOFF
commands = [
COMMAND_ONOFF
]
@staticmethod
def supported(domain, features):
"""Test if state is supported."""
return domain in (
group.DOMAIN,
switch.DOMAIN,
fan.DOMAIN,
light.DOMAIN,
cover.DOMAIN,
media_player.DOMAIN,
)
def sync_attributes(self):
"""Return OnOff attributes for a sync request."""
return {}
def query_attributes(self):
"""Return OnOff query attributes."""
if self.state.domain == cover.DOMAIN:
return {'on': self.state.state != cover.STATE_CLOSED}
return {'on': self.state.state != STATE_OFF}
async def execute(self, hass, command, params):
"""Execute an OnOff command."""
domain = self.state.domain
if domain == cover.DOMAIN:
service_domain = domain
if params['on']:
service = cover.SERVICE_OPEN_COVER
else:
service = cover.SERVICE_CLOSE_COVER
elif domain == group.DOMAIN:
service_domain = HA_DOMAIN
service = SERVICE_TURN_ON if params['on'] else SERVICE_TURN_OFF
else:
service_domain = domain
service = SERVICE_TURN_ON if params['on'] else SERVICE_TURN_OFF
await hass.services.async_call(service_domain, service, {
ATTR_ENTITY_ID: self.state.entity_id
}, blocking=True)
@register_trait
class ColorSpectrumTrait(_Trait):
"""Trait to offer color spectrum functionality.
https://developers.google.com/actions/smarthome/traits/colorspectrum
"""
name = TRAIT_COLOR_SPECTRUM
commands = [
COMMAND_COLOR_ABSOLUTE
]
@staticmethod
def supported(domain, features):
"""Test if state is supported."""
if domain != light.DOMAIN:
return False
return features & (light.SUPPORT_RGB_COLOR | light.SUPPORT_XY_COLOR)
def sync_attributes(self):
"""Return color spectrum attributes for a sync request."""
# Other colorModel is hsv
return {'colorModel': 'rgb'}
def query_attributes(self):
"""Return color spectrum query attributes."""
response = {}
# No need to handle XY color because light component will always
# convert XY to RGB if possible (which is when brightness is available)
color_rgb = self.state.attributes.get(light.ATTR_RGB_COLOR)
if color_rgb is not None:
response['color'] = {
'spectrumRGB': int(color_util.color_rgb_to_hex(
color_rgb[0], color_rgb[1], color_rgb[2]), 16),
}
return response
def can_execute(self, command, params):
"""Test if command can be executed."""
return (command in self.commands and
'spectrumRGB' in params.get('color', {}))
async def execute(self, hass, command, params):
"""Execute a color spectrum command."""
# Convert integer to hex format and left pad with 0's till length 6
hex_value = "{0:06x}".format(params['color']['spectrumRGB'])
color = color_util.rgb_hex_to_rgb_list(hex_value)
await hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id,
light.ATTR_RGB_COLOR: color
}, blocking=True)
@register_trait
class ColorTemperatureTrait(_Trait):
"""Trait to offer color temperature functionality.
https://developers.google.com/actions/smarthome/traits/colortemperature
"""
name = TRAIT_COLOR_TEMP
commands = [
COMMAND_COLOR_ABSOLUTE
]
@staticmethod
def supported(domain, features):
"""Test if state is supported."""
if domain != light.DOMAIN:
return False
return features & light.SUPPORT_COLOR_TEMP
def sync_attributes(self):
"""Return color temperature attributes for a sync request."""
attrs = self.state.attributes
return {
'temperatureMinK': color_util.color_temperature_mired_to_kelvin(
attrs.get(light.ATTR_MIN_MIREDS)),
'temperatureMaxK': color_util.color_temperature_mired_to_kelvin(
attrs.get(light.ATTR_MAX_MIREDS)),
}
def query_attributes(self):
"""Return color temperature query attributes."""
response = {}
temp = self.state.attributes.get(light.ATTR_COLOR_TEMP)
if temp is not None:
response['color'] = {
'temperature':
color_util.color_temperature_mired_to_kelvin(temp)
}
return response
def can_execute(self, command, params):
"""Test if command can be executed."""
return (command in self.commands and
'temperature' in params.get('color', {}))
async def execute(self, hass, command, params):
"""Execute a color temperature command."""
temp = color_util.color_temperature_kelvin_to_mired(
params['color']['temperature'])
min_temp = self.state.attributes[light.ATTR_MIN_MIREDS]
max_temp = self.state.attributes[light.ATTR_MAX_MIREDS]
if temp < min_temp or temp > max_temp:
raise SmartHomeError(
ERR_VALUE_OUT_OF_RANGE,
"Temperature should be between {} and {}".format(min_temp,
max_temp))
await hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id,
light.ATTR_COLOR_TEMP: temp,
}, blocking=True)
@register_trait
class SceneTrait(_Trait):
"""Trait to offer scene functionality.
https://developers.google.com/actions/smarthome/traits/scene
"""
name = TRAIT_SCENE
commands = [
COMMAND_ACTIVATE_SCENE
]
@staticmethod
def supported(domain, features):
"""Test if state is supported."""
return domain in (scene.DOMAIN, script.DOMAIN)
def sync_attributes(self):
"""Return scene attributes for a sync request."""
# Neither supported domain can support sceneReversible
return {}
def query_attributes(self):
"""Return scene query attributes."""
return {}
async def execute(self, hass, command, params):
"""Execute a scene command."""
# Don't block for scripts as they can be slow.
await hass.services.async_call(self.state.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id
}, blocking=self.state.domain != script.DOMAIN)
@register_trait
class TemperatureSettingTrait(_Trait):
"""Trait to offer handling both temperature point and modes functionality.
https://developers.google.com/actions/smarthome/traits/temperaturesetting
"""
name = TRAIT_TEMPERATURE_SETTING
commands = [
COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT,
COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE,
COMMAND_THERMOSTAT_SET_MODE,
]
# We do not support "on" as we are unable to know how to restore
# the last mode.
hass_to_google = {
climate.STATE_HEAT: 'heat',
climate.STATE_COOL: 'cool',
climate.STATE_OFF: 'off',
climate.STATE_AUTO: 'heatcool',
}
google_to_hass = {value: key for key, value in hass_to_google.items()}
@staticmethod
def supported(domain, features):
"""Test if state is supported."""
if domain != climate.DOMAIN:
return False
return features & climate.SUPPORT_OPERATION_MODE
def sync_attributes(self):
"""Return temperature point and modes attributes for a sync request."""
modes = []
for mode in self.state.attributes.get(climate.ATTR_OPERATION_LIST, []):
google_mode = self.hass_to_google.get(mode)
if google_mode is not None:
modes.append(google_mode)
return {
'availableThermostatModes': ','.join(modes),
'thermostatTemperatureUnit': _google_temp_unit(self.state),
}
def query_attributes(self):
"""Return temperature point and modes query attributes."""
attrs = self.state.attributes
response = {}
operation = attrs.get(climate.ATTR_OPERATION_MODE)
if operation is not None and operation in self.hass_to_google:
response['thermostatMode'] = self.hass_to_google[operation]
unit = self.state.attributes[ATTR_UNIT_OF_MEASUREMENT]
current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE)
if current_temp is not None:
response['thermostatTemperatureAmbient'] = \
round(temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1)
current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY)
if current_humidity is not None:
response['thermostatHumidityAmbient'] = current_humidity
if (operation == climate.STATE_AUTO and
climate.ATTR_TARGET_TEMP_HIGH in attrs and
climate.ATTR_TARGET_TEMP_LOW in attrs):
response['thermostatTemperatureSetpointHigh'] = \
round(temp_util.convert(attrs[climate.ATTR_TARGET_TEMP_HIGH],
unit, TEMP_CELSIUS), 1)
response['thermostatTemperatureSetpointLow'] = \
round(temp_util.convert(attrs[climate.ATTR_TARGET_TEMP_LOW],
unit, TEMP_CELSIUS), 1)
else:
target_temp = attrs.get(climate.ATTR_TEMPERATURE)
if target_temp is not None:
response['thermostatTemperatureSetpoint'] = round(
temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1)
return response
async def execute(self, hass, command, params):
"""Execute a temperature point or mode command."""
# All sent in temperatures are always in Celsius
unit = self.state.attributes[ATTR_UNIT_OF_MEASUREMENT]
min_temp = self.state.attributes[climate.ATTR_MIN_TEMP]
max_temp = self.state.attributes[climate.ATTR_MAX_TEMP]
if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT:
temp = temp_util.convert(params['thermostatTemperatureSetpoint'],
TEMP_CELSIUS, unit)
if temp < min_temp or temp > max_temp:
raise SmartHomeError(
ERR_VALUE_OUT_OF_RANGE,
"Temperature should be between {} and {}".format(min_temp,
max_temp))
await hass.services.async_call(
climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, {
ATTR_ENTITY_ID: self.state.entity_id,
climate.ATTR_TEMPERATURE: temp
}, blocking=True)
elif command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE:
temp_high = temp_util.convert(
params['thermostatTemperatureSetpointHigh'], TEMP_CELSIUS,
unit)
if temp_high < min_temp or temp_high > max_temp:
raise SmartHomeError(
ERR_VALUE_OUT_OF_RANGE,
"Upper bound for temperature range should be between "
"{} and {}".format(min_temp, max_temp))
temp_low = temp_util.convert(
params['thermostatTemperatureSetpointLow'], TEMP_CELSIUS, unit)
if temp_low < min_temp or temp_low > max_temp:
raise SmartHomeError(
ERR_VALUE_OUT_OF_RANGE,
"Lower bound for temperature range should be between "
"{} and {}".format(min_temp, max_temp))
await hass.services.async_call(
climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, {
ATTR_ENTITY_ID: self.state.entity_id,
climate.ATTR_TARGET_TEMP_HIGH: temp_high,
climate.ATTR_TARGET_TEMP_LOW: temp_low,
}, blocking=True)
elif command == COMMAND_THERMOSTAT_SET_MODE:
await hass.services.async_call(
climate.DOMAIN, climate.SERVICE_SET_OPERATION_MODE, {
ATTR_ENTITY_ID: self.state.entity_id,
climate.ATTR_OPERATION_MODE:
self.google_to_hass[params['thermostatMode']],
}, blocking=True)

View File

@ -257,12 +257,16 @@ def async_setup(hass, config):
@asyncio.coroutine @asyncio.coroutine
def reload_service_handler(service): def reload_service_handler(service):
"""Remove all groups and load new ones from config.""" """Remove all user-defined groups and load new ones from config."""
auto = list(filter(lambda e: not e.user_defined, component.entities))
conf = yield from component.async_prepare_reload() conf = yield from component.async_prepare_reload()
if conf is None: if conf is None:
return return
yield from _async_process_config(hass, conf, component) yield from _async_process_config(hass, conf, component)
yield from component.async_add_entities(auto)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_RELOAD, reload_service_handler, DOMAIN, SERVICE_RELOAD, reload_service_handler,
schema=RELOAD_SERVICE_SCHEMA) schema=RELOAD_SERVICE_SCHEMA)
@ -407,7 +411,7 @@ class Group(Entity):
self.group_off = None self.group_off = None
self.visible = visible self.visible = visible
self.control = control self.control = control
self._user_defined = user_defined self.user_defined = user_defined
self._order = order self._order = order
self._assumed_state = False self._assumed_state = False
self._async_unsub_state_changed = None self._async_unsub_state_changed = None
@ -497,7 +501,7 @@ class Group(Entity):
ATTR_ENTITY_ID: self.tracking, ATTR_ENTITY_ID: self.tracking,
ATTR_ORDER: self._order, ATTR_ORDER: self._order,
} }
if not self._user_defined: if not self.user_defined:
data[ATTR_AUTO] = True data[ATTR_AUTO] = True
if self.view: if self.view:
data[ATTR_VIEW] = True data[ATTR_VIEW] = True

View File

@ -1,4 +1,4 @@
"""Support for Apple Homekit. """Support for Apple HomeKit.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/homekit/ https://home-assistant.io/components/homekit/
@ -11,17 +11,20 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT,
TEMP_CELSIUS, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.climate import (
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.util import get_local_ip from homeassistant.util import get_local_ip
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
TYPES = Registry() TYPES = Registry()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_RE_VALID_PINCODE = re.compile(r"^(\d{3}-\d{2}-\d{3})$") _RE_VALID_PINCODE = r"^(\d{3}-\d{2}-\d{3})$"
DOMAIN = 'homekit' DOMAIN = 'homekit'
REQUIREMENTS = ['HAP-python==1.1.5'] REQUIREMENTS = ['HAP-python==1.1.7']
BRIDGE_NAME = 'Home Assistant' BRIDGE_NAME = 'Home Assistant'
CONF_PIN_CODE = 'pincode' CONF_PIN_CODE = 'pincode'
@ -31,10 +34,10 @@ HOMEKIT_FILE = '.homekit.state'
def valid_pin(value): def valid_pin(value):
"""Validate pin code value.""" """Validate pin code value."""
match = _RE_VALID_PINCODE.findall(value.strip()) match = re.match(_RE_VALID_PINCODE, str(value).strip())
if match == []: if not match:
raise vol.Invalid("Pin must be in the format: '123-45-678'") raise vol.Invalid("Pin must be in the format: '123-45-678'")
return match[0] return match.group(0)
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
@ -47,14 +50,14 @@ CONFIG_SCHEMA = vol.Schema({
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Setup the homekit component.""" """Setup the HomeKit component."""
_LOGGER.debug("Begin setup homekit") _LOGGER.debug("Begin setup HomeKit")
conf = config[DOMAIN] conf = config[DOMAIN]
port = conf.get(CONF_PORT) port = conf.get(CONF_PORT)
pin = str.encode(conf.get(CONF_PIN_CODE)) pin = str.encode(conf.get(CONF_PIN_CODE))
homekit = Homekit(hass, port) homekit = HomeKit(hass, port)
homekit.setup_bridge(pin) homekit.setup_bridge(pin)
hass.bus.async_listen_once( hass.bus.async_listen_once(
@ -63,18 +66,18 @@ def async_setup(hass, config):
def import_types(): def import_types():
"""Import all types from files in the homekit dir.""" """Import all types from files in the HomeKit directory."""
_LOGGER.debug("Import type files.") _LOGGER.debug("Import type files.")
# pylint: disable=unused-variable # pylint: disable=unused-variable
from .covers import Window # noqa F401 from . import ( # noqa F401
# pylint: disable=unused-variable covers, security_systems, sensors, switches, thermostats)
from .sensors import TemperatureSensor # noqa F401
def get_accessory(hass, state): def get_accessory(hass, state):
"""Take state and return an accessory object if supported.""" """Take state and return an accessory object if supported."""
if state.domain == 'sensor': if state.domain == 'sensor':
if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS: unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT:
_LOGGER.debug("Add \"%s\" as \"%s\"", _LOGGER.debug("Add \"%s\" as \"%s\"",
state.entity_id, 'TemperatureSensor') state.entity_id, 'TemperatureSensor')
return TYPES['TemperatureSensor'](hass, state.entity_id, return TYPES['TemperatureSensor'](hass, state.entity_id,
@ -87,14 +90,35 @@ def get_accessory(hass, state):
state.entity_id, 'Window') state.entity_id, 'Window')
return TYPES['Window'](hass, state.entity_id, state.name) return TYPES['Window'](hass, state.entity_id, state.name)
elif state.domain == 'alarm_control_panel':
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id,
'SecuritySystem')
return TYPES['SecuritySystem'](hass, state.entity_id, state.name)
elif state.domain == 'climate':
support_auto = False
features = state.attributes.get(ATTR_SUPPORTED_FEATURES)
# Check if climate device supports auto mode
if (features & SUPPORT_TARGET_TEMPERATURE_HIGH) \
and (features & SUPPORT_TARGET_TEMPERATURE_LOW):
support_auto = True
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Thermostat')
return TYPES['Thermostat'](hass, state.entity_id,
state.name, support_auto)
elif state.domain == 'switch' or state.domain == 'remote' \
or state.domain == 'input_boolean':
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Switch')
return TYPES['Switch'](hass, state.entity_id, state.name)
return None return None
class Homekit(): class HomeKit():
"""Class to handle all actions between homekit and Home Assistant.""" """Class to handle all actions between HomeKit and Home Assistant."""
def __init__(self, hass, port): def __init__(self, hass, port):
"""Initialize a homekit object.""" """Initialize a HomeKit object."""
self._hass = hass self._hass = hass
self._port = port self._port = port
self.bridge = None self.bridge = None
@ -103,8 +127,7 @@ class Homekit():
def setup_bridge(self, pin): def setup_bridge(self, pin):
"""Setup the bridge component to track all accessories.""" """Setup the bridge component to track all accessories."""
from .accessories import HomeBridge from .accessories import HomeBridge
self.bridge = HomeBridge(BRIDGE_NAME, pincode=pin) self.bridge = HomeBridge(BRIDGE_NAME, 'homekit.bridge', pin)
self.bridge.set_accessory_info('homekit.bridge')
def start_driver(self, event): def start_driver(self, event):
"""Start the accessory driver.""" """Start the accessory driver."""

View File

@ -1,55 +1,69 @@
"""Extend the basic Accessory and Bridge functions.""" """Extend the basic Accessory and Bridge functions."""
import logging
from pyhap.accessory import Accessory, Bridge, Category from pyhap.accessory import Accessory, Bridge, Category
from .const import ( from .const import (
SERVICES_ACCESSORY_INFO, MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER,
CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER) CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER)
_LOGGER = logging.getLogger(__name__)
def set_accessory_info(acc, model, manufacturer=MANUFACTURER,
serial_number='0000'):
"""Set the default accessory information."""
service = acc.get_service(SERV_ACCESSORY_INFO)
service.get_characteristic(CHAR_MODEL).set_value(model)
service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer)
service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number)
def add_preload_service(acc, service, chars=None, opt_chars=None):
"""Define and return a service to be available for the accessory."""
from pyhap.loader import get_serv_loader, get_char_loader
service = get_serv_loader().get(service)
if chars:
chars = chars if isinstance(chars, list) else [chars]
for char_name in chars:
char = get_char_loader().get(char_name)
service.add_characteristic(char)
if opt_chars:
opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars]
for opt_char_name in opt_chars:
opt_char = get_char_loader().get(opt_char_name)
service.add_opt_characteristic(opt_char)
acc.add_service(service)
return service
def override_properties(char, new_properties):
"""Override characteristic property values."""
char.properties.update(new_properties)
class HomeAccessory(Accessory): class HomeAccessory(Accessory):
"""Class to extend the Accessory class.""" """Class to extend the Accessory class."""
ALL_CATEGORIES = Category def __init__(self, display_name, model, category='OTHER', **kwargs):
def __init__(self, display_name):
"""Initialize a Accessory object.""" """Initialize a Accessory object."""
super().__init__(display_name) super().__init__(display_name, **kwargs)
set_accessory_info(self, model)
self.category = getattr(Category, category, Category.OTHER)
def set_category(self, category): def _set_services(self):
"""Set the category of the accessory.""" add_preload_service(self, SERV_ACCESSORY_INFO)
self.category = category
def add_preload_service(self, service):
"""Define the services to be available for the accessory."""
from pyhap.loader import get_serv_loader
self.add_service(get_serv_loader().get(service))
def set_accessory_info(self, model, manufacturer=MANUFACTURER,
serial_number='0000'):
"""Set the default accessory information."""
service_info = self.get_service(SERVICES_ACCESSORY_INFO)
service_info.get_characteristic(CHAR_MODEL) \
.set_value(model)
service_info.get_characteristic(CHAR_MANUFACTURER) \
.set_value(manufacturer)
service_info.get_characteristic(CHAR_SERIAL_NUMBER) \
.set_value(serial_number)
class HomeBridge(Bridge): class HomeBridge(Bridge):
"""Class to extend the Bridge class.""" """Class to extend the Bridge class."""
def __init__(self, display_name, pincode): def __init__(self, display_name, model, pincode, **kwargs):
"""Initialize a Bridge object.""" """Initialize a Bridge object."""
super().__init__(display_name, pincode=pincode) super().__init__(display_name, pincode=pincode, **kwargs)
set_accessory_info(self, model)
def set_accessory_info(self, model, manufacturer=MANUFACTURER, def _set_services(self):
serial_number='0000'): add_preload_service(self, SERV_ACCESSORY_INFO)
"""Set the default accessory information.""" add_preload_service(self, SERV_BRIDGING_STATE)
service_info = self.get_service(SERVICES_ACCESSORY_INFO)
service_info.get_characteristic(CHAR_MODEL) \
.set_value(model)
service_info.get_characteristic(CHAR_MANUFACTURER) \
.set_value(manufacturer)
service_info.get_characteristic(CHAR_SERIAL_NUMBER) \
.set_value(serial_number)

View File

@ -1,18 +1,36 @@
"""Constants used be the homekit component.""" """Constants used be the HomeKit component."""
MANUFACTURER = 'HomeAssistant' MANUFACTURER = 'HomeAssistant'
# Service: AccessoryInfomation # Services
SERVICES_ACCESSORY_INFO = 'AccessoryInformation' SERV_ACCESSORY_INFO = 'AccessoryInformation'
CHAR_MODEL = 'Model' SERV_BRIDGING_STATE = 'BridgingState'
CHAR_MANUFACTURER = 'Manufacturer' SERV_SECURITY_SYSTEM = 'SecuritySystem'
CHAR_SERIAL_NUMBER = 'SerialNumber' SERV_SWITCH = 'Switch'
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
SERV_THERMOSTAT = 'Thermostat'
SERV_WINDOW_COVERING = 'WindowCovering'
# Service: TemperatureSensor # Characteristics
SERVICES_TEMPERATURE_SENSOR = 'TemperatureSensor' CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_CATEGORY = 'Category'
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
# Service: WindowCovering CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
SERVICES_WINDOW_COVERING = 'WindowCovering'
CHAR_CURRENT_POSITION = 'CurrentPosition' CHAR_CURRENT_POSITION = 'CurrentPosition'
CHAR_TARGET_POSITION = 'TargetPosition' CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
CHAR_LINK_QUALITY = 'LinkQuality'
CHAR_MANUFACTURER = 'Manufacturer'
CHAR_MODEL = 'Model'
CHAR_ON = 'On'
CHAR_POSITION_STATE = 'PositionState' CHAR_POSITION_STATE = 'PositionState'
CHAR_REACHABLE = 'Reachable'
CHAR_SERIAL_NUMBER = 'SerialNumber'
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
CHAR_TARGET_POSITION = 'TargetPosition'
CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
# Properties
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}

View File

@ -5,9 +5,9 @@ from homeassistant.components.cover import ATTR_CURRENT_POSITION
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
from . import TYPES from . import TYPES
from .accessories import HomeAccessory from .accessories import HomeAccessory, add_preload_service
from .const import ( from .const import (
SERVICES_WINDOW_COVERING, CHAR_CURRENT_POSITION, SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION,
CHAR_TARGET_POSITION, CHAR_POSITION_STATE) CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
@ -23,10 +23,7 @@ class Window(HomeAccessory):
def __init__(self, hass, entity_id, display_name): def __init__(self, hass, entity_id, display_name):
"""Initialize a Window accessory object.""" """Initialize a Window accessory object."""
super().__init__(display_name) super().__init__(display_name, entity_id, 'WINDOW')
self.set_category(self.ALL_CATEGORIES.WINDOW)
self.set_accessory_info(entity_id)
self.add_preload_service(SERVICES_WINDOW_COVERING)
self._hass = hass self._hass = hass
self._entity_id = entity_id self._entity_id = entity_id
@ -34,13 +31,16 @@ class Window(HomeAccessory):
self.current_position = None self.current_position = None
self.homekit_target = None self.homekit_target = None
self.service_cover = self.get_service(SERVICES_WINDOW_COVERING) self.serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = self.service_cover. \ self.char_current_position = self.serv_cover. \
get_characteristic(CHAR_CURRENT_POSITION) get_characteristic(CHAR_CURRENT_POSITION)
self.char_target_position = self.service_cover. \ self.char_target_position = self.serv_cover. \
get_characteristic(CHAR_TARGET_POSITION) get_characteristic(CHAR_TARGET_POSITION)
self.char_position_state = self.service_cover. \ self.char_position_state = self.serv_cover. \
get_characteristic(CHAR_POSITION_STATE) get_characteristic(CHAR_POSITION_STATE)
self.char_current_position.value = 0
self.char_target_position.value = 0
self.char_position_state.value = 0
self.char_target_position.setter_callback = self.move_cover self.char_target_position.setter_callback = self.move_cover
@ -53,7 +53,7 @@ class Window(HomeAccessory):
self._hass, self._entity_id, self.update_cover_position) self._hass, self._entity_id, self.update_cover_position)
def move_cover(self, value): def move_cover(self, value):
"""Move cover to value if call came from homekit.""" """Move cover to value if call came from HomeKit."""
if value != self.current_position: if value != self.current_position:
_LOGGER.debug("%s: Set position to %d", self._entity_id, value) _LOGGER.debug("%s: Set position to %d", self._entity_id, value)
self.homekit_target = value self.homekit_target = value

View File

@ -0,0 +1,92 @@
"""Class to hold all alarm control panel accessories."""
import logging
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
ATTR_ENTITY_ID, ATTR_CODE)
from homeassistant.helpers.event import async_track_state_change
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE,
CHAR_TARGET_SECURITY_STATE)
_LOGGER = logging.getLogger(__name__)
HASS_TO_HOMEKIT = {STATE_ALARM_DISARMED: 3, STATE_ALARM_ARMED_HOME: 0,
STATE_ALARM_ARMED_AWAY: 1, STATE_ALARM_ARMED_NIGHT: 2}
HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()}
STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
STATE_ALARM_ARMED_HOME: 'alarm_arm_home',
STATE_ALARM_ARMED_AWAY: 'alarm_arm_away',
STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night'}
@TYPES.register('SecuritySystem')
class SecuritySystem(HomeAccessory):
"""Generate an SecuritySystem accessory for an alarm control panel."""
def __init__(self, hass, entity_id, display_name, alarm_code=None):
"""Initialize a SecuritySystem accessory object."""
super().__init__(display_name, entity_id, 'ALARM_SYSTEM')
self._hass = hass
self._entity_id = entity_id
self._alarm_code = alarm_code
self.flag_target_state = False
self.service_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
self.char_current_state = self.service_alarm. \
get_characteristic(CHAR_CURRENT_SECURITY_STATE)
self.char_current_state.value = 3
self.char_target_state = self.service_alarm. \
get_characteristic(CHAR_TARGET_SECURITY_STATE)
self.char_target_state.value = 3
self.char_target_state.setter_callback = self.set_security_state
def run(self):
"""Method called be object after driver is started."""
state = self._hass.states.get(self._entity_id)
self.update_security_state(new_state=state)
async_track_state_change(self._hass, self._entity_id,
self.update_security_state)
def set_security_state(self, value):
"""Move security state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set security state to %d",
self._entity_id, value)
self.flag_target_state = True
hass_value = HOMEKIT_TO_HASS[value]
service = STATE_TO_SERVICE[hass_value]
params = {ATTR_ENTITY_ID: self._entity_id}
if self._alarm_code is not None:
params[ATTR_CODE] = self._alarm_code
self._hass.services.call('alarm_control_panel', service, params)
def update_security_state(self, entity_id=None,
old_state=None, new_state=None):
"""Update security state after state changed."""
if new_state is None:
return
hass_state = new_state.state
if hass_state not in HASS_TO_HOMEKIT:
return
current_security_state = HASS_TO_HOMEKIT[hass_state]
self.char_current_state.set_value(current_security_state)
_LOGGER.debug("%s: Updated current state to %s (%d)",
self._entity_id, hass_state,
current_security_state)
if not self.flag_target_state:
self.char_target_state.set_value(current_security_state,
should_callback=False)
elif self.char_target_state.get_value() \
== self.char_current_state.get_value():
self.flag_target_state = False

View File

@ -1,38 +1,54 @@
"""Class to hold all sensor accessories.""" """Class to hold all sensor accessories."""
import logging import logging
from homeassistant.const import STATE_UNKNOWN from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS)
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
from . import TYPES from . import TYPES
from .accessories import HomeAccessory from .accessories import (
HomeAccessory, add_preload_service, override_properties)
from .const import ( from .const import (
SERVICES_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE) SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def calc_temperature(state, unit=TEMP_CELSIUS):
"""Calculate temperature from state and unit.
Always return temperature as Celsius value.
Conversion is handled on the device.
"""
try:
value = float(state)
except ValueError:
return None
return round((value - 32) / 1.8, 2) if unit == TEMP_FAHRENHEIT else value
@TYPES.register('TemperatureSensor') @TYPES.register('TemperatureSensor')
class TemperatureSensor(HomeAccessory): class TemperatureSensor(HomeAccessory):
"""Generate a TemperatureSensor accessory for a temperature sensor. """Generate a TemperatureSensor accessory for a temperature sensor.
Sensor entity must return either temperature in °C or STATE_UNKNOWN. Sensor entity must return temperature in °C, °F.
""" """
def __init__(self, hass, entity_id, display_name): def __init__(self, hass, entity_id, display_name):
"""Initialize a TemperatureSensor accessory object.""" """Initialize a TemperatureSensor accessory object."""
super().__init__(display_name) super().__init__(display_name, entity_id, 'SENSOR')
self.set_category(self.ALL_CATEGORIES.SENSOR)
self.set_accessory_info(entity_id)
self.add_preload_service(SERVICES_TEMPERATURE_SENSOR)
self._hass = hass self._hass = hass
self._entity_id = entity_id self._entity_id = entity_id
self.service_temp = self.get_service(SERVICES_TEMPERATURE_SENSOR) self.serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = self.service_temp. \ self.char_temp = self.serv_temp. \
get_characteristic(CHAR_CURRENT_TEMPERATURE) get_characteristic(CHAR_CURRENT_TEMPERATURE)
override_properties(self.char_temp, PROP_CELSIUS)
self.char_temp.value = 0
self.unit = None
def run(self): def run(self):
"""Method called be object after driver is started.""" """Method called be object after driver is started."""
@ -48,6 +64,9 @@ class TemperatureSensor(HomeAccessory):
if new_state is None: if new_state is None:
return return
temperature = new_state.state unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT]
if temperature != STATE_UNKNOWN: temperature = calc_temperature(new_state.state, unit)
self.char_temp.set_value(float(temperature)) if temperature is not None:
self.char_temp.set_value(temperature)
_LOGGER.debug("%s: Current temperature set to %d°C",
self._entity_id, temperature)

View File

@ -0,0 +1,62 @@
"""Class to hold all switch accessories."""
import logging
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import split_entity_id
from homeassistant.helpers.event import async_track_state_change
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import SERV_SWITCH, CHAR_ON
_LOGGER = logging.getLogger(__name__)
@TYPES.register('Switch')
class Switch(HomeAccessory):
"""Generate a Switch accessory."""
def __init__(self, hass, entity_id, display_name):
"""Initialize a Switch accessory object to represent a remote."""
super().__init__(display_name, entity_id, 'SWITCH')
self._hass = hass
self._entity_id = entity_id
self._domain = split_entity_id(entity_id)[0]
self.flag_target_state = False
self.service_switch = add_preload_service(self, SERV_SWITCH)
self.char_on = self.service_switch.get_characteristic(CHAR_ON)
self.char_on.value = False
self.char_on.setter_callback = self.set_state
def run(self):
"""Method called be object after driver is started."""
state = self._hass.states.get(self._entity_id)
self.update_state(new_state=state)
async_track_state_change(self._hass, self._entity_id,
self.update_state)
def set_state(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set switch state to %s",
self._entity_id, value)
self.flag_target_state = True
service = 'turn_on' if value else 'turn_off'
self._hass.services.call(self._domain, service,
{ATTR_ENTITY_ID: self._entity_id})
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update switch state after state changed."""
if new_state is None:
return
current_state = (new_state.state == 'on')
if not self.flag_target_state:
_LOGGER.debug("%s: Set current state to %s",
self._entity_id, current_state)
self.char_on.set_value(current_state, should_callback=False)
else:
self.flag_target_state = False

View File

@ -0,0 +1,245 @@
"""Class to hold all thermostat accessories."""
import logging
from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
STATE_HEAT, STATE_COOL, STATE_AUTO)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS, TEMP_FAHRENHEIT)
from homeassistant.helpers.event import async_track_state_change
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
_LOGGER = logging.getLogger(__name__)
STATE_OFF = 'off'
UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1}
UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()}
HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1,
STATE_COOL: 2, STATE_AUTO: 3}
HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
@TYPES.register('Thermostat')
class Thermostat(HomeAccessory):
"""Generate a Thermostat accessory for a climate."""
def __init__(self, hass, entity_id, display_name, support_auto=False):
"""Initialize a Thermostat accessory object."""
super().__init__(display_name, entity_id, 'THERMOSTAT')
self._hass = hass
self._entity_id = entity_id
self._call_timer = None
self.heat_cool_flag_target_state = False
self.temperature_flag_target_state = False
self.coolingthresh_flag_target_state = False
self.heatingthresh_flag_target_state = False
extra_chars = None
# Add additional characteristics if auto mode is supported
if support_auto:
extra_chars = [CHAR_COOLING_THRESHOLD_TEMPERATURE,
CHAR_HEATING_THRESHOLD_TEMPERATURE]
# Preload the thermostat service
self.service_thermostat = add_preload_service(self, SERV_THERMOSTAT,
extra_chars)
# Current and target mode characteristics
self.char_current_heat_cool = self.service_thermostat. \
get_characteristic(CHAR_CURRENT_HEATING_COOLING)
self.char_current_heat_cool.value = 0
self.char_target_heat_cool = self.service_thermostat. \
get_characteristic(CHAR_TARGET_HEATING_COOLING)
self.char_target_heat_cool.value = 0
self.char_target_heat_cool.setter_callback = self.set_heat_cool
# Current and target temperature characteristics
self.char_current_temp = self.service_thermostat. \
get_characteristic(CHAR_CURRENT_TEMPERATURE)
self.char_current_temp.value = 21.0
self.char_target_temp = self.service_thermostat. \
get_characteristic(CHAR_TARGET_TEMPERATURE)
self.char_target_temp.value = 21.0
self.char_target_temp.setter_callback = self.set_target_temperature
# Display units characteristic
self.char_display_units = self.service_thermostat. \
get_characteristic(CHAR_TEMP_DISPLAY_UNITS)
self.char_display_units.value = 0
# If the device supports it: high and low temperature characteristics
if support_auto:
self.char_cooling_thresh_temp = self.service_thermostat. \
get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE)
self.char_cooling_thresh_temp.value = 23.0
self.char_cooling_thresh_temp.setter_callback = \
self.set_cooling_threshold
self.char_heating_thresh_temp = self.service_thermostat. \
get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE)
self.char_heating_thresh_temp.value = 19.0
self.char_heating_thresh_temp.setter_callback = \
self.set_heating_threshold
else:
self.char_cooling_thresh_temp = None
self.char_heating_thresh_temp = None
def run(self):
"""Method called be object after driver is started."""
state = self._hass.states.get(self._entity_id)
self.update_thermostat(new_state=state)
async_track_state_change(self._hass, self._entity_id,
self.update_thermostat)
def set_heat_cool(self, value):
"""Move operation mode to value if call came from HomeKit."""
if value in HC_HOMEKIT_TO_HASS:
_LOGGER.debug("%s: Set heat-cool to %d", self._entity_id, value)
self.heat_cool_flag_target_state = True
hass_value = HC_HOMEKIT_TO_HASS[value]
self._hass.services.call('climate', 'set_operation_mode',
{ATTR_ENTITY_ID: self._entity_id,
ATTR_OPERATION_MODE: hass_value})
def set_cooling_threshold(self, value):
"""Set cooling threshold temp to value if call came from HomeKit."""
_LOGGER.debug("%s: Set cooling threshold temperature to %.2f",
self._entity_id, value)
self.coolingthresh_flag_target_state = True
low = self.char_heating_thresh_temp.get_value()
self._hass.services.call(
'climate', 'set_temperature',
{ATTR_ENTITY_ID: self._entity_id,
ATTR_TARGET_TEMP_HIGH: value,
ATTR_TARGET_TEMP_LOW: low})
def set_heating_threshold(self, value):
"""Set heating threshold temp to value if call came from HomeKit."""
_LOGGER.debug("%s: Set heating threshold temperature to %.2f",
self._entity_id, value)
self.heatingthresh_flag_target_state = True
# Home assistant always wants to set low and high at the same time
high = self.char_cooling_thresh_temp.get_value()
self._hass.services.call(
'climate', 'set_temperature',
{ATTR_ENTITY_ID: self._entity_id,
ATTR_TARGET_TEMP_LOW: value,
ATTR_TARGET_TEMP_HIGH: high})
def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit."""
_LOGGER.debug("%s: Set target temperature to %.2f",
self._entity_id, value)
self.temperature_flag_target_state = True
self._hass.services.call(
'climate', 'set_temperature',
{ATTR_ENTITY_ID: self._entity_id,
ATTR_TEMPERATURE: value})
def update_thermostat(self, entity_id=None,
old_state=None, new_state=None):
"""Update security state after state changed."""
if new_state is None:
return
# Update current temperature
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
if current_temp is not None:
self.char_current_temp.set_value(current_temp)
# Update target temperature
target_temp = new_state.attributes.get(ATTR_TEMPERATURE)
if target_temp is not None:
if not self.temperature_flag_target_state:
self.char_target_temp.set_value(target_temp,
should_callback=False)
else:
self.temperature_flag_target_state = False
# Update cooling threshold temperature if characteristic exists
if self.char_cooling_thresh_temp is not None:
cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
if cooling_thresh is not None:
if not self.coolingthresh_flag_target_state:
self.char_cooling_thresh_temp.set_value(
cooling_thresh, should_callback=False)
else:
self.coolingthresh_flag_target_state = False
# Update heating threshold temperature if characteristic exists
if self.char_heating_thresh_temp is not None:
heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
if heating_thresh is not None:
if not self.heatingthresh_flag_target_state:
self.char_heating_thresh_temp.set_value(
heating_thresh, should_callback=False)
else:
self.heatingthresh_flag_target_state = False
# Update display units
display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if display_units is not None \
and display_units in UNIT_HASS_TO_HOMEKIT:
self.char_display_units.set_value(
UNIT_HASS_TO_HOMEKIT[display_units])
# Update target operation mode
operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE)
if operation_mode is not None \
and operation_mode in HC_HASS_TO_HOMEKIT:
if not self.heat_cool_flag_target_state:
self.char_target_heat_cool.set_value(
HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False)
else:
self.heat_cool_flag_target_state = False
# Set current operation mode based on temperatures and target mode
if operation_mode == STATE_HEAT:
if current_temp < target_temp:
current_operation_mode = STATE_HEAT
else:
current_operation_mode = STATE_OFF
elif operation_mode == STATE_COOL:
if current_temp > target_temp:
current_operation_mode = STATE_COOL
else:
current_operation_mode = STATE_OFF
elif operation_mode == STATE_AUTO:
# Check if auto is supported
if self.char_cooling_thresh_temp is not None:
lower_temp = self.char_heating_thresh_temp.get_value()
upper_temp = self.char_cooling_thresh_temp.get_value()
if current_temp < lower_temp:
current_operation_mode = STATE_HEAT
elif current_temp > upper_temp:
current_operation_mode = STATE_COOL
else:
current_operation_mode = STATE_OFF
else:
# Check if heating or cooling are supported
heat = STATE_HEAT in new_state.attributes[ATTR_OPERATION_LIST]
cool = STATE_COOL in new_state.attributes[ATTR_OPERATION_LIST]
if current_temp < target_temp and heat:
current_operation_mode = STATE_HEAT
elif current_temp > target_temp and cool:
current_operation_mode = STATE_COOL
else:
current_operation_mode = STATE_OFF
else:
current_operation_mode = STATE_OFF
self.char_current_heat_cool.set_value(
HC_HASS_TO_HOMEKIT[current_operation_mode])

View File

@ -4,21 +4,18 @@ This module provides WSGI application to serve the Home Assistant API.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/http/ https://home-assistant.io/components/http/
""" """
import asyncio
from ipaddress import ip_network from ipaddress import ip_network
import json
import logging import logging
import os import os
import ssl import ssl
from aiohttp import web from aiohttp import web
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently from aiohttp.web_exceptions import HTTPMovedPermanently
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
SERVER_PORT, CONTENT_TYPE_JSON, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, SERVER_PORT)
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,)
from homeassistant.core import is_callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.remote as rem import homeassistant.remote as rem
import homeassistant.util as hass_util import homeassistant.util as hass_util
@ -28,10 +25,13 @@ from .auth import setup_auth
from .ban import setup_bans from .ban import setup_bans
from .cors import setup_cors from .cors import setup_cors
from .real_ip import setup_real_ip from .real_ip import setup_real_ip
from .const import KEY_AUTHENTICATED, KEY_REAL_IP
from .static import ( from .static import (
CachingFileResponse, CachingStaticResource, staticresource_middleware) CachingFileResponse, CachingStaticResource, staticresource_middleware)
# Import as alias
from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa
from .view import HomeAssistantView # noqa
REQUIREMENTS = ['aiohttp_cors==0.6.0'] REQUIREMENTS = ['aiohttp_cors==0.6.0']
DOMAIN = 'http' DOMAIN = 'http'
@ -98,8 +98,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Set up the HTTP API and debug interface.""" """Set up the HTTP API and debug interface."""
conf = config.get(DOMAIN) conf = config.get(DOMAIN)
@ -135,16 +134,14 @@ def async_setup(hass, config):
is_ban_enabled=is_ban_enabled is_ban_enabled=is_ban_enabled
) )
@asyncio.coroutine async def stop_server(event):
def stop_server(event):
"""Stop the server.""" """Stop the server."""
yield from server.stop() await server.stop()
@asyncio.coroutine async def start_server(event):
def start_server(event):
"""Start the server.""" """Start the server."""
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server)
yield from server.start() await server.start()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server)
@ -252,13 +249,11 @@ class HomeAssistantHTTP(object):
return return
if cache_headers: if cache_headers:
@asyncio.coroutine async def serve_file(request):
def serve_file(request):
"""Serve file from disk.""" """Serve file from disk."""
return CachingFileResponse(path) return CachingFileResponse(path)
else: else:
@asyncio.coroutine async def serve_file(request):
def serve_file(request):
"""Serve file from disk.""" """Serve file from disk."""
return web.FileResponse(path) return web.FileResponse(path)
@ -276,10 +271,13 @@ class HomeAssistantHTTP(object):
self.app.router.add_route('GET', url_pattern, serve_file) self.app.router.add_route('GET', url_pattern, serve_file)
@asyncio.coroutine async def start(self):
def start(self):
"""Start the WSGI server.""" """Start the WSGI server."""
yield from self.app.startup() # We misunderstood the startup signal. You're not allowed to change
# anything during startup. Temp workaround.
# pylint: disable=protected-access
self.app._on_startup.freeze()
await self.app.startup()
if self.ssl_certificate: if self.ssl_certificate:
try: try:
@ -298,133 +296,24 @@ class HomeAssistantHTTP(object):
# Aiohttp freezes apps after start so that no changes can be made. # Aiohttp freezes apps after start so that no changes can be made.
# However in Home Assistant components can be discovered after boot. # However in Home Assistant components can be discovered after boot.
# This will now raise a RunTimeError. # This will now raise a RunTimeError.
# To work around this we now fake that we are frozen. # To work around this we now prevent the router from getting frozen
# A more appropriate fix would be to create a new app and self.app._router.freeze = lambda: None
# re-register all redirects, views, static paths.
self.app._frozen = True # pylint: disable=protected-access
self._handler = self.app.make_handler(loop=self.hass.loop) self._handler = self.app.make_handler(loop=self.hass.loop)
try: try:
self.server = yield from self.hass.loop.create_server( self.server = await self.hass.loop.create_server(
self._handler, self.server_host, self.server_port, ssl=context) self._handler, self.server_host, self.server_port, ssl=context)
except OSError as error: except OSError as error:
_LOGGER.error("Failed to create HTTP server at port %d: %s", _LOGGER.error("Failed to create HTTP server at port %d: %s",
self.server_port, error) self.server_port, error)
# pylint: disable=protected-access async def stop(self):
self.app._middlewares = tuple(self.app._prepare_middleware())
self.app._frozen = False
@asyncio.coroutine
def stop(self):
"""Stop the WSGI server.""" """Stop the WSGI server."""
if self.server: if self.server:
self.server.close() self.server.close()
yield from self.server.wait_closed() await self.server.wait_closed()
yield from self.app.shutdown() await self.app.shutdown()
if self._handler: if self._handler:
yield from self._handler.shutdown(10) await self._handler.shutdown(10)
yield from self.app.cleanup() await self.app.cleanup()
class HomeAssistantView(object):
"""Base view for all views."""
url = None
extra_urls = []
requires_auth = True # Views inheriting from this class can override this
# pylint: disable=no-self-use
def json(self, result, status_code=200, headers=None):
"""Return a JSON response."""
msg = json.dumps(
result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8')
response = web.Response(
body=msg, content_type=CONTENT_TYPE_JSON, status=status_code,
headers=headers)
response.enable_compression()
return response
def json_message(self, message, status_code=200, message_code=None,
headers=None):
"""Return a JSON message response."""
data = {'message': message}
if message_code is not None:
data['code'] = message_code
return self.json(data, status_code, headers=headers)
@asyncio.coroutine
# pylint: disable=no-self-use
def file(self, request, fil):
"""Return a file."""
assert isinstance(fil, str), 'only string paths allowed'
return web.FileResponse(fil)
def register(self, router):
"""Register the view with a router."""
assert self.url is not None, 'No url set for view'
urls = [self.url] + self.extra_urls
for method in ('get', 'post', 'delete', 'put'):
handler = getattr(self, method, None)
if not handler:
continue
handler = request_handler_factory(self, handler)
for url in urls:
router.add_route(method, url, handler)
# aiohttp_cors does not work with class based views
# self.app.router.add_route('*', self.url, self, name=self.name)
# for url in self.extra_urls:
# self.app.router.add_route('*', url, self)
def request_handler_factory(view, handler):
"""Wrap the handler classes."""
assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \
"Handler should be a coroutine or a callback."
@asyncio.coroutine
def handle(request):
"""Handle incoming request."""
if not request.app['hass'].is_running:
return web.Response(status=503)
authenticated = request.get(KEY_AUTHENTICATED, False)
if view.requires_auth and not authenticated:
raise HTTPUnauthorized()
_LOGGER.info('Serving %s to %s (auth: %s)',
request.path, request.get(KEY_REAL_IP), authenticated)
result = handler(request, **request.match_info)
if asyncio.iscoroutine(result):
result = yield from result
if isinstance(result, web.StreamResponse):
# The method handler returned a ready-made Response, how nice of it
return result
status_code = 200
if isinstance(result, tuple):
result, status_code = result
if isinstance(result, str):
result = result.encode('utf-8')
elif result is None:
result = b''
elif not isinstance(result, bytes):
assert False, ('Result should be None, string, bytes or Response. '
'Got: {}').format(result)
return web.Response(body=result, status=status_code)
return handle

View File

@ -1,5 +1,5 @@
"""Authentication for HTTP component.""" """Authentication for HTTP component."""
import asyncio
import base64 import base64
import hmac import hmac
import logging import logging
@ -20,13 +20,12 @@ _LOGGER = logging.getLogger(__name__)
def setup_auth(app, trusted_networks, api_password): def setup_auth(app, trusted_networks, api_password):
"""Create auth middleware for the app.""" """Create auth middleware for the app."""
@middleware @middleware
@asyncio.coroutine async def auth_middleware(request, handler):
def auth_middleware(request, handler):
"""Authenticate as middleware.""" """Authenticate as middleware."""
# If no password set, just always set authenticated=True # If no password set, just always set authenticated=True
if api_password is None: if api_password is None:
request[KEY_AUTHENTICATED] = True request[KEY_AUTHENTICATED] = True
return (yield from handler(request)) return await handler(request)
# Check authentication # Check authentication
authenticated = False authenticated = False
@ -50,10 +49,9 @@ def setup_auth(app, trusted_networks, api_password):
authenticated = True authenticated = True
request[KEY_AUTHENTICATED] = authenticated request[KEY_AUTHENTICATED] = authenticated
return (yield from handler(request)) return await handler(request)
@asyncio.coroutine async def auth_startup(app):
def auth_startup(app):
"""Initialize auth middleware when app starts up.""" """Initialize auth middleware when app starts up."""
app.middlewares.append(auth_middleware) app.middlewares.append(auth_middleware)

View File

@ -1,5 +1,5 @@
"""Ban logic for HTTP component.""" """Ban logic for HTTP component."""
import asyncio
from collections import defaultdict from collections import defaultdict
from datetime import datetime from datetime import datetime
from ipaddress import ip_address from ipaddress import ip_address
@ -38,11 +38,10 @@ SCHEMA_IP_BAN_ENTRY = vol.Schema({
@callback @callback
def setup_bans(hass, app, login_threshold): def setup_bans(hass, app, login_threshold):
"""Create IP Ban middleware for the app.""" """Create IP Ban middleware for the app."""
@asyncio.coroutine async def ban_startup(app):
def ban_startup(app):
"""Initialize bans when app starts up.""" """Initialize bans when app starts up."""
app.middlewares.append(ban_middleware) app.middlewares.append(ban_middleware)
app[KEY_BANNED_IPS] = yield from hass.async_add_job( app[KEY_BANNED_IPS] = await hass.async_add_job(
load_ip_bans_config, hass.config.path(IP_BANS_FILE)) load_ip_bans_config, hass.config.path(IP_BANS_FILE))
app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int) app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int)
app[KEY_LOGIN_THRESHOLD] = login_threshold app[KEY_LOGIN_THRESHOLD] = login_threshold
@ -51,12 +50,11 @@ def setup_bans(hass, app, login_threshold):
@middleware @middleware
@asyncio.coroutine async def ban_middleware(request, handler):
def ban_middleware(request, handler):
"""IP Ban middleware.""" """IP Ban middleware."""
if KEY_BANNED_IPS not in request.app: if KEY_BANNED_IPS not in request.app:
_LOGGER.error('IP Ban middleware loaded but banned IPs not loaded') _LOGGER.error('IP Ban middleware loaded but banned IPs not loaded')
return (yield from handler(request)) return await handler(request)
# Verify if IP is not banned # Verify if IP is not banned
ip_address_ = request[KEY_REAL_IP] ip_address_ = request[KEY_REAL_IP]
@ -67,14 +65,13 @@ def ban_middleware(request, handler):
raise HTTPForbidden() raise HTTPForbidden()
try: try:
return (yield from handler(request)) return await handler(request)
except HTTPUnauthorized: except HTTPUnauthorized:
yield from process_wrong_login(request) await process_wrong_login(request)
raise raise
@asyncio.coroutine async def process_wrong_login(request):
def process_wrong_login(request):
"""Process a wrong login attempt.""" """Process a wrong login attempt."""
remote_addr = request[KEY_REAL_IP] remote_addr = request[KEY_REAL_IP]
@ -98,7 +95,7 @@ def process_wrong_login(request):
request.app[KEY_BANNED_IPS].append(new_ban) request.app[KEY_BANNED_IPS].append(new_ban)
hass = request.app['hass'] hass = request.app['hass']
yield from hass.async_add_job( await hass.async_add_job(
update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban) update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban)
_LOGGER.warning( _LOGGER.warning(

View File

@ -1,5 +1,5 @@
"""Provide cors support for the HTTP component.""" """Provide cors support for the HTTP component."""
import asyncio
from aiohttp.hdrs import ACCEPT, ORIGIN, CONTENT_TYPE from aiohttp.hdrs import ACCEPT, ORIGIN, CONTENT_TYPE
@ -27,8 +27,7 @@ def setup_cors(app, origins):
) for host in origins ) for host in origins
}) })
@asyncio.coroutine async def cors_startup(app):
def cors_startup(app):
"""Initialize cors when app starts up.""" """Initialize cors when app starts up."""
cors_added = set() cors_added = set()

View File

@ -1,5 +1,5 @@
"""Decorator for view methods to help with data validation.""" """Decorator for view methods to help with data validation."""
import asyncio
from functools import wraps from functools import wraps
import logging import logging
@ -24,16 +24,15 @@ class RequestDataValidator:
def __call__(self, method): def __call__(self, method):
"""Decorate a function.""" """Decorate a function."""
@asyncio.coroutine
@wraps(method) @wraps(method)
def wrapper(view, request, *args, **kwargs): async def wrapper(view, request, *args, **kwargs):
"""Wrap a request handler with data validation.""" """Wrap a request handler with data validation."""
data = None data = None
try: try:
data = yield from request.json() data = await request.json()
except ValueError: except ValueError:
if not self._allow_empty or \ if not self._allow_empty or \
(yield from request.content.read()) != b'': (await request.content.read()) != b'':
_LOGGER.error('Invalid JSON received.') _LOGGER.error('Invalid JSON received.')
return view.json_message('Invalid JSON.', 400) return view.json_message('Invalid JSON.', 400)
data = {} data = {}
@ -45,7 +44,7 @@ class RequestDataValidator:
return view.json_message( return view.json_message(
'Message format incorrect: {}'.format(err), 400) 'Message format incorrect: {}'.format(err), 400)
result = yield from method(view, request, *args, **kwargs) result = await method(view, request, *args, **kwargs)
return result return result
return wrapper return wrapper

View File

@ -1,5 +1,5 @@
"""Middleware to fetch real IP.""" """Middleware to fetch real IP."""
import asyncio
from ipaddress import ip_address from ipaddress import ip_address
from aiohttp.web import middleware from aiohttp.web import middleware
@ -14,8 +14,7 @@ from .const import KEY_REAL_IP
def setup_real_ip(app, use_x_forwarded_for): def setup_real_ip(app, use_x_forwarded_for):
"""Create IP Ban middleware for the app.""" """Create IP Ban middleware for the app."""
@middleware @middleware
@asyncio.coroutine async def real_ip_middleware(request, handler):
def real_ip_middleware(request, handler):
"""Real IP middleware.""" """Real IP middleware."""
if (use_x_forwarded_for and if (use_x_forwarded_for and
X_FORWARDED_FOR in request.headers): X_FORWARDED_FOR in request.headers):
@ -25,10 +24,9 @@ def setup_real_ip(app, use_x_forwarded_for):
request[KEY_REAL_IP] = \ request[KEY_REAL_IP] = \
ip_address(request.transport.get_extra_info('peername')[0]) ip_address(request.transport.get_extra_info('peername')[0])
return (yield from handler(request)) return await handler(request)
@asyncio.coroutine async def app_startup(app):
def app_startup(app):
"""Initialize bans when app starts up.""" """Initialize bans when app starts up."""
app.middlewares.append(real_ip_middleware) app.middlewares.append(real_ip_middleware)

View File

@ -1,5 +1,5 @@
"""Static file handling for HTTP component.""" """Static file handling for HTTP component."""
import asyncio
import re import re
from aiohttp import hdrs from aiohttp import hdrs
@ -14,8 +14,7 @@ _FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
class CachingStaticResource(StaticResource): class CachingStaticResource(StaticResource):
"""Static Resource handler that will add cache headers.""" """Static Resource handler that will add cache headers."""
@asyncio.coroutine async def _handle(self, request):
def _handle(self, request):
filename = URL(request.match_info['filename']).path filename = URL(request.match_info['filename']).path
try: try:
# PyLint is wrong about resolve not being a member. # PyLint is wrong about resolve not being a member.
@ -32,13 +31,14 @@ class CachingStaticResource(StaticResource):
raise HTTPNotFound() from error raise HTTPNotFound() from error
if filepath.is_dir(): if filepath.is_dir():
return (yield from super()._handle(request)) return await super()._handle(request)
elif filepath.is_file(): elif filepath.is_file():
return CachingFileResponse(filepath, chunk_size=self._chunk_size) return CachingFileResponse(filepath, chunk_size=self._chunk_size)
else: else:
raise HTTPNotFound raise HTTPNotFound
# pylint: disable=too-many-ancestors
class CachingFileResponse(FileResponse): class CachingFileResponse(FileResponse):
"""FileSender class that caches output if not in dev mode.""" """FileSender class that caches output if not in dev mode."""
@ -48,26 +48,24 @@ class CachingFileResponse(FileResponse):
orig_sendfile = self._sendfile orig_sendfile = self._sendfile
@asyncio.coroutine async def sendfile(request, fobj, count):
def sendfile(request, fobj, count):
"""Sendfile that includes a cache header.""" """Sendfile that includes a cache header."""
cache_time = 31 * 86400 # = 1 month cache_time = 31 * 86400 # = 1 month
self.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format( self.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format(
cache_time) cache_time)
yield from orig_sendfile(request, fobj, count) await orig_sendfile(request, fobj, count)
# Overwriting like this because __init__ can change implementation. # Overwriting like this because __init__ can change implementation.
self._sendfile = sendfile self._sendfile = sendfile
@middleware @middleware
@asyncio.coroutine async def staticresource_middleware(request, handler):
def staticresource_middleware(request, handler):
"""Middleware to strip out fingerprint from fingerprinted assets.""" """Middleware to strip out fingerprint from fingerprinted assets."""
path = request.path path = request.path
if not path.startswith('/static/') and not path.startswith('/frontend'): if not path.startswith('/static/') and not path.startswith('/frontend'):
return (yield from handler(request)) return await handler(request)
fingerprinted = _FINGERPRINT.match(request.match_info['filename']) fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
@ -75,4 +73,4 @@ def staticresource_middleware(request, handler):
request.match_info['filename'] = \ request.match_info['filename'] = \
'{}.{}'.format(*fingerprinted.groups()) '{}.{}'.format(*fingerprinted.groups())
return (yield from handler(request)) return await handler(request)

View File

@ -0,0 +1,121 @@
"""
This module provides WSGI application to serve the Home Assistant API.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/http/
"""
import asyncio
import json
import logging
from aiohttp import web
from aiohttp.web_exceptions import HTTPUnauthorized
import homeassistant.remote as rem
from homeassistant.core import is_callback
from homeassistant.const import CONTENT_TYPE_JSON
from .const import KEY_AUTHENTICATED, KEY_REAL_IP
_LOGGER = logging.getLogger(__name__)
class HomeAssistantView(object):
"""Base view for all views."""
url = None
extra_urls = []
requires_auth = True # Views inheriting from this class can override this
# pylint: disable=no-self-use
def json(self, result, status_code=200, headers=None):
"""Return a JSON response."""
msg = json.dumps(
result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8')
response = web.Response(
body=msg, content_type=CONTENT_TYPE_JSON, status=status_code,
headers=headers)
response.enable_compression()
return response
def json_message(self, message, status_code=200, message_code=None,
headers=None):
"""Return a JSON message response."""
data = {'message': message}
if message_code is not None:
data['code'] = message_code
return self.json(data, status_code, headers=headers)
# pylint: disable=no-self-use
async def file(self, request, fil):
"""Return a file."""
assert isinstance(fil, str), 'only string paths allowed'
return web.FileResponse(fil)
def register(self, router):
"""Register the view with a router."""
assert self.url is not None, 'No url set for view'
urls = [self.url] + self.extra_urls
for method in ('get', 'post', 'delete', 'put'):
handler = getattr(self, method, None)
if not handler:
continue
handler = request_handler_factory(self, handler)
for url in urls:
router.add_route(method, url, handler)
# aiohttp_cors does not work with class based views
# self.app.router.add_route('*', self.url, self, name=self.name)
# for url in self.extra_urls:
# self.app.router.add_route('*', url, self)
def request_handler_factory(view, handler):
"""Wrap the handler classes."""
assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \
"Handler should be a coroutine or a callback."
async def handle(request):
"""Handle incoming request."""
if not request.app['hass'].is_running:
return web.Response(status=503)
authenticated = request.get(KEY_AUTHENTICATED, False)
if view.requires_auth and not authenticated:
raise HTTPUnauthorized()
_LOGGER.info('Serving %s to %s (auth: %s)',
request.path, request.get(KEY_REAL_IP), authenticated)
result = handler(request, **request.match_info)
if asyncio.iscoroutine(result):
result = await result
if isinstance(result, web.StreamResponse):
# The method handler returned a ready-made Response, how nice of it
return result
status_code = 200
if isinstance(result, tuple):
result, status_code = result
if isinstance(result, str):
result = result.encode('utf-8')
elif result is None:
result = b''
elif not isinstance(result, bytes):
assert False, ('Result should be None, string, bytes or Response. '
'Got: {}').format(result)
return web.Response(body=result, status=status_code)
return handle

View File

@ -4,20 +4,24 @@ This component provides basic support for the Philips Hue system.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/hue/ https://home-assistant.io/components/hue/
""" """
import asyncio
import json import json
from functools import partial
import logging import logging
import os import os
import socket import socket
import async_timeout
import requests import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.discovery import SERVICE_HUE from homeassistant.components.discovery import SERVICE_HUE
from homeassistant.const import CONF_FILENAME, CONF_HOST from homeassistant.const import CONF_FILENAME, CONF_HOST
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery, aiohttp_client
from homeassistant import config_entries
REQUIREMENTS = ['phue==1.0'] REQUIREMENTS = ['phue==1.0', 'aiohue==0.3.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -133,13 +137,14 @@ def bridge_discovered(hass, service, discovery_info):
def setup_bridge(host, hass, filename=None, allow_unreachable=False, def setup_bridge(host, hass, filename=None, allow_unreachable=False,
allow_in_emulated_hue=True, allow_hue_groups=True): allow_in_emulated_hue=True, allow_hue_groups=True,
username=None):
"""Set up a given Hue bridge.""" """Set up a given Hue bridge."""
# Only register a device once # Only register a device once
if socket.gethostbyname(host) in hass.data[DOMAIN]: if socket.gethostbyname(host) in hass.data[DOMAIN]:
return return
bridge = HueBridge(host, hass, filename, allow_unreachable, bridge = HueBridge(host, hass, filename, username, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups) allow_in_emulated_hue, allow_hue_groups)
bridge.setup() bridge.setup()
@ -164,13 +169,14 @@ def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
class HueBridge(object): class HueBridge(object):
"""Manages a single Hue bridge.""" """Manages a single Hue bridge."""
def __init__(self, host, hass, filename, allow_unreachable=False, def __init__(self, host, hass, filename, username, allow_unreachable=False,
allow_in_emulated_hue=True, allow_hue_groups=True): allow_in_emulated_hue=True, allow_hue_groups=True):
"""Initialize the system.""" """Initialize the system."""
self.host = host self.host = host
self.bridge_id = socket.gethostbyname(host) self.bridge_id = socket.gethostbyname(host)
self.hass = hass self.hass = hass
self.filename = filename self.filename = filename
self.username = username
self.allow_unreachable = allow_unreachable self.allow_unreachable = allow_unreachable
self.allow_in_emulated_hue = allow_in_emulated_hue self.allow_in_emulated_hue = allow_in_emulated_hue
self.allow_hue_groups = allow_hue_groups self.allow_hue_groups = allow_hue_groups
@ -189,10 +195,14 @@ class HueBridge(object):
import phue import phue
try: try:
self.bridge = phue.Bridge( kwargs = {}
self.host, if self.username is not None:
config_file_path=self.hass.config.path(self.filename)) kwargs['username'] = self.username
except (ConnectionRefusedError, OSError): # Wrong host was given if self.filename is not None:
kwargs['config_file_path'] = \
self.hass.config.path(self.filename)
self.bridge = phue.Bridge(self.host, **kwargs)
except OSError: # Wrong host was given
_LOGGER.error("Error connecting to the Hue bridge at %s", _LOGGER.error("Error connecting to the Hue bridge at %s",
self.host) self.host)
return return
@ -204,6 +214,7 @@ class HueBridge(object):
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unknown error connecting with Hue bridge at %s", _LOGGER.exception("Unknown error connecting with Hue bridge at %s",
self.host) self.host)
return
# If we came here and configuring this host, mark as done # If we came here and configuring this host, mark as done
if self.config_request_id: if self.config_request_id:
@ -260,3 +271,112 @@ class HueBridge(object):
def set_group(self, light_id, command): def set_group(self, light_id, command):
"""Change light settings for a group. See phue for detail.""" """Change light settings for a group. See phue for detail."""
return self.bridge.set_group(light_id, command) return self.bridge.set_group(light_id, command)
@config_entries.HANDLERS.register(DOMAIN)
class HueFlowHandler(config_entries.ConfigFlowHandler):
"""Handle a Hue config flow."""
VERSION = 1
def __init__(self):
"""Initialize the Hue flow."""
self.host = None
@property
def _websession(self):
"""Return a websession.
Cannot assign in init because hass variable is not set yet.
"""
return aiohttp_client.async_get_clientsession(self.hass)
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
from aiohue.discovery import discover_nupnp
if user_input is not None:
self.host = user_input['host']
return await self.async_step_link()
try:
with async_timeout.timeout(5):
bridges = await discover_nupnp(websession=self._websession)
except asyncio.TimeoutError:
return self.async_abort(
reason='Unable to discover Hue bridges.'
)
if not bridges:
return self.async_abort(
reason='No Philips Hue bridges discovered.'
)
# Find already configured hosts
configured_hosts = set(
entry.data['host'] for entry
in self.hass.config_entries.async_entries(DOMAIN))
hosts = [bridge.host for bridge in bridges
if bridge.host not in configured_hosts]
if not hosts:
return self.async_abort(
reason='All Philips Hue bridges are already configured.'
)
elif len(hosts) == 1:
self.host = hosts[0]
return await self.async_step_link()
return self.async_show_form(
step_id='init',
title='Pick Hue Bridge',
data_schema=vol.Schema({
vol.Required('host'): vol.In(hosts)
})
)
async def async_step_link(self, user_input=None):
"""Attempt to link with the Hue bridge."""
import aiohue
errors = {}
if user_input is not None:
bridge = aiohue.Bridge(self.host, websession=self._websession)
try:
with async_timeout.timeout(5):
# Create auth token
await bridge.create_user('home-assistant')
# Fetches name and id
await bridge.initialize()
except (asyncio.TimeoutError, aiohue.RequestError,
aiohue.LinkButtonNotPressed):
errors['base'] = 'Failed to register, please try again.'
except aiohue.AiohueException:
errors['base'] = 'Unknown linking error occurred.'
_LOGGER.exception('Uknown Hue linking error occurred')
else:
return self.async_create_entry(
title=bridge.config.name,
data={
'host': bridge.host,
'bridge_id': bridge.config.bridgeid,
'username': bridge.username,
}
)
return self.async_show_form(
step_id='link',
title='Link Hub',
description=CONFIG_INSTRUCTIONS,
errors=errors,
)
async def async_setup_entry(hass, entry):
"""Set up a bridge for a config entry."""
await hass.async_add_job(partial(
setup_bridge, entry.data['host'], hass,
username=entry.data['username']))
return True

View File

@ -22,7 +22,7 @@ from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
REQUIREMENTS = ['ihcsdk==2.1.1'] REQUIREMENTS = ['ihcsdk==2.2.0']
DOMAIN = 'ihc' DOMAIN = 'ihc'
IHC_DATA = 'ihc' IHC_DATA = 'ihc'

View File

@ -43,7 +43,7 @@ DEFAULT_TIMEOUT = 10
DEFAULT_CONFIDENCE = 80 DEFAULT_CONFIDENCE = 80
SOURCE_SCHEMA = vol.Schema({ SOURCE_SCHEMA = vol.Schema({
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_domain('camera'),
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
}) })

View File

@ -35,7 +35,6 @@ CONF_COMPONENT_CONFIG = 'component_config'
CONF_COMPONENT_CONFIG_GLOB = 'component_config_glob' CONF_COMPONENT_CONFIG_GLOB = 'component_config_glob'
CONF_COMPONENT_CONFIG_DOMAIN = 'component_config_domain' CONF_COMPONENT_CONFIG_DOMAIN = 'component_config_domain'
CONF_RETRY_COUNT = 'max_retries' CONF_RETRY_COUNT = 'max_retries'
CONF_RETRY_QUEUE = 'retry_queue_limit'
DEFAULT_DATABASE = 'home_assistant' DEFAULT_DATABASE = 'home_assistant'
DEFAULT_VERIFY_SSL = True DEFAULT_VERIFY_SSL = True
@ -43,14 +42,17 @@ DOMAIN = 'influxdb'
TIMEOUT = 5 TIMEOUT = 5
RETRY_DELAY = 20 RETRY_DELAY = 20
QUEUE_BACKLOG_SECONDS = 10 QUEUE_BACKLOG_SECONDS = 30
BATCH_TIMEOUT = 1
BATCH_BUFFER_SIZE = 100
COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema({ COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema({
vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string, vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string,
}) })
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.deprecated(CONF_RETRY_QUEUE), vol.Schema({ DOMAIN: vol.All(vol.Schema({
vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_HOST): cv.string,
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
@ -68,7 +70,6 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_SSL): cv.boolean, vol.Optional(CONF_SSL): cv.boolean,
vol.Optional(CONF_RETRY_COUNT, default=0): cv.positive_int, vol.Optional(CONF_RETRY_COUNT, default=0): cv.positive_int,
vol.Optional(CONF_RETRY_QUEUE, default=20): cv.positive_int,
vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string, vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string,
vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string, vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string,
vol.Optional(CONF_TAGS, default={}): vol.Optional(CONF_TAGS, default={}):
@ -143,18 +144,18 @@ def setup(hass, config):
"READ/WRITE", exc) "READ/WRITE", exc)
return False return False
def influx_handle_event(event): def event_to_json(event):
"""Send an event to Influx.""" """Add an event to the outgoing Influx list."""
state = event.data.get('new_state') state = event.data.get('new_state')
if state is None or state.state in ( if state is None or state.state in (
STATE_UNKNOWN, '', STATE_UNAVAILABLE) or \ STATE_UNKNOWN, '', STATE_UNAVAILABLE) or \
state.entity_id in blacklist_e or state.domain in blacklist_d: state.entity_id in blacklist_e or state.domain in blacklist_d:
return True return
try: try:
if (whitelist_e and state.entity_id not in whitelist_e) or \ if (whitelist_e and state.entity_id not in whitelist_e) or \
(whitelist_d and state.domain not in whitelist_d): (whitelist_d and state.domain not in whitelist_d):
return True return
_include_state = _include_value = False _include_state = _include_value = False
@ -183,61 +184,59 @@ def setup(hass, config):
else: else:
include_uom = False include_uom = False
json_body = [ json = {
{
'measurement': measurement, 'measurement': measurement,
'tags': { 'tags': {
'domain': state.domain, 'domain': state.domain,
'entity_id': state.object_id, 'entity_id': state.object_id,
}, },
'time': event.time_fired, 'time': event.time_fired,
'fields': { 'fields': {}
} }
}
]
if _include_state: if _include_state:
json_body[0]['fields']['state'] = state.state json['fields']['state'] = state.state
if _include_value: if _include_value:
json_body[0]['fields']['value'] = _state_as_value json['fields']['value'] = _state_as_value
for key, value in state.attributes.items(): for key, value in state.attributes.items():
if key in tags_attributes: if key in tags_attributes:
json_body[0]['tags'][key] = value json['tags'][key] = value
elif key != 'unit_of_measurement' or include_uom: elif key != 'unit_of_measurement' or include_uom:
# If the key is already in fields # If the key is already in fields
if key in json_body[0]['fields']: if key in json['fields']:
key = key + "_" key = key + "_"
# Prevent column data errors in influxDB. # Prevent column data errors in influxDB.
# For each value we try to cast it as float # For each value we try to cast it as float
# But if we can not do it we store the value # But if we can not do it we store the value
# as string add "_str" postfix to the field key # as string add "_str" postfix to the field key
try: try:
json_body[0]['fields'][key] = float(value) json['fields'][key] = float(value)
except (ValueError, TypeError): except (ValueError, TypeError):
new_key = "{}_str".format(key) new_key = "{}_str".format(key)
new_value = str(value) new_value = str(value)
json_body[0]['fields'][new_key] = new_value json['fields'][new_key] = new_value
if RE_DIGIT_TAIL.match(new_value): if RE_DIGIT_TAIL.match(new_value):
json_body[0]['fields'][key] = float( json['fields'][key] = float(
RE_DECIMAL.sub('', new_value)) RE_DECIMAL.sub('', new_value))
json_body[0]['tags'].update(tags) # Infinity is not a valid float in InfluxDB
if (key, float("inf")) in json['fields'].items():
del json['fields'][key]
try: json['tags'].update(tags)
influx.write_points(json_body)
return True return json
except (exceptions.InfluxDBClientError, IOError):
return False
instance = hass.data[DOMAIN] = InfluxThread( instance = hass.data[DOMAIN] = InfluxThread(
hass, influx_handle_event, max_tries) hass, influx, event_to_json, max_tries)
instance.start() instance.start()
def shutdown(event): def shutdown(event):
"""Shut down the thread.""" """Shut down the thread."""
instance.queue.put(None) instance.queue.put(None)
instance.join() instance.join()
influx.close()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
@ -247,12 +246,15 @@ def setup(hass, config):
class InfluxThread(threading.Thread): class InfluxThread(threading.Thread):
"""A threaded event handler class.""" """A threaded event handler class."""
def __init__(self, hass, event_handler, max_tries): def __init__(self, hass, influx, event_to_json, max_tries):
"""Initialize the listener.""" """Initialize the listener."""
threading.Thread.__init__(self, name='InfluxDB') threading.Thread.__init__(self, name='InfluxDB')
self.queue = queue.Queue() self.queue = queue.Queue()
self.event_handler = event_handler self.influx = influx
self.event_to_json = event_to_json
self.max_tries = max_tries self.max_tries = max_tries
self.write_errors = 0
self.shutdown = False
hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener) hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener)
def _event_listener(self, event): def _event_listener(self, event):
@ -260,40 +262,76 @@ class InfluxThread(threading.Thread):
item = (time.monotonic(), event) item = (time.monotonic(), event)
self.queue.put(item) self.queue.put(item)
def run(self): @staticmethod
"""Process incoming events.""" def batch_timeout():
"""Return number of seconds to wait for more events."""
return BATCH_TIMEOUT
def get_events_json(self):
"""Return a batch of events formatted for writing."""
queue_seconds = QUEUE_BACKLOG_SECONDS + self.max_tries*RETRY_DELAY queue_seconds = QUEUE_BACKLOG_SECONDS + self.max_tries*RETRY_DELAY
write_error = False count = 0
dropped = False json = []
while True: dropped = 0
item = self.queue.get()
try:
while len(json) < BATCH_BUFFER_SIZE and not self.shutdown:
timeout = None if count == 0 else self.batch_timeout()
item = self.queue.get(timeout=timeout)
count += 1
if item is None: if item is None:
self.queue.task_done() self.shutdown = True
return else:
timestamp, event = item timestamp, event = item
age = time.monotonic() - timestamp age = time.monotonic() - timestamp
if age < queue_seconds: if age < queue_seconds:
for retry in range(self.max_tries+1): event_json = self.event_to_json(event)
if self.event_handler(event): if event_json:
if write_error: json.append(event_json)
_LOGGER.error("Resumed writing to InfluxDB") else:
write_error = False dropped += 1
dropped = False
break
elif retry < self.max_tries:
time.sleep(RETRY_DELAY)
elif not write_error:
_LOGGER.error("Error writing to InfluxDB")
write_error = True
elif not dropped:
_LOGGER.warning("Dropping old events to catch up")
dropped = True
except queue.Empty:
pass
if dropped:
_LOGGER.warning("Catching up, dropped %d old events", dropped)
return count, json
def write_to_influxdb(self, json):
"""Write preprocessed events to influxdb, with retry."""
from influxdb import exceptions
for retry in range(self.max_tries+1):
try:
self.influx.write_points(json)
if self.write_errors:
_LOGGER.error("Resumed, lost %d events", self.write_errors)
self.write_errors = 0
_LOGGER.debug("Wrote %d events", len(json))
break
except (exceptions.InfluxDBClientError, IOError):
if retry < self.max_tries:
time.sleep(RETRY_DELAY)
else:
if not self.write_errors:
_LOGGER.exception("Write error")
self.write_errors += len(json)
def run(self):
"""Process incoming events."""
while not self.shutdown:
count, json = self.get_events_json()
if json:
self.write_to_influxdb(json)
for _ in range(count):
self.queue.task_done() self.queue.task_done()
def block_till_done(self): def block_till_done(self):

View File

@ -4,117 +4,211 @@ Support for INSTEON PowerLinc Modem.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/insteon_plm/ https://home-assistant.io/components/insteon_plm/
""" """
import logging
import asyncio import asyncio
import collections
import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import ( from homeassistant.const import (CONF_PORT, EVENT_HOMEASSISTANT_STOP,
CONF_PORT, EVENT_HOMEASSISTANT_STOP) CONF_PLATFORM)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['insteonplm==0.7.5'] REQUIREMENTS = ['insteonplm==0.8.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'insteon_plm' DOMAIN = 'insteon_plm'
CONF_OVERRIDE = 'device_override' CONF_OVERRIDE = 'device_override'
CONF_ADDRESS = 'address'
CONF_CAT = 'cat'
CONF_SUBCAT = 'subcat'
CONF_FIRMWARE = 'firmware'
CONF_PRODUCT_KEY = 'product_key'
CONF_DEVICE_OVERRIDE_SCHEMA = vol.All(
cv.deprecated(CONF_PLATFORM), vol.Schema({
vol.Required(CONF_ADDRESS): cv.string,
vol.Optional(CONF_CAT): cv.byte,
vol.Optional(CONF_SUBCAT): cv.byte,
vol.Optional(CONF_FIRMWARE): cv.byte,
vol.Optional(CONF_PRODUCT_KEY): cv.byte,
vol.Optional(CONF_PLATFORM): cv.string,
}))
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_PORT): cv.string, vol.Required(CONF_PORT): cv.string,
vol.Optional(CONF_OVERRIDE, default=[]): cv.ensure_list_csv, vol.Optional(CONF_OVERRIDE): vol.All(
cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA])
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
PLM_PLATFORMS = {
'binary_sensor': ['binary_sensor'],
'light': ['light'],
'switch': ['switch'],
}
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Set up the connection to the PLM.""" """Set up the connection to the PLM."""
import insteonplm import insteonplm
ipdb = IPDB()
conf = config[DOMAIN] conf = config[DOMAIN]
port = conf.get(CONF_PORT) port = conf.get(CONF_PORT)
overrides = conf.get(CONF_OVERRIDE) overrides = conf.get(CONF_OVERRIDE, [])
@callback @callback
def async_plm_new_device(device): def async_plm_new_device(device):
"""Detect device from transport to be delegated to platform.""" """Detect device from transport to be delegated to platform."""
name = device.get('address') for state_key in device.states:
address = device.get('address_hex') platform_info = ipdb[device.states[state_key]]
capabilities = device.get('capabilities', []) platform = platform_info.platform
if platform is not None:
_LOGGER.info("New INSTEON PLM device: %s (%s) %s",
device.address,
device.states[state_key].name,
platform)
_LOGGER.info("New INSTEON PLM device: %s (%s) %r",
name, address, capabilities)
loadlist = []
for platform in PLM_PLATFORMS:
caplist = PLM_PLATFORMS.get(platform)
for key in capabilities:
if key in caplist:
loadlist.append(platform)
loadlist = sorted(set(loadlist))
for loadplatform in loadlist:
hass.async_add_job( hass.async_add_job(
discovery.async_load_platform( discovery.async_load_platform(
hass, loadplatform, DOMAIN, discovered=[device], hass, platform, DOMAIN,
discovered={'address': device.address.hex,
'state_key': state_key},
hass_config=config)) hass_config=config))
_LOGGER.info("Looking for PLM on %s", port) _LOGGER.info("Looking for PLM on %s", port)
plm = yield from insteonplm.Connection.create(device=port, loop=hass.loop) conn = yield from insteonplm.Connection.create(
device=port,
loop=hass.loop,
workdir=hass.config.config_dir)
for device in overrides: plm = conn.protocol
for device_override in overrides:
# #
# Override the device default capabilities for a specific address # Override the device default capabilities for a specific address
# #
if isinstance(device['platform'], list): address = device_override.get('address')
plm.protocol.devices.add_override( for prop in device_override:
device['address'], 'capabilities', device['platform']) if prop in [CONF_CAT, CONF_SUBCAT]:
else: plm.devices.add_override(address, prop,
plm.protocol.devices.add_override( device_override[prop])
device['address'], 'capabilities', [device['platform']]) elif prop in [CONF_FIRMWARE, CONF_PRODUCT_KEY]:
plm.devices.add_override(address, CONF_PRODUCT_KEY,
device_override[prop])
hass.data['insteon_plm'] = plm hass.data['insteon_plm'] = plm
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, plm.close) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close)
plm.protocol.devices.add_device_callback(async_plm_new_device, {}) plm.devices.add_device_callback(async_plm_new_device)
return True return True
def common_attributes(entity): State = collections.namedtuple('Product', 'stateType platform')
"""Return the device state attributes."""
attributes = {}
attributekeys = {
'address': 'INSTEON Address',
'description': 'Description',
'model': 'Model',
'cat': 'Category',
'subcat': 'Subcategory',
'firmware': 'Firmware',
'product_key': 'Product Key'
}
hexkeys = ['cat', 'subcat', 'firmware']
for key in attributekeys: class IPDB(object):
name = attributekeys[key] """Embodies the INSTEON Product Database static data and access methods."""
val = entity.get_attr(key)
if val is not None: def __init__(self):
if key in hexkeys: """Create the INSTEON Product Database (IPDB)."""
attributes[name] = hex(int(val)) from insteonplm.states.onOff import (OnOffSwitch,
OnOffSwitch_OutletTop,
OnOffSwitch_OutletBottom,
OpenClosedRelay)
from insteonplm.states.dimmable import (DimmableSwitch,
DimmableSwitch_Fan)
from insteonplm.states.sensor import (VariableSensor,
OnOffSensor,
SmokeCO2Sensor,
IoLincSensor)
self.states = [State(OnOffSwitch_OutletTop, 'switch'),
State(OnOffSwitch_OutletBottom, 'switch'),
State(OpenClosedRelay, 'switch'),
State(OnOffSwitch, 'switch'),
State(IoLincSensor, 'binary_sensor'),
State(SmokeCO2Sensor, 'sensor'),
State(OnOffSensor, 'binary_sensor'),
State(VariableSensor, 'sensor'),
State(DimmableSwitch_Fan, 'fan'),
State(DimmableSwitch, 'light')]
def __len__(self):
"""Return the number of INSTEON state types mapped to HA platforms."""
return len(self.states)
def __iter__(self):
"""Itterate through the INSTEON state types to HA platforms."""
for product in self.states:
yield product
def __getitem__(self, key):
"""Return a Home Assistant platform from an INSTEON state type."""
for state in self.states:
if isinstance(key, state.stateType):
return state
return None
class InsteonPLMEntity(Entity):
"""INSTEON abstract base entity."""
def __init__(self, device, state_key):
"""Initialize the INSTEON PLM binary sensor."""
self._insteon_device_state = device.states[state_key]
self._insteon_device = device
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def address(self):
"""Return the address of the node."""
return self._insteon_device.address.human
@property
def group(self):
"""Return the INSTEON group that the entity responds to."""
return self._insteon_device_state.group
@property
def name(self):
"""Return the name of the node (used for Entity_ID)."""
name = ''
if self._insteon_device_state.group == 0x01:
name = self._insteon_device.id
else: else:
attributes[name] = val name = '{:s}_{:d}'.format(self._insteon_device.id,
self._insteon_device_state.group)
return name
@property
def device_state_attributes(self):
"""Provide attributes for display on device card."""
attributes = {
'INSTEON Address': self.address,
'INSTEON Group': self.group
}
return attributes return attributes
@callback
def async_entity_update(self, deviceid, statename, val):
"""Receive notification from transport that new data exists."""
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_added_to_hass(self):
"""Register INSTEON update events."""
self._insteon_device_state.register_updates(
self.async_entity_update)

View File

@ -70,5 +70,5 @@ class JuicenetDevice(Entity):
@property @property
def unique_id(self): def unique_id(self):
"""Return an unique ID.""" """Return a unique ID."""
return "{}-{}".format(self.device.id(), self.type) return "{}-{}".format(self.device.id(), self.type)

View File

@ -4,17 +4,20 @@ Connects to KNX platform.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/knx/ https://home-assistant.io/components/knx/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.const import (
CONF_ENTITY_ID, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.script import Script from homeassistant.helpers.script import Script
REQUIREMENTS = ['xknx==0.8.3'] REQUIREMENTS = ['xknx==0.8.4']
DOMAIN = "knx" DOMAIN = "knx"
DATA_KNX = "data_knx" DATA_KNX = "data_knx"
@ -26,6 +29,9 @@ CONF_KNX_LOCAL_IP = "local_ip"
CONF_KNX_FIRE_EVENT = "fire_event" CONF_KNX_FIRE_EVENT = "fire_event"
CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter" CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter"
CONF_KNX_STATE_UPDATER = "state_updater" CONF_KNX_STATE_UPDATER = "state_updater"
CONF_KNX_EXPOSE = "expose"
CONF_KNX_EXPOSE_TYPE = "type"
CONF_KNX_EXPOSE_ADDRESS = "address"
SERVICE_KNX_SEND = "send" SERVICE_KNX_SEND = "send"
SERVICE_KNX_ATTR_ADDRESS = "address" SERVICE_KNX_ATTR_ADDRESS = "address"
@ -45,6 +51,12 @@ ROUTING_SCHEMA = vol.Schema({
vol.Required(CONF_KNX_LOCAL_IP): cv.string, vol.Required(CONF_KNX_LOCAL_IP): cv.string,
}) })
EXPOSE_SCHEMA = vol.Schema({
vol.Required(CONF_KNX_EXPOSE_TYPE): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_KNX_EXPOSE_ADDRESS): cv.string,
})
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_KNX_CONFIG): cv.string, vol.Optional(CONF_KNX_CONFIG): cv.string,
@ -56,6 +68,10 @@ CONFIG_SCHEMA = vol.Schema({
vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, 'fire_ev'): vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, 'fire_ev'):
vol.All(cv.ensure_list, [cv.string]), vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean, vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean,
vol.Optional(CONF_KNX_EXPOSE):
vol.All(
cv.ensure_list,
[EXPOSE_SCHEMA]),
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -66,13 +82,13 @@ SERVICE_KNX_SEND_SCHEMA = vol.Schema({
}) })
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Set up the KNX component.""" """Set up the KNX component."""
from xknx.exceptions import XKNXException from xknx.exceptions import XKNXException
try: try:
hass.data[DATA_KNX] = KNXModule(hass, config) hass.data[DATA_KNX] = KNXModule(hass, config)
yield from hass.data[DATA_KNX].start() hass.data[DATA_KNX].async_create_exposures()
await hass.data[DATA_KNX].start()
except XKNXException as ex: except XKNXException as ex:
_LOGGER.warning("Can't connect to KNX interface: %s", ex) _LOGGER.warning("Can't connect to KNX interface: %s", ex)
@ -88,6 +104,7 @@ def async_setup(hass, config):
('light', 'Light'), ('light', 'Light'),
('sensor', 'Sensor'), ('sensor', 'Sensor'),
('binary_sensor', 'BinarySensor'), ('binary_sensor', 'BinarySensor'),
('scene', 'Scene'),
('notify', 'Notification')): ('notify', 'Notification')):
found_devices = _get_devices(hass, discovery_type) found_devices = _get_devices(hass, discovery_type)
hass.async_add_job( hass.async_add_job(
@ -122,26 +139,25 @@ class KNXModule(object):
self.connected = False self.connected = False
self.init_xknx() self.init_xknx()
self.register_callbacks() self.register_callbacks()
self.exposures = []
def init_xknx(self): def init_xknx(self):
"""Initialize of KNX object.""" """Initialize of KNX object."""
from xknx import XKNX from xknx import XKNX
self.xknx = XKNX(config=self.config_file(), loop=self.hass.loop) self.xknx = XKNX(config=self.config_file(), loop=self.hass.loop)
@asyncio.coroutine async def start(self):
def start(self):
"""Start KNX object. Connect to tunneling or Routing device.""" """Start KNX object. Connect to tunneling or Routing device."""
connection_config = self.connection_config() connection_config = self.connection_config()
yield from self.xknx.start( await self.xknx.start(
state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER], state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER],
connection_config=connection_config) connection_config=connection_config)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
self.connected = True self.connected = True
@asyncio.coroutine async def stop(self, event):
def stop(self, event):
"""Stop KNX object. Disconnect from tunneling or Routing device.""" """Stop KNX object. Disconnect from tunneling or Routing device."""
yield from self.xknx.stop() await self.xknx.stop()
def config_file(self): def config_file(self):
"""Resolve and return the full path of xknx.yaml if configured.""" """Resolve and return the full path of xknx.yaml if configured."""
@ -202,8 +218,27 @@ class KNXModule(object):
self.xknx.telegram_queue.register_telegram_received_cb( self.xknx.telegram_queue.register_telegram_received_cb(
self.telegram_received_cb, address_filters) self.telegram_received_cb, address_filters)
@asyncio.coroutine @callback
def telegram_received_cb(self, telegram): def async_create_exposures(self):
"""Create exposures."""
if CONF_KNX_EXPOSE not in self.config[DOMAIN]:
return
for to_expose in self.config[DOMAIN][CONF_KNX_EXPOSE]:
expose_type = to_expose.get(CONF_KNX_EXPOSE_TYPE)
entity_id = to_expose.get(CONF_ENTITY_ID)
address = to_expose.get(CONF_KNX_EXPOSE_ADDRESS)
if expose_type in ['time', 'date', 'datetime']:
exposure = KNXExposeTime(
self.xknx, expose_type, address)
exposure.async_register()
self.exposures.append(exposure)
else:
exposure = KNXExposeSensor(
self.hass, self.xknx, expose_type, entity_id, address)
exposure.async_register()
self.exposures.append(exposure)
async def telegram_received_cb(self, telegram):
"""Call invoked after a KNX telegram was received.""" """Call invoked after a KNX telegram was received."""
self.hass.bus.fire('knx_event', { self.hass.bus.fire('knx_event', {
'address': telegram.group_address.str(), 'address': telegram.group_address.str(),
@ -212,8 +247,7 @@ class KNXModule(object):
# False signals XKNX to proceed with processing telegrams. # False signals XKNX to proceed with processing telegrams.
return False return False
@asyncio.coroutine async def service_send_to_knx_bus(self, call):
def service_send_to_knx_bus(self, call):
"""Service for sending an arbitrary KNX message to the KNX bus.""" """Service for sending an arbitrary KNX message to the KNX bus."""
from xknx.knx import Telegram, GroupAddress, DPTBinary, DPTArray from xknx.knx import Telegram, GroupAddress, DPTBinary, DPTArray
attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD) attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD)
@ -230,7 +264,7 @@ class KNXModule(object):
telegram = Telegram() telegram = Telegram()
telegram.payload = payload telegram.payload = payload
telegram.group_address = address telegram.group_address = address
yield from self.xknx.telegrams.put(telegram) await self.xknx.telegrams.put(telegram)
class KNXAutomation(): class KNXAutomation():
@ -248,3 +282,59 @@ class KNXAutomation():
hass.data[DATA_KNX].xknx, self.script.async_run, hass.data[DATA_KNX].xknx, self.script.async_run,
hook=hook, counter=counter) hook=hook, counter=counter)
device.actions.append(self.action) device.actions.append(self.action)
class KNXExposeTime(object):
"""Object to Expose Time/Date object to KNX bus."""
def __init__(self, xknx, expose_type, address):
"""Initialize of Expose class."""
self.xknx = xknx
self.type = expose_type
self.address = address
self.device = None
@callback
def async_register(self):
"""Register listener."""
from xknx.devices import DateTime, DateTimeBroadcastType
broadcast_type_string = self.type.upper()
broadcast_type = DateTimeBroadcastType[broadcast_type_string]
self.device = DateTime(
self.xknx,
'Time',
broadcast_type=broadcast_type,
group_address=self.address)
self.xknx.devices.add(self.device)
class KNXExposeSensor(object):
"""Object to Expose HASS entity to KNX bus."""
def __init__(self, hass, xknx, expose_type, entity_id, address):
"""Initialize of Expose class."""
self.hass = hass
self.xknx = xknx
self.type = expose_type
self.entity_id = entity_id
self.address = address
self.device = None
@callback
def async_register(self):
"""Register listener."""
from xknx.devices import ExposeSensor
self.device = ExposeSensor(
self.xknx,
name=self.entity_id,
group_address=self.address,
value_type=self.type)
self.xknx.devices.add(self.device)
async_track_state_change(
self.hass, self.entity_id, self._async_entity_changed)
async def _async_entity_changed(self, entity_id, old_state, new_state):
"""Callback after entity changed."""
if new_state is None:
return
await self.device.set(float(new_state.state))

View File

@ -12,7 +12,8 @@ import os
import voluptuous as vol import voluptuous as vol
from homeassistant.components import group from homeassistant.components.group import \
ENTITY_ID_FORMAT as GROUP_ENTITY_ID_FORMAT
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON,
STATE_ON) STATE_ON)
@ -21,6 +22,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers import intent
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
@ -29,7 +31,7 @@ DEPENDENCIES = ['group']
SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = timedelta(seconds=30)
GROUP_NAME_ALL_LIGHTS = 'all lights' GROUP_NAME_ALL_LIGHTS = 'all lights'
ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format('all_lights') ENTITY_ID_ALL_LIGHTS = GROUP_ENTITY_ID_FORMAT.format('all_lights')
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -84,8 +86,6 @@ LIGHT_PROFILES_FILE = "light_profiles.csv"
PROP_TO_ATTR = { PROP_TO_ATTR = {
'brightness': ATTR_BRIGHTNESS, 'brightness': ATTR_BRIGHTNESS,
'color_temp': ATTR_COLOR_TEMP, 'color_temp': ATTR_COLOR_TEMP,
'min_mireds': ATTR_MIN_MIREDS,
'max_mireds': ATTR_MAX_MIREDS,
'rgb_color': ATTR_RGB_COLOR, 'rgb_color': ATTR_RGB_COLOR,
'xy_color': ATTR_XY_COLOR, 'xy_color': ATTR_XY_COLOR,
'white_value': ATTR_WHITE_VALUE, 'white_value': ATTR_WHITE_VALUE,
@ -135,6 +135,8 @@ PROFILE_SCHEMA = vol.Schema(
vol.ExactSequence((str, cv.small_float, cv.small_float, cv.byte)) vol.ExactSequence((str, cv.small_float, cv.small_float, cv.byte))
) )
INTENT_SET = 'HassLightSet'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -206,8 +208,9 @@ def async_turn_off(hass, entity_id=None, transition=None):
DOMAIN, SERVICE_TURN_OFF, data)) DOMAIN, SERVICE_TURN_OFF, data))
@callback
@bind_hass @bind_hass
def toggle(hass, entity_id=None, transition=None): def async_toggle(hass, entity_id=None, transition=None):
"""Toggle all or specified light.""" """Toggle all or specified light."""
data = { data = {
key: value for key, value in [ key: value for key, value in [
@ -216,7 +219,14 @@ def toggle(hass, entity_id=None, transition=None):
] if value is not None ] if value is not None
} }
hass.services.call(DOMAIN, SERVICE_TOGGLE, data) hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_TOGGLE, data))
@bind_hass
def toggle(hass, entity_id=None, transition=None):
"""Toggle all or specified light."""
hass.add_job(async_toggle, hass, entity_id, transition)
def preprocess_turn_on_alternatives(params): def preprocess_turn_on_alternatives(params):
@ -228,7 +238,12 @@ def preprocess_turn_on_alternatives(params):
color_name = params.pop(ATTR_COLOR_NAME, None) color_name = params.pop(ATTR_COLOR_NAME, None)
if color_name is not None: if color_name is not None:
try:
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name) params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
except ValueError:
_LOGGER.warning('Got unknown color %s, falling back to white',
color_name)
params[ATTR_RGB_COLOR] = (255, 255, 255)
kelvin = params.pop(ATTR_KELVIN, None) kelvin = params.pop(ATTR_KELVIN, None)
if kelvin is not None: if kelvin is not None:
@ -240,20 +255,79 @@ def preprocess_turn_on_alternatives(params):
params[ATTR_BRIGHTNESS] = int(255 * brightness_pct/100) params[ATTR_BRIGHTNESS] = int(255 * brightness_pct/100)
@asyncio.coroutine class SetIntentHandler(intent.IntentHandler):
def async_setup(hass, config): """Handle set color intents."""
intent_type = INTENT_SET
slot_schema = {
vol.Required('name'): cv.string,
vol.Optional('color'): color_util.color_name_to_rgb,
vol.Optional('brightness'): vol.All(vol.Coerce(int), vol.Range(0, 100))
}
async def async_handle(self, intent_obj):
"""Handle the hass intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
state = hass.helpers.intent.async_match_state(
slots['name']['value'],
[state for state in hass.states.async_all()
if state.domain == DOMAIN])
service_data = {
ATTR_ENTITY_ID: state.entity_id,
}
speech_parts = []
if 'color' in slots:
intent.async_test_feature(
state, SUPPORT_RGB_COLOR, 'changing colors')
service_data[ATTR_RGB_COLOR] = slots['color']['value']
# Use original passed in value of the color because we don't have
# human readable names for that internally.
speech_parts.append('the color {}'.format(
intent_obj.slots['color']['value']))
if 'brightness' in slots:
intent.async_test_feature(
state, SUPPORT_BRIGHTNESS, 'changing brightness')
service_data[ATTR_BRIGHTNESS_PCT] = slots['brightness']['value']
speech_parts.append('{}% brightness'.format(
slots['brightness']['value']))
await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, service_data)
response = intent_obj.create_response()
if not speech_parts: # No attributes changed
speech = 'Turned on {}'.format(state.name)
else:
parts = ['Changed {} to'.format(state.name)]
for index, part in enumerate(speech_parts):
if index == 0:
parts.append(' {}'.format(part))
elif index != len(speech_parts) - 1:
parts.append(', {}'.format(part))
else:
parts.append(' and {}'.format(part))
speech = ''.join(parts)
response.async_set_speech(speech)
return response
async def async_setup(hass, config):
"""Expose light control via state machine and services.""" """Expose light control via state machine and services."""
component = EntityComponent( component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS) _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS)
yield from component.async_setup(config) await component.async_setup(config)
# load profiles from files # load profiles from files
profiles_valid = yield from Profiles.load_profiles(hass) profiles_valid = await Profiles.load_profiles(hass)
if not profiles_valid: if not profiles_valid:
return False return False
@asyncio.coroutine async def async_handle_light_service(service):
def async_handle_light_service(service):
"""Handle a turn light on or off service call.""" """Handle a turn light on or off service call."""
# Get the validated data # Get the validated data
params = service.data.copy() params = service.data.copy()
@ -267,18 +341,18 @@ def async_setup(hass, config):
update_tasks = [] update_tasks = []
for light in target_lights: for light in target_lights:
if service.service == SERVICE_TURN_ON: if service.service == SERVICE_TURN_ON:
yield from light.async_turn_on(**params) await light.async_turn_on(**params)
elif service.service == SERVICE_TURN_OFF: elif service.service == SERVICE_TURN_OFF:
yield from light.async_turn_off(**params) await light.async_turn_off(**params)
else: else:
yield from light.async_toggle(**params) await light.async_toggle(**params)
if not light.should_poll: if not light.should_poll:
continue continue
update_tasks.append(light.async_update_ha_state(True)) update_tasks.append(light.async_update_ha_state(True))
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) await asyncio.wait(update_tasks, loop=hass.loop)
# Listen for light on and light off service calls. # Listen for light on and light off service calls.
hass.services.async_register( hass.services.async_register(
@ -293,6 +367,8 @@ def async_setup(hass, config):
DOMAIN, SERVICE_TOGGLE, async_handle_light_service, DOMAIN, SERVICE_TOGGLE, async_handle_light_service,
schema=LIGHT_TOGGLE_SCHEMA) schema=LIGHT_TOGGLE_SCHEMA)
hass.helpers.intent.async_register(SetIntentHandler())
return True return True
@ -302,8 +378,7 @@ class Profiles:
_all = None _all = None
@classmethod @classmethod
@asyncio.coroutine async def load_profiles(cls, hass):
def load_profiles(cls, hass):
"""Load and cache profiles.""" """Load and cache profiles."""
def load_profile_data(hass): def load_profile_data(hass):
"""Load built-in profiles and custom profiles.""" """Load built-in profiles and custom profiles."""
@ -333,7 +408,7 @@ class Profiles:
return None return None
return profiles return profiles
cls._all = yield from hass.async_add_job(load_profile_data, hass) cls._all = await hass.async_add_job(load_profile_data, hass)
return cls._all is not None return cls._all is not None
@classmethod @classmethod
@ -399,6 +474,10 @@ class Light(ToggleEntity):
"""Return optional state attributes.""" """Return optional state attributes."""
data = {} data = {}
if self.supported_features & SUPPORT_COLOR_TEMP:
data[ATTR_MIN_MIREDS] = self.min_mireds
data[ATTR_MAX_MIREDS] = self.max_mireds
if self.is_on: if self.is_on:
for prop, attr in PROP_TO_ATTR.items(): for prop, attr in PROP_TO_ATTR.items():
value = getattr(self, prop) value = getattr(self, prop)

View File

@ -28,11 +28,11 @@ SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up the demo light platform.""" """Set up the demo light platform."""
add_devices_callback([ add_devices_callback([
DemoLight("Bed Light", False, True, effect_list=LIGHT_EFFECT_LIST, DemoLight(1, "Bed Light", False, True, effect_list=LIGHT_EFFECT_LIST,
effect=LIGHT_EFFECT_LIST[0]), effect=LIGHT_EFFECT_LIST[0]),
DemoLight("Ceiling Lights", True, True, DemoLight(2, "Ceiling Lights", True, True,
LIGHT_COLORS[0], LIGHT_TEMPS[1]), LIGHT_COLORS[0], LIGHT_TEMPS[1]),
DemoLight("Kitchen Lights", True, True, DemoLight(3, "Kitchen Lights", True, True,
LIGHT_COLORS[1], LIGHT_TEMPS[0]) LIGHT_COLORS[1], LIGHT_TEMPS[0])
]) ])
@ -40,10 +40,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class DemoLight(Light): class DemoLight(Light):
"""Representation of a demo light.""" """Representation of a demo light."""
def __init__(self, name, state, available=False, rgb=None, ct=None, def __init__(self, unique_id, name, state, available=False, rgb=None,
brightness=180, xy_color=(.5, .5), white=200, ct=None, brightness=180, xy_color=(.5, .5), white=200,
effect_list=None, effect=None): effect_list=None, effect=None):
"""Initialize the light.""" """Initialize the light."""
self._unique_id = unique_id
self._name = name self._name = name
self._state = state self._state = state
self._rgb = rgb self._rgb = rgb
@ -64,6 +65,11 @@ class DemoLight(Light):
"""Return the name of the light if any.""" """Return the name of the light if any."""
return self._name return self._name
@property
def unique_id(self):
"""Return unique ID for light."""
return self._unique_id
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return availability.""" """Return availability."""

View File

@ -0,0 +1,290 @@
"""
This platform allows several lights to be grouped into one light.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.group/
"""
import logging
import itertools
from typing import List, Tuple, Optional, Iterator, Any, Callable
from collections import Counter
import voluptuous as vol
from homeassistant.core import State, callback
from homeassistant.components import light
from homeassistant.const import (STATE_ON, ATTR_ENTITY_ID, CONF_NAME,
CONF_ENTITIES, STATE_UNAVAILABLE,
ATTR_SUPPORTED_FEATURES)
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
from homeassistant.components.light import (
SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_COLOR_TEMP,
SUPPORT_TRANSITION, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_XY_COLOR,
SUPPORT_WHITE_VALUE, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_XY_COLOR,
ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_COLOR_TEMP, ATTR_MIN_MIREDS,
ATTR_MAX_MIREDS, ATTR_EFFECT_LIST, ATTR_EFFECT, ATTR_FLASH,
ATTR_TRANSITION)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Light Group'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_ENTITIES): cv.entities_domain(light.DOMAIN)
})
SUPPORT_GROUP_LIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT
| SUPPORT_FLASH | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION
| SUPPORT_XY_COLOR | SUPPORT_WHITE_VALUE)
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_devices, discovery_info=None) -> None:
"""Initialize light.group platform."""
async_add_devices([LightGroup(config.get(CONF_NAME),
config[CONF_ENTITIES])])
class LightGroup(light.Light):
"""Representation of a light group."""
def __init__(self, name: str, entity_ids: List[str]) -> None:
"""Initialize a light group."""
self._name = name # type: str
self._entity_ids = entity_ids # type: List[str]
self._is_on = False # type: bool
self._available = False # type: bool
self._brightness = None # type: Optional[int]
self._xy_color = None # type: Optional[Tuple[float, float]]
self._rgb_color = None # type: Optional[Tuple[int, int, int]]
self._color_temp = None # type: Optional[int]
self._min_mireds = 154 # type: Optional[int]
self._max_mireds = 500 # type: Optional[int]
self._white_value = None # type: Optional[int]
self._effect_list = None # type: Optional[List[str]]
self._effect = None # type: Optional[str]
self._supported_features = 0 # type: int
self._async_unsub_state_changed = None
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@callback
def async_state_changed_listener(entity_id: str, old_state: State,
new_state: State):
"""Handle child updates."""
self.async_schedule_update_ha_state(True)
self._async_unsub_state_changed = async_track_state_change(
self.hass, self._entity_ids, async_state_changed_listener)
await self.async_update()
async def async_will_remove_from_hass(self):
"""Callback when removed from HASS."""
if self._async_unsub_state_changed is not None:
self._async_unsub_state_changed()
self._async_unsub_state_changed = None
@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name
@property
def is_on(self) -> bool:
"""Return the on/off state of the light group."""
return self._is_on
@property
def available(self) -> bool:
"""Return whether the light group is available."""
return self._available
@property
def brightness(self) -> Optional[int]:
"""Return the brightness of this light group between 0..255."""
return self._brightness
@property
def xy_color(self) -> Optional[Tuple[float, float]]:
"""Return the XY color value [float, float]."""
return self._xy_color
@property
def rgb_color(self) -> Optional[Tuple[int, int, int]]:
"""Return the RGB color value [int, int, int]."""
return self._rgb_color
@property
def color_temp(self) -> Optional[int]:
"""Return the CT color value in mireds."""
return self._color_temp
@property
def min_mireds(self) -> Optional[int]:
"""Return the coldest color_temp that this light group supports."""
return self._min_mireds
@property
def max_mireds(self) -> Optional[int]:
"""Return the warmest color_temp that this light group supports."""
return self._max_mireds
@property
def white_value(self) -> Optional[int]:
"""Return the white value of this light group between 0..255."""
return self._white_value
@property
def effect_list(self) -> Optional[List[str]]:
"""Return the list of supported effects."""
return self._effect_list
@property
def effect(self) -> Optional[str]:
"""Return the current effect."""
return self._effect
@property
def supported_features(self) -> int:
"""Flag supported features."""
return self._supported_features
@property
def should_poll(self) -> bool:
"""No polling needed for a light group."""
return False
async def async_turn_on(self, **kwargs):
"""Forward the turn_on command to all lights in the light group."""
data = {ATTR_ENTITY_ID: self._entity_ids}
if ATTR_BRIGHTNESS in kwargs:
data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS]
if ATTR_XY_COLOR in kwargs:
data[ATTR_XY_COLOR] = kwargs[ATTR_XY_COLOR]
if ATTR_RGB_COLOR in kwargs:
data[ATTR_RGB_COLOR] = kwargs[ATTR_RGB_COLOR]
if ATTR_COLOR_TEMP in kwargs:
data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP]
if ATTR_WHITE_VALUE in kwargs:
data[ATTR_WHITE_VALUE] = kwargs[ATTR_WHITE_VALUE]
if ATTR_EFFECT in kwargs:
data[ATTR_EFFECT] = kwargs[ATTR_EFFECT]
if ATTR_TRANSITION in kwargs:
data[ATTR_TRANSITION] = kwargs[ATTR_TRANSITION]
if ATTR_FLASH in kwargs:
data[ATTR_FLASH] = kwargs[ATTR_FLASH]
await self.hass.services.async_call(
light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True)
async def async_turn_off(self, **kwargs):
"""Forward the turn_off command to all lights in the light group."""
data = {ATTR_ENTITY_ID: self._entity_ids}
if ATTR_TRANSITION in kwargs:
data[ATTR_TRANSITION] = kwargs[ATTR_TRANSITION]
await self.hass.services.async_call(
light.DOMAIN, light.SERVICE_TURN_OFF, data, blocking=True)
async def async_update(self):
"""Query all members and determine the light group state."""
all_states = [self.hass.states.get(x) for x in self._entity_ids]
states = list(filter(None, all_states))
on_states = [state for state in states if state.state == STATE_ON]
self._is_on = len(on_states) > 0
self._available = any(state.state != STATE_UNAVAILABLE
for state in states)
self._brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS)
self._xy_color = _reduce_attribute(
on_states, ATTR_XY_COLOR, reduce=_mean_tuple)
self._rgb_color = _reduce_attribute(
on_states, ATTR_RGB_COLOR, reduce=_mean_tuple)
if self._rgb_color is not None:
self._rgb_color = tuple(map(int, self._rgb_color))
self._white_value = _reduce_attribute(on_states, ATTR_WHITE_VALUE)
self._color_temp = _reduce_attribute(on_states, ATTR_COLOR_TEMP)
self._min_mireds = _reduce_attribute(
states, ATTR_MIN_MIREDS, default=154, reduce=min)
self._max_mireds = _reduce_attribute(
states, ATTR_MAX_MIREDS, default=500, reduce=max)
self._effect_list = None
all_effect_lists = list(
_find_state_attributes(states, ATTR_EFFECT_LIST))
if all_effect_lists:
# Merge all effects from all effect_lists with a union merge.
self._effect_list = list(set().union(*all_effect_lists))
self._effect = None
all_effects = list(_find_state_attributes(on_states, ATTR_EFFECT))
if all_effects:
# Report the most common effect.
effects_count = Counter(itertools.chain(all_effects))
self._effect = effects_count.most_common(1)[0][0]
self._supported_features = 0
for support in _find_state_attributes(states, ATTR_SUPPORTED_FEATURES):
# Merge supported features by emulating support for every feature
# we find.
self._supported_features |= support
# Bitwise-and the supported features with the GroupedLight's features
# so that we don't break in the future when a new feature is added.
self._supported_features &= SUPPORT_GROUP_LIGHT
def _find_state_attributes(states: List[State],
key: str) -> Iterator[Any]:
"""Find attributes with matching key from states."""
for state in states:
value = state.attributes.get(key)
if value is not None:
yield value
def _mean_int(*args):
"""Return the mean of the supplied values."""
return int(sum(args) / len(args))
def _mean_tuple(*args):
"""Return the mean values along the columns of the supplied values."""
return tuple(sum(l) / len(l) for l in zip(*args))
# https://github.com/PyCQA/pylint/issues/1831
# pylint: disable=bad-whitespace
def _reduce_attribute(states: List[State],
key: str,
default: Optional[Any] = None,
reduce: Callable[..., Any] = _mean_int) -> Any:
"""Find the first attribute matching key from states.
If none are found, return default.
"""
attrs = list(_find_state_attributes(states, key))
if not attrs:
return default
if len(attrs) == 1:
return attrs[0]
return reduce(*attrs)

View File

@ -287,22 +287,21 @@ class HueLight(Light):
if self.info.get('manufacturername') == 'OSRAM': if self.info.get('manufacturername') == 'OSRAM':
color_hue, sat = color_util.color_xy_to_hs( color_hue, sat = color_util.color_xy_to_hs(
*kwargs[ATTR_XY_COLOR]) *kwargs[ATTR_XY_COLOR])
command['hue'] = color_hue command['hue'] = color_hue / 360 * 65535
command['sat'] = sat command['sat'] = sat / 100 * 255
else: else:
command['xy'] = kwargs[ATTR_XY_COLOR] command['xy'] = kwargs[ATTR_XY_COLOR]
elif ATTR_RGB_COLOR in kwargs: elif ATTR_RGB_COLOR in kwargs:
if self.info.get('manufacturername') == 'OSRAM': if self.info.get('manufacturername') == 'OSRAM':
hsv = color_util.color_RGB_to_hsv( hsv = color_util.color_RGB_to_hsv(
*(int(val) for val in kwargs[ATTR_RGB_COLOR])) *(int(val) for val in kwargs[ATTR_RGB_COLOR]))
command['hue'] = hsv[0] command['hue'] = hsv[0] / 360 * 65535
command['sat'] = hsv[1] command['sat'] = hsv[1] / 100 * 255
command['bri'] = hsv[2] command['bri'] = hsv[2] / 100 * 255
else: else:
xyb = color_util.color_RGB_to_xy( xyb = color_util.color_RGB_to_xy(
*(int(val) for val in kwargs[ATTR_RGB_COLOR])) *(int(val) for val in kwargs[ATTR_RGB_COLOR]))
command['xy'] = xyb[0], xyb[1] command['xy'] = xyb[0], xyb[1]
command['bri'] = xyb[2]
elif ATTR_COLOR_TEMP in kwargs: elif ATTR_COLOR_TEMP in kwargs:
temp = kwargs[ATTR_COLOR_TEMP] temp = kwargs[ATTR_COLOR_TEMP]
command['ct'] = max(self.min_mireds, min(temp, self.max_mireds)) command['ct'] = max(self.min_mireds, min(temp, self.max_mireds))

View File

@ -213,9 +213,10 @@ class Hyperion(Light):
except (KeyError, IndexError): except (KeyError, IndexError):
pass pass
if not response['info']['activeLedColor']: led_color = response['info']['activeLedColor']
if not led_color or led_color[0]['RGB value'] == [0, 0, 0]:
# Get the active effect # Get the active effect
if response['info']['activeEffects']: if response['info'].get('activeEffects'):
self._rgb_color = [175, 0, 255] self._rgb_color = [175, 0, 255]
self._icon = 'mdi:lava-lamp' self._icon = 'mdi:lava-lamp'
try: try:

View File

@ -2,15 +2,14 @@
Support for Insteon lights via PowerLinc Modem. Support for Insteon lights via PowerLinc Modem.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/insteon_plm/ https://home-assistant.io/components/light.insteon_plm/
""" """
import logging
import asyncio import asyncio
import logging
from homeassistant.core import callback from homeassistant.components.insteon_plm import InsteonPLMEntity
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -24,96 +23,47 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Insteon PLM device.""" """Set up the Insteon PLM device."""
plm = hass.data['insteon_plm'] plm = hass.data['insteon_plm']
device_list = [] address = discovery_info['address']
for device in discovery_info: device = plm.devices[address]
name = device.get('address') state_key = discovery_info['state_key']
address = device.get('address_hex')
dimmable = bool('dimmable' in device.get('capabilities'))
_LOGGER.info("Registered %s with light platform", name) _LOGGER.debug('Adding device %s entity %s to Light platform',
device.address.hex, device.states[state_key].name)
device_list.append( new_entity = InsteonPLMDimmerDevice(device, state_key)
InsteonPLMDimmerDevice(hass, plm, address, name, dimmable)
)
async_add_devices(device_list) async_add_devices([new_entity])
class InsteonPLMDimmerDevice(Light): class InsteonPLMDimmerDevice(InsteonPLMEntity, Light):
"""A Class for an Insteon device.""" """A Class for an Insteon device."""
def __init__(self, hass, plm, address, name, dimmable):
"""Initialize the light."""
self._hass = hass
self._plm = plm.protocol
self._address = address
self._name = name
self._dimmable = dimmable
self._plm.add_update_callback(
self.async_light_update, {'address': self._address})
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def address(self):
"""Return the address of the node."""
return self._address
@property
def name(self):
"""Return the name of the node."""
return self._name
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
onlevel = self._plm.get_device_attr(self._address, 'onlevel') onlevel = self._insteon_device_state.value
_LOGGER.debug("on level for %s is %s", self._address, onlevel)
return int(onlevel) return int(onlevel)
@property @property
def is_on(self): def is_on(self):
"""Return the boolean response if the node is on.""" """Return the boolean response if the node is on."""
onlevel = self._plm.get_device_attr(self._address, 'onlevel') return bool(self.brightness)
_LOGGER.debug("on level for %s is %s", self._address, onlevel)
return bool(onlevel)
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
if self._dimmable:
return SUPPORT_BRIGHTNESS return SUPPORT_BRIGHTNESS
@property
def device_state_attributes(self):
"""Provide attributes for display on device card."""
insteon_plm = get_component('insteon_plm')
return insteon_plm.common_attributes(self)
def get_attr(self, key):
"""Return specified attribute for this device."""
return self._plm.get_device_attr(self.address, key)
@callback
def async_light_update(self, message):
"""Receive notification from transport that new data exists."""
_LOGGER.info("Received update callback from PLM for %s", self._address)
self._hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine @asyncio.coroutine
def async_turn_on(self, **kwargs): def async_turn_on(self, **kwargs):
"""Turn device on.""" """Turn device on."""
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
brightness = int(kwargs[ATTR_BRIGHTNESS]) brightness = int(kwargs[ATTR_BRIGHTNESS])
self._insteon_device_state.set_level(brightness)
else: else:
brightness = MAX_BRIGHTNESS self._insteon_device_state.on()
self._plm.turn_on(self._address, brightness=brightness)
@asyncio.coroutine @asyncio.coroutine
def async_turn_off(self, **kwargs): def async_turn_off(self, **kwargs):
"""Turn device off.""" """Turn device off."""
self._plm.turn_off(self._address) self._insteon_device_state.off()

View File

@ -4,7 +4,6 @@ Support for KNX/IP lights.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.knx/ https://home-assistant.io/components/light.knx/
""" """
import asyncio
import voluptuous as vol import voluptuous as vol
@ -37,8 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Set up lights for KNX platform.""" """Set up lights for KNX platform."""
if discovery_info is not None: if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices) async_add_devices_discovery(hass, discovery_info, async_add_devices)
@ -86,11 +85,10 @@ class KNXLight(Light):
@callback @callback
def async_register_callbacks(self): def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed.""" """Register callbacks to update hass after device was changed."""
@asyncio.coroutine async def after_update_callback(device):
def after_update_callback(device):
"""Call after device was updated.""" """Call after device was updated."""
# pylint: disable=unused-argument # pylint: disable=unused-argument
yield from self.async_update_ha_state() await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback) self.device.register_device_updated_cb(after_update_callback)
@property @property
@ -111,8 +109,8 @@ class KNXLight(Light):
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
return self.device.brightness \ return self.device.current_brightness \
if self.device.supports_dimming else \ if self.device.supports_brightness else \
None None
@property @property
@ -124,7 +122,7 @@ class KNXLight(Light):
def rgb_color(self): def rgb_color(self):
"""Return the RBG color value.""" """Return the RBG color value."""
if self.device.supports_color: if self.device.supports_color:
return self.device.current_color() return self.device.current_color
return None return None
@property @property
@ -156,23 +154,23 @@ class KNXLight(Light):
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
flags = 0 flags = 0
if self.device.supports_dimming: if self.device.supports_brightness:
flags |= SUPPORT_BRIGHTNESS flags |= SUPPORT_BRIGHTNESS
if self.device.supports_color: if self.device.supports_color:
flags |= SUPPORT_RGB_COLOR flags |= SUPPORT_RGB_COLOR
return flags return flags
@asyncio.coroutine async def async_turn_on(self, **kwargs):
def async_turn_on(self, **kwargs):
"""Turn the light on.""" """Turn the light on."""
if ATTR_BRIGHTNESS in kwargs and self.device.supports_dimming: if ATTR_BRIGHTNESS in kwargs:
yield from self.device.set_brightness(int(kwargs[ATTR_BRIGHTNESS])) if self.device.supports_brightness:
await self.device.set_brightness(int(kwargs[ATTR_BRIGHTNESS]))
elif ATTR_RGB_COLOR in kwargs: elif ATTR_RGB_COLOR in kwargs:
yield from self.device.set_color(kwargs[ATTR_RGB_COLOR]) if self.device.supports_color:
await self.device.set_color(kwargs[ATTR_RGB_COLOR])
else: else:
yield from self.device.set_on() await self.device.set_on()
@asyncio.coroutine async def async_turn_off(self, **kwargs):
def async_turn_off(self, **kwargs):
"""Turn the light off.""" """Turn the light off."""
yield from self.device.set_off() await self.device.set_off()

View File

@ -123,8 +123,10 @@ def aiolifx_effects():
return aiolifx_effects_module return aiolifx_effects_module
@asyncio.coroutine async def async_setup_platform(hass,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config,
async_add_devices,
discovery_info=None):
"""Set up the LIFX platform.""" """Set up the LIFX platform."""
if sys.platform == 'win32': if sys.platform == 'win32':
_LOGGER.warning("The lifx platform is known to not work on Windows. " _LOGGER.warning("The lifx platform is known to not work on Windows. "
@ -169,13 +171,15 @@ def find_hsbk(**kwargs):
if ATTR_RGB_COLOR in kwargs: if ATTR_RGB_COLOR in kwargs:
hue, saturation, brightness = \ hue, saturation, brightness = \
color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR]) color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR])
saturation = convert_8_to_16(saturation) hue = int(hue / 360 * 65535)
brightness = convert_8_to_16(brightness) saturation = int(saturation / 100 * 65535)
brightness = int(brightness / 100 * 65535)
kelvin = 3500 kelvin = 3500
if ATTR_XY_COLOR in kwargs: if ATTR_XY_COLOR in kwargs:
hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR])
saturation = convert_8_to_16(saturation) hue = int(hue / 360 * 65535)
saturation = int(saturation / 100 * 65535)
kelvin = 3500 kelvin = 3500
if ATTR_COLOR_TEMP in kwargs: if ATTR_COLOR_TEMP in kwargs:
@ -212,8 +216,7 @@ class LIFXManager(object):
def register_set_state(self): def register_set_state(self):
"""Register the LIFX set_state service call.""" """Register the LIFX set_state service call."""
@asyncio.coroutine async def service_handler(service):
def async_service_handle(service):
"""Apply a service.""" """Apply a service."""
tasks = [] tasks = []
for light in self.service_to_entities(service): for light in self.service_to_entities(service):
@ -221,36 +224,34 @@ class LIFXManager(object):
task = light.async_set_state(**service.data) task = light.async_set_state(**service.data)
tasks.append(self.hass.async_add_job(task)) tasks.append(self.hass.async_add_job(task))
if tasks: if tasks:
yield from asyncio.wait(tasks, loop=self.hass.loop) await asyncio.wait(tasks, loop=self.hass.loop)
self.hass.services.async_register( self.hass.services.async_register(
DOMAIN, SERVICE_LIFX_SET_STATE, async_service_handle, DOMAIN, SERVICE_LIFX_SET_STATE, service_handler,
schema=LIFX_SET_STATE_SCHEMA) schema=LIFX_SET_STATE_SCHEMA)
def register_effects(self): def register_effects(self):
"""Register the LIFX effects as hass service calls.""" """Register the LIFX effects as hass service calls."""
@asyncio.coroutine async def service_handler(service):
def async_service_handle(service):
"""Apply a service, i.e. start an effect.""" """Apply a service, i.e. start an effect."""
entities = self.service_to_entities(service) entities = self.service_to_entities(service)
if entities: if entities:
yield from self.start_effect( await self.start_effect(
entities, service.service, **service.data) entities, service.service, **service.data)
self.hass.services.async_register( self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_PULSE, async_service_handle, DOMAIN, SERVICE_EFFECT_PULSE, service_handler,
schema=LIFX_EFFECT_PULSE_SCHEMA) schema=LIFX_EFFECT_PULSE_SCHEMA)
self.hass.services.async_register( self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_COLORLOOP, async_service_handle, DOMAIN, SERVICE_EFFECT_COLORLOOP, service_handler,
schema=LIFX_EFFECT_COLORLOOP_SCHEMA) schema=LIFX_EFFECT_COLORLOOP_SCHEMA)
self.hass.services.async_register( self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_STOP, async_service_handle, DOMAIN, SERVICE_EFFECT_STOP, service_handler,
schema=LIFX_EFFECT_STOP_SCHEMA) schema=LIFX_EFFECT_STOP_SCHEMA)
@asyncio.coroutine async def start_effect(self, entities, service, **kwargs):
def start_effect(self, entities, service, **kwargs):
"""Start a light effect on entities.""" """Start a light effect on entities."""
devices = list(map(lambda l: l.device, entities)) devices = list(map(lambda l: l.device, entities))
@ -262,7 +263,7 @@ class LIFXManager(object):
mode=kwargs.get(ATTR_MODE), mode=kwargs.get(ATTR_MODE),
hsbk=find_hsbk(**kwargs), hsbk=find_hsbk(**kwargs),
) )
yield from self.effects_conductor.start(effect, devices) await self.effects_conductor.start(effect, devices)
elif service == SERVICE_EFFECT_COLORLOOP: elif service == SERVICE_EFFECT_COLORLOOP:
preprocess_turn_on_alternatives(kwargs) preprocess_turn_on_alternatives(kwargs)
@ -278,9 +279,9 @@ class LIFXManager(object):
transition=kwargs.get(ATTR_TRANSITION), transition=kwargs.get(ATTR_TRANSITION),
brightness=brightness, brightness=brightness,
) )
yield from self.effects_conductor.start(effect, devices) await self.effects_conductor.start(effect, devices)
elif service == SERVICE_EFFECT_STOP: elif service == SERVICE_EFFECT_STOP:
yield from self.effects_conductor.stop(devices) await self.effects_conductor.stop(devices)
def service_to_entities(self, service): def service_to_entities(self, service):
"""Return the known devices that a service call mentions.""" """Return the known devices that a service call mentions."""
@ -295,25 +296,24 @@ class LIFXManager(object):
@callback @callback
def register(self, device): def register(self, device):
"""Handle newly detected bulb.""" """Handle aiolifx detected bulb."""
self.hass.async_add_job(self.async_register(device)) self.hass.async_add_job(self.register_new_device(device))
@asyncio.coroutine async def register_new_device(self, device):
def async_register(self, device):
"""Handle newly detected bulb.""" """Handle newly detected bulb."""
if device.mac_addr in self.entities: if device.mac_addr in self.entities:
entity = self.entities[device.mac_addr] entity = self.entities[device.mac_addr]
entity.registered = True entity.registered = True
_LOGGER.debug("%s register AGAIN", entity.who) _LOGGER.debug("%s register AGAIN", entity.who)
yield from entity.update_hass() await entity.update_hass()
else: else:
_LOGGER.debug("%s register NEW", device.ip_addr) _LOGGER.debug("%s register NEW", device.ip_addr)
# Read initial state # Read initial state
ack = AwaitAioLIFX().wait ack = AwaitAioLIFX().wait
version_resp = yield from ack(device.get_version) version_resp = await ack(device.get_version)
if version_resp: if version_resp:
color_resp = yield from ack(device.get_color) color_resp = await ack(device.get_color)
if version_resp is None or color_resp is None: if version_resp is None or color_resp is None:
_LOGGER.error("Failed to initialize %s", device.ip_addr) _LOGGER.error("Failed to initialize %s", device.ip_addr)
@ -335,7 +335,7 @@ class LIFXManager(object):
@callback @callback
def unregister(self, device): def unregister(self, device):
"""Handle disappearing bulbs.""" """Handle aiolifx disappearing bulbs."""
if device.mac_addr in self.entities: if device.mac_addr in self.entities:
entity = self.entities[device.mac_addr] entity = self.entities[device.mac_addr]
_LOGGER.debug("%s unregister", entity.who) _LOGGER.debug("%s unregister", entity.who)
@ -359,15 +359,14 @@ class AwaitAioLIFX:
self.message = message self.message = message
self.event.set() self.event.set()
@asyncio.coroutine async def wait(self, method):
def wait(self, method):
"""Call an aiolifx method and wait for its response.""" """Call an aiolifx method and wait for its response."""
self.device = None self.device = None
self.message = None self.message = None
self.event.clear() self.event.clear()
method(callb=self.callback) method(callb=self.callback)
yield from self.event.wait() await self.event.wait()
return self.message return self.message
@ -464,21 +463,19 @@ class LIFXLight(Light):
return 'lifx_effect_' + effect.name return 'lifx_effect_' + effect.name
return None return None
@asyncio.coroutine async def update_hass(self, now=None):
def update_hass(self, now=None):
"""Request new status and push it to hass.""" """Request new status and push it to hass."""
self.postponed_update = None self.postponed_update = None
yield from self.async_update() await self.async_update()
yield from self.async_update_ha_state() await self.async_update_ha_state()
@asyncio.coroutine async def update_during_transition(self, when):
def update_during_transition(self, when):
"""Update state at the start and end of a transition.""" """Update state at the start and end of a transition."""
if self.postponed_update: if self.postponed_update:
self.postponed_update() self.postponed_update()
# Transition has started # Transition has started
yield from self.update_hass() await self.update_hass()
# Transition has ended # Transition has ended
if when > 0: if when > 0:
@ -486,28 +483,25 @@ class LIFXLight(Light):
self.hass, self.update_hass, self.hass, self.update_hass,
util.dt.utcnow() + timedelta(milliseconds=when)) util.dt.utcnow() + timedelta(milliseconds=when))
@asyncio.coroutine async def async_turn_on(self, **kwargs):
def async_turn_on(self, **kwargs):
"""Turn the device on.""" """Turn the device on."""
kwargs[ATTR_POWER] = True kwargs[ATTR_POWER] = True
self.hass.async_add_job(self.async_set_state(**kwargs)) self.hass.async_add_job(self.set_state(**kwargs))
@asyncio.coroutine async def async_turn_off(self, **kwargs):
def async_turn_off(self, **kwargs):
"""Turn the device off.""" """Turn the device off."""
kwargs[ATTR_POWER] = False kwargs[ATTR_POWER] = False
self.hass.async_add_job(self.async_set_state(**kwargs)) self.hass.async_add_job(self.set_state(**kwargs))
@asyncio.coroutine async def set_state(self, **kwargs):
def async_set_state(self, **kwargs):
"""Set a color on the light and turn it on/off.""" """Set a color on the light and turn it on/off."""
with (yield from self.lock): async with self.lock:
bulb = self.device bulb = self.device
yield from self.effects_conductor.stop([bulb]) await self.effects_conductor.stop([bulb])
if ATTR_EFFECT in kwargs: if ATTR_EFFECT in kwargs:
yield from self.default_effect(**kwargs) await self.default_effect(**kwargs)
return return
if ATTR_INFRARED in kwargs: if ATTR_INFRARED in kwargs:
@ -529,51 +523,47 @@ class LIFXLight(Light):
if not self.is_on: if not self.is_on:
if power_off: if power_off:
yield from self.set_power(ack, False) await self.set_power(ack, False)
if hsbk: if hsbk:
yield from self.set_color(ack, hsbk, kwargs) await self.set_color(ack, hsbk, kwargs)
if power_on: if power_on:
yield from self.set_power(ack, True, duration=fade) await self.set_power(ack, True, duration=fade)
else: else:
if power_on: if power_on:
yield from self.set_power(ack, True) await self.set_power(ack, True)
if hsbk: if hsbk:
yield from self.set_color(ack, hsbk, kwargs, duration=fade) await self.set_color(ack, hsbk, kwargs, duration=fade)
if power_off: if power_off:
yield from self.set_power(ack, False, duration=fade) await self.set_power(ack, False, duration=fade)
# Avoid state ping-pong by holding off updates as the state settles # Avoid state ping-pong by holding off updates as the state settles
yield from asyncio.sleep(0.3) await asyncio.sleep(0.3)
# Update when the transition starts and ends # Update when the transition starts and ends
yield from self.update_during_transition(fade) await self.update_during_transition(fade)
@asyncio.coroutine async def set_power(self, ack, pwr, duration=0):
def set_power(self, ack, pwr, duration=0):
"""Send a power change to the device.""" """Send a power change to the device."""
yield from ack(partial(self.device.set_power, pwr, duration=duration)) await ack(partial(self.device.set_power, pwr, duration=duration))
@asyncio.coroutine async def set_color(self, ack, hsbk, kwargs, duration=0):
def set_color(self, ack, hsbk, kwargs, duration=0):
"""Send a color change to the device.""" """Send a color change to the device."""
hsbk = merge_hsbk(self.device.color, hsbk) hsbk = merge_hsbk(self.device.color, hsbk)
yield from ack(partial(self.device.set_color, hsbk, duration=duration)) await ack(partial(self.device.set_color, hsbk, duration=duration))
@asyncio.coroutine async def default_effect(self, **kwargs):
def default_effect(self, **kwargs):
"""Start an effect with default parameters.""" """Start an effect with default parameters."""
service = kwargs[ATTR_EFFECT] service = kwargs[ATTR_EFFECT]
data = { data = {
ATTR_ENTITY_ID: self.entity_id, ATTR_ENTITY_ID: self.entity_id,
} }
yield from self.hass.services.async_call(DOMAIN, service, data) await self.hass.services.async_call(DOMAIN, service, data)
@asyncio.coroutine async def async_update(self):
def async_update(self):
"""Update bulb status.""" """Update bulb status."""
_LOGGER.debug("%s async_update", self.who) _LOGGER.debug("%s async_update", self.who)
if self.available and not self.lock.locked(): if self.available and not self.lock.locked():
yield from AwaitAioLIFX().wait(self.device.get_color) await AwaitAioLIFX().wait(self.device.get_color)
class LIFXWhite(LIFXLight): class LIFXWhite(LIFXLight):
@ -612,15 +602,17 @@ class LIFXColor(LIFXLight):
"""Return the RGB value.""" """Return the RGB value."""
hue, sat, bri, _ = self.device.color hue, sat, bri, _ = self.device.color
return color_util.color_hsv_to_RGB( hue = hue / 65535 * 360
hue, convert_16_to_8(sat), convert_16_to_8(bri)) sat = sat / 65535 * 100
bri = bri / 65535 * 100
return color_util.color_hsv_to_RGB(hue, sat, bri)
class LIFXStrip(LIFXColor): class LIFXStrip(LIFXColor):
"""Representation of a LIFX light strip with multiple zones.""" """Representation of a LIFX light strip with multiple zones."""
@asyncio.coroutine async def set_color(self, ack, hsbk, kwargs, duration=0):
def set_color(self, ack, hsbk, kwargs, duration=0):
"""Send a color change to the device.""" """Send a color change to the device."""
bulb = self.device bulb = self.device
num_zones = len(bulb.color_zones) num_zones = len(bulb.color_zones)
@ -630,7 +622,7 @@ class LIFXStrip(LIFXColor):
# Fast track: setting all zones to the same brightness and color # Fast track: setting all zones to the same brightness and color
# can be treated as a single-zone bulb. # can be treated as a single-zone bulb.
if hsbk[2] is not None and hsbk[3] is not None: if hsbk[2] is not None and hsbk[3] is not None:
yield from super().set_color(ack, hsbk, kwargs, duration) await super().set_color(ack, hsbk, kwargs, duration)
return return
zones = list(range(0, num_zones)) zones = list(range(0, num_zones))
@ -639,11 +631,11 @@ class LIFXStrip(LIFXColor):
# Zone brightness is not reported when powered off # Zone brightness is not reported when powered off
if not self.is_on and hsbk[2] is None: if not self.is_on and hsbk[2] is None:
yield from self.set_power(ack, True) await self.set_power(ack, True)
yield from asyncio.sleep(0.3) await asyncio.sleep(0.3)
yield from self.update_color_zones() await self.update_color_zones()
yield from self.set_power(ack, False) await self.set_power(ack, False)
yield from asyncio.sleep(0.3) await asyncio.sleep(0.3)
# Send new color to each zone # Send new color to each zone
for index, zone in enumerate(zones): for index, zone in enumerate(zones):
@ -655,23 +647,21 @@ class LIFXStrip(LIFXColor):
color=zone_hsbk, color=zone_hsbk,
duration=duration, duration=duration,
apply=apply) apply=apply)
yield from ack(set_zone) await ack(set_zone)
@asyncio.coroutine async def async_update(self):
def async_update(self):
"""Update strip status.""" """Update strip status."""
if self.available and not self.lock.locked(): if self.available and not self.lock.locked():
yield from super().async_update() await super().async_update()
yield from self.update_color_zones() await self.update_color_zones()
@asyncio.coroutine async def update_color_zones(self):
def update_color_zones(self):
"""Get updated color information for each zone.""" """Get updated color information for each zone."""
zone = 0 zone = 0
top = 1 top = 1
while self.available and zone < top: while self.available and zone < top:
# Each get_color_zones can update 8 zones at once # Each get_color_zones can update 8 zones at once
resp = yield from AwaitAioLIFX().wait(partial( resp = await AwaitAioLIFX().wait(partial(
self.device.get_color_zones, self.device.get_color_zones,
start_index=zone)) start_index=zone))
if resp: if resp:

View File

@ -17,6 +17,7 @@ from homeassistant.components.light import (
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH,
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.color import color_temperature_mired_to_kelvin
from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.restore_state import async_get_last_state
REQUIREMENTS = ['limitlessled==1.1.0'] REQUIREMENTS = ['limitlessled==1.1.0']
@ -222,6 +223,16 @@ class LimitlessLEDGroup(Light):
"""Return the brightness property.""" """Return the brightness property."""
return self._brightness return self._brightness
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
return 154
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
return 370
@property @property
def color_temp(self): def color_temp(self):
"""Return the temperature property.""" """Return the temperature property."""
@ -310,8 +321,11 @@ class LimitlessLEDGroup(Light):
def limitlessled_temperature(self): def limitlessled_temperature(self):
"""Convert Home Assistant color temperature units to percentage.""" """Convert Home Assistant color temperature units to percentage."""
width = self.max_mireds - self.min_mireds max_kelvin = color_temperature_mired_to_kelvin(self.min_mireds)
temperature = 1 - (self._temperature - self.min_mireds) / width min_kelvin = color_temperature_mired_to_kelvin(self.max_mireds)
width = max_kelvin - min_kelvin
kelvin = color_temperature_mired_to_kelvin(self._temperature)
temperature = (kelvin - min_kelvin) / width
return max(0, min(1, temperature)) return max(0, min(1, temperature))
def limitlessled_brightness(self): def limitlessled_brightness(self):

View File

@ -169,3 +169,13 @@ xiaomi_miio_set_scene:
scene: scene:
description: Number of the fixed scene, between 1 and 4. description: Number of the fixed scene, between 1 and 4.
example: 1 example: 1
xiaomi_miio_set_delayed_turn_off:
description: Delayed turn off.
fields:
entity_id:
description: Name of the light entity.
example: 'light.xiaomi_miio'
time_period:
description: Time period for the delayed turn off.
example: "5, '0:05', {'minutes': 5}"

View File

@ -8,6 +8,8 @@ import asyncio
from functools import partial from functools import partial
import logging import logging
from math import ceil from math import ceil
from datetime import timedelta
import datetime
import voluptuous as vol import voluptuous as vol
@ -18,16 +20,24 @@ from homeassistant.components.light import (
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, ) from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, )
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.util import dt
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Xiaomi Philips Light' DEFAULT_NAME = 'Xiaomi Philips Light'
PLATFORM = 'xiaomi_miio' DATA_KEY = 'light.xiaomi_miio'
CONF_MODEL = 'model'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_MODEL): vol.In(
['philips.light.sread1',
'philips.light.ceiling',
'philips.light.zyceiling',
'philips.light.bulb']),
}) })
REQUIREMENTS = ['python-miio==0.3.7'] REQUIREMENTS = ['python-miio==0.3.7']
@ -36,25 +46,38 @@ REQUIREMENTS = ['python-miio==0.3.7']
CCT_MIN = 1 CCT_MIN = 1
CCT_MAX = 100 CCT_MAX = 100
DELAYED_TURN_OFF_MAX_DEVIATION = 4
SUCCESS = ['ok'] SUCCESS = ['ok']
ATTR_MODEL = 'model' ATTR_MODEL = 'model'
ATTR_SCENE = 'scene' ATTR_SCENE = 'scene'
ATTR_DELAYED_TURN_OFF = 'delayed_turn_off'
ATTR_TIME_PERIOD = 'time_period'
SERVICE_SET_SCENE = 'xiaomi_miio_set_scene' SERVICE_SET_SCENE = 'xiaomi_miio_set_scene'
SERVICE_SET_DELAYED_TURN_OFF = 'xiaomi_miio_set_delayed_turn_off'
XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({ XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
}) })
SERVICE_SCHEMA_SCENE = XIAOMI_MIIO_SERVICE_SCHEMA.extend({ SERVICE_SCHEMA_SET_SCENE = XIAOMI_MIIO_SERVICE_SCHEMA.extend({
vol.Required(ATTR_SCENE): vol.Required(ATTR_SCENE):
vol.All(vol.Coerce(int), vol.Clamp(min=1, max=4)) vol.All(vol.Coerce(int), vol.Clamp(min=1, max=4))
}) })
SERVICE_SCHEMA_SET_DELAYED_TURN_OFF = XIAOMI_MIIO_SERVICE_SCHEMA.extend({
vol.Required(ATTR_TIME_PERIOD):
vol.All(cv.time_period, cv.positive_timedelta)
})
SERVICE_TO_METHOD = { SERVICE_TO_METHOD = {
SERVICE_SET_DELAYED_TURN_OFF: {
'method': 'async_set_delayed_turn_off',
'schema': SERVICE_SCHEMA_SET_DELAYED_TURN_OFF},
SERVICE_SET_SCENE: { SERVICE_SET_SCENE: {
'method': 'async_set_scene', 'method': 'async_set_scene',
'schema': SERVICE_SCHEMA_SCENE} 'schema': SERVICE_SCHEMA_SET_SCENE},
} }
@ -63,46 +86,48 @@ SERVICE_TO_METHOD = {
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the light from config.""" """Set up the light from config."""
from miio import Device, DeviceException from miio import Device, DeviceException
if PLATFORM not in hass.data: if DATA_KEY not in hass.data:
hass.data[PLATFORM] = {} hass.data[DATA_KEY] = {}
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
token = config.get(CONF_TOKEN) token = config.get(CONF_TOKEN)
model = config.get(CONF_MODEL)
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
if model is None:
try: try:
light = Device(host, token) miio_device = Device(host, token)
device_info = light.info() device_info = miio_device.info()
_LOGGER.info("%s %s %s initialized", model = device_info.model
device_info.model, _LOGGER.info("%s %s %s detected",
model,
device_info.firmware_version, device_info.firmware_version,
device_info.hardware_version) device_info.hardware_version)
except DeviceException:
raise PlatformNotReady
if device_info.model == 'philips.light.sread1': if model == 'philips.light.sread1':
from miio import PhilipsEyecare from miio import PhilipsEyecare
light = PhilipsEyecare(host, token) light = PhilipsEyecare(host, token)
device = XiaomiPhilipsEyecareLamp(name, light, device_info) device = XiaomiPhilipsEyecareLamp(name, light, model)
elif device_info.model == 'philips.light.ceiling': elif model in ['philips.light.ceiling', 'philips.light.zyceiling']:
from miio import Ceil from miio import Ceil
light = Ceil(host, token) light = Ceil(host, token)
device = XiaomiPhilipsCeilingLamp(name, light, device_info) device = XiaomiPhilipsCeilingLamp(name, light, model)
elif device_info.model == 'philips.light.bulb': elif model == 'philips.light.bulb':
from miio import PhilipsBulb from miio import PhilipsBulb
light = PhilipsBulb(host, token) light = PhilipsBulb(host, token)
device = XiaomiPhilipsLightBall(name, light, device_info) device = XiaomiPhilipsLightBall(name, light, model)
else: else:
_LOGGER.error( _LOGGER.error(
'Unsupported device found! Please create an issue at ' 'Unsupported device found! Please create an issue at '
'https://github.com/rytilahti/python-miio/issues ' 'https://github.com/rytilahti/python-miio/issues '
'and provide the following data: %s', device_info.model) 'and provide the following data: %s', model)
return False return False
except DeviceException: hass.data[DATA_KEY][host] = device
raise PlatformNotReady
hass.data[PLATFORM][host] = device
async_add_devices([device], update_before_add=True) async_add_devices([device], update_before_add=True)
@asyncio.coroutine @asyncio.coroutine
@ -113,10 +138,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if key != ATTR_ENTITY_ID} if key != ATTR_ENTITY_ID}
entity_ids = service.data.get(ATTR_ENTITY_ID) entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids: if entity_ids:
target_devices = [dev for dev in hass.data[PLATFORM].values() target_devices = [dev for dev in hass.data[DATA_KEY].values()
if dev.entity_id in entity_ids] if dev.entity_id in entity_ids]
else: else:
target_devices = hass.data[PLATFORM].values() target_devices = hass.data[DATA_KEY].values()
update_tasks = [] update_tasks = []
for target_device in target_devices: for target_device in target_devices:
@ -136,10 +161,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class XiaomiPhilipsGenericLight(Light): class XiaomiPhilipsGenericLight(Light):
"""Representation of a Xiaomi Philips Light.""" """Representation of a Xiaomi Philips Light."""
def __init__(self, name, light, device_info): def __init__(self, name, light, model):
"""Initialize the light device.""" """Initialize the light device."""
self._name = name self._name = name
self._device_info = device_info self._model = model
self._brightness = None self._brightness = None
self._color_temp = None self._color_temp = None
@ -147,7 +172,9 @@ class XiaomiPhilipsGenericLight(Light):
self._light = light self._light = light
self._state = None self._state = None
self._state_attrs = { self._state_attrs = {
ATTR_MODEL: self._device_info.model, ATTR_MODEL: self._model,
ATTR_SCENE: None,
ATTR_DELAYED_TURN_OFF: None,
} }
@property @property
@ -217,14 +244,14 @@ class XiaomiPhilipsGenericLight(Light):
if result: if result:
self._brightness = brightness self._brightness = brightness
else:
self._state = yield from self._try_command( yield from self._try_command(
"Turning the light on failed.", self._light.on) "Turning the light on failed.", self._light.on)
@asyncio.coroutine @asyncio.coroutine
def async_turn_off(self, **kwargs): def async_turn_off(self, **kwargs):
"""Turn the light off.""" """Turn the light off."""
self._state = yield from self._try_command( yield from self._try_command(
"Turning the light off failed.", self._light.off) "Turning the light off failed.", self._light.off)
@asyncio.coroutine @asyncio.coroutine
@ -238,7 +265,18 @@ class XiaomiPhilipsGenericLight(Light):
self._state = state.is_on self._state = state.is_on
self._brightness = ceil((255 / 100.0) * state.brightness) self._brightness = ceil((255 / 100.0) * state.brightness)
delayed_turn_off = self.delayed_turn_off_timestamp(
state.delay_off_countdown,
dt.utcnow(),
self._state_attrs[ATTR_DELAYED_TURN_OFF])
self._state_attrs.update({
ATTR_SCENE: state.scene,
ATTR_DELAYED_TURN_OFF: delayed_turn_off,
})
except DeviceException as ex: except DeviceException as ex:
self._state = None
_LOGGER.error("Got exception while fetching the state: %s", ex) _LOGGER.error("Got exception while fetching the state: %s", ex)
@asyncio.coroutine @asyncio.coroutine
@ -248,6 +286,13 @@ class XiaomiPhilipsGenericLight(Light):
"Setting a fixed scene failed.", "Setting a fixed scene failed.",
self._light.set_scene, scene) self._light.set_scene, scene)
@asyncio.coroutine
def async_set_delayed_turn_off(self, time_period: timedelta):
"""Set delay off. The unit is different per device."""
yield from self._try_command(
"Setting the delay off failed.",
self._light.delay_off, time_period.total_seconds())
@staticmethod @staticmethod
def translate(value, left_min, left_max, right_min, right_max): def translate(value, left_min, left_max, right_min, right_max):
"""Map a value from left span to right span.""" """Map a value from left span to right span."""
@ -256,6 +301,28 @@ class XiaomiPhilipsGenericLight(Light):
value_scaled = float(value - left_min) / float(left_span) value_scaled = float(value - left_min) / float(left_span)
return int(right_min + (value_scaled * right_span)) return int(right_min + (value_scaled * right_span))
@staticmethod
def delayed_turn_off_timestamp(countdown: int,
current: datetime,
previous: datetime):
"""Update the turn off timestamp only if necessary."""
if countdown > 0:
new = current.replace(microsecond=0) + \
timedelta(seconds=countdown)
if previous is None:
return new
lower = timedelta(seconds=-DELAYED_TURN_OFF_MAX_DEVIATION)
upper = timedelta(seconds=DELAYED_TURN_OFF_MAX_DEVIATION)
diff = previous - new
if lower < diff < upper:
return previous
return new
return None
class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
"""Representation of a Xiaomi Philips Light Ball.""" """Representation of a Xiaomi Philips Light Ball."""
@ -339,7 +406,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
self._brightness = brightness self._brightness = brightness
else: else:
self._state = yield from self._try_command( yield from self._try_command(
"Turning the light on failed.", self._light.on) "Turning the light on failed.", self._light.on)
@asyncio.coroutine @asyncio.coroutine
@ -357,7 +424,18 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
CCT_MIN, CCT_MAX, CCT_MIN, CCT_MAX,
self.max_mireds, self.min_mireds) self.max_mireds, self.min_mireds)
delayed_turn_off = self.delayed_turn_off_timestamp(
state.delay_off_countdown,
dt.utcnow(),
self._state_attrs[ATTR_DELAYED_TURN_OFF])
self._state_attrs.update({
ATTR_SCENE: state.scene,
ATTR_DELAYED_TURN_OFF: delayed_turn_off,
})
except DeviceException as ex: except DeviceException as ex:
self._state = None
_LOGGER.error("Got exception while fetching the state: %s", ex) _LOGGER.error("Got exception while fetching the state: %s", ex)

View File

@ -47,6 +47,11 @@ CONFIG_SCHEMA = vol.Schema({
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
ALL_EVENT_TYPES = [
EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
]
GROUP_BY_MINUTES = 15 GROUP_BY_MINUTES = 15
CONTINUOUS_DOMAINS = ['proximity', 'sensor'] CONTINUOUS_DOMAINS = ['proximity', 'sensor']
@ -266,15 +271,18 @@ def humanify(events):
def _get_events(hass, config, start_day, end_day): def _get_events(hass, config, start_day, end_day):
"""Get events for a period of time.""" """Get events for a period of time."""
from homeassistant.components.recorder.models import Events from homeassistant.components.recorder.models import Events, States
from homeassistant.components.recorder.util import ( from homeassistant.components.recorder.util import (
execute, session_scope) execute, session_scope)
with session_scope(hass=hass) as session: with session_scope(hass=hass) as session:
query = session.query(Events).order_by( query = session.query(Events).order_by(Events.time_fired) \
Events.time_fired).filter( .outerjoin(States, (Events.event_id == States.event_id)) \
(Events.time_fired > start_day) & .filter(Events.event_type.in_(ALL_EVENT_TYPES)) \
(Events.time_fired < end_day)) .filter((Events.time_fired > start_day)
& (Events.time_fired < end_day)) \
.filter((States.last_updated == States.last_changed)
| (States.state_id.is_(None)))
events = execute(query) events = execute(query)
return humanify(_exclude_events(events, config)) return humanify(_exclude_events(events, config))

View File

@ -25,6 +25,10 @@ DEPENDENCIES = ['apple_tv']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_APPLE_TV = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | \
SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_SEEK | \
SUPPORT_STOP | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@ -79,7 +83,7 @@ class AppleTvDevice(MediaPlayerDevice):
@property @property
def unique_id(self): def unique_id(self):
"""Return an unique ID.""" """Return a unique ID."""
return self.atv.metadata.device_id return self.atv.metadata.device_id
@property @property
@ -196,14 +200,7 @@ class AppleTvDevice(MediaPlayerDevice):
@property @property
def supported_features(self): def supported_features(self):
"""Flag media player features that are supported.""" """Flag media player features that are supported."""
features = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA return SUPPORT_APPLE_TV
if self._playing is None or self.state == STATE_IDLE:
return features
features |= SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_SEEK | \
SUPPORT_STOP | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK
return features
@asyncio.coroutine @asyncio.coroutine
def async_turn_on(self): def async_turn_on(self):

View File

@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.cast/ https://home-assistant.io/components/media_player.cast/
""" """
# pylint: disable=import-error # pylint: disable=import-error
import asyncio
import logging import logging
import threading import threading
import functools import functools
@ -135,8 +134,7 @@ def _async_create_cast_device(hass, chromecast):
return None return None
@asyncio.coroutine async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_devices, discovery_info=None): async_add_devices, discovery_info=None):
"""Set up the cast platform.""" """Set up the cast platform."""
import pychromecast import pychromecast
@ -187,7 +185,7 @@ def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
try: try:
func = functools.partial(pychromecast.Chromecast, *want_host, func = functools.partial(pychromecast.Chromecast, *want_host,
tries=SOCKET_CLIENT_RETRIES) tries=SOCKET_CLIENT_RETRIES)
chromecast = yield from hass.async_add_job(func) chromecast = await hass.async_add_job(func)
except pychromecast.ChromecastConnectionError as err: except pychromecast.ChromecastConnectionError as err:
_LOGGER.warning("Can't set up chromecast on %s: %s", _LOGGER.warning("Can't set up chromecast on %s: %s",
want_host[0], err) want_host[0], err)
@ -420,7 +418,7 @@ class CastDevice(MediaPlayerDevice):
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return an unique ID.""" """Return a unique ID."""
if self.cast.uuid is not None: if self.cast.uuid is not None:
return str(self.cast.uuid) return str(self.cast.uuid)
return None return None
@ -439,8 +437,7 @@ class CastDevice(MediaPlayerDevice):
self.cast_status = self.cast.status self.cast_status = self.cast.status
self.media_status = self.cast.media_controller.status self.media_status = self.cast.media_controller.status
@asyncio.coroutine async def async_will_remove_from_hass(self) -> None:
def async_will_remove_from_hass(self):
"""Disconnect Chromecast object when removed.""" """Disconnect Chromecast object when removed."""
self._async_disconnect() self._async_disconnect()

View File

@ -0,0 +1,303 @@
"""
Support for interfacing with an instance of Channels (https://getchannels.com).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.channels/
"""
import logging
import voluptuous as vol
from homeassistant.components.media_player import (
MEDIA_TYPE_CHANNEL, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_EPISODE,
MEDIA_TYPE_VIDEO, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP,
SUPPORT_VOLUME_MUTE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, DOMAIN, PLATFORM_SCHEMA,
MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_PORT, CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING,
ATTR_ENTITY_ID)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DATA_CHANNELS = 'channels'
DEFAULT_NAME = 'Channels'
DEFAULT_PORT = 57000
FEATURE_SUPPORT = SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | \
SUPPORT_VOLUME_MUTE | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_PLAY_MEDIA | SUPPORT_SELECT_SOURCE
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
SERVICE_SEEK_FORWARD = 'channels_seek_forward'
SERVICE_SEEK_BACKWARD = 'channels_seek_backward'
SERVICE_SEEK_BY = 'channels_seek_by'
# Service call validation schemas
ATTR_SECONDS = 'seconds'
CHANNELS_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
})
CHANNELS_SEEK_BY_SCHEMA = CHANNELS_SCHEMA.extend({
vol.Required(ATTR_SECONDS): vol.Coerce(int),
})
REQUIREMENTS = ['pychannels==1.0.0']
# pylint: disable=unused-argument, abstract-method
# pylint: disable=too-many-instance-attributes
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Channels platform."""
device = ChannelsPlayer(
config.get('name', DEFAULT_NAME),
config.get(CONF_HOST),
config.get(CONF_PORT, DEFAULT_PORT)
)
if DATA_CHANNELS not in hass.data:
hass.data[DATA_CHANNELS] = []
add_devices([device], True)
hass.data[DATA_CHANNELS].append(device)
def service_handler(service):
"""Handler for services."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids:
devices = [device for device in hass.data[DATA_CHANNELS]
if device.entity_id in entity_ids]
else:
devices = hass.data[DATA_CHANNELS]
for device in devices:
if service.service == SERVICE_SEEK_FORWARD:
device.seek_forward()
elif service.service == SERVICE_SEEK_BACKWARD:
device.seek_backward()
elif service.service == SERVICE_SEEK_BY:
seconds = service.data.get('seconds')
device.seek_by(seconds)
hass.services.register(
DOMAIN, SERVICE_SEEK_FORWARD, service_handler,
schema=CHANNELS_SCHEMA)
hass.services.register(
DOMAIN, SERVICE_SEEK_BACKWARD, service_handler,
schema=CHANNELS_SCHEMA)
hass.services.register(
DOMAIN, SERVICE_SEEK_BY, service_handler,
schema=CHANNELS_SEEK_BY_SCHEMA)
class ChannelsPlayer(MediaPlayerDevice):
"""Representation of a Channels instance."""
# pylint: disable=too-many-public-methods
def __init__(self, name, host, port):
"""Initialize the Channels app."""
from pychannels import Channels
self._name = name
self._host = host
self._port = port
self.client = Channels(self._host, self._port)
self.status = None
self.muted = None
self.channel_number = None
self.channel_name = None
self.channel_image_url = None
self.now_playing_title = None
self.now_playing_episode_title = None
self.now_playing_season_number = None
self.now_playing_episode_number = None
self.now_playing_summary = None
self.now_playing_image_url = None
self.favorite_channels = []
def update_favorite_channels(self):
"""Update the favorite channels from the client."""
self.favorite_channels = self.client.favorite_channels()
def update_state(self, state_hash):
"""Update all the state properties with the passed in dictionary."""
self.status = state_hash.get('status', "stopped")
self.muted = state_hash.get('muted', False)
channel_hash = state_hash.get('channel')
np_hash = state_hash.get('now_playing')
if channel_hash:
self.channel_number = channel_hash.get('channel_number')
self.channel_name = channel_hash.get('channel_name')
self.channel_image_url = channel_hash.get('channel_image_url')
else:
self.channel_number = None
self.channel_name = None
self.channel_image_url = None
if np_hash:
self.now_playing_title = np_hash.get('title')
self.now_playing_episode_title = np_hash.get('episode_title')
self.now_playing_season_number = np_hash.get('season_number')
self.now_playing_episode_number = np_hash.get('episode_number')
self.now_playing_summary = np_hash.get('summary')
self.now_playing_image_url = np_hash.get('image_url')
else:
self.now_playing_title = None
self.now_playing_episode_title = None
self.now_playing_season_number = None
self.now_playing_episode_number = None
self.now_playing_summary = None
self.now_playing_image_url = None
@property
def name(self):
"""Return the name of the player."""
return self._name
@property
def state(self):
"""Return the state of the player."""
if self.status == 'stopped':
return STATE_IDLE
if self.status == 'paused':
return STATE_PAUSED
if self.status == 'playing':
return STATE_PLAYING
return None
def update(self):
"""Retrieve latest state."""
self.update_favorite_channels()
self.update_state(self.client.status())
@property
def source_list(self):
"""List of favorite channels."""
sources = [channel['name'] for channel in self.favorite_channels]
return sources
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self.muted
@property
def media_content_id(self):
"""Content ID of current playing channel."""
return self.channel_number
@property
def media_content_type(self):
"""Content type of current playing media."""
return MEDIA_TYPE_CHANNEL
@property
def media_image_url(self):
"""Image url of current playing media."""
if self.now_playing_image_url:
return self.now_playing_image_url
elif self.channel_image_url:
return self.channel_image_url
return 'https://getchannels.com/assets/img/icon-1024.png'
@property
def media_title(self):
"""Title of current playing media."""
if self.state:
return self.now_playing_title
return None
@property
def supported_features(self):
"""Flag of media commands that are supported."""
return FEATURE_SUPPORT
def mute_volume(self, mute):
"""Mute (true) or unmute (false) player."""
if mute != self.muted:
response = self.client.toggle_muted()
self.update_state(response)
def media_stop(self):
"""Send media_stop command to player."""
self.status = "stopped"
response = self.client.stop()
self.update_state(response)
def media_play(self):
"""Send media_play command to player."""
response = self.client.resume()
self.update_state(response)
def media_pause(self):
"""Send media_pause command to player."""
response = self.client.pause()
self.update_state(response)
def media_next_track(self):
"""Seek ahead."""
response = self.client.skip_forward()
self.update_state(response)
def media_previous_track(self):
"""Seek back."""
response = self.client.skip_backward()
self.update_state(response)
def select_source(self, source):
"""Select a channel to tune to."""
for channel in self.favorite_channels:
if channel["name"] == source:
response = self.client.play_channel(channel["number"])
self.update_state(response)
break
def play_media(self, media_type, media_id, **kwargs):
"""Send the play_media command to the player."""
if media_type == MEDIA_TYPE_CHANNEL:
response = self.client.play_channel(media_id)
self.update_state(response)
elif media_type in [MEDIA_TYPE_VIDEO, MEDIA_TYPE_EPISODE,
MEDIA_TYPE_TVSHOW]:
response = self.client.play_recording(media_id)
self.update_state(response)
def seek_forward(self):
"""Seek forward in the timeline."""
response = self.client.seek_forward()
self.update_state(response)
def seek_backward(self):
"""Seek backward in the timeline."""
response = self.client.seek_backward()
self.update_state(response)
def seek_by(self, seconds):
"""Seek backward in the timeline."""
response = self.client.seek(seconds)
self.update_state(response)

View File

@ -37,11 +37,13 @@ YOUTUBE_PLAYER_SUPPORT = \
MUSIC_PLAYER_SUPPORT = \ MUSIC_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST | \
SUPPORT_PLAY | SUPPORT_SHUFFLE_SET SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
NETFLIX_PLAYER_SUPPORT = \ NETFLIX_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
class AbstractDemoPlayer(MediaPlayerDevice): class AbstractDemoPlayer(MediaPlayerDevice):
@ -284,15 +286,7 @@ class DemoMusicPlayer(AbstractDemoPlayer):
@property @property
def supported_features(self): def supported_features(self):
"""Flag media player features that are supported.""" """Flag media player features that are supported."""
support = MUSIC_PLAYER_SUPPORT return MUSIC_PLAYER_SUPPORT
if self._cur_track > 0:
support |= SUPPORT_PREVIOUS_TRACK
if self._cur_track < len(self.tracks) - 1:
support |= SUPPORT_NEXT_TRACK
return support
def media_previous_track(self): def media_previous_track(self):
"""Send previous track command.""" """Send previous track command."""
@ -379,15 +373,7 @@ class DemoTVShowPlayer(AbstractDemoPlayer):
@property @property
def supported_features(self): def supported_features(self):
"""Flag media player features that are supported.""" """Flag media player features that are supported."""
support = NETFLIX_PLAYER_SUPPORT return NETFLIX_PLAYER_SUPPORT
if self._cur_episode > 1:
support |= SUPPORT_PREVIOUS_TRACK
if self._cur_episode < self._episode_count:
support |= SUPPORT_NEXT_TRACK
return support
def media_previous_track(self): def media_previous_track(self):
"""Send previous track command.""" """Send previous track command."""

View File

@ -21,7 +21,7 @@ from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pyemby==1.4'] REQUIREMENTS = ['pyemby==1.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

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