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

3
.gitignore vendored
View File

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

View File

@ -6,12 +6,10 @@ addons:
matrix:
fast_finish: true
include:
- python: "3.4.2"
- python: "3.5.3"
env: TOXENV=lint
- python: "3.4.2"
- python: "3.5.3"
env: TOXENV=pylint
- python: "3.4.2"
env: TOXENV=py34
# - python: "3.5"
# env: TOXENV=typing
- python: "3.5.3"
@ -30,4 +28,15 @@ cache:
install: pip install -U tox coveralls
language: python
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

4
CODEOWNERS Executable file → Normal file
View File

@ -43,6 +43,7 @@ homeassistant/components/hassio.py @home-assistant/hassio
# Individual components
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
homeassistant/components/binary_sensor/hikvision.py @mezz64
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/camera/yi.py @bachya
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/light/tplink.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/mediaroom.py @dgomes
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/*/bmw_connected_drive.py @ChristianKuehnel
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/homekit/* @cdce8p

View File

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

View File

@ -112,18 +112,13 @@ def async_from_config_dict(config: Dict[str, Any],
if not loader.PREPARED:
yield from hass.async_add_job(loader.prepare, hass)
# Make a copy because we are mutating it.
config = OrderedDict(config)
# Merge packages
conf_util.merge_packages_config(
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)
yield from hass.config_entries.async_load()

View File

@ -156,9 +156,10 @@ def async_setup(hass, config):
hass.services.async_register(
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
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(
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(
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
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyalarmdotcom==0.3.0']
REQUIREMENTS = ['pyalarmdotcom==0.3.1']
_LOGGER = logging.getLogger(__name__)

View File

@ -26,7 +26,7 @@ DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'CONCORD232'
DEFAULT_PORT = 5007
SCAN_INTERVAL = timedelta(seconds=1)
SCAN_INTERVAL = timedelta(seconds=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
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
https://home-assistant.io/components/alarm_control_panel.egardia/
"""
import asyncio
import logging
import requests
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_UNKNOWN)
import homeassistant.exceptions as exc
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pythonegardia==1.0.26']
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
from homeassistant.components.egardia import (
EGARDIA_DEVICE, EGARDIA_SERVER,
REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES,
CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT
)
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'
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 = {
'ARM': STATE_ALARM_ARMED_AWAY,
'DAY HOME': STATE_ALARM_ARMED_HOME,
'DISARM': STATE_ALARM_DISARMED,
'HOME': STATE_ALARM_ARMED_HOME,
'TRIGGERED': STATE_ALARM_TRIGGERED,
'UNKNOWN': STATE_UNKNOWN,
'ARMHOME': STATE_ALARM_ARMED_HOME,
'TRIGGERED': STATE_ALARM_TRIGGERED
}
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):
"""Set up the Egardia platform."""
from pythonegardia import egardiadevice
from pythonegardia import egardiaserver
name = config.get(CONF_NAME)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
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)
device = EgardiaAlarm(
discovery_info['name'],
hass.data[EGARDIA_DEVICE],
discovery_info[CONF_REPORT_SERVER_ENABLED],
discovery_info.get(CONF_REPORT_SERVER_CODES),
discovery_info[CONF_REPORT_SERVER_PORT])
# add egardia alarm device
add_devices([device], True)
class EgardiaAlarm(alarm.AlarmControlPanel):
"""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."""
self._name = name
self._egardiasystem = egardiasystem
self._status = None
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
def name(self):
@ -156,31 +91,20 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
def lookupstatusfromcode(self, statuscode):
"""Look at the rs_codes and returns the status from the code."""
status = 'UNKNOWN'
if self._rs_codes is not None:
statuscode = str(statuscode).strip()
for i in self._rs_codes:
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
status = next((
status_group.upper() for status_group, codes
in self._rs_codes.items() for code in codes
if statuscode == code), 'UNKNOWN')
return status
def parsestatus(self, status):
"""Parse the status."""
_LOGGER.debug("Parsing status %s", status)
# Ignore the statuscode if it is IGNORE
if status.lower().strip() != CONF_REPORT_SERVER_CODES_IGNORE:
_LOGGER.debug("Not ignoring status")
newstatus = ([v for k, v in STATES.items()
if status.upper() == k][0])
if status.lower().strip() != REPORT_SERVER_CODES_IGNORE:
_LOGGER.debug("Not ignoring status %s", status)
newstatus = STATES.get(status.upper())
_LOGGER.debug("newstatus %s", newstatus)
self._status = newstatus
else:
_LOGGER.error("Ignoring status")

View File

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

View File

@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
_CONFIGURING = {}
REQUIREMENTS = ['py-august==0.3.0']
REQUIREMENTS = ['py-august==0.4.0']
DEFAULT_TIMEOUT = 10
ACTIVITY_FETCH_LIMIT = 10
@ -159,7 +159,7 @@ class AugustData:
self._api = api
self._access_token = access_token
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._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
https://home-assistant.io/components/binary_sensor/
"""
import asyncio
from datetime import timedelta
import logging
@ -28,6 +28,7 @@ DEVICE_CLASSES = [
'gas', # On means gas detected, Off means no gas (clear)
'heat', # On means hot, Off means normal
'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
'motion', # On means motion detected, Off means no motion (clear)
'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))
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Track states and offer events for binary sensors."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_setup(config)
await component.async_setup(config)
return True

View File

@ -27,7 +27,7 @@ DEFAULT_NAME = 'Alarm'
DEFAULT_PORT = '5007'
DEFAULT_SSL = False
SCAN_INTERVAL = datetime.timedelta(seconds=1)
SCAN_INTERVAL = datetime.timedelta(seconds=10)
ZONE_TYPES_SCHEMA = vol.Schema({
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,
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
REQUIREMENTS = ['pyhik==0.1.4']
REQUIREMENTS = ['pyhik==0.1.8']
_LOGGER = logging.getLogger(__name__)
CONF_IGNORED = 'ignored'
@ -48,6 +48,9 @@ DEVICE_CLASS_MAP = {
'Face Detection': 'motion',
'Scene Change Detection': 'motion',
'I/O': None,
'Unattended Baggage': 'motion',
'Attended Baggage': 'motion',
'Recording Failure': None,
}
CUSTOMIZE_SCHEMA = vol.Schema({
@ -211,7 +214,7 @@ class HikvisionBinarySensor(BinarySensorDevice):
@property
def unique_id(self):
"""Return an unique ID."""
"""Return a unique ID."""
return self._id
@property

View File

@ -2,86 +2,56 @@
Support for INSTEON dimmers via PowerLinc Modem.
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 logging
from homeassistant.core import callback
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.loader import get_component
from homeassistant.components.insteon_plm import InsteonPLMEntity
DEPENDENCIES = ['insteon_plm']
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {'openClosedSensor': 'opening',
'motionSensor': 'motion',
'doorSensor': 'door',
'leakSensor': 'moisture'}
@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']
device_list = []
for device in discovery_info:
name = device.get('address')
address = device.get('address_hex')
address = discovery_info['address']
device = plm.devices[address]
state_key = discovery_info['state_key']
_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(
InsteonPLMBinarySensorDevice(hass, plm, address, name)
)
new_entity = InsteonPLMBinarySensor(device, state_key)
async_add_devices(device_list)
async_add_devices([new_entity])
class InsteonPLMBinarySensorDevice(BinarySensorDevice):
"""A Class for an Insteon device."""
class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
"""A Class for an Insteon device entity."""
def __init__(self, hass, plm, address, name):
"""Initialize the binarysensor."""
self._hass = hass
self._plm = plm.protocol
self._address = address
self._name = name
self._plm.add_update_callback(
self.async_binarysensor_update, {'address': self._address})
def __init__(self, device, state_key):
"""Initialize the INSTEON PLM binary sensor."""
super().__init__(device, state_key)
self._sensor_type = SENSOR_TYPES.get(self._insteon_device_state.name)
@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
def device_class(self):
"""Return the class of this sensor."""
return self._sensor_type
@property
def is_on(self):
"""Return the boolean response if the node is on."""
sensorstate = self._plm.get_device_attr(self._address, 'sensorstate')
_LOGGER.info("Sensor state for %s is %s", self._address, sensorstate)
sensorstate = self._insteon_device_state.value
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:
device_type = _detect_device_type(node)
subnode_id = int(node.nid[-1])
if device_type == 'opening':
# Door/window sensors use an optional "negative" node
if subnode_id == 4:
if (device_type == 'opening' or device_type == 'moisture'):
# These sensors use an optional "negative" subnode 2 to snag
# 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
# as a separate binary_sensor
device = ISYBinarySensorHeartbeat(node, parent_device)
parent_device.add_heartbeat_device(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:
# We don't yet have any special logic for other sensor types,
# 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
https://home-assistant.io/components/binary_sensor.knx/
"""
import asyncio
import voluptuous as vol
@ -26,6 +25,7 @@ CONF_DEFAULT_HOOK = 'on'
CONF_COUNTER = 'counter'
CONF_DEFAULT_COUNTER = 1
CONF_ACTION = 'action'
CONF_RESET_AFTER = 'reset_after'
CONF__ACTION = 'turn_off_action'
@ -49,12 +49,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICE_CLASS): cv.string,
vol.Optional(CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT):
cv.positive_int,
vol.Optional(CONF_RESET_AFTER): cv.positive_int,
vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up binary sensor(s) for KNX platform."""
if discovery_info is not None:
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,
group_address=config.get(CONF_ADDRESS),
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)
entity = KNXBinarySensor(hass, binary_sensor)
@ -111,11 +113,10 @@ class KNXBinarySensor(BinarySensorDevice):
@callback
def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
@asyncio.coroutine
def after_update_callback(device):
async def after_update_callback(device):
"""Call after device was updated."""
# 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)
@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'
elif value == 'both_click':
click_type = 'both'
elif value == 'shake':
click_type = 'shake'
else:
_LOGGER.warning("Unsupported click_type detected: %s", value)
return False
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
at https://home-assistant.io/components/binary_sensor.zha/
"""
import asyncio
import logging
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
@ -25,8 +24,8 @@ CLASS_MAPPING = {
}
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the Zigbee Home Automation binary sensors."""
discovery_info = zha.get_discovery_info(hass, discovery_info)
if discovery_info is None:
@ -39,19 +38,19 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
device_class = None
cluster = in_clusters[IasZone.cluster_id]
if discovery_info['new_join']:
yield from cluster.bind()
await cluster.bind()
ieee = cluster.endpoint.device.application.ieee
yield from cluster.write_attributes({'cie_addr': ieee})
await cluster.write_attributes({'cie_addr': ieee})
try:
zone_type = yield from cluster['zone_type']
zone_type = await cluster['zone_type']
device_class = CLASS_MAPPING.get(zone_type, None)
except Exception: # pylint: disable=broad-except
# If we fail to read from the device, use a non-specific class
pass
sensor = BinarySensor(device_class, **discovery_info)
async_add_devices([sensor])
async_add_devices([sensor], update_before_add=True)
class BinarySensor(zha.Entity, BinarySensorDevice):
@ -66,6 +65,11 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
from zigpy.zcl.clusters.security import IasZone
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
@property
def should_poll(self) -> bool:
"""Let zha handle polling."""
return False
@property
def is_on(self) -> bool:
"""Return True if entity is on."""
@ -83,7 +87,18 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
if command_id == 0:
self._state = args[0] & 3
_LOGGER.debug("Updated alarm state: %s", self._state)
self.schedule_update_ha_state()
self.async_schedule_update_ha_state()
elif command_id == 1:
_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')
yield from response.prepare(request)
def write(img_bytes):
async def write(img_bytes):
"""Write image to stream."""
response.write(bytes(
await response.write(bytes(
'--frameboundary\r\n'
'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format(
@ -282,15 +282,14 @@ class Camera(Entity):
break
if img_bytes and img_bytes != last_image:
write(img_bytes)
yield from write(img_bytes)
# Chrome seems to always ignore first picture,
# print it twice.
if last_image is None:
write(img_bytes)
yield from write(img_bytes)
last_image = img_bytes
yield from response.drain()
yield from asyncio.sleep(.5)

View File

@ -33,6 +33,9 @@ DEFAULT_PORT = 5000
DEFAULT_USERNAME = 'admin'
DEFAULT_PASSWORD = '888888'
DEFAULT_ARGUMENTS = '-q:v 2'
DEFAULT_PROFILE = 0
CONF_PROFILE = "profile"
ATTR_PAN = "pan"
ATTR_TILT = "tilt"
@ -57,6 +60,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
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({
@ -67,8 +72,7 @@ SERVICE_PTZ_SCHEMA = vol.Schema({
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a ONVIF camera."""
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_HOST)):
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,
schema=SERVICE_PTZ_SCHEMA)
async_add_devices([ONVIFHassCamera(hass, config)])
add_devices([ONVIFHassCamera(hass, config)])
class ONVIFHassCamera(Camera):
@ -114,10 +118,17 @@ class ONVIFHassCamera(Camera):
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)
)
media_service = camera.create_media_service()
stream_uri = media_service.GetStreamUri(
{'StreamSetup': {'Stream': 'RTP-Unicast', 'Transport': 'RTSP'}}
)
self._input = stream_uri.Uri.replace(
self._profiles = media_service.GetProfiles()
self._profile_index = config.get(CONF_PROFILE)
if self._profile_index >= len(self._profiles):
_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(
config.get(CONF_USERNAME),
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)
return False
add_devices([RaspberryCamera(setup_config)])
class RaspberryCamera(Camera):
"""Representation of a Raspberry Pi camera."""

View File

@ -38,8 +38,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass,
config,
async_add_devices,
discovery_info=None):
"""Set up a Yi Camera."""
_LOGGER.debug('Received configuration: %s', config)
async_add_devices([YiCamera(hass, config)], True)
@ -107,31 +109,29 @@ class YiCamera(Camera):
self.user, self.passwd, self.host, self.port, self.path,
latest_dir, videos[-1])
@asyncio.coroutine
def async_camera_image(self):
async def async_camera_image(self):
"""Return a still image response from the camera."""
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:
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,
extra_cmd=self._extra_arguments), loop=self.hass.loop)
self._last_url = url
return self._last_image
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg
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)
yield from async_aiohttp_proxy_stream(
await async_aiohttp_proxy_stream(
self.hass, request, stream,
'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.util import Throttle
REQUIREMENTS = ['py-canary==0.4.0']
REQUIREMENTS = ['py-canary==0.4.1']
_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)
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up climate devices."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
yield from component.async_setup(config)
await component.async_setup(config)
@asyncio.coroutine
def async_away_mode_set_service(service):
async def async_away_mode_set_service(service):
"""Set away mode on target climate devices."""
target_climate = component.async_extract_from_service(service)
@ -253,23 +251,22 @@ def async_setup(hass, config):
update_tasks = []
for climate in target_climate:
if away_mode:
yield from climate.async_turn_away_mode_on()
await climate.async_turn_away_mode_on()
else:
yield from climate.async_turn_away_mode_off()
await climate.async_turn_away_mode_off()
if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service,
schema=SET_AWAY_MODE_SCHEMA)
@asyncio.coroutine
def async_hold_mode_set_service(service):
async def async_hold_mode_set_service(service):
"""Set hold mode on target climate devices."""
target_climate = component.async_extract_from_service(service)
@ -277,21 +274,20 @@ def async_setup(hass, config):
update_tasks = []
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:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service,
schema=SET_HOLD_MODE_SCHEMA)
@asyncio.coroutine
def async_aux_heat_set_service(service):
async def async_aux_heat_set_service(service):
"""Set auxiliary heater on target climate devices."""
target_climate = component.async_extract_from_service(service)
@ -300,23 +296,22 @@ def async_setup(hass, config):
update_tasks = []
for climate in target_climate:
if aux_heat:
yield from climate.async_turn_aux_heat_on()
await climate.async_turn_aux_heat_on()
else:
yield from climate.async_turn_aux_heat_off()
await climate.async_turn_aux_heat_off()
if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service,
schema=SET_AUX_HEAT_SCHEMA)
@asyncio.coroutine
def async_temperature_set_service(service):
async def async_temperature_set_service(service):
"""Set temperature on the target climate devices."""
target_climate = component.async_extract_from_service(service)
@ -333,21 +328,20 @@ def async_setup(hass, config):
else:
kwargs[value] = temp
yield from climate.async_set_temperature(**kwargs)
await climate.async_set_temperature(**kwargs)
if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service,
schema=SET_TEMPERATURE_SCHEMA)
@asyncio.coroutine
def async_humidity_set_service(service):
async def async_humidity_set_service(service):
"""Set humidity on the target climate devices."""
target_climate = component.async_extract_from_service(service)
@ -355,20 +349,19 @@ def async_setup(hass, config):
update_tasks = []
for climate in target_climate:
yield from climate.async_set_humidity(humidity)
await climate.async_set_humidity(humidity)
if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service,
schema=SET_HUMIDITY_SCHEMA)
@asyncio.coroutine
def async_fan_mode_set_service(service):
async def async_fan_mode_set_service(service):
"""Set fan mode on target climate devices."""
target_climate = component.async_extract_from_service(service)
@ -376,20 +369,19 @@ def async_setup(hass, config):
update_tasks = []
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:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service,
schema=SET_FAN_MODE_SCHEMA)
@asyncio.coroutine
def async_operation_set_service(service):
async def async_operation_set_service(service):
"""Set operating mode on the target climate devices."""
target_climate = component.async_extract_from_service(service)
@ -397,20 +389,19 @@ def async_setup(hass, config):
update_tasks = []
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:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service,
schema=SET_OPERATION_MODE_SCHEMA)
@asyncio.coroutine
def async_swing_set_service(service):
async def async_swing_set_service(service):
"""Set swing mode on the target climate devices."""
target_climate = component.async_extract_from_service(service)
@ -418,36 +409,35 @@ def async_setup(hass, config):
update_tasks = []
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:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register(
DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service,
schema=SET_SWING_MODE_SCHEMA)
@asyncio.coroutine
def async_on_off_service(service):
async def async_on_off_service(service):
"""Handle on/off calls."""
target_climate = component.async_extract_from_service(service)
update_tasks = []
for climate in target_climate:
if service.service == SERVICE_TURN_ON:
yield from climate.async_turn_on()
await climate.async_turn_on()
elif service.service == SERVICE_TURN_OFF:
yield from climate.async_turn_off()
await climate.async_turn_off()
if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
await asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register(
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
https://home-assistant.io/components/climate.knx/
"""
import asyncio
import voluptuous as vol
@ -61,8 +60,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up climate(s) for KNX platform."""
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices)
@ -135,11 +134,10 @@ class KNXClimate(ClimateDevice):
def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
@asyncio.coroutine
def after_update_callback(device):
async def after_update_callback(device):
"""Call after device was updated."""
# 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)
@property
@ -187,14 +185,13 @@ class KNXClimate(ClimateDevice):
"""Return the maximum temperature."""
return self.device.target_temperature_max
@asyncio.coroutine
def async_set_temperature(self, **kwargs):
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
yield from self.device.set_target_temperature(temperature)
yield from self.async_update_ha_state()
await self.device.set_target_temperature(temperature)
await self.async_update_ha_state()
@property
def current_operation(self):
@ -210,10 +207,9 @@ class KNXClimate(ClimateDevice):
operation_mode in
self.device.get_supported_operation_modes()]
@asyncio.coroutine
def async_set_operation_mode(self, operation_mode):
async def async_set_operation_mode(self, operation_mode):
"""Set operation mode."""
if self.device.supports_operation_mode:
from xknx.knx import HVACOperationMode
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'
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):
"""Set up the Nest thermostat."""
@ -58,6 +54,10 @@ class NestThermostat(ClimateDevice):
self.device = device
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
self._operation_list = [STATE_OFF]
@ -70,11 +70,16 @@ class NestThermostat(ClimateDevice):
if self.device.can_heat and self.device.can_cool:
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)
# feature of device
self._has_fan = self.device.has_fan
if self._has_fan:
self._support_flags = (self._support_flags | SUPPORT_FAN_MODE)
# data attributes
self._away = None
@ -95,7 +100,7 @@ class NestThermostat(ClimateDevice):
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
return self._support_flags
@property
def unique_id(self):
@ -162,6 +167,7 @@ class NestThermostat(ClimateDevice):
def set_temperature(self, **kwargs):
"""Set new target temperature."""
import nest
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if self._mode == NEST_MODE_HEAT_COOL:
@ -170,7 +176,10 @@ class NestThermostat(ClimateDevice):
else:
temp = kwargs.get(ATTR_TEMPERATURE)
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
self.device.target = temp
try:
self.device.target = temp
except nest.nest.APIError:
_LOGGER.error("An error occured while setting the temperature")
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
@ -205,11 +214,14 @@ class NestThermostat(ClimateDevice):
@property
def fan_list(self):
"""List of available fan modes."""
return self._fan_list
if self._has_fan:
return self._fan_list
return None
def set_fan_mode(self, fan_mode):
"""Turn fan on/off."""
self.device.fan = fan_mode.lower()
if self._has_fan:
self.device.fan = fan_mode.lower()
@property
def min_temp(self):

View File

@ -15,12 +15,12 @@ import async_timeout
import voluptuous as vol
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.aiohttp_client import async_get_clientsession
from homeassistant.util import dt as dt_util
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 .const import CONFIG_DIR, DOMAIN, SERVERS
@ -51,7 +51,6 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({
GOOGLE_ENTITY_SCHEMA = vol.Schema({
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])
})
@ -175,7 +174,7 @@ class Cloud:
"""If an entity should be exposed."""
return conf['filter'](entity.entity_id)
self._gactions_config = ga_sh.Config(
self._gactions_config = ga_h.Config(
should_expose=should_expose,
agent_user_id=self.claims['cognito:username'],
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."""
class ExpiredCode(CloudError):
"""Raised when an expired code is encountered."""
class InvalidCode(CloudError):
"""Raised when an invalid code is submitted."""
class PasswordChangeRequired(CloudError):
"""Raised when a password change is required."""
@ -42,10 +34,8 @@ class UnknownError(CloudError):
AWS_EXCEPTIONS = {
'UserNotFoundException': UserNotFound,
'NotAuthorizedException': Unauthenticated,
'ExpiredCodeException': ExpiredCode,
'UserNotConfirmedException': UserNotConfirmed,
'PasswordResetRequiredException': PasswordChangeRequired,
'CodeMismatchException': InvalidCode,
}
@ -69,17 +59,6 @@ def register(cloud, email, password):
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):
"""Resend email confirmation."""
from botocore.exceptions import ClientError
@ -107,18 +86,6 @@ def forgot_password(cloud, email):
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):
"""Log user in and fetch certificate."""
cognito = _authenticate(cloud, email, password)

View File

@ -23,10 +23,8 @@ def async_setup(hass):
hass.http.register_view(CloudLogoutView)
hass.http.register_view(CloudAccountView)
hass.http.register_view(CloudRegisterView)
hass.http.register_view(CloudConfirmRegisterView)
hass.http.register_view(CloudResendConfirmView)
hass.http.register_view(CloudForgotPasswordView)
hass.http.register_view(CloudConfirmForgotPasswordView)
_CLOUD_ERRORS = {
@ -34,8 +32,6 @@ _CLOUD_ERRORS = {
auth_api.UserNotConfirmed: (400, 'Email not confirmed.'),
auth_api.Unauthenticated: (401, 'Authentication failed.'),
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.')
}
@ -149,31 +145,6 @@ class CloudRegisterView(HomeAssistantView):
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):
"""Resend email confirmation code."""
@ -220,33 +191,6 @@ class CloudForgotPasswordView(HomeAssistantView):
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):
"""Generate the auth data JSON response."""
claims = cloud.claims

View File

@ -1,6 +1,7 @@
"""Module to handle messages from Home Assistant cloud."""
import asyncio
import logging
import pprint
from aiohttp import hdrs, client_exceptions, WSMsgType
@ -154,7 +155,9 @@ class CloudIoT:
disconnect_warn = 'Received invalid JSON.'
break
_LOGGER.debug("Received message: %s", msg)
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("Received message:\n%s\n",
pprint.pformat(msg))
response = {
'msgid': msg['msgid'],
@ -176,7 +179,9 @@ class CloudIoT:
_LOGGER.exception("Error handling message")
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)
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.util import Throttle
REQUIREMENTS = [
'https://github.com/balloob/coinbase-python/archive/'
'3a35efe13ef728a1cc18204b4f25be1fcb1c6006.zip#coinbase==2.0.8a1']
REQUIREMENTS = ['coinbase==2.1.0']
_LOGGER = logging.getLogger(__name__)

View File

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

View File

@ -97,10 +97,10 @@ class ConfigManagerFlowIndexView(HomeAssistantView):
flow for flow in hass.config_entries.flow.async_progress()
if flow['source'] != config_entries.SOURCE_USER])
@asyncio.coroutine
@RequestDataValidator(vol.Schema({
vol.Required('domain'): str,
}))
@asyncio.coroutine
def post(self, request, data):
"""Handle a POST request."""
hass = request.app['hass']
@ -139,8 +139,8 @@ class ConfigManagerFlowResourceView(HomeAssistantView):
return self.json(result)
@asyncio.coroutine
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
@asyncio.coroutine
def post(self, request, flow_id, data):
"""Handle a POST request."""
hass = request.app['hass']
@ -163,7 +163,7 @@ class ConfigManagerFlowResourceView(HomeAssistantView):
hass = request.app['hass']
try:
hass.config_entries.async_abort(flow_id)
hass.config_entries.flow.async_abort(flow_id)
except config_entries.UnknownFlow:
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
https://home-assistant.io/components/conversation/
"""
import asyncio
import logging
import re
@ -67,8 +66,7 @@ def async_register(hass, intent_type, utterances):
conf.append(_create_matcher(utterance))
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Register the process service."""
config = config.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)
@asyncio.coroutine
def process(service):
async def process(service):
"""Parse text into commands."""
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(
DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA)
hass.http.register_view(ConversationProcessView)
async_register(hass, intent.INTENT_TURN_ON,
['Turn {name} on', 'Turn on {name}'])
async_register(hass, intent.INTENT_TURN_OFF,
['Turn {name} off', 'Turn off {name}'])
async_register(hass, intent.INTENT_TOGGLE,
['Toggle {name}', '{name} toggle'])
# We strip trailing 's' from name because our state matcher will fail
# if a letter is not there. By removing 's' we can match singular and
# plural names.
async_register(hass, intent.INTENT_TURN_ON, [
'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
def _create_matcher(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+)}')
# Pattern to extract text from OPTIONAL part. Matches [the color]
optional_matcher = re.compile(r'\[([\w ]+)\] *')
pattern = ['^']
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)
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('$')
return re.compile(''.join(pattern), re.I)
@asyncio.coroutine
def _process(hass, text):
async def _process(hass, text):
"""Process a line of text."""
intents = hass.data.get(DOMAIN, {})
@ -137,7 +159,7 @@ def _process(hass, text):
if not match:
continue
response = yield from hass.helpers.intent.async_handle(
response = await hass.helpers.intent.async_handle(
DOMAIN, intent_type,
{key: {'value': value} for key, value
in match.groupdict().items()}, text)
@ -153,12 +175,15 @@ class ConversationProcessView(http.HomeAssistantView):
@RequestDataValidator(vol.Schema({
vol.Required('text'): str,
}))
@asyncio.coroutine
def post(self, request, data):
async def post(self, request, data):
"""Send a request for processing."""
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:
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)
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Track states and offer events for covers."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS)
yield from component.async_setup(config)
await component.async_setup(config)
@asyncio.coroutine
def async_handle_cover_service(service):
async def async_handle_cover_service(service):
"""Handle calls to the cover services."""
covers = component.async_extract_from_service(service)
method = SERVICE_TO_METHOD.get(service.service)
@ -169,13 +167,13 @@ def async_setup(hass, config):
# call method
update_tasks = []
for cover in covers:
yield from getattr(cover, method['method'])(**params)
await getattr(cover, method['method'])(**params)
if not cover.should_poll:
continue
update_tasks.append(cover.async_update_ha_state(True))
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:
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
https://home-assistant.io/components/cover.knx/
"""
import asyncio
import voluptuous as vol
@ -50,8 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up cover(s) for KNX platform."""
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices)
@ -106,11 +105,10 @@ class KNXCover(CoverDevice):
@callback
def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
@asyncio.coroutine
def after_update_callback(device):
async def after_update_callback(device):
"""Call after device was updated."""
# 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)
@property
@ -147,32 +145,28 @@ class KNXCover(CoverDevice):
"""Return if the cover is closed."""
return self.device.is_closed()
@asyncio.coroutine
def async_close_cover(self, **kwargs):
async def async_close_cover(self, **kwargs):
"""Close the cover."""
if not self.device.is_closed():
yield from self.device.set_down()
await self.device.set_down()
self.start_auto_updater()
@asyncio.coroutine
def async_open_cover(self, **kwargs):
async def async_open_cover(self, **kwargs):
"""Open the cover."""
if not self.device.is_open():
yield from self.device.set_up()
await self.device.set_up()
self.start_auto_updater()
@asyncio.coroutine
def async_set_cover_position(self, **kwargs):
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
if ATTR_POSITION in kwargs:
position = kwargs[ATTR_POSITION]
yield from self.device.set_position(position)
await self.device.set_position(position)
self.start_auto_updater()
@asyncio.coroutine
def async_stop_cover(self, **kwargs):
async def async_stop_cover(self, **kwargs):
"""Stop the cover."""
yield from self.device.stop()
await self.device.stop()
self.stop_auto_updater()
@property
@ -182,12 +176,11 @@ class KNXCover(CoverDevice):
return None
return self.device.current_angle()
@asyncio.coroutine
def async_set_cover_tilt_position(self, **kwargs):
async def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
if ATTR_TILT_POSITION in kwargs:
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):
"""Start the autoupdater to update HASS while cover is moving."""

View File

@ -118,6 +118,17 @@ def async_setup(hass, config):
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
tasks2.append(bootstrap.async_setup_component(
hass, 'script',

View File

@ -77,11 +77,14 @@ ATTR_MAC = 'mac'
ATTR_NAME = 'name'
ATTR_SOURCE_TYPE = 'source_type'
ATTR_VENDOR = 'vendor'
ATTR_CONSIDER_HOME = 'consider_home'
SOURCE_TYPE_GPS = 'gps'
SOURCE_TYPE_ROUTER = 'router'
SOURCE_TYPE_BLUETOOTH = 'bluetooth'
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({
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,
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
@ -109,7 +125,7 @@ def is_on(hass: HomeAssistantType, entity_id: str = None):
def see(hass: HomeAssistantType, mac: str = None, dev_id: str = None,
host_name: str = None, location_name: str = 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."""
data = {key: value for key, value in
((ATTR_MAC, mac),
@ -203,12 +219,10 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
@asyncio.coroutine
def async_see_service(call):
"""Service to see a device."""
args = {key: value for key, value in call.data.items() if key in
(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)
yield from tracker.async_see(**call.data)
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
yield from tracker.async_setup_tracked_device()
@ -240,23 +254,26 @@ class DeviceTracker(object):
dev.mac)
def see(self, mac: str = None, dev_id: str = None, host_name: str = None,
location_name: str = None, gps: GPSType = None, gps_accuracy=None,
battery: str = None, attributes: dict = None,
source_type: str = SOURCE_TYPE_GPS, picture: str = None,
icon: 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,
picture: str = None, icon: str = None,
consider_home: timedelta = None):
"""Notify the device tracker that you see a device."""
self.hass.add_job(
self.async_see(mac, dev_id, host_name, location_name, gps,
gps_accuracy, battery, attributes, source_type,
picture, icon)
picture, icon, consider_home)
)
@asyncio.coroutine
def async_see(self, mac: str = None, dev_id: str = None,
host_name: str = None, location_name: str = None,
gps: GPSType = None, gps_accuracy=None, battery: str = None,
attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
picture: str = None, icon: str = None):
def async_see(
self, mac: str = None, dev_id: str = None, host_name: 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,
picture: str = None, icon: str = None,
consider_home: timedelta = None):
"""Notify the device tracker that you see a device.
This method is a coroutine.
@ -275,7 +292,7 @@ class DeviceTracker(object):
if device:
yield from device.async_seen(
host_name, location_name, gps, gps_accuracy, battery,
attributes, source_type)
attributes, source_type, consider_home)
if device.track:
yield from device.async_update_ha_state()
return
@ -283,7 +300,7 @@ class DeviceTracker(object):
# If no device can be found, create it
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
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('_', ' '),
picture=picture, icon=icon,
hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
@ -384,9 +401,10 @@ class Device(Entity):
host_name = None # type: str
location_name = None # type: str
gps = None # type: GPSType
gps_accuracy = 0
gps_accuracy = 0 # type: int
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
vendor = None # type: str
icon = None # type: str
@ -476,14 +494,16 @@ class Device(Entity):
@asyncio.coroutine
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,
source_type: str = SOURCE_TYPE_GPS):
source_type: str = SOURCE_TYPE_GPS,
consider_home: timedelta = None):
"""Mark the device as seen."""
self.source_type = source_type
self.last_seen = dt_util.utcnow()
self.host_name = host_name
self.location_name = location_name
self.consider_home = consider_home or self.consider_home
if battery:
self.battery = battery

View File

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

View File

@ -189,10 +189,12 @@ class Icloud(DeviceScanner):
for device in self.api.devices:
status = device.status(DEVICESTATUSSET)
devicename = slugify(status['name'].replace(' ', '', 99))
if devicename not in self.devices:
self.devices[devicename] = device
self._intervals[devicename] = 1
self._overridestates[devicename] = None
if devicename in self.devices:
_LOGGER.error('Multiple devices with name: %s', devicename)
continue
self.devices[devicename] = device
self._intervals[devicename] = 1
self._overridestates[devicename] = None
except PyiCloudNoDevicesException:
_LOGGER.error('No iCloud Devices found!')
@ -319,14 +321,6 @@ class Icloud(DeviceScanner):
def determine_interval(self, devicename, latitude, longitude, battery):
"""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)
if ((currentzone is not None and
@ -335,22 +329,48 @@ class Icloud(DeviceScanner):
self._overridestates.get(devicename) == 'away')):
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
if currentzone is not None:
self._intervals[devicename] = 30
return
if distancefromhome is None:
if mindistance is None:
return
if distancefromhome > 25:
self._intervals[devicename] = round(distancefromhome / 2, 0)
elif distancefromhome > 10:
self._intervals[devicename] = 5
else:
self._intervals[devicename] = 1
if battery is not None and battery <= 33 and distancefromhome > 3:
self._intervals[devicename] = self._intervals[devicename] * 2
# Calculate out how long it would take for the device to drive to the
# nearest zone at 120 km/h:
interval = round(mindistance / 2, 0)
# Never poll more than once per minute
interval = max(interval, 1)
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):
"""Update the device_tracker entity."""

View File

@ -44,14 +44,15 @@ class TeslaDeviceTracker(object):
_LOGGER.debug("Updating device position: %s", name)
dev_id = slugify(device.uniq_name)
location = device.get_location()
lat = location['latitude']
lon = location['longitude']
attrs = {
'trackr_id': dev_id,
'id': dev_id,
'name': name
}
self.see(
dev_id=dev_id, host_name=name,
gps=(lat, lon), attributes=attrs
)
if location:
lat = location['latitude']
lon = location['longitude']
attrs = {
'trackr_id': dev_id,
'id': dev_id,
'name': name
}
self.see(
dev_id=dev_id, host_name=name,
gps=(lat, lon), attributes=attrs
)

View File

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

View File

@ -71,6 +71,7 @@ SERVICE_HANDLERS = {
'sabnzbd': ('sensor', 'sabnzbd'),
'bose_soundtouch': ('media_player', 'soundtouch'),
'bluesound': ('media_player', 'bluesound'),
'songpal': ('media_player', 'songpal'),
}
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
https://home-assistant.io/components/emulated_hue/
"""
import asyncio
import logging
import voluptuous as vol
@ -111,17 +110,15 @@ def setup(hass, yaml_config):
config.upnp_bind_multicast, config.advertise_ip,
config.advertise_port)
@asyncio.coroutine
def stop_emulated_hue_bridge(event):
async def stop_emulated_hue_bridge(event):
"""Stop the emulated hue bridge."""
upnp_listener.stop()
yield from server.stop()
await server.stop()
@asyncio.coroutine
def start_emulated_hue_bridge(event):
async def start_emulated_hue_bridge(event):
"""Start the emulated hue bridge."""
upnp_listener.start()
yield from server.start()
await server.start()
hass.bus.async_listen_once(
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.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations
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'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
@ -379,6 +380,8 @@ def async_setup(hass, config):
async_setup_themes(hass, conf.get(CONF_THEMES))
hass.http.register_view(TranslationsView)
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):
"""Fingerprint a file."""
with open(path) as fil:
@ -553,6 +573,8 @@ def _is_latest(js_option, request):
Set according to user's preference and URL override.
"""
import hass_frontend
if request is None:
return js_option == 'latest'
@ -573,25 +595,5 @@ def _is_latest(js_option, request):
return js_option == 'latest'
useragent = request.headers.get('User-Agent')
if not useragent:
return False
from user_agents import parse
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
return useragent and hass_frontend.version(useragent)

View File

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

View File

@ -22,25 +22,6 @@ DEFAULT_EXPOSED_DOMAINS = [
CLIMATE_MODE_HEATCOOL = '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.'
TYPE_LIGHT = PREFIX_TYPES + 'LIGHT'
TYPE_SWITCH = PREFIX_TYPES + 'SWITCH'
@ -50,3 +31,12 @@ TYPE_THERMOSTAT = PREFIX_TYPES + 'THERMOSTAT'
SERVICE_REQUEST_SYNC = 'request_sync'
HOMEGRAPH_URL = 'https://homegraph.googleapis.com/'
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.web import Request, Response # NOQA
from homeassistant.const import HTTP_UNAUTHORIZED
# Typing imports
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
from homeassistant.components.http import HomeAssistantView
@ -27,7 +25,8 @@ from .const import (
CONF_ENTITY_CONFIG,
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__)
@ -83,8 +82,7 @@ class GoogleAssistantView(HomeAssistantView):
"""Handle Google Assistant requests."""
auth = request.headers.get(AUTHORIZATION, None)
if 'Bearer {}'.format(self.access_token) != auth:
return self.json_message(
"missing authorization", status_code=HTTP_UNAUTHORIZED)
return self.json_message("missing authorization", status_code=401)
message = yield from request.json() # type: dict
result = yield from async_handle_message(

View File

@ -1,5 +1,6 @@
"""Support for Google Assistant Smart Home API."""
import asyncio
import collections
from itertools import product
import logging
# Typing imports
@ -9,447 +10,222 @@ from aiohttp.web import Request, Response # NOQA
from typing import Dict, Tuple, Any, Optional # NOQA
from homeassistant.helpers.entity import Entity # NOQA
from homeassistant.core import HomeAssistant # NOQA
from homeassistant.util import color
from homeassistant.util.unit_system import UnitSystem # NOQA
from homeassistant.util.decorator import Registry
from homeassistant.core import callback
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT,
STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON,
TEMP_FAHRENHEIT, TEMP_CELSIUS,
CONF_NAME, CONF_TYPE
)
CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES)
from homeassistant.components import (
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 (
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,
CONF_ALIASES, CONF_ROOM_HINT, CLIMATE_SUPPORTED_MODES,
CLIMATE_MODE_HEATCOOL
CONF_ALIASES, CONF_ROOM_HINT,
ERR_NOT_SUPPORTED, ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE,
ERR_UNKNOWN_ERROR
)
from .helpers import SmartHomeError
HANDLERS = Registry()
QUERY_HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__)
# Mapping is [actions schema, primary trait, optional features]
# optional is SUPPORT_* = (trait, command)
MAPPING_COMPONENT = {
group.DOMAIN: [TYPE_SWITCH, TRAIT_ONOFF, None],
scene.DOMAIN: [TYPE_SCENE, TRAIT_SCENE, None],
script.DOMAIN: [TYPE_SCENE, TRAIT_SCENE, None],
switch.DOMAIN: [TYPE_SWITCH, TRAIT_ONOFF, None],
fan.DOMAIN: [TYPE_SWITCH, TRAIT_ONOFF, None],
light.DOMAIN: [
TYPE_LIGHT, TRAIT_ONOFF, {
light.SUPPORT_BRIGHTNESS: TRAIT_BRIGHTNESS,
light.SUPPORT_RGB_COLOR: TRAIT_RGB_COLOR,
light.SUPPORT_COLOR_TEMP: TRAIT_COLOR_TEMP,
DOMAIN_TO_GOOGLE_TYPES = {
group.DOMAIN: TYPE_SWITCH,
scene.DOMAIN: TYPE_SCENE,
script.DOMAIN: TYPE_SCENE,
switch.DOMAIN: TYPE_SWITCH,
fan.DOMAIN: TYPE_SWITCH,
light.DOMAIN: TYPE_LIGHT,
cover.DOMAIN: TYPE_SWITCH,
media_player.DOMAIN: TYPE_SWITCH,
climate.DOMAIN: TYPE_THERMOSTAT,
}
def deep_update(target, source):
"""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 _GoogleEntity:
"""Adaptation of Entity expressed in Google's terms."""
def __init__(self, hass, config, state):
self.hass = hass
self.config = config
self.state = state
@property
def entity_id(self):
"""Return entity ID."""
return self.state.entity_id
@callback
def traits(self):
"""Return traits for entity."""
state = self.state
domain = state.domain
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
return [Trait(state) for Trait in trait.TRAITS
if Trait.supported(domain, features)]
@callback
def sync_serialize(self):
"""Serialize entity for a SYNC response.
https://developers.google.com/actions/smarthome/create-app#actiondevicessync
"""
traits = self.traits()
state = self.state
# Found no supported traits for this entity
if not traits:
return None
entity_config = self.config.entity_config.get(state.entity_id, {})
device = {
'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],
}
],
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]
# use aliases
aliases = entity_config.get(CONF_ALIASES)
if aliases:
device['name']['nicknames'] = aliases
# add room hint if annotated
room = entity_config.get(CONF_ROOM_HINT)
if room:
device['roomHint'] = room
for trt in traits:
device['attributes'].update(trt.sync_attributes())
return device
@callback
def query_serialize(self):
"""Serialize entity for a QUERY response.
https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
"""
state = self.state
if state.state == STATE_UNAVAILABLE:
return {'online': False}
attrs = {'online': True}
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(
ERR_NOT_SUPPORTED,
'Unable to execute {} for {}'.format(command,
self.state.entity_id))
@callback
def async_update(self):
"""Update the entity with latest info from Home Assistant."""
self.state = self.hass.states.get(self.entity_id)
"""Error code used for SmartHomeError class."""
ERROR_NOT_SUPPORTED = "notSupported"
async def async_handle_message(hass, config, message):
"""Handle incoming API messages."""
response = await _process(hass, config, message)
class SmartHomeError(Exception):
"""Google Assistant Smart Home errors."""
def __init__(self, code, msg):
"""Log error code."""
super(SmartHomeError, self).__init__(msg)
_LOGGER.error(
"An error has occurred in Google SmartHome: %s."
"Error code: %s", msg, code
)
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 {}
def entity_to_device(entity: Entity, config: Config, units: UnitSystem):
"""Convert a hass entity into a google actions device."""
entity_config = config.entity_config.get(entity.entity_id, {})
google_domain = entity_config.get(CONF_TYPE)
class_data = MAPPING_COMPONENT.get(
google_domain or entity.domain)
if class_data is None:
return None
device = {
'id': entity.entity_id,
'name': {},
'attributes': {},
'traits': [],
'willReportState': False,
}
device['type'] = class_data[0]
device['traits'].append(class_data[1])
# handle custom names
device['name']['name'] = entity_config.get(CONF_NAME) or entity.name
# use aliases
aliases = entity_config.get(CONF_ALIASES)
if aliases:
device['name']['nicknames'] = aliases
# add room hint if annotated
room = entity_config.get(CONF_ROOM_HINT)
if room:
device['roomHint'] = room
# add trait if entity supports feature
if class_data[2]:
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
def celsius(deg: Optional[float], units: UnitSystem) -> Optional[float]:
"""Convert a float to Celsius and rounds to one decimal place."""
if deg is None:
return None
return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1)
@QUERY_HANDLERS.register(sensor.DOMAIN)
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:
raise SmartHomeError(
ERROR_NOT_SUPPORTED,
"Sensor type {} is not supported".format(google_domain)
)
# check if we have a string value to convert it to number
value = entity.state
if isinstance(entity.state, str):
try:
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)
def query_response_climate(
entity: Entity, config: Config, units: UnitSystem) -> dict:
"""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}
@QUERY_HANDLERS.register(media_player.DOMAIN)
def query_response_media_player(
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)
if 'errorCode' in response['payload']:
_LOGGER.error('Error handling message %s: %s',
message, response['payload'])
return response
def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict:
"""Take an entity and return a properly formatted device object."""
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."""
async def _process(hass, config, message):
"""Process a message."""
request_id = message.get('requestId') # type: str
inputs = message.get('inputs') # type: list
if len(inputs) > 1:
_LOGGER.warning('Got unexpected more than 1 input. %s', message)
if len(inputs) != 1:
return {
'requestId': request_id,
'payload': {'errorCode': ERR_PROTOCOL_ERROR}
}
# Only use first input
intent = inputs[0].get('intent')
payload = inputs[0].get('payload')
handler = HANDLERS.get(inputs[0].get('intent'))
handler = HANDLERS.get(intent)
if handler is None:
return {
'requestId': request_id,
'payload': {'errorCode': ERR_PROTOCOL_ERROR}
}
if handler:
result = yield from handler(hass, config, payload)
else:
result = {'errorCode': 'protocolError'}
return {'requestId': request_id, 'payload': result}
try:
result = await handler(hass, config, inputs[0].get('payload'))
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')
@asyncio.coroutine
def async_devices_sync(hass, config: Config, payload):
"""Handle action.devices.SYNC request."""
async def async_devices_sync(hass, config, payload):
"""Handle action.devices.SYNC request.
https://developers.google.com/actions/smarthome/create-app#actiondevicessync
"""
devices = []
for entity in hass.states.async_all():
if not config.should_expose(entity):
for state in hass.states.async_all():
if not config.should_expose(state):
continue
device = entity_to_device(entity, config, hass.config.units)
if device is None:
_LOGGER.warning("No mapping for %s domain", entity.domain)
entity = _GoogleEntity(hass, config, state)
serialized = entity.sync_serialize()
if serialized is None:
_LOGGER.debug("No mapping for %s domain", entity.state)
continue
devices.append(device)
devices.append(serialized)
return {
'agentUserId': config.agent_user_id,
@ -458,53 +234,79 @@ def async_devices_sync(hass, config: Config, payload):
@HANDLERS.register('action.devices.QUERY')
@asyncio.coroutine
def async_devices_query(hass, config, payload):
"""Handle action.devices.QUERY request."""
async def async_devices_query(hass, config, payload):
"""Handle action.devices.QUERY request.
https://developers.google.com/actions/smarthome/create-app#actiondevicesquery
"""
devices = {}
for device in payload.get('devices', []):
devid = device.get('id')
# In theory this should never happen
if not devid:
_LOGGER.error('Device missing ID: %s', device)
continue
devid = device['id']
state = hass.states.get(devid)
if not state:
# If we can't find a state, the device is offline
devices[devid] = {'online': False}
else:
try:
devices[devid] = query_device(state, config, hass.config.units)
except SmartHomeError as error:
devices[devid] = {'errorCode': error.code}
continue
devices[devid] = _GoogleEntity(hass, config, state).query_serialize()
return {'devices': devices}
@HANDLERS.register('action.devices.EXECUTE')
@asyncio.coroutine
def handle_devices_execute(hass, config, payload):
"""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)
async def handle_devices_execute(hass, config, payload):
"""Handle action.devices.EXECUTE request.
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
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()
if conf is None:
return
yield from _async_process_config(hass, conf, component)
yield from component.async_add_entities(auto)
hass.services.async_register(
DOMAIN, SERVICE_RELOAD, reload_service_handler,
schema=RELOAD_SERVICE_SCHEMA)
@ -407,7 +411,7 @@ class Group(Entity):
self.group_off = None
self.visible = visible
self.control = control
self._user_defined = user_defined
self.user_defined = user_defined
self._order = order
self._assumed_state = False
self._async_unsub_state_changed = None
@ -497,7 +501,7 @@ class Group(Entity):
ATTR_ENTITY_ID: self.tracking,
ATTR_ORDER: self._order,
}
if not self._user_defined:
if not self.user_defined:
data[ATTR_AUTO] = True
if self.view:
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
https://home-assistant.io/components/homekit/
@ -11,17 +11,20 @@ import voluptuous as vol
from homeassistant.const import (
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.decorator import Registry
TYPES = Registry()
_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'
REQUIREMENTS = ['HAP-python==1.1.5']
REQUIREMENTS = ['HAP-python==1.1.7']
BRIDGE_NAME = 'Home Assistant'
CONF_PIN_CODE = 'pincode'
@ -30,11 +33,11 @@ HOMEKIT_FILE = '.homekit.state'
def valid_pin(value):
"""Validate pincode value."""
match = _RE_VALID_PINCODE.findall(value.strip())
if match == []:
"""Validate pin code value."""
match = re.match(_RE_VALID_PINCODE, str(value).strip())
if not match:
raise vol.Invalid("Pin must be in the format: '123-45-678'")
return match[0]
return match.group(0)
CONFIG_SCHEMA = vol.Schema({
@ -47,14 +50,14 @@ CONFIG_SCHEMA = vol.Schema({
@asyncio.coroutine
def async_setup(hass, config):
"""Setup the homekit component."""
_LOGGER.debug("Begin setup homekit")
"""Setup the HomeKit component."""
_LOGGER.debug("Begin setup HomeKit")
conf = config[DOMAIN]
port = conf.get(CONF_PORT)
pin = str.encode(conf.get(CONF_PIN_CODE))
homekit = Homekit(hass, port)
homekit = HomeKit(hass, port)
homekit.setup_bridge(pin)
hass.bus.async_listen_once(
@ -63,18 +66,18 @@ def async_setup(hass, config):
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.")
# pylint: disable=unused-variable
from .covers import Window # noqa F401
# pylint: disable=unused-variable
from .sensors import TemperatureSensor # noqa F401
from . import ( # noqa F401
covers, security_systems, sensors, switches, thermostats)
def get_accessory(hass, state):
"""Take state and return an accessory object if supported."""
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\"",
state.entity_id, 'TemperatureSensor')
return TYPES['TemperatureSensor'](hass, state.entity_id,
@ -87,14 +90,35 @@ def get_accessory(hass, state):
state.entity_id, 'Window')
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
class Homekit():
"""Class to handle all actions between homekit and Home Assistant."""
class HomeKit():
"""Class to handle all actions between HomeKit and Home Assistant."""
def __init__(self, hass, port):
"""Initialize a homekit object."""
"""Initialize a HomeKit object."""
self._hass = hass
self._port = port
self.bridge = None
@ -103,8 +127,7 @@ class Homekit():
def setup_bridge(self, pin):
"""Setup the bridge component to track all accessories."""
from .accessories import HomeBridge
self.bridge = HomeBridge(BRIDGE_NAME, pincode=pin)
self.bridge.set_accessory_info('homekit.bridge')
self.bridge = HomeBridge(BRIDGE_NAME, 'homekit.bridge', pin)
def start_driver(self, event):
"""Start the accessory driver."""

View File

@ -1,55 +1,69 @@
"""Extend the basic Accessory and Bridge functions."""
import logging
from pyhap.accessory import Accessory, Bridge, Category
from .const import (
SERVICES_ACCESSORY_INFO, MANUFACTURER,
SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER,
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 to extend the Accessory class."""
ALL_CATEGORIES = Category
def __init__(self, display_name):
def __init__(self, display_name, model, category='OTHER', **kwargs):
"""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):
"""Set the category of the accessory."""
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)
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
class HomeBridge(Bridge):
"""Class to extend the Bridge class."""
def __init__(self, display_name, pincode):
def __init__(self, display_name, model, pincode, **kwargs):
"""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,
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)
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
add_preload_service(self, SERV_BRIDGING_STATE)

View File

@ -1,18 +1,36 @@
"""Constants used be the homekit component."""
"""Constants used be the HomeKit component."""
MANUFACTURER = 'HomeAssistant'
# Service: AccessoryInfomation
SERVICES_ACCESSORY_INFO = 'AccessoryInformation'
CHAR_MODEL = 'Model'
CHAR_MANUFACTURER = 'Manufacturer'
CHAR_SERIAL_NUMBER = 'SerialNumber'
# Services
SERV_ACCESSORY_INFO = 'AccessoryInformation'
SERV_BRIDGING_STATE = 'BridgingState'
SERV_SECURITY_SYSTEM = 'SecuritySystem'
SERV_SWITCH = 'Switch'
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
SERV_THERMOSTAT = 'Thermostat'
SERV_WINDOW_COVERING = 'WindowCovering'
# Service: TemperatureSensor
SERVICES_TEMPERATURE_SENSOR = 'TemperatureSensor'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
# Service: WindowCovering
SERVICES_WINDOW_COVERING = 'WindowCovering'
# Characteristics
CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier'
CHAR_CATEGORY = 'Category'
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
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_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 . import TYPES
from .accessories import HomeAccessory
from .accessories import HomeAccessory, add_preload_service
from .const import (
SERVICES_WINDOW_COVERING, CHAR_CURRENT_POSITION,
SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION,
CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
@ -23,10 +23,7 @@ class Window(HomeAccessory):
def __init__(self, hass, entity_id, display_name):
"""Initialize a Window accessory object."""
super().__init__(display_name)
self.set_category(self.ALL_CATEGORIES.WINDOW)
self.set_accessory_info(entity_id)
self.add_preload_service(SERVICES_WINDOW_COVERING)
super().__init__(display_name, entity_id, 'WINDOW')
self._hass = hass
self._entity_id = entity_id
@ -34,13 +31,16 @@ class Window(HomeAccessory):
self.current_position = None
self.homekit_target = None
self.service_cover = self.get_service(SERVICES_WINDOW_COVERING)
self.char_current_position = self.service_cover. \
self.serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = self.serv_cover. \
get_characteristic(CHAR_CURRENT_POSITION)
self.char_target_position = self.service_cover. \
self.char_target_position = self.serv_cover. \
get_characteristic(CHAR_TARGET_POSITION)
self.char_position_state = self.service_cover. \
self.char_position_state = self.serv_cover. \
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
@ -53,7 +53,7 @@ class Window(HomeAccessory):
self._hass, self._entity_id, self.update_cover_position)
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:
_LOGGER.debug("%s: Set position to %d", self._entity_id, 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."""
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 . import TYPES
from .accessories import HomeAccessory
from .accessories import (
HomeAccessory, add_preload_service, override_properties)
from .const import (
SERVICES_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE)
SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
_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')
class TemperatureSensor(HomeAccessory):
"""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):
"""Initialize a TemperatureSensor accessory object."""
super().__init__(display_name)
self.set_category(self.ALL_CATEGORIES.SENSOR)
self.set_accessory_info(entity_id)
self.add_preload_service(SERVICES_TEMPERATURE_SENSOR)
super().__init__(display_name, entity_id, 'SENSOR')
self._hass = hass
self._entity_id = entity_id
self.service_temp = self.get_service(SERVICES_TEMPERATURE_SENSOR)
self.char_temp = self.service_temp. \
self.serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = self.serv_temp. \
get_characteristic(CHAR_CURRENT_TEMPERATURE)
override_properties(self.char_temp, PROP_CELSIUS)
self.char_temp.value = 0
self.unit = None
def run(self):
"""Method called be object after driver is started."""
@ -48,6 +64,9 @@ class TemperatureSensor(HomeAccessory):
if new_state is None:
return
temperature = new_state.state
if temperature != STATE_UNKNOWN:
self.char_temp.set_value(float(temperature))
unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT]
temperature = calc_temperature(new_state.state, unit)
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
https://home-assistant.io/components/http/
"""
import asyncio
from ipaddress import ip_network
import json
import logging
import os
import ssl
from aiohttp import web
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently
from aiohttp.web_exceptions import HTTPMovedPermanently
import voluptuous as vol
from homeassistant.const import (
SERVER_PORT, CONTENT_TYPE_JSON,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,)
from homeassistant.core import is_callback
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, SERVER_PORT)
import homeassistant.helpers.config_validation as cv
import homeassistant.remote as rem
import homeassistant.util as hass_util
@ -28,10 +25,13 @@ from .auth import setup_auth
from .ban import setup_bans
from .cors import setup_cors
from .real_ip import setup_real_ip
from .const import KEY_AUTHENTICATED, KEY_REAL_IP
from .static import (
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']
DOMAIN = 'http'
@ -98,8 +98,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up the HTTP API and debug interface."""
conf = config.get(DOMAIN)
@ -135,16 +134,14 @@ def async_setup(hass, config):
is_ban_enabled=is_ban_enabled
)
@asyncio.coroutine
def stop_server(event):
async def stop_server(event):
"""Stop the server."""
yield from server.stop()
await server.stop()
@asyncio.coroutine
def start_server(event):
async def start_server(event):
"""Start the 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)
@ -252,13 +249,11 @@ class HomeAssistantHTTP(object):
return
if cache_headers:
@asyncio.coroutine
def serve_file(request):
async def serve_file(request):
"""Serve file from disk."""
return CachingFileResponse(path)
else:
@asyncio.coroutine
def serve_file(request):
async def serve_file(request):
"""Serve file from disk."""
return web.FileResponse(path)
@ -276,10 +271,13 @@ class HomeAssistantHTTP(object):
self.app.router.add_route('GET', url_pattern, serve_file)
@asyncio.coroutine
def start(self):
async def start(self):
"""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:
try:
@ -298,133 +296,24 @@ class HomeAssistantHTTP(object):
# Aiohttp freezes apps after start so that no changes can be made.
# However in Home Assistant components can be discovered after boot.
# This will now raise a RunTimeError.
# To work around this we now fake that we are frozen.
# A more appropriate fix would be to create a new app and
# re-register all redirects, views, static paths.
self.app._frozen = True # pylint: disable=protected-access
# To work around this we now prevent the router from getting frozen
self.app._router.freeze = lambda: None
self._handler = self.app.make_handler(loop=self.hass.loop)
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)
except OSError as error:
_LOGGER.error("Failed to create HTTP server at port %d: %s",
self.server_port, error)
# pylint: disable=protected-access
self.app._middlewares = tuple(self.app._prepare_middleware())
self.app._frozen = False
@asyncio.coroutine
def stop(self):
async def stop(self):
"""Stop the WSGI server."""
if self.server:
self.server.close()
yield from self.server.wait_closed()
yield from self.app.shutdown()
await self.server.wait_closed()
await self.app.shutdown()
if self._handler:
yield from self._handler.shutdown(10)
yield from 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
await self._handler.shutdown(10)
await self.app.cleanup()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
"""Static file handling for HTTP component."""
import asyncio
import re
from aiohttp import hdrs
@ -14,8 +14,7 @@ _FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
class CachingStaticResource(StaticResource):
"""Static Resource handler that will add cache headers."""
@asyncio.coroutine
def _handle(self, request):
async def _handle(self, request):
filename = URL(request.match_info['filename']).path
try:
# PyLint is wrong about resolve not being a member.
@ -32,13 +31,14 @@ class CachingStaticResource(StaticResource):
raise HTTPNotFound() from error
if filepath.is_dir():
return (yield from super()._handle(request))
return await super()._handle(request)
elif filepath.is_file():
return CachingFileResponse(filepath, chunk_size=self._chunk_size)
else:
raise HTTPNotFound
# pylint: disable=too-many-ancestors
class CachingFileResponse(FileResponse):
"""FileSender class that caches output if not in dev mode."""
@ -48,26 +48,24 @@ class CachingFileResponse(FileResponse):
orig_sendfile = self._sendfile
@asyncio.coroutine
def sendfile(request, fobj, count):
async def sendfile(request, fobj, count):
"""Sendfile that includes a cache header."""
cache_time = 31 * 86400 # = 1 month
self.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format(
cache_time)
yield from orig_sendfile(request, fobj, count)
await orig_sendfile(request, fobj, count)
# Overwriting like this because __init__ can change implementation.
self._sendfile = sendfile
@middleware
@asyncio.coroutine
def staticresource_middleware(request, handler):
async def staticresource_middleware(request, handler):
"""Middleware to strip out fingerprint from fingerprinted assets."""
path = request.path
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'])
@ -75,4 +73,4 @@ def staticresource_middleware(request, handler):
request.match_info['filename'] = \
'{}.{}'.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
https://home-assistant.io/components/hue/
"""
import asyncio
import json
from functools import partial
import logging
import os
import socket
import async_timeout
import requests
import voluptuous as vol
from homeassistant.components.discovery import SERVICE_HUE
from homeassistant.const import CONF_FILENAME, CONF_HOST
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__)
@ -133,13 +137,14 @@ def bridge_discovered(hass, service, discovery_info):
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."""
# Only register a device once
if socket.gethostbyname(host) in hass.data[DOMAIN]:
return
bridge = HueBridge(host, hass, filename, allow_unreachable,
bridge = HueBridge(host, hass, filename, username, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups)
bridge.setup()
@ -164,13 +169,14 @@ def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
class HueBridge(object):
"""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):
"""Initialize the system."""
self.host = host
self.bridge_id = socket.gethostbyname(host)
self.hass = hass
self.filename = filename
self.username = username
self.allow_unreachable = allow_unreachable
self.allow_in_emulated_hue = allow_in_emulated_hue
self.allow_hue_groups = allow_hue_groups
@ -189,10 +195,14 @@ class HueBridge(object):
import phue
try:
self.bridge = phue.Bridge(
self.host,
config_file_path=self.hass.config.path(self.filename))
except (ConnectionRefusedError, OSError): # Wrong host was given
kwargs = {}
if self.username is not None:
kwargs['username'] = self.username
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",
self.host)
return
@ -204,6 +214,7 @@ class HueBridge(object):
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unknown error connecting with Hue bridge at %s",
self.host)
return
# If we came here and configuring this host, mark as done
if self.config_request_id:
@ -260,3 +271,112 @@ class HueBridge(object):
def set_group(self, light_id, command):
"""Change light settings for a group. See phue for detail."""
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
from homeassistant.helpers.typing import HomeAssistantType
REQUIREMENTS = ['ihcsdk==2.1.1']
REQUIREMENTS = ['ihcsdk==2.2.0']
DOMAIN = 'ihc'
IHC_DATA = 'ihc'

View File

@ -43,7 +43,7 @@ DEFAULT_TIMEOUT = 10
DEFAULT_CONFIDENCE = 80
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,
})

View File

@ -35,7 +35,6 @@ CONF_COMPONENT_CONFIG = 'component_config'
CONF_COMPONENT_CONFIG_GLOB = 'component_config_glob'
CONF_COMPONENT_CONFIG_DOMAIN = 'component_config_domain'
CONF_RETRY_COUNT = 'max_retries'
CONF_RETRY_QUEUE = 'retry_queue_limit'
DEFAULT_DATABASE = 'home_assistant'
DEFAULT_VERIFY_SSL = True
@ -43,14 +42,17 @@ DOMAIN = 'influxdb'
TIMEOUT = 5
RETRY_DELAY = 20
QUEUE_BACKLOG_SECONDS = 10
QUEUE_BACKLOG_SECONDS = 30
BATCH_TIMEOUT = 1
BATCH_BUFFER_SIZE = 100
COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema({
vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string,
})
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.Inclusive(CONF_USERNAME, '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_SSL): cv.boolean,
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_OVERRIDE_MEASUREMENT): cv.string,
vol.Optional(CONF_TAGS, default={}):
@ -143,18 +144,18 @@ def setup(hass, config):
"READ/WRITE", exc)
return False
def influx_handle_event(event):
"""Send an event to Influx."""
def event_to_json(event):
"""Add an event to the outgoing Influx list."""
state = event.data.get('new_state')
if state is None or state.state in (
STATE_UNKNOWN, '', STATE_UNAVAILABLE) or \
state.entity_id in blacklist_e or state.domain in blacklist_d:
return True
return
try:
if (whitelist_e and state.entity_id not in whitelist_e) or \
(whitelist_d and state.domain not in whitelist_d):
return True
return
_include_state = _include_value = False
@ -183,61 +184,59 @@ def setup(hass, config):
else:
include_uom = False
json_body = [
{
'measurement': measurement,
'tags': {
'domain': state.domain,
'entity_id': state.object_id,
},
'time': event.time_fired,
'fields': {
}
}
]
json = {
'measurement': measurement,
'tags': {
'domain': state.domain,
'entity_id': state.object_id,
},
'time': event.time_fired,
'fields': {}
}
if _include_state:
json_body[0]['fields']['state'] = state.state
json['fields']['state'] = state.state
if _include_value:
json_body[0]['fields']['value'] = _state_as_value
json['fields']['value'] = _state_as_value
for key, value in state.attributes.items():
if key in tags_attributes:
json_body[0]['tags'][key] = value
json['tags'][key] = value
elif key != 'unit_of_measurement' or include_uom:
# If the key is already in fields
if key in json_body[0]['fields']:
if key in json['fields']:
key = key + "_"
# Prevent column data errors in influxDB.
# For each value we try to cast it as float
# But if we can not do it we store the value
# as string add "_str" postfix to the field key
try:
json_body[0]['fields'][key] = float(value)
json['fields'][key] = float(value)
except (ValueError, TypeError):
new_key = "{}_str".format(key)
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):
json_body[0]['fields'][key] = float(
json['fields'][key] = float(
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:
influx.write_points(json_body)
return True
except (exceptions.InfluxDBClientError, IOError):
return False
json['tags'].update(tags)
return json
instance = hass.data[DOMAIN] = InfluxThread(
hass, influx_handle_event, max_tries)
hass, influx, event_to_json, max_tries)
instance.start()
def shutdown(event):
"""Shut down the thread."""
instance.queue.put(None)
instance.join()
influx.close()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
@ -247,12 +246,15 @@ def setup(hass, config):
class InfluxThread(threading.Thread):
"""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."""
threading.Thread.__init__(self, name='InfluxDB')
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.write_errors = 0
self.shutdown = False
hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener)
def _event_listener(self, event):
@ -260,41 +262,77 @@ class InfluxThread(threading.Thread):
item = (time.monotonic(), event)
self.queue.put(item)
def run(self):
"""Process incoming events."""
@staticmethod
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
write_error = False
dropped = False
count = 0
json = []
while True:
item = self.queue.get()
dropped = 0
if item is None:
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:
self.shutdown = True
else:
timestamp, event = item
age = time.monotonic() - timestamp
if age < queue_seconds:
event_json = self.event_to_json(event)
if event_json:
json.append(event_json)
else:
dropped += 1
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()
return
timestamp, event = item
age = time.monotonic() - timestamp
if age < queue_seconds:
for retry in range(self.max_tries+1):
if self.event_handler(event):
if write_error:
_LOGGER.error("Resumed writing to InfluxDB")
write_error = False
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
self.queue.task_done()
def block_till_done(self):
"""Block till all events processed."""

View File

@ -4,117 +4,211 @@ Support for INSTEON PowerLinc Modem.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/insteon_plm/
"""
import logging
import asyncio
import collections
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import (
CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.const import (CONF_PORT, EVENT_HOMEASSISTANT_STOP,
CONF_PLATFORM)
import homeassistant.helpers.config_validation as cv
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__)
DOMAIN = 'insteon_plm'
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({
DOMAIN: vol.Schema({
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)
PLM_PLATFORMS = {
'binary_sensor': ['binary_sensor'],
'light': ['light'],
'switch': ['switch'],
}
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the connection to the PLM."""
import insteonplm
ipdb = IPDB()
conf = config[DOMAIN]
port = conf.get(CONF_PORT)
overrides = conf.get(CONF_OVERRIDE)
overrides = conf.get(CONF_OVERRIDE, [])
@callback
def async_plm_new_device(device):
"""Detect device from transport to be delegated to platform."""
name = device.get('address')
address = device.get('address_hex')
capabilities = device.get('capabilities', [])
for state_key in device.states:
platform_info = ipdb[device.states[state_key]]
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(
discovery.async_load_platform(
hass, loadplatform, DOMAIN, discovered=[device],
hass_config=config))
hass.async_add_job(
discovery.async_load_platform(
hass, platform, DOMAIN,
discovered={'address': device.address.hex,
'state_key': state_key},
hass_config=config))
_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
#
if isinstance(device['platform'], list):
plm.protocol.devices.add_override(
device['address'], 'capabilities', device['platform'])
else:
plm.protocol.devices.add_override(
device['address'], 'capabilities', [device['platform']])
address = device_override.get('address')
for prop in device_override:
if prop in [CONF_CAT, CONF_SUBCAT]:
plm.devices.add_override(address, prop,
device_override[prop])
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.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
def common_attributes(entity):
"""Return the device state attributes."""
attributes = {}
attributekeys = {
'address': 'INSTEON Address',
'description': 'Description',
'model': 'Model',
'cat': 'Category',
'subcat': 'Subcategory',
'firmware': 'Firmware',
'product_key': 'Product Key'
}
State = collections.namedtuple('Product', 'stateType platform')
hexkeys = ['cat', 'subcat', 'firmware']
for key in attributekeys:
name = attributekeys[key]
val = entity.get_attr(key)
if val is not None:
if key in hexkeys:
attributes[name] = hex(int(val))
else:
attributes[name] = val
return attributes
class IPDB(object):
"""Embodies the INSTEON Product Database static data and access methods."""
def __init__(self):
"""Create the INSTEON Product Database (IPDB)."""
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:
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
@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
def unique_id(self):
"""Return an unique ID."""
"""Return a unique ID."""
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
https://home-assistant.io/components/knx/
"""
import asyncio
import logging
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
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.script import Script
REQUIREMENTS = ['xknx==0.8.3']
REQUIREMENTS = ['xknx==0.8.4']
DOMAIN = "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_FILTER = "fire_event_filter"
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_ATTR_ADDRESS = "address"
@ -45,6 +51,12 @@ ROUTING_SCHEMA = vol.Schema({
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({
DOMAIN: vol.Schema({
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.All(cv.ensure_list, [cv.string]),
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)
@ -66,13 +82,13 @@ SERVICE_KNX_SEND_SCHEMA = vol.Schema({
})
@asyncio.coroutine
def async_setup(hass, config):
async def async_setup(hass, config):
"""Set up the KNX component."""
from xknx.exceptions import XKNXException
try:
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:
_LOGGER.warning("Can't connect to KNX interface: %s", ex)
@ -88,6 +104,7 @@ def async_setup(hass, config):
('light', 'Light'),
('sensor', 'Sensor'),
('binary_sensor', 'BinarySensor'),
('scene', 'Scene'),
('notify', 'Notification')):
found_devices = _get_devices(hass, discovery_type)
hass.async_add_job(
@ -122,26 +139,25 @@ class KNXModule(object):
self.connected = False
self.init_xknx()
self.register_callbacks()
self.exposures = []
def init_xknx(self):
"""Initialize of KNX object."""
from xknx import XKNX
self.xknx = XKNX(config=self.config_file(), loop=self.hass.loop)
@asyncio.coroutine
def start(self):
async def start(self):
"""Start KNX object. Connect to tunneling or Routing device."""
connection_config = self.connection_config()
yield from self.xknx.start(
await self.xknx.start(
state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER],
connection_config=connection_config)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
self.connected = True
@asyncio.coroutine
def stop(self, event):
async def stop(self, event):
"""Stop KNX object. Disconnect from tunneling or Routing device."""
yield from self.xknx.stop()
await self.xknx.stop()
def config_file(self):
"""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.telegram_received_cb, address_filters)
@asyncio.coroutine
def telegram_received_cb(self, telegram):
@callback
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."""
self.hass.bus.fire('knx_event', {
'address': telegram.group_address.str(),
@ -212,8 +247,7 @@ class KNXModule(object):
# False signals XKNX to proceed with processing telegrams.
return False
@asyncio.coroutine
def service_send_to_knx_bus(self, call):
async def service_send_to_knx_bus(self, call):
"""Service for sending an arbitrary KNX message to the KNX bus."""
from xknx.knx import Telegram, GroupAddress, DPTBinary, DPTArray
attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD)
@ -230,7 +264,7 @@ class KNXModule(object):
telegram = Telegram()
telegram.payload = payload
telegram.group_address = address
yield from self.xknx.telegrams.put(telegram)
await self.xknx.telegrams.put(telegram)
class KNXAutomation():
@ -248,3 +282,59 @@ class KNXAutomation():
hass.data[DATA_KNX].xknx, self.script.async_run,
hook=hook, counter=counter)
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
from homeassistant.components import group
from homeassistant.components.group import \
ENTITY_ID_FORMAT as GROUP_ENTITY_ID_FORMAT
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_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.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers import intent
from homeassistant.loader import bind_hass
import homeassistant.util.color as color_util
@ -29,7 +31,7 @@ DEPENDENCIES = ['group']
SCAN_INTERVAL = timedelta(seconds=30)
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 + '.{}'
@ -84,8 +86,6 @@ LIGHT_PROFILES_FILE = "light_profiles.csv"
PROP_TO_ATTR = {
'brightness': ATTR_BRIGHTNESS,
'color_temp': ATTR_COLOR_TEMP,
'min_mireds': ATTR_MIN_MIREDS,
'max_mireds': ATTR_MAX_MIREDS,
'rgb_color': ATTR_RGB_COLOR,
'xy_color': ATTR_XY_COLOR,
'white_value': ATTR_WHITE_VALUE,
@ -135,6 +135,8 @@ PROFILE_SCHEMA = vol.Schema(
vol.ExactSequence((str, cv.small_float, cv.small_float, cv.byte))
)
INTENT_SET = 'HassLightSet'
_LOGGER = logging.getLogger(__name__)
@ -206,8 +208,9 @@ def async_turn_off(hass, entity_id=None, transition=None):
DOMAIN, SERVICE_TURN_OFF, data))
@callback
@bind_hass
def toggle(hass, entity_id=None, transition=None):
def async_toggle(hass, entity_id=None, transition=None):
"""Toggle all or specified light."""
data = {
key: value for key, value in [
@ -216,7 +219,14 @@ def toggle(hass, entity_id=None, transition=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):
@ -228,7 +238,12 @@ def preprocess_turn_on_alternatives(params):
color_name = params.pop(ATTR_COLOR_NAME, None)
if color_name is not None:
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
try:
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)
if kelvin is not None:
@ -240,20 +255,79 @@ def preprocess_turn_on_alternatives(params):
params[ATTR_BRIGHTNESS] = int(255 * brightness_pct/100)
@asyncio.coroutine
def async_setup(hass, config):
class SetIntentHandler(intent.IntentHandler):
"""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."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS)
yield from component.async_setup(config)
await component.async_setup(config)
# load profiles from files
profiles_valid = yield from Profiles.load_profiles(hass)
profiles_valid = await Profiles.load_profiles(hass)
if not profiles_valid:
return False
@asyncio.coroutine
def async_handle_light_service(service):
async def async_handle_light_service(service):
"""Handle a turn light on or off service call."""
# Get the validated data
params = service.data.copy()
@ -267,18 +341,18 @@ def async_setup(hass, config):
update_tasks = []
for light in target_lights:
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:
yield from light.async_turn_off(**params)
await light.async_turn_off(**params)
else:
yield from light.async_toggle(**params)
await light.async_toggle(**params)
if not light.should_poll:
continue
update_tasks.append(light.async_update_ha_state(True))
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.
hass.services.async_register(
@ -293,6 +367,8 @@ def async_setup(hass, config):
DOMAIN, SERVICE_TOGGLE, async_handle_light_service,
schema=LIGHT_TOGGLE_SCHEMA)
hass.helpers.intent.async_register(SetIntentHandler())
return True
@ -302,8 +378,7 @@ class Profiles:
_all = None
@classmethod
@asyncio.coroutine
def load_profiles(cls, hass):
async def load_profiles(cls, hass):
"""Load and cache profiles."""
def load_profile_data(hass):
"""Load built-in profiles and custom profiles."""
@ -333,7 +408,7 @@ class Profiles:
return None
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
@classmethod
@ -399,6 +474,10 @@ class Light(ToggleEntity):
"""Return optional state attributes."""
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:
for prop, attr in PROP_TO_ATTR.items():
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):
"""Set up the demo light platform."""
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]),
DemoLight("Ceiling Lights", True, True,
DemoLight(2, "Ceiling Lights", True, True,
LIGHT_COLORS[0], LIGHT_TEMPS[1]),
DemoLight("Kitchen Lights", True, True,
DemoLight(3, "Kitchen Lights", True, True,
LIGHT_COLORS[1], LIGHT_TEMPS[0])
])
@ -40,10 +40,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class DemoLight(Light):
"""Representation of a demo light."""
def __init__(self, name, state, available=False, rgb=None, ct=None,
brightness=180, xy_color=(.5, .5), white=200,
def __init__(self, unique_id, name, state, available=False, rgb=None,
ct=None, brightness=180, xy_color=(.5, .5), white=200,
effect_list=None, effect=None):
"""Initialize the light."""
self._unique_id = unique_id
self._name = name
self._state = state
self._rgb = rgb
@ -64,6 +65,11 @@ class DemoLight(Light):
"""Return the name of the light if any."""
return self._name
@property
def unique_id(self):
"""Return unique ID for light."""
return self._unique_id
@property
def available(self) -> bool:
"""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':
color_hue, sat = color_util.color_xy_to_hs(
*kwargs[ATTR_XY_COLOR])
command['hue'] = color_hue
command['sat'] = sat
command['hue'] = color_hue / 360 * 65535
command['sat'] = sat / 100 * 255
else:
command['xy'] = kwargs[ATTR_XY_COLOR]
elif ATTR_RGB_COLOR in kwargs:
if self.info.get('manufacturername') == 'OSRAM':
hsv = color_util.color_RGB_to_hsv(
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
command['hue'] = hsv[0]
command['sat'] = hsv[1]
command['bri'] = hsv[2]
command['hue'] = hsv[0] / 360 * 65535
command['sat'] = hsv[1] / 100 * 255
command['bri'] = hsv[2] / 100 * 255
else:
xyb = color_util.color_RGB_to_xy(
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
command['xy'] = xyb[0], xyb[1]
command['bri'] = xyb[2]
elif ATTR_COLOR_TEMP in kwargs:
temp = kwargs[ATTR_COLOR_TEMP]
command['ct'] = max(self.min_mireds, min(temp, self.max_mireds))

View File

@ -213,9 +213,10 @@ class Hyperion(Light):
except (KeyError, IndexError):
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
if response['info']['activeEffects']:
if response['info'].get('activeEffects'):
self._rgb_color = [175, 0, 255]
self._icon = 'mdi:lava-lamp'
try:

View File

@ -2,15 +2,14 @@
Support for Insteon lights via PowerLinc Modem.
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 logging
from homeassistant.core import callback
from homeassistant.components.insteon_plm import InsteonPLMEntity
from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
from homeassistant.loader import get_component
_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."""
plm = hass.data['insteon_plm']
device_list = []
for device in discovery_info:
name = device.get('address')
address = device.get('address_hex')
dimmable = bool('dimmable' in device.get('capabilities'))
address = discovery_info['address']
device = plm.devices[address]
state_key = discovery_info['state_key']
_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(
InsteonPLMDimmerDevice(hass, plm, address, name, dimmable)
)
new_entity = InsteonPLMDimmerDevice(device, state_key)
async_add_devices(device_list)
async_add_devices([new_entity])
class InsteonPLMDimmerDevice(Light):
class InsteonPLMDimmerDevice(InsteonPLMEntity, Light):
"""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
def brightness(self):
"""Return the brightness of this light between 0..255."""
onlevel = self._plm.get_device_attr(self._address, 'onlevel')
_LOGGER.debug("on level for %s is %s", self._address, onlevel)
onlevel = self._insteon_device_state.value
return int(onlevel)
@property
def is_on(self):
"""Return the boolean response if the node is on."""
onlevel = self._plm.get_device_attr(self._address, 'onlevel')
_LOGGER.debug("on level for %s is %s", self._address, onlevel)
return bool(onlevel)
return bool(self.brightness)
@property
def supported_features(self):
"""Flag supported features."""
if self._dimmable:
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())
return SUPPORT_BRIGHTNESS
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""Turn device on."""
if ATTR_BRIGHTNESS in kwargs:
brightness = int(kwargs[ATTR_BRIGHTNESS])
self._insteon_device_state.set_level(brightness)
else:
brightness = MAX_BRIGHTNESS
self._plm.turn_on(self._address, brightness=brightness)
self._insteon_device_state.on()
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""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
https://home-assistant.io/components/light.knx/
"""
import asyncio
import voluptuous as vol
@ -37,8 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up lights for KNX platform."""
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices)
@ -86,11 +85,10 @@ class KNXLight(Light):
@callback
def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
@asyncio.coroutine
def after_update_callback(device):
async def after_update_callback(device):
"""Call after device was updated."""
# 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)
@property
@ -111,8 +109,8 @@ class KNXLight(Light):
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self.device.brightness \
if self.device.supports_dimming else \
return self.device.current_brightness \
if self.device.supports_brightness else \
None
@property
@ -124,7 +122,7 @@ class KNXLight(Light):
def rgb_color(self):
"""Return the RBG color value."""
if self.device.supports_color:
return self.device.current_color()
return self.device.current_color
return None
@property
@ -156,23 +154,23 @@ class KNXLight(Light):
def supported_features(self):
"""Flag supported features."""
flags = 0
if self.device.supports_dimming:
if self.device.supports_brightness:
flags |= SUPPORT_BRIGHTNESS
if self.device.supports_color:
flags |= SUPPORT_RGB_COLOR
return flags
@asyncio.coroutine
def async_turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs):
"""Turn the light on."""
if ATTR_BRIGHTNESS in kwargs and self.device.supports_dimming:
yield from self.device.set_brightness(int(kwargs[ATTR_BRIGHTNESS]))
if ATTR_BRIGHTNESS in kwargs:
if self.device.supports_brightness:
await self.device.set_brightness(int(kwargs[ATTR_BRIGHTNESS]))
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:
yield from self.device.set_on()
await self.device.set_on()
@asyncio.coroutine
def async_turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""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
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass,
config,
async_add_devices,
discovery_info=None):
"""Set up the LIFX platform."""
if sys.platform == 'win32':
_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:
hue, saturation, brightness = \
color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR])
saturation = convert_8_to_16(saturation)
brightness = convert_8_to_16(brightness)
hue = int(hue / 360 * 65535)
saturation = int(saturation / 100 * 65535)
brightness = int(brightness / 100 * 65535)
kelvin = 3500
if ATTR_XY_COLOR in kwargs:
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
if ATTR_COLOR_TEMP in kwargs:
@ -212,8 +216,7 @@ class LIFXManager(object):
def register_set_state(self):
"""Register the LIFX set_state service call."""
@asyncio.coroutine
def async_service_handle(service):
async def service_handler(service):
"""Apply a service."""
tasks = []
for light in self.service_to_entities(service):
@ -221,36 +224,34 @@ class LIFXManager(object):
task = light.async_set_state(**service.data)
tasks.append(self.hass.async_add_job(task))
if tasks:
yield from asyncio.wait(tasks, loop=self.hass.loop)
await asyncio.wait(tasks, loop=self.hass.loop)
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)
def register_effects(self):
"""Register the LIFX effects as hass service calls."""
@asyncio.coroutine
def async_service_handle(service):
async def service_handler(service):
"""Apply a service, i.e. start an effect."""
entities = self.service_to_entities(service)
if entities:
yield from self.start_effect(
await self.start_effect(
entities, service.service, **service.data)
self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_PULSE, async_service_handle,
DOMAIN, SERVICE_EFFECT_PULSE, service_handler,
schema=LIFX_EFFECT_PULSE_SCHEMA)
self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_COLORLOOP, async_service_handle,
DOMAIN, SERVICE_EFFECT_COLORLOOP, service_handler,
schema=LIFX_EFFECT_COLORLOOP_SCHEMA)
self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_STOP, async_service_handle,
DOMAIN, SERVICE_EFFECT_STOP, service_handler,
schema=LIFX_EFFECT_STOP_SCHEMA)
@asyncio.coroutine
def start_effect(self, entities, service, **kwargs):
async def start_effect(self, entities, service, **kwargs):
"""Start a light effect on entities."""
devices = list(map(lambda l: l.device, entities))
@ -262,7 +263,7 @@ class LIFXManager(object):
mode=kwargs.get(ATTR_MODE),
hsbk=find_hsbk(**kwargs),
)
yield from self.effects_conductor.start(effect, devices)
await self.effects_conductor.start(effect, devices)
elif service == SERVICE_EFFECT_COLORLOOP:
preprocess_turn_on_alternatives(kwargs)
@ -278,9 +279,9 @@ class LIFXManager(object):
transition=kwargs.get(ATTR_TRANSITION),
brightness=brightness,
)
yield from self.effects_conductor.start(effect, devices)
await self.effects_conductor.start(effect, devices)
elif service == SERVICE_EFFECT_STOP:
yield from self.effects_conductor.stop(devices)
await self.effects_conductor.stop(devices)
def service_to_entities(self, service):
"""Return the known devices that a service call mentions."""
@ -295,25 +296,24 @@ class LIFXManager(object):
@callback
def register(self, device):
"""Handle newly detected bulb."""
self.hass.async_add_job(self.async_register(device))
"""Handle aiolifx detected bulb."""
self.hass.async_add_job(self.register_new_device(device))
@asyncio.coroutine
def async_register(self, device):
async def register_new_device(self, device):
"""Handle newly detected bulb."""
if device.mac_addr in self.entities:
entity = self.entities[device.mac_addr]
entity.registered = True
_LOGGER.debug("%s register AGAIN", entity.who)
yield from entity.update_hass()
await entity.update_hass()
else:
_LOGGER.debug("%s register NEW", device.ip_addr)
# Read initial state
ack = AwaitAioLIFX().wait
version_resp = yield from ack(device.get_version)
version_resp = await ack(device.get_version)
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:
_LOGGER.error("Failed to initialize %s", device.ip_addr)
@ -335,7 +335,7 @@ class LIFXManager(object):
@callback
def unregister(self, device):
"""Handle disappearing bulbs."""
"""Handle aiolifx disappearing bulbs."""
if device.mac_addr in self.entities:
entity = self.entities[device.mac_addr]
_LOGGER.debug("%s unregister", entity.who)
@ -359,15 +359,14 @@ class AwaitAioLIFX:
self.message = message
self.event.set()
@asyncio.coroutine
def wait(self, method):
async def wait(self, method):
"""Call an aiolifx method and wait for its response."""
self.device = None
self.message = None
self.event.clear()
method(callb=self.callback)
yield from self.event.wait()
await self.event.wait()
return self.message
@ -464,21 +463,19 @@ class LIFXLight(Light):
return 'lifx_effect_' + effect.name
return None
@asyncio.coroutine
def update_hass(self, now=None):
async def update_hass(self, now=None):
"""Request new status and push it to hass."""
self.postponed_update = None
yield from self.async_update()
yield from self.async_update_ha_state()
await self.async_update()
await self.async_update_ha_state()
@asyncio.coroutine
def update_during_transition(self, when):
async def update_during_transition(self, when):
"""Update state at the start and end of a transition."""
if self.postponed_update:
self.postponed_update()
# Transition has started
yield from self.update_hass()
await self.update_hass()
# Transition has ended
if when > 0:
@ -486,28 +483,25 @@ class LIFXLight(Light):
self.hass, self.update_hass,
util.dt.utcnow() + timedelta(milliseconds=when))
@asyncio.coroutine
def async_turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs):
"""Turn the device on."""
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
def async_turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""Turn the device off."""
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
def async_set_state(self, **kwargs):
async def set_state(self, **kwargs):
"""Set a color on the light and turn it on/off."""
with (yield from self.lock):
async with self.lock:
bulb = self.device
yield from self.effects_conductor.stop([bulb])
await self.effects_conductor.stop([bulb])
if ATTR_EFFECT in kwargs:
yield from self.default_effect(**kwargs)
await self.default_effect(**kwargs)
return
if ATTR_INFRARED in kwargs:
@ -529,51 +523,47 @@ class LIFXLight(Light):
if not self.is_on:
if power_off:
yield from self.set_power(ack, False)
await self.set_power(ack, False)
if hsbk:
yield from self.set_color(ack, hsbk, kwargs)
await self.set_color(ack, hsbk, kwargs)
if power_on:
yield from self.set_power(ack, True, duration=fade)
await self.set_power(ack, True, duration=fade)
else:
if power_on:
yield from self.set_power(ack, True)
await self.set_power(ack, True)
if hsbk:
yield from self.set_color(ack, hsbk, kwargs, duration=fade)
await self.set_color(ack, hsbk, kwargs, duration=fade)
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
yield from asyncio.sleep(0.3)
await asyncio.sleep(0.3)
# Update when the transition starts and ends
yield from self.update_during_transition(fade)
await self.update_during_transition(fade)
@asyncio.coroutine
def set_power(self, ack, pwr, duration=0):
async def set_power(self, ack, pwr, duration=0):
"""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
def set_color(self, ack, hsbk, kwargs, duration=0):
async def set_color(self, ack, hsbk, kwargs, duration=0):
"""Send a color change to the device."""
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
def default_effect(self, **kwargs):
async def default_effect(self, **kwargs):
"""Start an effect with default parameters."""
service = kwargs[ATTR_EFFECT]
data = {
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
def async_update(self):
async def async_update(self):
"""Update bulb status."""
_LOGGER.debug("%s async_update", self.who)
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):
@ -612,15 +602,17 @@ class LIFXColor(LIFXLight):
"""Return the RGB value."""
hue, sat, bri, _ = self.device.color
return color_util.color_hsv_to_RGB(
hue, convert_16_to_8(sat), convert_16_to_8(bri))
hue = hue / 65535 * 360
sat = sat / 65535 * 100
bri = bri / 65535 * 100
return color_util.color_hsv_to_RGB(hue, sat, bri)
class LIFXStrip(LIFXColor):
"""Representation of a LIFX light strip with multiple zones."""
@asyncio.coroutine
def set_color(self, ack, hsbk, kwargs, duration=0):
async def set_color(self, ack, hsbk, kwargs, duration=0):
"""Send a color change to the device."""
bulb = self.device
num_zones = len(bulb.color_zones)
@ -630,7 +622,7 @@ class LIFXStrip(LIFXColor):
# Fast track: setting all zones to the same brightness and color
# can be treated as a single-zone bulb.
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
zones = list(range(0, num_zones))
@ -639,11 +631,11 @@ class LIFXStrip(LIFXColor):
# Zone brightness is not reported when powered off
if not self.is_on and hsbk[2] is None:
yield from self.set_power(ack, True)
yield from asyncio.sleep(0.3)
yield from self.update_color_zones()
yield from self.set_power(ack, False)
yield from asyncio.sleep(0.3)
await self.set_power(ack, True)
await asyncio.sleep(0.3)
await self.update_color_zones()
await self.set_power(ack, False)
await asyncio.sleep(0.3)
# Send new color to each zone
for index, zone in enumerate(zones):
@ -655,23 +647,21 @@ class LIFXStrip(LIFXColor):
color=zone_hsbk,
duration=duration,
apply=apply)
yield from ack(set_zone)
await ack(set_zone)
@asyncio.coroutine
def async_update(self):
async def async_update(self):
"""Update strip status."""
if self.available and not self.lock.locked():
yield from super().async_update()
yield from self.update_color_zones()
await super().async_update()
await self.update_color_zones()
@asyncio.coroutine
def update_color_zones(self):
async def update_color_zones(self):
"""Get updated color information for each zone."""
zone = 0
top = 1
while self.available and zone < top:
# 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,
start_index=zone))
if resp:

View File

@ -17,6 +17,7 @@ from homeassistant.components.light import (
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH,
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
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
REQUIREMENTS = ['limitlessled==1.1.0']
@ -222,6 +223,16 @@ class LimitlessLEDGroup(Light):
"""Return the brightness property."""
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
def color_temp(self):
"""Return the temperature property."""
@ -310,8 +321,11 @@ class LimitlessLEDGroup(Light):
def limitlessled_temperature(self):
"""Convert Home Assistant color temperature units to percentage."""
width = self.max_mireds - self.min_mireds
temperature = 1 - (self._temperature - self.min_mireds) / width
max_kelvin = color_temperature_mired_to_kelvin(self.min_mireds)
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))
def limitlessled_brightness(self):

View File

@ -169,3 +169,13 @@ xiaomi_miio_set_scene:
scene:
description: Number of the fixed scene, between 1 and 4.
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
import logging
from math import ceil
from datetime import timedelta
import datetime
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.exceptions import PlatformNotReady
from homeassistant.util import dt
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Xiaomi Philips Light'
PLATFORM = 'xiaomi_miio'
DATA_KEY = 'light.xiaomi_miio'
CONF_MODEL = 'model'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
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_MODEL): vol.In(
['philips.light.sread1',
'philips.light.ceiling',
'philips.light.zyceiling',
'philips.light.bulb']),
})
REQUIREMENTS = ['python-miio==0.3.7']
@ -36,25 +46,38 @@ REQUIREMENTS = ['python-miio==0.3.7']
CCT_MIN = 1
CCT_MAX = 100
DELAYED_TURN_OFF_MAX_DEVIATION = 4
SUCCESS = ['ok']
ATTR_MODEL = 'model'
ATTR_SCENE = 'scene'
ATTR_DELAYED_TURN_OFF = 'delayed_turn_off'
ATTR_TIME_PERIOD = 'time_period'
SERVICE_SET_SCENE = 'xiaomi_miio_set_scene'
SERVICE_SET_DELAYED_TURN_OFF = 'xiaomi_miio_set_delayed_turn_off'
XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({
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.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_SET_DELAYED_TURN_OFF: {
'method': 'async_set_delayed_turn_off',
'schema': SERVICE_SCHEMA_SET_DELAYED_TURN_OFF},
SERVICE_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):
"""Set up the light from config."""
from miio import Device, DeviceException
if PLATFORM not in hass.data:
hass.data[PLATFORM] = {}
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {}
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
token = config.get(CONF_TOKEN)
model = config.get(CONF_MODEL)
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
try:
light = Device(host, token)
device_info = light.info()
_LOGGER.info("%s %s %s initialized",
device_info.model,
device_info.firmware_version,
device_info.hardware_version)
if model is None:
try:
miio_device = Device(host, token)
device_info = miio_device.info()
model = device_info.model
_LOGGER.info("%s %s %s detected",
model,
device_info.firmware_version,
device_info.hardware_version)
except DeviceException:
raise PlatformNotReady
if device_info.model == 'philips.light.sread1':
from miio import PhilipsEyecare
light = PhilipsEyecare(host, token)
device = XiaomiPhilipsEyecareLamp(name, light, device_info)
elif device_info.model == 'philips.light.ceiling':
from miio import Ceil
light = Ceil(host, token)
device = XiaomiPhilipsCeilingLamp(name, light, device_info)
elif device_info.model == 'philips.light.bulb':
from miio import PhilipsBulb
light = PhilipsBulb(host, token)
device = XiaomiPhilipsLightBall(name, light, device_info)
else:
_LOGGER.error(
'Unsupported device found! Please create an issue at '
'https://github.com/rytilahti/python-miio/issues '
'and provide the following data: %s', device_info.model)
return False
if model == 'philips.light.sread1':
from miio import PhilipsEyecare
light = PhilipsEyecare(host, token)
device = XiaomiPhilipsEyecareLamp(name, light, model)
elif model in ['philips.light.ceiling', 'philips.light.zyceiling']:
from miio import Ceil
light = Ceil(host, token)
device = XiaomiPhilipsCeilingLamp(name, light, model)
elif model == 'philips.light.bulb':
from miio import PhilipsBulb
light = PhilipsBulb(host, token)
device = XiaomiPhilipsLightBall(name, light, model)
else:
_LOGGER.error(
'Unsupported device found! Please create an issue at '
'https://github.com/rytilahti/python-miio/issues '
'and provide the following data: %s', model)
return False
except DeviceException:
raise PlatformNotReady
hass.data[PLATFORM][host] = device
hass.data[DATA_KEY][host] = device
async_add_devices([device], update_before_add=True)
@asyncio.coroutine
@ -113,10 +138,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if key != ATTR_ENTITY_ID}
entity_ids = service.data.get(ATTR_ENTITY_ID)
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]
else:
target_devices = hass.data[PLATFORM].values()
target_devices = hass.data[DATA_KEY].values()
update_tasks = []
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):
"""Representation of a Xiaomi Philips Light."""
def __init__(self, name, light, device_info):
def __init__(self, name, light, model):
"""Initialize the light device."""
self._name = name
self._device_info = device_info
self._model = model
self._brightness = None
self._color_temp = None
@ -147,7 +172,9 @@ class XiaomiPhilipsGenericLight(Light):
self._light = light
self._state = None
self._state_attrs = {
ATTR_MODEL: self._device_info.model,
ATTR_MODEL: self._model,
ATTR_SCENE: None,
ATTR_DELAYED_TURN_OFF: None,
}
@property
@ -217,14 +244,14 @@ class XiaomiPhilipsGenericLight(Light):
if result:
self._brightness = brightness
self._state = yield from self._try_command(
"Turning the light on failed.", self._light.on)
else:
yield from self._try_command(
"Turning the light on failed.", self._light.on)
@asyncio.coroutine
def async_turn_off(self, **kwargs):
"""Turn the light off."""
self._state = yield from self._try_command(
yield from self._try_command(
"Turning the light off failed.", self._light.off)
@asyncio.coroutine
@ -236,9 +263,20 @@ class XiaomiPhilipsGenericLight(Light):
_LOGGER.debug("Got new state: %s", state)
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:
self._state = None
_LOGGER.error("Got exception while fetching the state: %s", ex)
@asyncio.coroutine
@ -248,6 +286,13 @@ class XiaomiPhilipsGenericLight(Light):
"Setting a fixed scene failed.",
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
def translate(value, left_min, left_max, right_min, right_max):
"""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)
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):
"""Representation of a Xiaomi Philips Light Ball."""
@ -339,7 +406,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
self._brightness = brightness
else:
self._state = yield from self._try_command(
yield from self._try_command(
"Turning the light on failed.", self._light.on)
@asyncio.coroutine
@ -351,13 +418,24 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
_LOGGER.debug("Got new state: %s", state)
self._state = state.is_on
self._brightness = ceil((255/100.0) * state.brightness)
self._brightness = ceil((255 / 100.0) * state.brightness)
self._color_temp = self.translate(
state.color_temperature,
CCT_MIN, CCT_MAX,
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:
self._state = None
_LOGGER.error("Got exception while fetching the state: %s", ex)

View File

@ -47,6 +47,11 @@ CONFIG_SCHEMA = vol.Schema({
}),
}, extra=vol.ALLOW_EXTRA)
ALL_EVENT_TYPES = [
EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
]
GROUP_BY_MINUTES = 15
CONTINUOUS_DOMAINS = ['proximity', 'sensor']
@ -266,15 +271,18 @@ def humanify(events):
def _get_events(hass, config, start_day, end_day):
"""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 (
execute, session_scope)
with session_scope(hass=hass) as session:
query = session.query(Events).order_by(
Events.time_fired).filter(
(Events.time_fired > start_day) &
(Events.time_fired < end_day))
query = session.query(Events).order_by(Events.time_fired) \
.outerjoin(States, (Events.event_id == States.event_id)) \
.filter(Events.event_type.in_(ALL_EVENT_TYPES)) \
.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)
return humanify(_exclude_events(events, config))

View File

@ -25,6 +25,10 @@ DEPENDENCIES = ['apple_tv']
_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
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@ -79,7 +83,7 @@ class AppleTvDevice(MediaPlayerDevice):
@property
def unique_id(self):
"""Return an unique ID."""
"""Return a unique ID."""
return self.atv.metadata.device_id
@property
@ -196,14 +200,7 @@ class AppleTvDevice(MediaPlayerDevice):
@property
def supported_features(self):
"""Flag media player features that are supported."""
features = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA
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
return SUPPORT_APPLE_TV
@asyncio.coroutine
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/
"""
# pylint: disable=import-error
import asyncio
import logging
import threading
import functools
@ -135,9 +134,8 @@ def _async_create_cast_device(hass, chromecast):
return None
@asyncio.coroutine
def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_devices, discovery_info=None):
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_devices, discovery_info=None):
"""Set up the cast platform."""
import pychromecast
@ -187,7 +185,7 @@ def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
try:
func = functools.partial(pychromecast.Chromecast, *want_host,
tries=SOCKET_CLIENT_RETRIES)
chromecast = yield from hass.async_add_job(func)
chromecast = await hass.async_add_job(func)
except pychromecast.ChromecastConnectionError as err:
_LOGGER.warning("Can't set up chromecast on %s: %s",
want_host[0], err)
@ -420,7 +418,7 @@ class CastDevice(MediaPlayerDevice):
@property
def unique_id(self) -> str:
"""Return an unique ID."""
"""Return a unique ID."""
if self.cast.uuid is not None:
return str(self.cast.uuid)
return None
@ -439,8 +437,7 @@ class CastDevice(MediaPlayerDevice):
self.cast_status = self.cast.status
self.media_status = self.cast.media_controller.status
@asyncio.coroutine
def async_will_remove_from_hass(self):
async def async_will_remove_from_hass(self) -> None:
"""Disconnect Chromecast object when removed."""
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 = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
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 = \
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):
@ -284,15 +286,7 @@ class DemoMusicPlayer(AbstractDemoPlayer):
@property
def supported_features(self):
"""Flag media player features that are supported."""
support = 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
return MUSIC_PLAYER_SUPPORT
def media_previous_track(self):
"""Send previous track command."""
@ -379,15 +373,7 @@ class DemoTVShowPlayer(AbstractDemoPlayer):
@property
def supported_features(self):
"""Flag media player features that are supported."""
support = 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
return NETFLIX_PLAYER_SUPPORT
def media_previous_track(self):
"""Send previous track command."""

View File

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

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