Merge pull request #13554 from home-assistant/rc

0.66.0
This commit is contained in:
Paulus Schoutsen 2018-03-30 14:42:50 -07:00 committed by GitHub
commit 0d62f472cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
393 changed files with 11384 additions and 5321 deletions

View file

@ -109,6 +109,9 @@ omit =
homeassistant/components/homematic/__init__.py homeassistant/components/homematic/__init__.py
homeassistant/components/*/homematic.py homeassistant/components/*/homematic.py
homeassistant/components/homematicip_cloud.py
homeassistant/components/*/homematicip_cloud.py
homeassistant/components/ihc/* homeassistant/components/ihc/*
homeassistant/components/*/ihc.py homeassistant/components/*/ihc.py
@ -309,6 +312,7 @@ omit =
homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/ialarm.py homeassistant/components/alarm_control_panel/ialarm.py
homeassistant/components/alarm_control_panel/ifttt.py
homeassistant/components/alarm_control_panel/manual_mqtt.py homeassistant/components/alarm_control_panel/manual_mqtt.py
homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py homeassistant/components/alarm_control_panel/simplisafe.py
@ -332,6 +336,7 @@ omit =
homeassistant/components/camera/foscam.py homeassistant/components/camera/foscam.py
homeassistant/components/camera/mjpeg.py homeassistant/components/camera/mjpeg.py
homeassistant/components/camera/onvif.py homeassistant/components/camera/onvif.py
homeassistant/components/camera/proxy.py
homeassistant/components/camera/ring.py homeassistant/components/camera/ring.py
homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/synology.py homeassistant/components/camera/synology.py
@ -403,20 +408,20 @@ omit =
homeassistant/components/image_processing/dlib_face_detect.py homeassistant/components/image_processing/dlib_face_detect.py
homeassistant/components/image_processing/dlib_face_identify.py homeassistant/components/image_processing/dlib_face_identify.py
homeassistant/components/image_processing/seven_segments.py homeassistant/components/image_processing/seven_segments.py
homeassistant/components/keyboard.py
homeassistant/components/keyboard_remote.py homeassistant/components/keyboard_remote.py
homeassistant/components/keyboard.py
homeassistant/components/light/avion.py homeassistant/components/light/avion.py
homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinksticklight.py
homeassistant/components/light/blinkt.py homeassistant/components/light/blinkt.py
homeassistant/components/light/decora.py
homeassistant/components/light/decora_wifi.py homeassistant/components/light/decora_wifi.py
homeassistant/components/light/decora.py
homeassistant/components/light/flux_led.py homeassistant/components/light/flux_led.py
homeassistant/components/light/greenwave.py homeassistant/components/light/greenwave.py
homeassistant/components/light/hue.py homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py homeassistant/components/light/hyperion.py
homeassistant/components/light/iglo.py homeassistant/components/light/iglo.py
homeassistant/components/light/lifx.py
homeassistant/components/light/lifx_legacy.py homeassistant/components/light/lifx_legacy.py
homeassistant/components/light/lifx.py
homeassistant/components/light/limitlessled.py homeassistant/components/light/limitlessled.py
homeassistant/components/light/mystrom.py homeassistant/components/light/mystrom.py
homeassistant/components/light/osramlightify.py homeassistant/components/light/osramlightify.py
@ -442,6 +447,7 @@ omit =
homeassistant/components/media_player/bluesound.py homeassistant/components/media_player/bluesound.py
homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py homeassistant/components/media_player/cast.py
homeassistant/components/media_player/channels.py
homeassistant/components/media_player/clementine.py homeassistant/components/media_player/clementine.py
homeassistant/components/media_player/cmus.py homeassistant/components/media_player/cmus.py
homeassistant/components/media_player/denon.py homeassistant/components/media_player/denon.py
@ -482,8 +488,8 @@ omit =
homeassistant/components/media_player/vlc.py homeassistant/components/media_player/vlc.py
homeassistant/components/media_player/volumio.py homeassistant/components/media_player/volumio.py
homeassistant/components/media_player/xiaomi_tv.py homeassistant/components/media_player/xiaomi_tv.py
homeassistant/components/media_player/yamaha.py
homeassistant/components/media_player/yamaha_musiccast.py homeassistant/components/media_player/yamaha_musiccast.py
homeassistant/components/media_player/yamaha.py
homeassistant/components/media_player/ziggo_mediabox_xl.py homeassistant/components/media_player/ziggo_mediabox_xl.py
homeassistant/components/mycroft.py homeassistant/components/mycroft.py
homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_lambda.py
@ -491,8 +497,8 @@ omit =
homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/ciscospark.py homeassistant/components/notify/ciscospark.py
homeassistant/components/notify/clickatell.py homeassistant/components/notify/clickatell.py
homeassistant/components/notify/clicksend.py
homeassistant/components/notify/clicksend_tts.py homeassistant/components/notify/clicksend_tts.py
homeassistant/components/notify/clicksend.py
homeassistant/components/notify/discord.py homeassistant/components/notify/discord.py
homeassistant/components/notify/free_mobile.py homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py homeassistant/components/notify/gntp.py
@ -517,6 +523,7 @@ omit =
homeassistant/components/notify/sendgrid.py homeassistant/components/notify/sendgrid.py
homeassistant/components/notify/simplepush.py homeassistant/components/notify/simplepush.py
homeassistant/components/notify/slack.py homeassistant/components/notify/slack.py
homeassistant/components/notify/stride.py
homeassistant/components/notify/smtp.py homeassistant/components/notify/smtp.py
homeassistant/components/notify/synology_chat.py homeassistant/components/notify/synology_chat.py
homeassistant/components/notify/syslog.py homeassistant/components/notify/syslog.py
@ -554,7 +561,6 @@ omit =
homeassistant/components/sensor/crimereports.py homeassistant/components/sensor/crimereports.py
homeassistant/components/sensor/cups.py homeassistant/components/sensor/cups.py
homeassistant/components/sensor/currencylayer.py homeassistant/components/sensor/currencylayer.py
homeassistant/components/sensor/darksky.py
homeassistant/components/sensor/deluge.py homeassistant/components/sensor/deluge.py
homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/deutsche_bahn.py
homeassistant/components/sensor/dht.py homeassistant/components/sensor/dht.py
@ -576,6 +582,7 @@ omit =
homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/folder.py homeassistant/components/sensor/folder.py
homeassistant/components/sensor/foobot.py
homeassistant/components/sensor/fritzbox_callmonitor.py homeassistant/components/sensor/fritzbox_callmonitor.py
homeassistant/components/sensor/fritzbox_netmonitor.py homeassistant/components/sensor/fritzbox_netmonitor.py
homeassistant/components/sensor/gearbest.py homeassistant/components/sensor/gearbest.py
@ -588,8 +595,8 @@ omit =
homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py homeassistant/components/sensor/htu21d.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/influxdb.py
homeassistant/components/sensor/irish_rail_transport.py homeassistant/components/sensor/irish_rail_transport.py
homeassistant/components/sensor/kwb.py homeassistant/components/sensor/kwb.py
@ -632,8 +639,8 @@ omit =
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/sense.py homeassistant/components/sensor/sense.py
homeassistant/components/sensor/sensehat.py homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial.py
homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/serial.py
homeassistant/components/sensor/shodan.py homeassistant/components/sensor/shodan.py
homeassistant/components/sensor/simulated.py homeassistant/components/sensor/simulated.py
homeassistant/components/sensor/skybeacon.py homeassistant/components/sensor/skybeacon.py
@ -647,6 +654,7 @@ omit =
homeassistant/components/sensor/supervisord.py homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_hydrological_data.py homeassistant/components/sensor/swiss_hydrological_data.py
homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/syncthru.py
homeassistant/components/sensor/synologydsm.py homeassistant/components/sensor/synologydsm.py
homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/sytadin.py homeassistant/components/sensor/sytadin.py
@ -656,6 +664,7 @@ omit =
homeassistant/components/sensor/tibber.py homeassistant/components/sensor/tibber.py
homeassistant/components/sensor/time_date.py homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/torque.py homeassistant/components/sensor/torque.py
homeassistant/components/sensor/trafikverket_weatherstation.py
homeassistant/components/sensor/transmission.py homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/travisci.py homeassistant/components/sensor/travisci.py
homeassistant/components/sensor/twitch.py homeassistant/components/sensor/twitch.py
@ -697,6 +706,7 @@ omit =
homeassistant/components/switch/telnet.py homeassistant/components/switch/telnet.py
homeassistant/components/switch/tplink.py homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py homeassistant/components/switch/transmission.py
homeassistant/components/switch/vesync.py
homeassistant/components/switch/xiaomi_miio.py homeassistant/components/switch/xiaomi_miio.py
homeassistant/components/telegram_bot/* homeassistant/components/telegram_bot/*
homeassistant/components/thingspeak.py homeassistant/components/thingspeak.py

View file

@ -12,19 +12,18 @@
## Checklist: ## Checklist:
- [ ] The code change is tested and works locally. - [ ] The code change is tested and works locally.
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
If user exposed functionality or configuration variables are added/changed: If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) - [ ] Documentation added/updated in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io)
If the code communicates with devices, web services, or third-party tools: If the code communicates with devices, web services, or third-party tools:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
- [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. - [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
- [ ] New files were added to `.coveragerc`. - [ ] New files were added to `.coveragerc`.
If the code does not interact with devices: If the code does not interact with devices:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
- [ ] Tests have been added to verify that the new code works. - [ ] Tests have been added to verify that the new code works.
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14 [ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14

1
.gitignore vendored
View file

@ -21,6 +21,7 @@ Icon
*.iml *.iml
# pytest # pytest
.pytest_cache
.cache .cache
# GITHUB Proposed Python stuff: # GITHUB Proposed Python stuff:

0
.gitmodules vendored
View file

View file

@ -49,6 +49,7 @@ homeassistant/components/camera/yi.py @bachya
homeassistant/components/climate/ephember.py @ttroy50 homeassistant/components/climate/ephember.py @ttroy50
homeassistant/components/climate/eq3btsmart.py @rytilahti homeassistant/components/climate/eq3btsmart.py @rytilahti
homeassistant/components/climate/sensibo.py @andrey-git homeassistant/components/climate/sensibo.py @andrey-git
homeassistant/components/cover/group.py @cdce8p
homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/cover/template.py @PhracturedBlue
homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/automatic.py @armills
homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/device_tracker/tile.py @bachya

View file

@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot
The process is straight-forward. The process is straight-forward.
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/devel/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0) - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
- Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant). - Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant).
- Write the code for your device, notification service, sensor, or IoT thing. - Write the code for your device, notification service, sensor, or IoT thing.
- Ensure tests work. - Ensure tests work.

View file

@ -4,10 +4,10 @@ homeassistant.util package
Submodules Submodules
---------- ----------
homeassistant.util.async module homeassistant.util.async_ module
------------------------------- -------------------------------
.. automodule:: homeassistant.util.async .. automodule:: homeassistant.util.async_
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View file

@ -272,7 +272,7 @@ def setup_and_run_hass(config_dir: str,
if args.open_ui: if args.open_ui:
# Imported here to avoid importing asyncio before monkey patch # Imported here to avoid importing asyncio before monkey patch
from homeassistant.util.async import run_callback_threadsafe from homeassistant.util.async_ import run_callback_threadsafe
def open_browser(event): def open_browser(event):
"""Open the webinterface in a browser.""" """Open the webinterface in a browser."""
@ -335,7 +335,8 @@ def main() -> int:
"""Start Home Assistant.""" """Start Home Assistant."""
validate_python() validate_python()
if os.environ.get('HASS_NO_MONKEY') != '1': monkey_patch_needed = sys.version_info[:3] < (3, 6, 3)
if monkey_patch_needed and os.environ.get('HASS_NO_MONKEY') != '1':
if sys.version_info[:2] >= (3, 6): if sys.version_info[:2] >= (3, 6):
monkey_patch.disable_c_asyncio() monkey_patch.disable_c_asyncio()
monkey_patch.patch_weakref_tasks() monkey_patch.patch_weakref_tasks()

View file

@ -86,14 +86,6 @@ def async_from_config_dict(config: Dict[str, Any],
if enable_log: if enable_log:
async_enable_logging(hass, verbose, log_rotate_days, log_file) async_enable_logging(hass, verbose, log_rotate_days, log_file)
if sys.version_info[:2] < (3, 5):
_LOGGER.warning(
'Python 3.4 support has been deprecated and will be removed in '
'the beginning of 2018. Please upgrade Python or your operating '
'system. More info: https://home-assistant.io/blog/2017/10/06/'
'deprecating-python-3.4-support/'
)
core_config = config.get(core.DOMAIN, {}) core_config = config.get(core.DOMAIN, {})
try: try:

View file

@ -12,13 +12,14 @@ import requests
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED) STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED,
STATE_ALARM_ARMED_NIGHT)
from homeassistant.components.egardia import ( from homeassistant.components.egardia import (
EGARDIA_DEVICE, EGARDIA_SERVER, EGARDIA_DEVICE, EGARDIA_SERVER,
REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES, REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES,
CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT
) )
REQUIREMENTS = ['pythonegardia==1.0.38'] DEPENDENCIES = ['egardia']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -27,6 +28,8 @@ STATES = {
'DAY HOME': STATE_ALARM_ARMED_HOME, 'DAY HOME': STATE_ALARM_ARMED_HOME,
'DISARM': STATE_ALARM_DISARMED, 'DISARM': STATE_ALARM_DISARMED,
'ARMHOME': STATE_ALARM_ARMED_HOME, 'ARMHOME': STATE_ALARM_ARMED_HOME,
'HOME': STATE_ALARM_ARMED_HOME,
'NIGHT HOME': STATE_ALARM_ARMED_NIGHT,
'TRIGGERED': STATE_ALARM_TRIGGERED 'TRIGGERED': STATE_ALARM_TRIGGERED
} }

View file

@ -0,0 +1,170 @@
"""
Interfaces with alarm control panels that have to be controlled through IFTTT.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.ifttt/
"""
import logging
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import (
DOMAIN, PLATFORM_SCHEMA)
from homeassistant.components.ifttt import (
ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE,
CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['ifttt']
_LOGGER = logging.getLogger(__name__)
ALLOWED_STATES = [
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME]
DATA_IFTTT_ALARM = 'ifttt_alarm'
DEFAULT_NAME = "Home"
CONF_EVENT_AWAY = "event_arm_away"
CONF_EVENT_HOME = "event_arm_home"
CONF_EVENT_NIGHT = "event_arm_night"
CONF_EVENT_DISARM = "event_disarm"
DEFAULT_EVENT_AWAY = "alarm_arm_away"
DEFAULT_EVENT_HOME = "alarm_arm_home"
DEFAULT_EVENT_NIGHT = "alarm_arm_night"
DEFAULT_EVENT_DISARM = "alarm_disarm"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_CODE): cv.string,
vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string,
vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string,
vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string,
vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
})
SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state"
PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_STATE): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a control panel managed through IFTTT."""
if DATA_IFTTT_ALARM not in hass.data:
hass.data[DATA_IFTTT_ALARM] = []
name = config.get(CONF_NAME)
code = config.get(CONF_CODE)
event_away = config.get(CONF_EVENT_AWAY)
event_home = config.get(CONF_EVENT_HOME)
event_night = config.get(CONF_EVENT_NIGHT)
event_disarm = config.get(CONF_EVENT_DISARM)
optimistic = config.get(CONF_OPTIMISTIC)
alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home,
event_night, event_disarm, optimistic)
hass.data[DATA_IFTTT_ALARM].append(alarmpanel)
add_devices([alarmpanel])
async def push_state_update(service):
"""Set the service state as device state attribute."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
state = service.data.get(ATTR_STATE)
devices = hass.data[DATA_IFTTT_ALARM]
if entity_ids:
devices = [d for d in devices if d.entity_id in entity_ids]
for device in devices:
device.push_alarm_state(state)
device.async_schedule_update_ha_state()
hass.services.register(DOMAIN, SERVICE_PUSH_ALARM_STATE, push_state_update,
schema=PUSH_ALARM_STATE_SERVICE_SCHEMA)
class IFTTTAlarmPanel(alarm.AlarmControlPanel):
"""Representation of an alarm control panel controlled throught IFTTT."""
def __init__(self, name, code, event_away, event_home, event_night,
event_disarm, optimistic):
"""Initialize the alarm control panel."""
self._name = name
self._code = code
self._event_away = event_away
self._event_home = event_home
self._event_night = event_night
self._event_disarm = event_disarm
self._optimistic = optimistic
self._state = None
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def assumed_state(self):
"""Notify that this platform return an assumed state."""
return True
@property
def code_format(self):
"""Return one or more characters."""
return None if self._code is None else '.+'
def alarm_disarm(self, code=None):
"""Send disarm command."""
if not self._check_code(code):
return
self.set_alarm_state(self._event_disarm, STATE_ALARM_DISARMED)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
if not self._check_code(code):
return
self.set_alarm_state(self._event_away, STATE_ALARM_ARMED_AWAY)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
if not self._check_code(code):
return
self.set_alarm_state(self._event_home, STATE_ALARM_ARMED_HOME)
def alarm_arm_night(self, code=None):
"""Send arm night command."""
if not self._check_code(code):
return
self.set_alarm_state(self._event_night, STATE_ALARM_ARMED_NIGHT)
def set_alarm_state(self, event, state):
"""Call the IFTTT trigger service to change the alarm state."""
data = {ATTR_EVENT: event}
self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data)
_LOGGER.debug("Called IFTTT component to trigger event %s", event)
if self._optimistic:
self._state = state
def push_alarm_state(self, value):
"""Push the alarm state to the given value."""
if value in ALLOWED_STATES:
_LOGGER.debug("Pushed the alarm state to %s", value)
self._state = value
def _check_code(self, code):
return self._code is None or self._code == code

View file

@ -69,3 +69,13 @@ alarmdecoder_alarm_toggle_chime:
code: code:
description: A required code to toggle the alarm control panel chime with. description: A required code to toggle the alarm control panel chime with.
example: 1234 example: 1234
ifttt_push_alarm_state:
description: Update the alarm state to the specified value.
fields:
entity_id:
description: Name of the alarm control panel which state has to be updated.
example: 'alarm_control_panel.downstairs'
state:
description: The state to which the alarm control panel has to be set.
example: 'armed_night'

View file

@ -438,9 +438,7 @@ class _LightCapabilities(_AlexaEntity):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & light.SUPPORT_BRIGHTNESS: if supported & light.SUPPORT_BRIGHTNESS:
yield _AlexaBrightnessController(self.entity) yield _AlexaBrightnessController(self.entity)
if supported & light.SUPPORT_RGB_COLOR: if supported & light.SUPPORT_COLOR:
yield _AlexaColorController(self.entity)
if supported & light.SUPPORT_XY_COLOR:
yield _AlexaColorController(self.entity) yield _AlexaColorController(self.entity)
if supported & light.SUPPORT_COLOR_TEMP: if supported & light.SUPPORT_COLOR_TEMP:
yield _AlexaColorTemperatureController(self.entity) yield _AlexaColorTemperatureController(self.entity)
@ -842,25 +840,16 @@ def async_api_adjust_brightness(hass, config, request, entity):
@asyncio.coroutine @asyncio.coroutine
def async_api_set_color(hass, config, request, entity): def async_api_set_color(hass, config, request, entity):
"""Process a set color request.""" """Process a set color request."""
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
rgb = color_util.color_hsb_to_RGB( rgb = color_util.color_hsb_to_RGB(
float(request[API_PAYLOAD]['color']['hue']), float(request[API_PAYLOAD]['color']['hue']),
float(request[API_PAYLOAD]['color']['saturation']), float(request[API_PAYLOAD]['color']['saturation']),
float(request[API_PAYLOAD]['color']['brightness']) float(request[API_PAYLOAD]['color']['brightness'])
) )
if supported & light.SUPPORT_RGB_COLOR > 0: yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id,
ATTR_ENTITY_ID: entity.entity_id, light.ATTR_RGB_COLOR: rgb,
light.ATTR_RGB_COLOR: rgb, }, blocking=False)
}, blocking=False)
else:
xyz = color_util.color_RGB_to_xy(*rgb)
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_XY_COLOR: (xyz[0], xyz[1]),
light.ATTR_BRIGHTNESS: xyz[2],
}, blocking=False)
return api_message(request) return api_message(request)

View file

@ -0,0 +1,118 @@
"""
Reads vehicle status from BMW connected drive portal.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.bmw_connected_drive/
"""
import asyncio
import logging
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
from homeassistant.components.binary_sensor import BinarySensorDevice
DEPENDENCIES = ['bmw_connected_drive']
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {
'lids': ['Doors', 'opening'],
'windows': ['Windows', 'opening'],
'door_lock_state': ['Door lock state', 'safety']
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the BMW sensors."""
accounts = hass.data[BMW_DOMAIN]
_LOGGER.debug('Found BMW accounts: %s',
', '.join([a.name for a in accounts]))
devices = []
for account in accounts:
for vehicle in account.account.vehicles:
for key, value in sorted(SENSOR_TYPES.items()):
device = BMWConnectedDriveSensor(account, vehicle, key,
value[0], value[1])
devices.append(device)
add_devices(devices, True)
class BMWConnectedDriveSensor(BinarySensorDevice):
"""Representation of a BMW vehicle binary sensor."""
def __init__(self, account, vehicle, attribute: str, sensor_name,
device_class):
"""Constructor."""
self._account = account
self._vehicle = vehicle
self._attribute = attribute
self._name = '{} {}'.format(self._vehicle.modelName, self._attribute)
self._sensor_name = sensor_name
self._device_class = device_class
self._state = None
@property
def should_poll(self) -> bool:
"""Data update is triggered from BMWConnectedDriveEntity."""
return False
@property
def name(self):
"""Return the name of the binary sensor."""
return self._name
@property
def device_class(self):
"""Return the class of the binary sensor."""
return self._device_class
@property
def is_on(self):
"""Return the state of the binary sensor."""
return self._state
@property
def device_state_attributes(self):
"""Return the state attributes of the binary sensor."""
vehicle_state = self._vehicle.state
result = {
'car': self._vehicle.modelName
}
if self._attribute == 'lids':
for lid in vehicle_state.lids:
result[lid.name] = lid.state.value
elif self._attribute == 'windows':
for window in vehicle_state.windows:
result[window.name] = window.state.value
elif self._attribute == 'door_lock_state':
result['door_lock_state'] = vehicle_state.door_lock_state.value
return result
def update(self):
"""Read new state data from the library."""
vehicle_state = self._vehicle.state
# device class opening: On means open, Off means closed
if self._attribute == 'lids':
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
self._state = not vehicle_state.all_lids_closed
if self._attribute == 'windows':
self._state = not vehicle_state.all_windows_closed
# device class safety: On means unsafe, Off means safe
if self._attribute == 'door_lock_state':
# Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED
self._state = bool(vehicle_state.door_lock_state.value
in ('SELECTIVELOCKED', 'UNLOCKED'))
def update_callback(self):
"""Schedule a state update."""
self.schedule_update_ha_state(True)
@asyncio.coroutine
def async_added_to_hass(self):
"""Add callback after being added to hass.
Show latest data after startup.
"""
self._account.add_update_listener(self.update_callback)

View file

@ -4,8 +4,6 @@ Support for deCONZ binary sensor.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.deconz/ https://home-assistant.io/components/binary_sensor.deconz/
""" """
import asyncio
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz import ( from homeassistant.components.deconz import (
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
@ -15,8 +13,8 @@ from homeassistant.core import callback
DEPENDENCIES = ['deconz'] DEPENDENCIES = ['deconz']
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Set up the deCONZ binary sensor.""" """Set up the deCONZ binary sensor."""
if discovery_info is None: if discovery_info is None:
return return
@ -25,8 +23,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
sensors = hass.data[DATA_DECONZ].sensors sensors = hass.data[DATA_DECONZ].sensors
entities = [] entities = []
for key in sorted(sensors.keys(), key=int): for sensor in sensors.values():
sensor = sensors[key]
if sensor and sensor.type in DECONZ_BINARY_SENSOR: if sensor and sensor.type in DECONZ_BINARY_SENSOR:
entities.append(DeconzBinarySensor(sensor)) entities.append(DeconzBinarySensor(sensor))
async_add_devices(entities, True) async_add_devices(entities, True)
@ -39,8 +36,7 @@ class DeconzBinarySensor(BinarySensorDevice):
"""Set up sensor and add update callback to get data from websocket.""" """Set up sensor and add update callback to get data from websocket."""
self._sensor = sensor self._sensor = sensor
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Subscribe sensors events.""" """Subscribe sensors events."""
self._sensor.register_async_callback(self.async_update_callback) self._sensor.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id
@ -96,9 +92,9 @@ class DeconzBinarySensor(BinarySensorDevice):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the sensor.""" """Return the state attributes of the sensor."""
from pydeconz.sensor import PRESENCE from pydeconz.sensor import PRESENCE
attr = { attr = {}
ATTR_BATTERY_LEVEL: self._sensor.battery, if self._sensor.battery:
} attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
if self._sensor.type in PRESENCE: if self._sensor.type in PRESENCE and self._sensor.dark:
attr['dark'] = self._sensor.dark attr['dark'] = self._sensor.dark
return attr return attr

View file

@ -12,7 +12,7 @@ from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.components.egardia import ( from homeassistant.components.egardia import (
EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES) EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['egardia']
EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion', EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion',
'Door Contact': 'opening', 'Door Contact': 'opening',
'IR': 'motion'} 'IR': 'motion'}

View file

@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {'openClosedSensor': 'opening', SENSOR_TYPES = {'openClosedSensor': 'opening',
'motionSensor': 'motion', 'motionSensor': 'motion',
'doorSensor': 'door', 'doorSensor': 'door',
'leakSensor': 'moisture'} 'wetLeakSensor': 'moisture'}
@asyncio.coroutine @asyncio.coroutine
@ -28,13 +28,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
address = discovery_info['address'] address = discovery_info['address']
device = plm.devices[address] device = plm.devices[address]
state_key = discovery_info['state_key'] state_key = discovery_info['state_key']
name = device.states[state_key].name
if name != 'dryLeakSensor':
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform',
device.address.hex, device.states[state_key].name)
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', new_entity = InsteonPLMBinarySensor(device, state_key)
device.address.hex, device.states[state_key].name)
new_entity = InsteonPLMBinarySensor(device, state_key) async_add_devices([new_entity])
async_add_devices([new_entity])
class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
@ -53,5 +54,4 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
@property @property
def is_on(self): def is_on(self):
"""Return the boolean response if the node is on.""" """Return the boolean response if the node is on."""
sensorstate = self._insteon_device_state.value return bool(self._insteon_device_state.value)
return bool(sensorstate)

View file

@ -9,6 +9,17 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASSES, DOMAIN, BinarySensorDevice) DEVICE_CLASSES, DOMAIN, BinarySensorDevice)
from homeassistant.const import STATE_ON from homeassistant.const import STATE_ON
SENSORS = {
'S_DOOR': 'door',
'S_MOTION': 'motion',
'S_SMOKE': 'smoke',
'S_SPRINKLER': 'safety',
'S_WATER_LEAK': 'safety',
'S_SOUND': 'sound',
'S_VIBRATION': 'vibration',
'S_MOISTURE': 'moisture',
}
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MySensors platform for binary sensors.""" """Set up the MySensors platform for binary sensors."""
@ -29,18 +40,7 @@ class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
def device_class(self): def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
pres = self.gateway.const.Presentation pres = self.gateway.const.Presentation
class_map = { device_class = SENSORS.get(pres(self.child_type).name)
pres.S_DOOR: 'opening', if device_class in DEVICE_CLASSES:
pres.S_MOTION: 'motion', return device_class
pres.S_SMOKE: 'smoke', return None
}
if float(self.gateway.protocol_version) >= 1.5:
class_map.update({
pres.S_SPRINKLER: 'sprinkler',
pres.S_WATER_LEAK: 'leak',
pres.S_SOUND: 'sound',
pres.S_VIBRATION: 'vibration',
pres.S_MOISTURE: 'moisture',
})
if class_map.get(self.child_type) in DEVICE_CLASSES:
return class_map.get(self.child_type)

View file

@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
from homeassistant.util import utcnow from homeassistant.util import utcnow
REQUIREMENTS = ['numpy==1.14.0'] REQUIREMENTS = ['numpy==1.14.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['holidays==0.9.3'] REQUIREMENTS = ['holidays==0.9.4']
# List of all countries currently supported by holidays # List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime # There seems to be no way to get the list out at runtime

View file

@ -37,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
BMW_COMPONENTS = ['device_tracker', 'sensor'] BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor']
UPDATE_INTERVAL = 5 # in minutes UPDATE_INTERVAL = 5 # in minutes

View file

@ -194,7 +194,9 @@ class WebDavCalendarData(object):
@staticmethod @staticmethod
def is_over(vevent): def is_over(vevent):
"""Return if the event is over.""" """Return if the event is over."""
return dt.now() > WebDavCalendarData.get_end_date(vevent) return dt.now() >= WebDavCalendarData.to_datetime(
WebDavCalendarData.get_end_date(vevent)
)
@staticmethod @staticmethod
def get_hass_date(obj): def get_hass_date(obj):
@ -230,4 +232,4 @@ class WebDavCalendarData(object):
else: else:
enddate = obj.dtstart.value + timedelta(days=1) enddate = obj.dtstart.value + timedelta(days=1)
return WebDavCalendarData.to_datetime(enddate) return enddate

View file

@ -62,7 +62,14 @@ class GoogleCalendarData(object):
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
"""Get the latest data.""" """Get the latest data."""
service = self.calendar_service.get() from httplib2 import ServerNotFoundError
try:
service = self.calendar_service.get()
except ServerNotFoundError:
_LOGGER.warning("Unable to connect to Google, using cached data")
return False
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
params['timeMin'] = dt.now().isoformat('T') params['timeMin'] = dt.now().isoformat('T')
params['calendarId'] = self.calendar_id params['calendarId'] = self.calendar_id

View file

@ -496,6 +496,10 @@ class TodoistProjectData(object):
# We had no valid tasks # We had no valid tasks
return True return True
# Make sure the task collection is reset to prevent an
# infinite collection repeating the same tasks
self.all_project_tasks.clear()
# Organize the best tasks (so users can see all the tasks # Organize the best tasks (so users can see all the tasks
# they have, organized) # they have, organized)
while project_tasks: while project_tasks:

View file

@ -21,7 +21,7 @@ from homeassistant.components.camera import (
PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, Camera) PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, Camera)
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util.async_ import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -6,6 +6,7 @@ https://home-assistant.io/components/camera.onvif/
""" """
import asyncio import asyncio
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
@ -103,92 +104,128 @@ class ONVIFHassCamera(Camera):
def __init__(self, hass, config): def __init__(self, hass, config):
"""Initialize a ONVIF camera.""" """Initialize a ONVIF camera."""
from onvif import ONVIFCamera, exceptions
super().__init__() super().__init__()
import onvif
self._username = config.get(CONF_USERNAME)
self._password = config.get(CONF_PASSWORD)
self._host = config.get(CONF_HOST)
self._port = config.get(CONF_PORT)
self._name = config.get(CONF_NAME) self._name = config.get(CONF_NAME)
self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS) self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS)
self._profile_index = config.get(CONF_PROFILE)
self._input = None self._input = None
camera = None self._media_service = \
onvif.ONVIFService('http://{}:{}/onvif/device_service'.format(
self._host, self._port),
self._username, self._password,
'{}/wsdl/media.wsdl'.format(os.path.dirname(
onvif.__file__)))
self._ptz_service = \
onvif.ONVIFService('http://{}:{}/onvif/device_service'.format(
self._host, self._port),
self._username, self._password,
'{}/wsdl/ptz.wsdl'.format(os.path.dirname(
onvif.__file__)))
def obtain_input_uri(self):
"""Set the input uri for the camera."""
from onvif import exceptions
_LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
self._host, self._port)
try: try:
_LOGGER.debug("Connecting with ONVIF Camera: %s on port %s", profiles = self._media_service.GetProfiles()
config.get(CONF_HOST), config.get(CONF_PORT))
camera = ONVIFCamera( if self._profile_index >= len(profiles):
config.get(CONF_HOST), config.get(CONF_PORT),
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)
)
media_service = camera.create_media_service()
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." _LOGGER.warning("ONVIF Camera '%s' doesn't provide profile %d."
" Using the last profile.", " Using the last profile.",
self._name, self._profile_index) self._name, self._profile_index)
self._profile_index = -1 self._profile_index = -1
req = media_service.create_type('GetStreamUri')
req = self._media_service.create_type('GetStreamUri')
# pylint: disable=protected-access # pylint: disable=protected-access
req.ProfileToken = self._profiles[self._profile_index]._token req.ProfileToken = profiles[self._profile_index]._token
self._input = media_service.GetStreamUri(req).Uri.replace( uri_no_auth = self._media_service.GetStreamUri(req).Uri
'rtsp://', 'rtsp://{}:{}@'.format( uri_for_log = uri_no_auth.replace(
config.get(CONF_USERNAME), 'rtsp://', 'rtsp://<user>:<password>@', 1)
config.get(CONF_PASSWORD)), 1) self._input = uri_no_auth.replace(
'rtsp://', 'rtsp://{}:{}@'.format(self._username,
self._password), 1)
_LOGGER.debug( _LOGGER.debug(
"ONVIF Camera Using the following URL for %s: %s", "ONVIF Camera Using the following URL for %s: %s",
self._name, self._input) self._name, uri_for_log)
except Exception as err: # we won't need the media service anymore
_LOGGER.error("Unable to communicate with ONVIF Camera: %s", err) self._media_service = None
raise
try:
self._ptz = camera.create_ptz_service()
except exceptions.ONVIFError as err: except exceptions.ONVIFError as err:
self._ptz = None _LOGGER.debug("Couldn't setup camera '%s'. Error: %s",
_LOGGER.warning("Unable to setup PTZ for ONVIF Camera: %s", err) self._name, err)
return
def perform_ptz(self, pan, tilt, zoom): def perform_ptz(self, pan, tilt, zoom):
"""Perform a PTZ action on the camera.""" """Perform a PTZ action on the camera."""
if self._ptz: from onvif import exceptions
if self._ptz_service:
pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0 pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0
tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0 tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0
zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0 zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0
req = {"Velocity": { req = {"Velocity": {
"PanTilt": {"_x": pan_val, "_y": tilt_val}, "PanTilt": {"_x": pan_val, "_y": tilt_val},
"Zoom": {"_x": zoom_val}}} "Zoom": {"_x": zoom_val}}}
self._ptz.ContinuousMove(req) try:
self._ptz_service.ContinuousMove(req)
except exceptions.ONVIFError as err:
if "Bad Request" in err.reason:
self._ptz_service = None
_LOGGER.debug("Camera '%s' doesn't support PTZ.",
self._name)
else:
_LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Callback when entity is added to hass.""" """Callback when entity is added to hass."""
if ONVIF_DATA not in self.hass.data: if ONVIF_DATA not in self.hass.data:
self.hass.data[ONVIF_DATA] = {} self.hass.data[ONVIF_DATA] = {}
self.hass.data[ONVIF_DATA][ENTITIES] = [] self.hass.data[ONVIF_DATA][ENTITIES] = []
self.hass.data[ONVIF_DATA][ENTITIES].append(self) self.hass.data[ONVIF_DATA][ENTITIES].append(self)
@asyncio.coroutine async def async_camera_image(self):
def async_camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
from haffmpeg import ImageFrame, IMAGE_JPEG from haffmpeg import ImageFrame, IMAGE_JPEG
if not self._input:
await self.hass.async_add_job(self.obtain_input_uri)
if not self._input:
return None
ffmpeg = ImageFrame( ffmpeg = ImageFrame(
self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop)
image = yield from asyncio.shield(ffmpeg.get_image( image = await asyncio.shield(ffmpeg.get_image(
self._input, output_format=IMAGE_JPEG, self._input, output_format=IMAGE_JPEG,
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
return image return image
@asyncio.coroutine async def handle_async_mjpeg_stream(self, request):
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera.""" """Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg from haffmpeg import CameraMjpeg
if not self._input:
await self.hass.async_add_job(self.obtain_input_uri)
if not self._input:
return None
stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary, stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary,
loop=self.hass.loop) loop=self.hass.loop)
yield from stream.open_camera( await stream.open_camera(
self._input, extra_cmd=self._ffmpeg_arguments) self._input, extra_cmd=self._ffmpeg_arguments)
yield from async_aiohttp_proxy_stream( await async_aiohttp_proxy_stream(
self.hass, request, stream, self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver') 'multipart/x-mixed-replace;boundary=ffserver')
yield from stream.close() await stream.close()
@property @property
def name(self): def name(self):

View file

@ -11,7 +11,7 @@ import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util.async_ import run_coroutine_threadsafe
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util

View file

@ -4,7 +4,6 @@ Support for Xeoma Cameras.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.xeoma/ https://home-assistant.io/components/camera.xeoma/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -14,7 +13,7 @@ from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['pyxeoma==1.3'] REQUIREMENTS = ['pyxeoma==1.4.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -41,8 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Discover and setup Xeoma Cameras.""" """Discover and setup Xeoma Cameras."""
from pyxeoma.xeoma import Xeoma, XeomaError from pyxeoma.xeoma import Xeoma, XeomaError
@ -53,8 +52,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
xeoma = Xeoma(host, login, password) xeoma = Xeoma(host, login, password)
try: try:
yield from xeoma.async_test_connection() await xeoma.async_test_connection()
discovered_image_names = yield from xeoma.async_get_image_names() discovered_image_names = await xeoma.async_get_image_names()
discovered_cameras = [ discovered_cameras = [
{ {
CONF_IMAGE_NAME: image_name, CONF_IMAGE_NAME: image_name,
@ -103,12 +102,11 @@ class XeomaCamera(Camera):
self._password = password self._password = password
self._last_image = None self._last_image = None
@asyncio.coroutine async def async_camera_image(self):
def async_camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
from pyxeoma.xeoma import XeomaError from pyxeoma.xeoma import XeomaError
try: try:
image = yield from self._xeoma.async_get_camera_image( image = await self._xeoma.async_get_camera_image(
self._image, self._username, self._password) self._image, self._username, self._password)
self._last_image = image self._last_image = image
except XeomaError as err: except XeomaError as err:

View file

@ -14,10 +14,10 @@ from homeassistant.components.climate import (
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH,
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE,
SUPPORT_TARGET_TEMPERATURE_LOW) SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_CONFIGURING = {} _CONFIGURING = {}
@ -50,7 +50,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE | SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH | SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH |
SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW) SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -122,6 +122,7 @@ class Thermostat(ClimateDevice):
self._climate_list = self.climate_list self._climate_list = self.climate_list
self._operation_list = ['auto', 'auxHeatOnly', 'cool', self._operation_list = ['auto', 'auxHeatOnly', 'cool',
'heat', 'off'] 'heat', 'off']
self._fan_list = ['auto', 'on']
self.update_without_throttle = False self.update_without_throttle = False
def update(self): def update(self):
@ -180,24 +181,29 @@ class Thermostat(ClimateDevice):
return self.thermostat['runtime']['desiredCool'] / 10.0 return self.thermostat['runtime']['desiredCool'] / 10.0
return None return None
@property
def desired_fan_mode(self):
"""Return the desired fan mode of operation."""
return self.thermostat['runtime']['desiredFanMode']
@property @property
def fan(self): def fan(self):
"""Return the current fan state.""" """Return the current fan status."""
if 'fan' in self.thermostat['equipmentStatus']: if 'fan' in self.thermostat['equipmentStatus']:
return STATE_ON return STATE_ON
return STATE_OFF return STATE_OFF
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self.thermostat['runtime']['desiredFanMode']
@property @property
def current_hold_mode(self): def current_hold_mode(self):
"""Return current hold mode.""" """Return current hold mode."""
mode = self._current_hold_mode mode = self._current_hold_mode
return None if mode == AWAY_MODE else mode return None if mode == AWAY_MODE else mode
@property
def fan_list(self):
"""Return the available fan modes."""
return self._fan_list
@property @property
def _current_hold_mode(self): def _current_hold_mode(self):
events = self.thermostat['events'] events = self.thermostat['events']
@ -206,7 +212,7 @@ class Thermostat(ClimateDevice):
if event['type'] == 'hold': if event['type'] == 'hold':
if event['holdClimateRef'] == 'away': if event['holdClimateRef'] == 'away':
if int(event['endDate'][0:4]) - \ if int(event['endDate'][0:4]) - \
int(event['startDate'][0:4]) <= 1: int(event['startDate'][0:4]) <= 1:
# A temporary hold from away climate is a hold # A temporary hold from away climate is a hold
return 'away' return 'away'
# A permanent hold from away climate # A permanent hold from away climate
@ -228,7 +234,7 @@ class Thermostat(ClimateDevice):
def current_operation(self): def current_operation(self):
"""Return current operation.""" """Return current operation."""
if self.operation_mode == 'auxHeatOnly' or \ if self.operation_mode == 'auxHeatOnly' or \
self.operation_mode == 'heatPump': self.operation_mode == 'heatPump':
return STATE_HEAT return STATE_HEAT
return self.operation_mode return self.operation_mode
@ -271,10 +277,11 @@ class Thermostat(ClimateDevice):
operation = STATE_HEAT operation = STATE_HEAT
else: else:
operation = status operation = status
return { return {
"actual_humidity": self.thermostat['runtime']['actualHumidity'], "actual_humidity": self.thermostat['runtime']['actualHumidity'],
"fan": self.fan, "fan": self.fan,
"mode": self.mode, "climate_mode": self.mode,
"operation": operation, "operation": operation,
"climate_list": self.climate_list, "climate_list": self.climate_list,
"fan_min_on_time": self.fan_min_on_time "fan_min_on_time": self.fan_min_on_time
@ -342,25 +349,46 @@ class Thermostat(ClimateDevice):
cool_temp_setpoint, heat_temp_setpoint, cool_temp_setpoint, heat_temp_setpoint,
self.hold_preference()) self.hold_preference())
_LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, " _LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, "
"cool=%s, is=%s", heat_temp, isinstance( "cool=%s, is=%s", heat_temp,
heat_temp, (int, float)), cool_temp, isinstance(heat_temp, (int, float)), cool_temp,
isinstance(cool_temp, (int, float))) isinstance(cool_temp, (int, float)))
self.update_without_throttle = True self.update_without_throttle = True
def set_fan_mode(self, fan_mode):
"""Set the fan mode. Valid values are "on" or "auto"."""
if (fan_mode.lower() != STATE_ON) and (fan_mode.lower() != STATE_AUTO):
error = "Invalid fan_mode value: Valid values are 'on' or 'auto'"
_LOGGER.error(error)
return
cool_temp = self.thermostat['runtime']['desiredCool'] / 10.0
heat_temp = self.thermostat['runtime']['desiredHeat'] / 10.0
self.data.ecobee.set_fan_mode(self.thermostat_index, fan_mode,
cool_temp, heat_temp,
self.hold_preference())
_LOGGER.info("Setting fan mode to: %s", fan_mode)
def set_temp_hold(self, temp): def set_temp_hold(self, temp):
"""Set temperature hold in modes other than auto.""" """Set temperature hold in modes other than auto.
# Set arbitrary range when not in auto mode
if self.current_operation == STATE_HEAT: Ecobee API: It is good practice to set the heat and cool hold
temperatures to be the same, if the thermostat is in either heat, cool,
auxHeatOnly, or off mode. If the thermostat is in auto mode, an
additional rule is required. The cool hold temperature must be greater
than the heat hold temperature by at least the amount in the
heatCoolMinDelta property.
https://www.ecobee.com/home/developer/api/examples/ex5.shtml
"""
if self.current_operation == STATE_HEAT or self.current_operation == \
STATE_COOL:
heat_temp = temp heat_temp = temp
cool_temp = temp + 20
elif self.current_operation == STATE_COOL:
heat_temp = temp - 20
cool_temp = temp cool_temp = temp
else: else:
# In auto mode set temperature between delta = self.thermostat['settings']['heatCoolMinDelta'] / 10
heat_temp = temp - 10 heat_temp = temp - delta
cool_temp = temp + 10 cool_temp = temp + delta
self.set_auto_temp_hold(heat_temp, cool_temp) self.set_auto_temp_hold(heat_temp, cool_temp)
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
@ -369,8 +397,8 @@ class Thermostat(ClimateDevice):
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
if self.current_operation == STATE_AUTO and (low_temp is not None or if self.current_operation == STATE_AUTO and \
high_temp is not None): (low_temp is not None or high_temp is not None):
self.set_auto_temp_hold(low_temp, high_temp) self.set_auto_temp_hold(low_temp, high_temp)
elif temp is not None: elif temp is not None:
self.set_temp_hold(temp) self.set_temp_hold(temp)

View file

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-eq3bt==0.1.9'] REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.41']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -248,6 +248,11 @@ class SensiboClimate(ClimateDevice):
return self._temperatures_list[-1] \ return self._temperatures_list[-1] \
if self._temperatures_list else super().max_temp if self._temperatures_list else super().max_temp
@property
def unique_id(self):
"""Return unique ID based on Sensibo ID."""
return self._id
@asyncio.coroutine @asyncio.coroutine
def async_set_temperature(self, **kwargs): def async_set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""

View file

@ -37,6 +37,7 @@ CONF_FILTER = 'filter'
CONF_GOOGLE_ACTIONS = 'google_actions' CONF_GOOGLE_ACTIONS = 'google_actions'
CONF_RELAYER = 'relayer' CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id' CONF_USER_POOL_ID = 'user_pool_id'
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
DEFAULT_MODE = 'production' DEFAULT_MODE = 'production'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
@ -75,6 +76,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_USER_POOL_ID): str, vol.Optional(CONF_USER_POOL_ID): str,
vol.Optional(CONF_REGION): str, vol.Optional(CONF_REGION): str,
vol.Optional(CONF_RELAYER): str, vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA, vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
}), }),
@ -110,7 +112,7 @@ class Cloud:
def __init__(self, hass, mode, alexa, google_actions, def __init__(self, hass, mode, alexa, google_actions,
cognito_client_id=None, user_pool_id=None, region=None, cognito_client_id=None, user_pool_id=None, region=None,
relayer=None): relayer=None, google_actions_sync_url=None):
"""Create an instance of Cloud.""" """Create an instance of Cloud."""
self.hass = hass self.hass = hass
self.mode = mode self.mode = mode
@ -128,6 +130,7 @@ class Cloud:
self.user_pool_id = user_pool_id self.user_pool_id = user_pool_id
self.region = region self.region = region
self.relayer = relayer self.relayer = relayer
self.google_actions_sync_url = google_actions_sync_url
else: else:
info = SERVERS[mode] info = SERVERS[mode]
@ -136,6 +139,7 @@ class Cloud:
self.user_pool_id = info['user_pool_id'] self.user_pool_id = info['user_pool_id']
self.region = info['region'] self.region = info['region']
self.relayer = info['relayer'] self.relayer = info['relayer']
self.google_actions_sync_url = info['google_actions_sync_url']
@property @property
def is_logged_in(self): def is_logged_in(self):

View file

@ -8,7 +8,9 @@ SERVERS = {
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u', 'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
'user_pool_id': 'us-east-1_87ll5WOP8', 'user_pool_id': 'us-east-1_87ll5WOP8',
'region': 'us-east-1', 'region': 'us-east-1',
'relayer': 'wss://cloud.hass.io:8000/websocket' 'relayer': 'wss://cloud.hass.io:8000/websocket',
'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.'
'amazonaws.com/prod/smart_home_sync'),
} }
} }

View file

@ -16,9 +16,9 @@ from .const import DOMAIN, REQUEST_TIMEOUT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@asyncio.coroutine async def async_setup(hass):
def async_setup(hass):
"""Initialize the HTTP API.""" """Initialize the HTTP API."""
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView) hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView) hass.http.register_view(CloudLogoutView)
hass.http.register_view(CloudAccountView) hass.http.register_view(CloudAccountView)
@ -38,12 +38,11 @@ _CLOUD_ERRORS = {
def _handle_cloud_errors(handler): def _handle_cloud_errors(handler):
"""Handle auth errors.""" """Handle auth errors."""
@asyncio.coroutine
@wraps(handler) @wraps(handler)
def error_handler(view, request, *args, **kwargs): async def error_handler(view, request, *args, **kwargs):
"""Handle exceptions that raise from the wrapped request handler.""" """Handle exceptions that raise from the wrapped request handler."""
try: try:
result = yield from handler(view, request, *args, **kwargs) result = await handler(view, request, *args, **kwargs)
return result return result
except (auth_api.CloudError, asyncio.TimeoutError) as err: except (auth_api.CloudError, asyncio.TimeoutError) as err:
@ -57,6 +56,31 @@ def _handle_cloud_errors(handler):
return error_handler return error_handler
class GoogleActionsSyncView(HomeAssistantView):
"""Trigger a Google Actions Smart Home Sync."""
url = '/api/cloud/google_actions/sync'
name = 'api:cloud:google_actions/sync'
@_handle_cloud_errors
async def post(self, request):
"""Trigger a Google Actions sync."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
websession = hass.helpers.aiohttp_client.async_get_clientsession()
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
await hass.async_add_job(auth_api.check_token, cloud)
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
req = await websession.post(
cloud.google_actions_sync_url, headers={
'authorization': cloud.id_token
})
return self.json({}, status_code=req.status)
class CloudLoginView(HomeAssistantView): class CloudLoginView(HomeAssistantView):
"""Login to Home Assistant cloud.""" """Login to Home Assistant cloud."""
@ -68,19 +92,18 @@ class CloudLoginView(HomeAssistantView):
vol.Required('email'): str, vol.Required('email'): str,
vol.Required('password'): str, vol.Required('password'): str,
})) }))
@asyncio.coroutine async def post(self, request, data):
def post(self, request, data):
"""Handle login request.""" """Handle login request."""
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(auth_api.login, cloud, data['email'], await hass.async_add_job(auth_api.login, cloud, data['email'],
data['password']) data['password'])
hass.async_add_job(cloud.iot.connect) hass.async_add_job(cloud.iot.connect)
# Allow cloud to start connecting. # Allow cloud to start connecting.
yield from asyncio.sleep(0, loop=hass.loop) await asyncio.sleep(0, loop=hass.loop)
return self.json(_account_data(cloud)) return self.json(_account_data(cloud))
@ -91,14 +114,13 @@ class CloudLogoutView(HomeAssistantView):
name = 'api:cloud:logout' name = 'api:cloud:logout'
@_handle_cloud_errors @_handle_cloud_errors
@asyncio.coroutine async def post(self, request):
def post(self, request):
"""Handle logout request.""" """Handle logout request."""
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from cloud.logout() await cloud.logout()
return self.json_message('ok') return self.json_message('ok')
@ -109,8 +131,7 @@ class CloudAccountView(HomeAssistantView):
url = '/api/cloud/account' url = '/api/cloud/account'
name = 'api:cloud:account' name = 'api:cloud:account'
@asyncio.coroutine async def get(self, request):
def get(self, request):
"""Get account info.""" """Get account info."""
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
@ -132,14 +153,13 @@ class CloudRegisterView(HomeAssistantView):
vol.Required('email'): str, vol.Required('email'): str,
vol.Required('password'): vol.All(str, vol.Length(min=6)), vol.Required('password'): vol.All(str, vol.Length(min=6)),
})) }))
@asyncio.coroutine async def post(self, request, data):
def post(self, request, data):
"""Handle registration request.""" """Handle registration request."""
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job( await hass.async_add_job(
auth_api.register, cloud, data['email'], data['password']) auth_api.register, cloud, data['email'], data['password'])
return self.json_message('ok') return self.json_message('ok')
@ -155,14 +175,13 @@ class CloudResendConfirmView(HomeAssistantView):
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('email'): str, vol.Required('email'): str,
})) }))
@asyncio.coroutine async def post(self, request, data):
def post(self, request, data):
"""Handle resending confirm email code request.""" """Handle resending confirm email code request."""
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job( await hass.async_add_job(
auth_api.resend_email_confirm, cloud, data['email']) auth_api.resend_email_confirm, cloud, data['email'])
return self.json_message('ok') return self.json_message('ok')
@ -178,14 +197,13 @@ class CloudForgotPasswordView(HomeAssistantView):
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('email'): str, vol.Required('email'): str,
})) }))
@asyncio.coroutine async def post(self, request, data):
def post(self, request, data):
"""Handle forgot password request.""" """Handle forgot password request."""
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN] cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job( await hass.async_add_job(
auth_api.forgot_password, cloud, data['email']) auth_api.forgot_password, cloud, data['email'])
return self.json_message('ok') return self.json_message('ok')

View file

@ -0,0 +1,24 @@
{
"config": {
"error": {
"invalid_object_id": "Ung\u00fcltige Objekt-ID"
},
"step": {
"init": {
"data": {
"object_id": "Objekt-ID"
},
"description": "Bitte gib eine Objekt_ID f\u00fcr das Test-Entity ein.",
"title": "W\u00e4hle eine Objekt-ID"
},
"name": {
"data": {
"name": "Name"
},
"description": "Bitte gib einen Namen f\u00fcr das Test-Entity ein",
"title": "Name des Test-Entity"
}
},
"title": "Beispiel Konfig-Eintrag"
}
}

View file

@ -0,0 +1,24 @@
{
"config": {
"error": {
"invalid_object_id": "Invalid object ID"
},
"step": {
"init": {
"data": {
"object_id": "Object ID"
},
"description": "Please enter an object_id for the test entity.",
"title": "Pick object id"
},
"name": {
"data": {
"name": "Name"
},
"description": "Please enter a name for the test entity.",
"title": "Name of the entity"
}
},
"title": "Config Entry Example"
}
}

View file

@ -0,0 +1,11 @@
{
"config": {
"step": {
"name": {
"data": {
"name": "Nimi"
}
}
}
}
}

View file

@ -0,0 +1,24 @@
{
"config": {
"error": {
"invalid_object_id": "\uc624\ube0c\uc81d\ud2b8 ID\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"init": {
"data": {
"object_id": "\uc624\ube0c\uc81d\ud2b8 ID"
},
"description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc624\ube0c\uc81d\ud2b8 ID \ub97c \uc785\ub825\ud558\uc138\uc694",
"title": "\uc624\ube0c\uc81d\ud2b8 ID \uc120\ud0dd"
},
"name": {
"data": {
"name": "\uc774\ub984"
},
"description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984\uc744 \uc785\ub825\ud558\uc138\uc694.",
"title": "\uad6c\uc131\uc694\uc18c \uc774\ub984"
}
},
"title": "\uc785\ub825 \uc608\uc81c \uad6c\uc131"
}
}

View file

@ -0,0 +1,24 @@
{
"config": {
"error": {
"invalid_object_id": "Ongeldig object ID"
},
"step": {
"init": {
"data": {
"object_id": "Object ID"
},
"description": "Voer een object_id in voor het testen van de entiteit.",
"title": "Kies object id"
},
"name": {
"data": {
"name": "Naam"
},
"description": "Voer een naam in voor het testen van de entiteit.",
"title": "Naam van de entiteit"
}
},
"title": "Voorbeeld van de config vermelding"
}
}

View file

@ -0,0 +1,24 @@
{
"config": {
"error": {
"invalid_object_id": "Ugyldig objekt ID"
},
"step": {
"init": {
"data": {
"object_id": "Objekt ID"
},
"description": "Vennligst skriv inn en object_id for testenheten.",
"title": "Velg objekt ID"
},
"name": {
"data": {
"name": "Navn"
},
"description": "Vennligst skriv inn et navn for testenheten.",
"title": "Navn p\u00e5 enheten"
}
},
"title": "Konfigureringseksempel"
}
}

View file

@ -0,0 +1,24 @@
{
"config": {
"error": {
"invalid_object_id": "Nieprawid\u0142owy identyfikator obiektu"
},
"step": {
"init": {
"data": {
"object_id": "Identyfikator obiektu"
},
"description": "Prosz\u0119 wprowadzi\u0107 identyfikator obiektu (object_id) dla jednostki testowej.",
"title": "Wybierz identyfikator obiektu"
},
"name": {
"data": {
"name": "Nazwa"
},
"description": "Prosz\u0119 wprowadzi\u0107 nazw\u0119 dla jednostki testowej.",
"title": "Nazwa jednostki"
}
},
"title": "Przyk\u0142ad wpisu do konfiguracji"
}
}

View file

@ -0,0 +1,15 @@
{
"config": {
"step": {
"init": {
"description": "Introduce\u021bi un obiect_id pentru entitatea testat\u0103.",
"title": "Alege\u021bi id-ul obiectului"
},
"name": {
"data": {
"name": "Nume"
}
}
}
}
}

View file

@ -0,0 +1,24 @@
{
"config": {
"error": {
"invalid_object_id": "Neveljaven ID objekta"
},
"step": {
"init": {
"data": {
"object_id": "ID objekta"
},
"description": "Prosimo, vnesite Id_objekta za testni subjekt.",
"title": "Izberite ID objekta"
},
"name": {
"data": {
"name": "Ime"
},
"description": "Vnesite ime za testni subjekt.",
"title": "Ime subjekta"
}
},
"title": "Primer nastavitve"
}
}

View file

@ -0,0 +1,24 @@
{
"config": {
"error": {
"invalid_object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng kh\u00f4ng h\u1ee3p l\u1ec7"
},
"step": {
"init": {
"data": {
"object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng"
},
"description": "Xin vui l\u00f2ng nh\u1eadp m\u1ed9t object_id cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.",
"title": "Ch\u1ecdn id \u0111\u1ed1i t\u01b0\u1ee3ng"
},
"name": {
"data": {
"name": "T\u00ean"
},
"description": "Xin vui l\u00f2ng nh\u1eadp t\u00ean cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.",
"title": "T\u00ean c\u1ee7a th\u1ef1c th\u1ec3"
}
},
"title": "V\u00ed d\u1ee5 v\u1ec1 c\u1ea5u h\u00ecnh th\u1ef1c th\u1ec3"
}
}

View file

@ -0,0 +1,24 @@
{
"config": {
"error": {
"invalid_object_id": "\u65e0\u6548\u7684\u5bf9\u8c61 ID"
},
"step": {
"init": {
"data": {
"object_id": "\u5bf9\u8c61 ID"
},
"description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u5bf9\u8c61 ID",
"title": "\u8bf7\u9009\u62e9\u5bf9\u8c61 ID"
},
"name": {
"data": {
"name": "\u540d\u79f0"
},
"description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u540d\u79f0",
"title": "\u8bbe\u5907\u540d\u79f0"
}
},
"title": "\u6837\u4f8b\u914d\u7f6e\u6761\u76ee"
}
}

View file

@ -62,13 +62,11 @@ class ExampleConfigFlow(config_entries.ConfigFlowHandler):
return (yield from self.async_step_name()) return (yield from self.async_step_name())
errors = { errors = {
'object_id': 'Invalid object id.' 'object_id': 'invalid_object_id'
} }
return self.async_show_form( return self.async_show_form(
title='Pick object id',
step_id='init', step_id='init',
description="Please enter an object_id for the test entity.",
data_schema=vol.Schema({ data_schema=vol.Schema({
'object_id': str 'object_id': str
}), }),
@ -92,9 +90,7 @@ class ExampleConfigFlow(config_entries.ConfigFlowHandler):
) )
return self.async_show_form( return self.async_show_form(
title='Name of the entity',
step_id='name', step_id='name',
description="Please enter a name for the test entity.",
data_schema=vol.Schema({ data_schema=vol.Schema({
'name': str 'name': str
}), }),

View file

@ -0,0 +1,24 @@
{
"config": {
"title": "Config Entry Example",
"step": {
"init": {
"title": "Pick object id",
"description": "Please enter an object_id for the test entity.",
"data": {
"object_id": "Object ID"
}
},
"name": {
"title": "Name of the entity",
"description": "Please enter a name for the test entity.",
"data": {
"name": "Name"
}
}
},
"error": {
"invalid_object_id": "Invalid object ID"
}
}
}

View file

@ -15,7 +15,7 @@ from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
ATTR_ENTITY_PICTURE ATTR_ENTITY_PICTURE
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.util.async import run_callback_threadsafe from homeassistant.util.async_ import run_callback_threadsafe
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_KEY_INSTANCE = 'configurator' _KEY_INSTANCE = 'configurator'

View file

@ -0,0 +1,271 @@
"""
This platform allows several cover to be grouped into one cover.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.group/
"""
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.cover import (
DOMAIN, PLATFORM_SCHEMA, CoverDevice, ATTR_POSITION,
ATTR_CURRENT_POSITION, ATTR_TILT_POSITION, ATTR_CURRENT_TILT_POSITION,
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION,
SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT,
SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION,
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION)
from homeassistant.const import (
ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES,
CONF_ENTITIES, CONF_NAME, STATE_CLOSED)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change
_LOGGER = logging.getLogger(__name__)
KEY_OPEN_CLOSE = 'open_close'
KEY_STOP = 'stop'
KEY_POSITION = 'position'
DEFAULT_NAME = 'Cover Group'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN),
})
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the Group Cover platform."""
async_add_devices(
[CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])])
class CoverGroup(CoverDevice):
"""Representation of a CoverGroup."""
def __init__(self, name, entities):
"""Initialize a CoverGroup entity."""
self._name = name
self._is_closed = False
self._cover_position = 100
self._tilt_position = None
self._supported_features = 0
self._assumed_state = True
self._entities = entities
self._covers = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(),
KEY_POSITION: set()}
self._tilts = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(),
KEY_POSITION: set()}
@callback
def update_supported_features(self, entity_id, old_state, new_state,
update_state=True):
"""Update dictionaries with supported features."""
if not new_state:
for values in self._covers.values():
values.discard(entity_id)
for values in self._tilts.values():
values.discard(entity_id)
if update_state:
self.async_schedule_update_ha_state(True)
return
features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if features & (SUPPORT_OPEN | SUPPORT_CLOSE):
self._covers[KEY_OPEN_CLOSE].add(entity_id)
else:
self._covers[KEY_OPEN_CLOSE].discard(entity_id)
if features & (SUPPORT_STOP):
self._covers[KEY_STOP].add(entity_id)
else:
self._covers[KEY_STOP].discard(entity_id)
if features & (SUPPORT_SET_POSITION):
self._covers[KEY_POSITION].add(entity_id)
else:
self._covers[KEY_POSITION].discard(entity_id)
if features & (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT):
self._tilts[KEY_OPEN_CLOSE].add(entity_id)
else:
self._tilts[KEY_OPEN_CLOSE].discard(entity_id)
if features & (SUPPORT_STOP_TILT):
self._tilts[KEY_STOP].add(entity_id)
else:
self._tilts[KEY_STOP].discard(entity_id)
if features & (SUPPORT_SET_TILT_POSITION):
self._tilts[KEY_POSITION].add(entity_id)
else:
self._tilts[KEY_POSITION].discard(entity_id)
if update_state:
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Register listeners."""
for entity_id in self._entities:
new_state = self.hass.states.get(entity_id)
self.update_supported_features(entity_id, None, new_state,
update_state=False)
async_track_state_change(self.hass, self._entities,
self.update_supported_features)
await self.async_update()
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def assumed_state(self):
"""Enable buttons even if at end position."""
return self._assumed_state
@property
def should_poll(self):
"""Disable polling for cover group."""
return False
@property
def supported_features(self):
"""Flag supported features for the cover."""
return self._supported_features
@property
def is_closed(self):
"""Return if all covers in group are closed."""
return self._is_closed
@property
def current_cover_position(self):
"""Return current position for all covers."""
return self._cover_position
@property
def current_cover_tilt_position(self):
"""Return current tilt position for all covers."""
return self._tilt_position
async def async_open_cover(self, **kwargs):
"""Move the covers up."""
data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
DOMAIN, SERVICE_OPEN_COVER, data, blocking=True)
async def async_close_cover(self, **kwargs):
"""Move the covers down."""
data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
DOMAIN, SERVICE_CLOSE_COVER, data, blocking=True)
async def async_stop_cover(self, **kwargs):
"""Fire the stop action."""
data = {ATTR_ENTITY_ID: self._covers[KEY_STOP]}
await self.hass.services.async_call(
DOMAIN, SERVICE_STOP_COVER, data, blocking=True)
async def async_set_cover_position(self, **kwargs):
"""Set covers position."""
data = {ATTR_ENTITY_ID: self._covers[KEY_POSITION],
ATTR_POSITION: kwargs[ATTR_POSITION]}
await self.hass.services.async_call(
DOMAIN, SERVICE_SET_COVER_POSITION, data, blocking=True)
async def async_open_cover_tilt(self, **kwargs):
"""Tilt covers open."""
data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
DOMAIN, SERVICE_OPEN_COVER_TILT, data, blocking=True)
async def async_close_cover_tilt(self, **kwargs):
"""Tilt covers closed."""
data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
DOMAIN, SERVICE_CLOSE_COVER_TILT, data, blocking=True)
async def async_stop_cover_tilt(self, **kwargs):
"""Stop cover tilt."""
data = {ATTR_ENTITY_ID: self._tilts[KEY_STOP]}
await self.hass.services.async_call(
DOMAIN, SERVICE_STOP_COVER_TILT, data, blocking=True)
async def async_set_cover_tilt_position(self, **kwargs):
"""Set tilt position."""
data = {ATTR_ENTITY_ID: self._tilts[KEY_POSITION],
ATTR_TILT_POSITION: kwargs[ATTR_TILT_POSITION]}
await self.hass.services.async_call(
DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data, blocking=True)
async def async_update(self):
"""Update state and attributes."""
self._assumed_state = False
self._is_closed = True
for entity_id in self._entities:
state = self.hass.states.get(entity_id)
if not state:
continue
if state.state != STATE_CLOSED:
self._is_closed = False
break
self._cover_position = None
if self._covers[KEY_POSITION]:
position = -1
self._cover_position = 0 if self.is_closed else 100
for entity_id in self._covers[KEY_POSITION]:
state = self.hass.states.get(entity_id)
pos = state.attributes.get(ATTR_CURRENT_POSITION)
if position == -1:
position = pos
elif position != pos:
self._assumed_state = True
break
else:
if position != -1:
self._cover_position = position
self._tilt_position = None
if self._tilts[KEY_POSITION]:
position = -1
self._tilt_position = 100
for entity_id in self._tilts[KEY_POSITION]:
state = self.hass.states.get(entity_id)
pos = state.attributes.get(ATTR_CURRENT_TILT_POSITION)
if position == -1:
position = pos
elif position != pos:
self._assumed_state = True
break
else:
if position != -1:
self._tilt_position = position
supported_features = 0
supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE \
if self._covers[KEY_OPEN_CLOSE] else 0
supported_features |= SUPPORT_STOP \
if self._covers[KEY_STOP] else 0
supported_features |= SUPPORT_SET_POSITION \
if self._covers[KEY_POSITION] else 0
supported_features |= SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT \
if self._tilts[KEY_OPEN_CLOSE] else 0
supported_features |= SUPPORT_STOP_TILT \
if self._tilts[KEY_STOP] else 0
supported_features |= SUPPORT_SET_TILT_POSITION \
if self._tilts[KEY_POSITION] else 0
self._supported_features = supported_features
if not self._assumed_state:
for entity_id in self._entities:
state = self.hass.states.get(entity_id)
if state and state.attributes.get(ATTR_ASSUMED_STATE):
self._assumed_state = True
break

View file

@ -234,7 +234,9 @@ class CoverTemplate(CoverDevice):
None is unknown, 0 is closed, 100 is fully open. None is unknown, 0 is closed, 100 is fully open.
""" """
return self._position if self._position_template or self._position_script:
return self._position
return None
@property @property
def current_cover_tilt_position(self): def current_cover_tilt_position(self):

View file

@ -4,8 +4,6 @@ Support for deCONZ devices.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/deconz/ https://home-assistant.io/components/deconz/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -19,7 +17,7 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.json import load_json, save_json from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['pydeconz==30'] REQUIREMENTS = ['pydeconz==32']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -57,30 +55,28 @@ Unlock your deCONZ gateway to register with Home Assistant.
""" """
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Set up services and configuration for deCONZ component.""" """Set up services and configuration for deCONZ component."""
result = False result = False
config_file = yield from hass.async_add_job( config_file = await hass.async_add_job(
load_json, hass.config.path(CONFIG_FILE)) load_json, hass.config.path(CONFIG_FILE))
@asyncio.coroutine async def async_deconz_discovered(service, discovery_info):
def async_deconz_discovered(service, discovery_info):
"""Call when deCONZ gateway has been found.""" """Call when deCONZ gateway has been found."""
deconz_config = {} deconz_config = {}
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST) deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT) deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
yield from async_request_configuration(hass, config, deconz_config) await async_request_configuration(hass, config, deconz_config)
if config_file: if config_file:
result = yield from async_setup_deconz(hass, config, config_file) result = await async_setup_deconz(hass, config, config_file)
if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]: if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]:
deconz_config = config[DOMAIN] deconz_config = config[DOMAIN]
if CONF_API_KEY in deconz_config: if CONF_API_KEY in deconz_config:
result = yield from async_setup_deconz(hass, config, deconz_config) result = await async_setup_deconz(hass, config, deconz_config)
else: else:
yield from async_request_configuration(hass, config, deconz_config) await async_request_configuration(hass, config, deconz_config)
return True return True
if not result: if not result:
@ -89,8 +85,7 @@ def async_setup(hass, config):
return True return True
@asyncio.coroutine async def async_setup_deconz(hass, config, deconz_config):
def async_setup_deconz(hass, config, deconz_config):
"""Set up a deCONZ session. """Set up a deCONZ session.
Load config, group, light and sensor data for server information. Load config, group, light and sensor data for server information.
@ -100,7 +95,7 @@ def async_setup_deconz(hass, config, deconz_config):
from pydeconz import DeconzSession from pydeconz import DeconzSession
websession = async_get_clientsession(hass) websession = async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, websession, **deconz_config) deconz = DeconzSession(hass.loop, websession, **deconz_config)
result = yield from deconz.async_load_parameters() result = await deconz.async_load_parameters()
if result is False: if result is False:
_LOGGER.error("Failed to communicate with deCONZ") _LOGGER.error("Failed to communicate with deCONZ")
return False return False
@ -113,8 +108,7 @@ def async_setup_deconz(hass, config, deconz_config):
hass, component, DOMAIN, {}, config)) hass, component, DOMAIN, {}, config))
deconz.start() deconz.start()
@asyncio.coroutine async def async_configure(call):
def async_configure(call):
"""Set attribute of device in deCONZ. """Set attribute of device in deCONZ.
Field is a string representing a specific device in deCONZ Field is a string representing a specific device in deCONZ
@ -140,7 +134,7 @@ def async_setup_deconz(hass, config, deconz_config):
if field is None: if field is None:
_LOGGER.error('Could not find the entity %s', entity_id) _LOGGER.error('Could not find the entity %s', entity_id)
return return
yield from deconz.async_put_state(field, data) await deconz.async_put_state(field, data)
hass.services.async_register( hass.services.async_register(
DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA) DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA)
@ -159,21 +153,19 @@ def async_setup_deconz(hass, config, deconz_config):
return True return True
@asyncio.coroutine async def async_request_configuration(hass, config, deconz_config):
def async_request_configuration(hass, config, deconz_config):
"""Request configuration steps from the user.""" """Request configuration steps from the user."""
configurator = hass.components.configurator configurator = hass.components.configurator
@asyncio.coroutine async def async_configuration_callback(data):
def async_configuration_callback(data):
"""Set up actions to do when our configuration callback is called.""" """Set up actions to do when our configuration callback is called."""
from pydeconz.utils import async_get_api_key from pydeconz.utils import async_get_api_key
api_key = yield from async_get_api_key(hass.loop, **deconz_config) api_key = await async_get_api_key(hass.loop, **deconz_config)
if api_key: if api_key:
deconz_config[CONF_API_KEY] = api_key deconz_config[CONF_API_KEY] = api_key
result = yield from async_setup_deconz(hass, config, deconz_config) result = await async_setup_deconz(hass, config, deconz_config)
if result: if result:
yield from hass.async_add_job( await hass.async_add_job(
save_json, hass.config.path(CONFIG_FILE), deconz_config) save_json, hass.config.path(CONFIG_FILE), deconz_config)
configurator.async_request_done(request_id) configurator.async_request_done(request_id)
return return

View file

@ -28,7 +28,7 @@ from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.loader import get_component from homeassistant.loader import get_component
import homeassistant.util as util import homeassistant.util as util
from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.util.yaml import dump from homeassistant.util.yaml import dump

View file

@ -73,7 +73,8 @@ class MikrotikScanner(DeviceScanner):
self.host, self.host,
self.username, self.username,
self.password, self.password,
port=int(self.port) port=int(self.port),
encoding='utf-8'
) )
try: try:

View file

@ -98,7 +98,8 @@ class UnifiScanner(DeviceScanner):
# Filter clients to provided SSID list # Filter clients to provided SSID list
if self._ssid_filter: if self._ssid_filter:
clients = [client for client in clients clients = [client for client in clients
if client['essid'] in self._ssid_filter] if 'essid' in client and
client['essid'] in self._ssid_filter]
self._clients = { self._clients = {
client['mac']: client client['mac']: client

View file

@ -6,7 +6,6 @@ Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
Knows which components handle certain types, will make sure they are Knows which components handle certain types, will make sure they are
loaded before the EVENT_PLATFORM_DISCOVERED is fired. loaded before the EVENT_PLATFORM_DISCOVERED is fired.
""" """
import asyncio
import json import json
from datetime import timedelta from datetime import timedelta
import logging import logging
@ -21,7 +20,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
REQUIREMENTS = ['netdisco==1.2.4'] REQUIREMENTS = ['netdisco==1.3.0']
DOMAIN = 'discovery' DOMAIN = 'discovery'
@ -39,6 +38,7 @@ SERVICE_TELLDUSLIVE = 'tellstick'
SERVICE_HUE = 'philips_hue' SERVICE_HUE = 'philips_hue'
SERVICE_DECONZ = 'deconz' SERVICE_DECONZ = 'deconz'
SERVICE_DAIKIN = 'daikin' SERVICE_DAIKIN = 'daikin'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None), SERVICE_HASS_IOS_APP: ('ios', None),
@ -54,6 +54,7 @@ SERVICE_HANDLERS = {
SERVICE_HUE: ('hue', None), SERVICE_HUE: ('hue', None),
SERVICE_DECONZ: ('deconz', None), SERVICE_DECONZ: ('deconz', None),
SERVICE_DAIKIN: ('daikin', None), SERVICE_DAIKIN: ('daikin', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
'google_cast': ('media_player', 'cast'), 'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'), 'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'), 'plex_mediaserver': ('media_player', 'plex'),
@ -84,8 +85,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Start a discovery service.""" """Start a discovery service."""
from netdisco.discovery import NetworkDiscovery from netdisco.discovery import NetworkDiscovery
@ -99,8 +99,7 @@ def async_setup(hass, config):
# Platforms ignore by config # Platforms ignore by config
ignored_platforms = config[DOMAIN][CONF_IGNORE] ignored_platforms = config[DOMAIN][CONF_IGNORE]
@asyncio.coroutine async def new_service_found(service, info):
def new_service_found(service, info):
"""Handle a new service if one is found.""" """Handle a new service if one is found."""
if service in ignored_platforms: if service in ignored_platforms:
logger.info("Ignoring service: %s %s", service, info) logger.info("Ignoring service: %s %s", service, info)
@ -124,15 +123,14 @@ def async_setup(hass, config):
component, platform = comp_plat component, platform = comp_plat
if platform is None: if platform is None:
yield from async_discover(hass, service, info, component, config) await async_discover(hass, service, info, component, config)
else: else:
yield from async_load_platform( await async_load_platform(
hass, component, platform, info, config) hass, component, platform, info, config)
@asyncio.coroutine async def scan_devices(now):
def scan_devices(now):
"""Scan for devices.""" """Scan for devices."""
results = yield from hass.async_add_job(_discover, netdisco) results = await hass.async_add_job(_discover, netdisco)
for result in results: for result in results:
hass.async_add_job(new_service_found(*result)) hass.async_add_job(new_service_found(*result))

View file

@ -13,7 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['DoorBirdPy==0.1.2'] REQUIREMENTS = ['DoorBirdPy==0.1.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -25,6 +25,8 @@ ATTR_OVERWRITE = 'overwrite'
CONF_DOWNLOAD_DIR = 'download_dir' CONF_DOWNLOAD_DIR = 'download_dir'
DOMAIN = 'downloader' DOMAIN = 'downloader'
DOWNLOAD_FAILED_EVENT = 'download_failed'
DOWNLOAD_COMPLETED_EVENT = 'download_completed'
SERVICE_DOWNLOAD_FILE = 'download_file' SERVICE_DOWNLOAD_FILE = 'download_file'
@ -133,9 +135,19 @@ def setup(hass, config):
fil.write(chunk) fil.write(chunk)
_LOGGER.debug("Downloading of %s done", url) _LOGGER.debug("Downloading of %s done", url)
hass.bus.fire(
"{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), {
'url': url,
'filename': filename
})
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.exception("ConnectionError occurred for %s", url) _LOGGER.exception("ConnectionError occurred for %s", url)
hass.bus.fire(
"{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), {
'url': url,
'filename': filename
})
# Remove file if we started downloading but failed # Remove file if we started downloading but failed
if final_path and os.path.isfile(final_path): if final_path and os.path.isfile(final_path):

View file

@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.util.json import save_json from homeassistant.util.json import save_json
REQUIREMENTS = ['python-ecobee-api==0.0.15'] REQUIREMENTS = ['python-ecobee-api==0.0.17']
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME,
EVENT_HOMEASSISTANT_STOP) EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['pythonegardia==1.0.38'] REQUIREMENTS = ['pythonegardia==1.0.39']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}) })
REQUIREMENTS = ['python-miio==0.3.7'] REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
ATTR_TEMPERATURE = 'temperature' ATTR_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity' ATTR_HUMIDITY = 'humidity'

View file

@ -0,0 +1,114 @@
"""
Fans on Zigbee Home Automation networks.
For more details on this platform, please refer to the documentation
at https://home-assistant.io/components/fan.zha/
"""
import asyncio
import logging
from homeassistant.components import zha
from homeassistant.components.fan import (
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED)
from homeassistant.const import STATE_UNKNOWN
DEPENDENCIES = ['zha']
_LOGGER = logging.getLogger(__name__)
# Additional speeds in zigbee's ZCL
# Spec is unclear as to what this value means. On King Of Fans HBUniversal
# receiver, this means Very High.
SPEED_ON = 'on'
# The fan speed is self-regulated
SPEED_AUTO = 'auto'
# When the heated/cooled space is occupied, the fan is always on
SPEED_SMART = 'smart'
SPEED_LIST = [
SPEED_OFF,
SPEED_LOW,
SPEED_MEDIUM,
SPEED_HIGH,
SPEED_ON,
SPEED_AUTO,
SPEED_SMART
]
VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)}
SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)}
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Zigbee Home Automation fans."""
discovery_info = zha.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
async_add_devices([ZhaFan(**discovery_info)], update_before_add=True)
class ZhaFan(zha.Entity, FanEntity):
"""Representation of a ZHA fan."""
_domain = DOMAIN
@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_SET_SPEED
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
return SPEED_LIST
@property
def speed(self) -> str:
"""Return the current speed."""
return self._state
@property
def is_on(self) -> bool:
"""Return true if entity is on."""
if self._state == STATE_UNKNOWN:
return False
return self._state != SPEED_OFF
@asyncio.coroutine
def async_turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn the entity on."""
if speed is None:
speed = SPEED_MEDIUM
yield from self.async_set_speed(speed)
@asyncio.coroutine
def async_turn_off(self, **kwargs) -> None:
"""Turn the entity off."""
yield from self.async_set_speed(SPEED_OFF)
@asyncio.coroutine
def async_set_speed(self: FanEntity, speed: str) -> None:
"""Set the speed of the fan."""
yield from self._endpoint.fan.write_attributes({
'fan_mode': SPEED_TO_VALUE[speed]})
self._state = speed
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_update(self):
"""Retrieve latest state."""
result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode'])
new_value = result.get('fan_mode', None)
self._state = VALUE_TO_SPEED.get(new_value, STATE_UNKNOWN)
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state.
False if entity pushes its state to HA.
"""
return False

View file

@ -24,7 +24,7 @@ from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20180310.0'] REQUIREMENTS = ['home-assistant-frontend==20180330.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']

View file

@ -243,7 +243,7 @@ class ColorSpectrumTrait(_Trait):
if domain != light.DOMAIN: if domain != light.DOMAIN:
return False return False
return features & (light.SUPPORT_RGB_COLOR | light.SUPPORT_XY_COLOR) return features & light.SUPPORT_COLOR
def sync_attributes(self): def sync_attributes(self):
"""Return color spectrum attributes for a sync request.""" """Return color spectrum attributes for a sync request."""
@ -254,13 +254,11 @@ class ColorSpectrumTrait(_Trait):
"""Return color spectrum query attributes.""" """Return color spectrum query attributes."""
response = {} response = {}
# No need to handle XY color because light component will always color_hs = self.state.attributes.get(light.ATTR_HS_COLOR)
# convert XY to RGB if possible (which is when brightness is available) if color_hs is not None:
color_rgb = self.state.attributes.get(light.ATTR_RGB_COLOR)
if color_rgb is not None:
response['color'] = { response['color'] = {
'spectrumRGB': int(color_util.color_rgb_to_hex( 'spectrumRGB': int(color_util.color_rgb_to_hex(
color_rgb[0], color_rgb[1], color_rgb[2]), 16), *color_util.color_hs_to_RGB(*color_hs)), 16),
} }
return response return response
@ -274,11 +272,12 @@ class ColorSpectrumTrait(_Trait):
"""Execute a color spectrum command.""" """Execute a color spectrum command."""
# Convert integer to hex format and left pad with 0's till length 6 # Convert integer to hex format and left pad with 0's till length 6
hex_value = "{0:06x}".format(params['color']['spectrumRGB']) hex_value = "{0:06x}".format(params['color']['spectrumRGB'])
color = color_util.rgb_hex_to_rgb_list(hex_value) color = color_util.color_RGB_to_hs(
*color_util.rgb_hex_to_rgb_list(hex_value))
await hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, { await hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: self.state.entity_id, ATTR_ENTITY_ID: self.state.entity_id,
light.ATTR_RGB_COLOR: color light.ATTR_HS_COLOR: color
}, blocking=True) }, blocking=True)

View file

@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity, async_generate_entity_id
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util.async_ import run_coroutine_threadsafe
DOMAIN = 'group' DOMAIN = 'group'

View file

@ -156,7 +156,7 @@ def async_setup(hass, config):
if 'frontend' in hass.config.components: if 'frontend' in hass.config.components:
yield from hass.components.frontend.async_register_built_in_panel( yield from hass.components.frontend.async_register_built_in_panel(
'hassio', 'Hass.io', 'mdi:access-point-network') 'hassio', 'Hass.io', 'mdi:home-assistant')
if 'http' in config: if 'http' in config:
yield from hassio.update_hass_api(config['http']) yield from hassio.update_hass_api(config['http'])

View file

@ -239,15 +239,16 @@ def get_state(hass, utc_point_in_time, entity_id, run=None):
def async_setup(hass, config): def async_setup(hass, config):
"""Set up the history hooks.""" """Set up the history hooks."""
filters = Filters() filters = Filters()
exclude = config[DOMAIN].get(CONF_EXCLUDE) conf = config.get(DOMAIN, {})
exclude = conf.get(CONF_EXCLUDE)
if exclude: if exclude:
filters.excluded_entities = exclude.get(CONF_ENTITIES, []) filters.excluded_entities = exclude.get(CONF_ENTITIES, [])
filters.excluded_domains = exclude.get(CONF_DOMAINS, []) filters.excluded_domains = exclude.get(CONF_DOMAINS, [])
include = config[DOMAIN].get(CONF_INCLUDE) include = conf.get(CONF_INCLUDE)
if include: if include:
filters.included_entities = include.get(CONF_ENTITIES, []) filters.included_entities = include.get(CONF_ENTITIES, [])
filters.included_domains = include.get(CONF_DOMAINS, []) filters.included_domains = include.get(CONF_DOMAINS, [])
use_include_order = config[DOMAIN].get(CONF_ORDER) use_include_order = conf.get(CONF_ORDER)
hass.http.register_view(HistoryPeriodView(filters, use_include_order)) hass.http.register_view(HistoryPeriodView(filters, use_include_order))
yield from hass.components.frontend.async_register_built_in_panel( yield from hass.components.frontend.async_register_built_in_panel(
@ -308,7 +309,7 @@ class HistoryPeriodView(HomeAssistantView):
result = yield from hass.async_add_job( result = yield from hass.async_add_job(
get_significant_states, hass, start_time, end_time, get_significant_states, hass, start_time, end_time,
entity_ids, self.filters, include_start_time_state) entity_ids, self.filters, include_start_time_state)
result = result.values() result = list(result.values())
if _LOGGER.isEnabledFor(logging.DEBUG): if _LOGGER.isEnabledFor(logging.DEBUG):
elapsed = time.perf_counter() - timer_start elapsed = time.perf_counter() - timer_start
_LOGGER.debug( _LOGGER.debug(
@ -318,7 +319,6 @@ class HistoryPeriodView(HomeAssistantView):
# by any entities explicitly included in the configuration. # by any entities explicitly included in the configuration.
if self.use_include_order: if self.use_include_order:
result = list(result)
sorted_result = [] sorted_result = []
for order_entity in self.filters.included_entities: for order_entity in self.filters.included_entities:
for state_list in result: for state_list in result:

View file

@ -3,154 +3,202 @@
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/homekit/ https://home-assistant.io/components/homekit/
""" """
import asyncio
import logging import logging
import re from zlib import adler32
import voluptuous as vol import voluptuous as vol
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT,
TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.climate import ( from homeassistant.components.climate import (
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.components.cover import SUPPORT_SET_POSITION
from homeassistant.const import (
ATTR_CODE, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.util import get_local_ip from homeassistant.util import get_local_ip
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from .const import (
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER,
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START)
from .util import (
validate_entity_config, show_setup_message)
TYPES = Registry() TYPES = Registry()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_RE_VALID_PINCODE = r"^(\d{3}-\d{2}-\d{3})$"
DOMAIN = 'homekit'
REQUIREMENTS = ['HAP-python==1.1.7'] REQUIREMENTS = ['HAP-python==1.1.7']
BRIDGE_NAME = 'Home Assistant'
CONF_PIN_CODE = 'pincode'
HOMEKIT_FILE = '.homekit.state'
def valid_pin(value):
"""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.group(0)
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All({ DOMAIN: vol.All({
vol.Optional(CONF_PORT, default=51826): vol.Coerce(int), vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PIN_CODE, default='123-45-678'): valid_pin, vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean,
vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA,
vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config,
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Setup the HomeKit component.""" """Setup the HomeKit component."""
_LOGGER.debug("Begin setup HomeKit") _LOGGER.debug('Begin setup HomeKit')
conf = config[DOMAIN] conf = config[DOMAIN]
port = conf.get(CONF_PORT) port = conf[CONF_PORT]
pin = str.encode(conf.get(CONF_PIN_CODE)) auto_start = conf[CONF_AUTO_START]
entity_filter = conf[CONF_FILTER]
entity_config = conf[CONF_ENTITY_CONFIG]
homekit = HomeKit(hass, port) homekit = HomeKit(hass, port, entity_filter, entity_config)
homekit.setup_bridge(pin) homekit.setup()
if auto_start:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start)
return True
def handle_homekit_service_start(service):
"""Handle start HomeKit service call."""
if homekit.started:
_LOGGER.warning('HomeKit is already running')
return
homekit.start()
hass.services.async_register(DOMAIN, SERVICE_HOMEKIT_START,
handle_homekit_service_start)
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, homekit.start_driver)
return True return True
def import_types(): def get_accessory(hass, state, aid, config):
"""Import all types from files in the HomeKit directory."""
_LOGGER.debug("Import type files.")
# pylint: disable=unused-variable
from . import ( # noqa F401
covers, security_systems, sensors, switches, thermostats)
def get_accessory(hass, state):
"""Take state and return an accessory object if supported.""" """Take state and return an accessory object if supported."""
if not aid:
_LOGGER.warning('The entitiy "%s" is not supported, since it '
'generates an invalid aid, please change it.',
state.entity_id)
return None
if state.domain == 'sensor': if state.domain == 'sensor':
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT: if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT:
_LOGGER.debug("Add \"%s\" as \"%s\"", _LOGGER.debug('Add "%s" as "%s"',
state.entity_id, 'TemperatureSensor') state.entity_id, 'TemperatureSensor')
return TYPES['TemperatureSensor'](hass, state.entity_id, return TYPES['TemperatureSensor'](hass, state.entity_id,
state.name) state.name, aid=aid)
elif unit == '%':
_LOGGER.debug('Add "%s" as %s"',
state.entity_id, 'HumiditySensor')
return TYPES['HumiditySensor'](hass, state.entity_id, state.name,
aid=aid)
elif state.domain == 'cover': elif state.domain == 'cover':
# Only add covers that support set_cover_position # Only add covers that support set_cover_position
if state.attributes.get(ATTR_SUPPORTED_FEATURES) & 4: features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
_LOGGER.debug("Add \"%s\" as \"%s\"", if features & SUPPORT_SET_POSITION:
state.entity_id, 'Window') _LOGGER.debug('Add "%s" as "%s"',
return TYPES['Window'](hass, state.entity_id, state.name) state.entity_id, 'WindowCovering')
return TYPES['WindowCovering'](hass, state.entity_id, state.name,
aid=aid)
elif state.domain == 'alarm_control_panel': elif state.domain == 'alarm_control_panel':
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, _LOGGER.debug('Add "%s" as "%s"', state.entity_id,
'SecuritySystem') 'SecuritySystem')
return TYPES['SecuritySystem'](hass, state.entity_id, state.name) return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
alarm_code=config.get(ATTR_CODE),
aid=aid)
elif state.domain == 'climate': elif state.domain == 'climate':
support_auto = False features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
features = state.attributes.get(ATTR_SUPPORTED_FEATURES) support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \
SUPPORT_TARGET_TEMPERATURE_HIGH
# Check if climate device supports auto mode # Check if climate device supports auto mode
if (features & SUPPORT_TARGET_TEMPERATURE_HIGH) \ support_auto = bool(features & support_temp_range)
and (features & SUPPORT_TARGET_TEMPERATURE_LOW):
support_auto = True _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat')
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Thermostat')
return TYPES['Thermostat'](hass, state.entity_id, return TYPES['Thermostat'](hass, state.entity_id,
state.name, support_auto) state.name, support_auto, aid=aid)
elif state.domain == 'light':
return TYPES['Light'](hass, state.entity_id, state.name, aid=aid)
elif state.domain == 'switch' or state.domain == 'remote' \ elif state.domain == 'switch' or state.domain == 'remote' \
or state.domain == 'input_boolean': or state.domain == 'input_boolean' or state.domain == 'script':
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Switch') _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch')
return TYPES['Switch'](hass, state.entity_id, state.name) return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid)
return None return None
def generate_aid(entity_id):
"""Generate accessory aid with zlib adler32."""
aid = adler32(entity_id.encode('utf-8'))
if aid == 0 or aid == 1:
return None
return aid
class HomeKit(): class HomeKit():
"""Class to handle all actions between HomeKit and Home Assistant.""" """Class to handle all actions between HomeKit and Home Assistant."""
def __init__(self, hass, port): def __init__(self, hass, port, entity_filter, entity_config):
"""Initialize a HomeKit object.""" """Initialize a HomeKit object."""
self._hass = hass self._hass = hass
self._port = port self._port = port
self._filter = entity_filter
self._config = entity_config
self.started = False
self.bridge = None self.bridge = None
self.driver = None self.driver = None
def setup_bridge(self, pin): def setup(self):
"""Setup the bridge component to track all accessories.""" """Setup bridge and accessory driver."""
from .accessories import HomeBridge from .accessories import HomeBridge, HomeDriver
self.bridge = HomeBridge(BRIDGE_NAME, 'homekit.bridge', pin)
def start_driver(self, event): self._hass.bus.async_listen_once(
"""Start the accessory driver.""" EVENT_HOMEASSISTANT_STOP, self.stop)
from pyhap.accessory_driver import AccessoryDriver
self._hass.bus.listen_once(
EVENT_HOMEASSISTANT_STOP, self.stop_driver)
import_types()
_LOGGER.debug("Start adding accessories.")
for state in self._hass.states.all():
acc = get_accessory(self._hass, state)
if acc is not None:
self.bridge.add_accessory(acc)
ip_address = get_local_ip()
path = self._hass.config.path(HOMEKIT_FILE) path = self._hass.config.path(HOMEKIT_FILE)
self.driver = AccessoryDriver(self.bridge, self._port, self.bridge = HomeBridge(self._hass)
ip_address, path) self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path)
_LOGGER.debug("Driver started")
def add_bridge_accessory(self, state):
"""Try adding accessory to bridge if configured beforehand."""
if not state or not self._filter(state.entity_id):
return
aid = generate_aid(state.entity_id)
conf = self._config.pop(state.entity_id, {})
acc = get_accessory(self._hass, state, aid, conf)
if acc is not None:
self.bridge.add_accessory(acc)
def start(self, *args):
"""Start the accessory driver."""
if self.started:
return
self.started = True
# pylint: disable=unused-variable
from . import ( # noqa F401
type_covers, type_lights, type_security_systems, type_sensors,
type_switches, type_thermostats)
for state in self._hass.states.all():
self.add_bridge_accessory(state)
self.bridge.set_broker(self.driver)
if not self.bridge.paired:
show_setup_message(self.bridge, self._hass)
_LOGGER.debug('Driver start')
self.driver.start() self.driver.start()
def stop_driver(self, event): def stop(self, *args):
"""Stop the accessory driver.""" """Stop the accessory driver."""
_LOGGER.debug("Driver stop") if not self.started:
if self.driver is not None: return
_LOGGER.debug('Driver stop')
if self.driver and self.driver.run_sentinel:
self.driver.stop() self.driver.stop()

View file

@ -2,15 +2,33 @@
import logging import logging
from pyhap.accessory import Accessory, Bridge, Category from pyhap.accessory import Accessory, Bridge, Category
from pyhap.accessory_driver import AccessoryDriver
from homeassistant.helpers.event import async_track_state_change
from .const import ( from .const import (
SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER, ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER) MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE,
CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
from .util import (
show_setup_message, dismiss_setup_message)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def add_preload_service(acc, service, 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)
acc.add_service(service)
return service
def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
serial_number='0000'): serial_number='0000'):
"""Set the default accessory information.""" """Set the default accessory information."""
@ -21,50 +39,70 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number)
def add_preload_service(acc, service, chars=None, opt_chars=None): def override_properties(char, properties=None, valid_values=None):
"""Define and return a service to be available for the accessory.""" """Override characteristic property values and valid values."""
from pyhap.loader import get_serv_loader, get_char_loader if properties:
service = get_serv_loader().get(service) char.properties.update(properties)
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
if valid_values:
def override_properties(char, new_properties): char.properties['ValidValues'].update(valid_values)
"""Override characteristic property values."""
char.properties.update(new_properties)
class HomeAccessory(Accessory): class HomeAccessory(Accessory):
"""Class to extend the Accessory class.""" """Adapter class for Accessory."""
def __init__(self, display_name, model, category='OTHER', **kwargs): # pylint: disable=no-member
def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL,
category='OTHER', **kwargs):
"""Initialize a Accessory object.""" """Initialize a Accessory object."""
super().__init__(display_name, **kwargs) super().__init__(name, **kwargs)
set_accessory_info(self, display_name, model) set_accessory_info(self, name, model)
self.category = getattr(Category, category, Category.OTHER) self.category = getattr(Category, category, Category.OTHER)
def _set_services(self): def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO) add_preload_service(self, SERV_ACCESSORY_INFO)
def run(self):
"""Method called by accessory 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)
class HomeBridge(Bridge): class HomeBridge(Bridge):
"""Class to extend the Bridge class.""" """Adapter class for Bridge."""
def __init__(self, display_name, model, pincode, **kwargs): def __init__(self, hass, name=BRIDGE_NAME,
model=BRIDGE_MODEL, **kwargs):
"""Initialize a Bridge object.""" """Initialize a Bridge object."""
super().__init__(display_name, pincode=pincode, **kwargs) super().__init__(name, **kwargs)
set_accessory_info(self, display_name, model) set_accessory_info(self, name, model)
self._hass = hass
def _set_services(self): def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO) add_preload_service(self, SERV_ACCESSORY_INFO)
add_preload_service(self, SERV_BRIDGING_STATE) add_preload_service(self, SERV_BRIDGING_STATE)
def setup_message(self):
"""Prevent print of pyhap setup message to terminal."""
pass
def add_paired_client(self, client_uuid, client_public):
"""Override super function to dismiss setup message if paired."""
super().add_paired_client(client_uuid, client_public)
dismiss_setup_message(self._hass)
def remove_paired_client(self, client_uuid):
"""Override super function to show setup message if unpaired."""
super().remove_paired_client(client_uuid)
show_setup_message(self, self._hass)
class HomeDriver(AccessoryDriver):
"""Adapter class for AccessoryDriver."""
def __init__(self, *args, **kwargs):
"""Initialize a AccessoryDriver object."""
super().__init__(*args, **kwargs)

View file

@ -1,31 +1,67 @@
"""Constants used be the HomeKit component.""" """Constants used be the HomeKit component."""
# #### MISC ####
DOMAIN = 'homekit'
HOMEKIT_FILE = '.homekit.state'
HOMEKIT_NOTIFY_ID = 4663548
# #### CONFIG ####
CONF_AUTO_START = 'auto_start'
CONF_ENTITY_CONFIG = 'entity_config'
CONF_FILTER = 'filter'
# #### CONFIG DEFAULTS ####
DEFAULT_AUTO_START = True
DEFAULT_PORT = 51827
# #### HOMEKIT COMPONENT SERVICES ####
SERVICE_HOMEKIT_START = 'start'
# #### STRING CONSTANTS ####
ACCESSORY_MODEL = 'homekit.accessory'
ACCESSORY_NAME = 'Home Accessory'
BRIDGE_MODEL = 'homekit.bridge'
BRIDGE_NAME = 'Home Assistant'
MANUFACTURER = 'HomeAssistant' MANUFACTURER = 'HomeAssistant'
# Services # #### Categories ####
CATEGORY_LIGHT = 'LIGHTBULB'
CATEGORY_SENSOR = 'SENSOR'
# #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_ACCESSORY_INFO = 'AccessoryInformation'
SERV_BRIDGING_STATE = 'BridgingState' SERV_BRIDGING_STATE = 'BridgingState'
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered,
# StatusLowBattery, Name
SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name
SERV_SECURITY_SYSTEM = 'SecuritySystem' SERV_SECURITY_SYSTEM = 'SecuritySystem'
SERV_SWITCH = 'Switch' SERV_SWITCH = 'Switch'
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
SERV_THERMOSTAT = 'Thermostat' SERV_THERMOSTAT = 'Thermostat'
SERV_WINDOW_COVERING = 'WindowCovering' SERV_WINDOW_COVERING = 'WindowCovering'
# Characteristics
# #### Characteristics ####
CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier' CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier'
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
CHAR_CATEGORY = 'Category' CHAR_CATEGORY = 'Category'
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
CHAR_CURRENT_POSITION = 'CurrentPosition' CHAR_CURRENT_POSITION = 'CurrentPosition'
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
CHAR_LINK_QUALITY = 'LinkQuality' CHAR_LINK_QUALITY = 'LinkQuality'
CHAR_MANUFACTURER = 'Manufacturer' CHAR_MANUFACTURER = 'Manufacturer'
CHAR_MODEL = 'Model' CHAR_MODEL = 'Model'
CHAR_NAME = 'Name' CHAR_NAME = 'Name'
CHAR_ON = 'On' CHAR_ON = 'On' # boolean
CHAR_POSITION_STATE = 'PositionState' CHAR_POSITION_STATE = 'PositionState'
CHAR_REACHABLE = 'Reachable' CHAR_REACHABLE = 'Reachable'
CHAR_SATURATION = 'Saturation' # percent
CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SERIAL_NUMBER = 'SerialNumber'
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_POSITION = 'TargetPosition'
@ -33,5 +69,5 @@ CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
# Properties # #### Properties ####
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}

View file

@ -1,72 +0,0 @@
"""Class to hold all sensor accessories."""
import logging
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, add_preload_service, override_properties)
from .const import (
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 temperature in °C, °F.
"""
def __init__(self, hass, entity_id, display_name):
"""Initialize a TemperatureSensor accessory object."""
super().__init__(display_name, entity_id, 'SENSOR')
self._hass = hass
self._entity_id = entity_id
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."""
state = self._hass.states.get(self._entity_id)
self.update_temperature(new_state=state)
async_track_state_change(
self._hass, self._entity_id, self.update_temperature)
def update_temperature(self, entity_id=None, old_state=None,
new_state=None):
"""Update temperature after state changed."""
if new_state is None:
return
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,4 @@
# Describes the format for available HomeKit services
start:
description: Starts the HomeKit component driver.

View file

@ -2,7 +2,6 @@
import logging import logging
from homeassistant.components.cover import ATTR_CURRENT_POSITION from homeassistant.components.cover import ATTR_CURRENT_POSITION
from homeassistant.helpers.event import async_track_state_change
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, add_preload_service from .accessories import HomeAccessory, add_preload_service
@ -14,16 +13,17 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@TYPES.register('Window') @TYPES.register('WindowCovering')
class Window(HomeAccessory): class WindowCovering(HomeAccessory):
"""Generate a Window accessory for a cover entity. """Generate a Window accessory for a cover entity.
The cover entity must support: set_cover_position. The cover entity must support: set_cover_position.
""" """
def __init__(self, hass, entity_id, display_name): def __init__(self, hass, entity_id, display_name, *args, **kwargs):
"""Initialize a Window accessory object.""" """Initialize a WindowCovering accessory object."""
super().__init__(display_name, entity_id, 'WINDOW') super().__init__(display_name, entity_id, 'WINDOW_COVERING',
*args, **kwargs)
self._hass = hass self._hass = hass
self._entity_id = entity_id self._entity_id = entity_id
@ -31,12 +31,12 @@ class Window(HomeAccessory):
self.current_position = None self.current_position = None
self.homekit_target = None self.homekit_target = None
self.serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
self.char_current_position = self.serv_cover. \ self.char_current_position = serv_cover. \
get_characteristic(CHAR_CURRENT_POSITION) get_characteristic(CHAR_CURRENT_POSITION)
self.char_target_position = self.serv_cover. \ self.char_target_position = serv_cover. \
get_characteristic(CHAR_TARGET_POSITION) get_characteristic(CHAR_TARGET_POSITION)
self.char_position_state = self.serv_cover. \ self.char_position_state = serv_cover. \
get_characteristic(CHAR_POSITION_STATE) get_characteristic(CHAR_POSITION_STATE)
self.char_current_position.value = 0 self.char_current_position.value = 0
self.char_target_position.value = 0 self.char_target_position.value = 0
@ -44,36 +44,28 @@ class Window(HomeAccessory):
self.char_target_position.setter_callback = self.move_cover self.char_target_position.setter_callback = self.move_cover
def run(self):
"""Method called be object after driver is started."""
state = self._hass.states.get(self._entity_id)
self.update_cover_position(new_state=state)
async_track_state_change(
self._hass, self._entity_id, self.update_cover_position)
def move_cover(self, value): def move_cover(self, value):
"""Move cover to value if call came from HomeKit.""" """Move cover to value if call came from HomeKit."""
self.char_target_position.set_value(value, should_callback=False)
if value != self.current_position: if value != self.current_position:
_LOGGER.debug("%s: Set position to %d", self._entity_id, value) _LOGGER.debug('%s: Set position to %d', self._entity_id, value)
self.homekit_target = value self.homekit_target = value
if value > self.current_position: if value > self.current_position:
self.char_position_state.set_value(1) self.char_position_state.set_value(1)
elif value < self.current_position: elif value < self.current_position:
self.char_position_state.set_value(0) self.char_position_state.set_value(0)
self._hass.services.call( self._hass.components.cover.set_cover_position(
'cover', 'set_cover_position', value, self._entity_id)
{'entity_id': self._entity_id, 'position': value})
def update_cover_position(self, entity_id=None, old_state=None, def update_state(self, entity_id=None, old_state=None, new_state=None):
new_state=None):
"""Update cover position after state changed.""" """Update cover position after state changed."""
if new_state is None: if new_state is None:
return return
current_position = new_state.attributes[ATTR_CURRENT_POSITION] current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
if current_position is None: if current_position is None:
return return
self.current_position = int(current_position) self.current_position = int(current_position)
self.char_current_position.set_value(self.current_position) self.char_current_position.set_value(self.current_position)

View file

@ -0,0 +1,153 @@
"""Class to hold all light accessories."""
import logging
from homeassistant.components.light import (
ATTR_HS_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, SUPPORT_COLOR)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
CATEGORY_LIGHT, SERV_LIGHTBULB,
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
_LOGGER = logging.getLogger(__name__)
RGB_COLOR = 'rgb_color'
@TYPES.register('Light')
class Light(HomeAccessory):
"""Generate a Light accessory for a light entity.
Currently supports: state, brightness, rgb_color.
"""
def __init__(self, hass, entity_id, name, *args, **kwargs):
"""Initialize a new Light accessory object."""
super().__init__(name, entity_id, CATEGORY_LIGHT, *args, **kwargs)
self._hass = hass
self._entity_id = entity_id
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
CHAR_HUE: False, CHAR_SATURATION: False,
RGB_COLOR: False}
self._state = 0
self.chars = []
self._features = self._hass.states.get(self._entity_id) \
.attributes.get(ATTR_SUPPORTED_FEATURES)
if self._features & SUPPORT_BRIGHTNESS:
self.chars.append(CHAR_BRIGHTNESS)
if self._features & SUPPORT_COLOR:
self.chars.append(CHAR_HUE)
self.chars.append(CHAR_SATURATION)
self._hue = None
self._saturation = None
serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars)
self.char_on = serv_light.get_characteristic(CHAR_ON)
self.char_on.setter_callback = self.set_state
self.char_on.value = self._state
if CHAR_BRIGHTNESS in self.chars:
self.char_brightness = serv_light \
.get_characteristic(CHAR_BRIGHTNESS)
self.char_brightness.setter_callback = self.set_brightness
self.char_brightness.value = 0
if CHAR_HUE in self.chars:
self.char_hue = serv_light.get_characteristic(CHAR_HUE)
self.char_hue.setter_callback = self.set_hue
self.char_hue.value = 0
if CHAR_SATURATION in self.chars:
self.char_saturation = serv_light \
.get_characteristic(CHAR_SATURATION)
self.char_saturation.setter_callback = self.set_saturation
self.char_saturation.value = 75
def set_state(self, value):
"""Set state if call came from HomeKit."""
if self._state == value:
return
_LOGGER.debug('%s: Set state to %d', self._entity_id, value)
self._flag[CHAR_ON] = True
self.char_on.set_value(value, should_callback=False)
if value == 1:
self._hass.components.light.turn_on(self._entity_id)
elif value == 0:
self._hass.components.light.turn_off(self._entity_id)
def set_brightness(self, value):
"""Set brightness if call came from HomeKit."""
_LOGGER.debug('%s: Set brightness to %d', self._entity_id, value)
self._flag[CHAR_BRIGHTNESS] = True
self.char_brightness.set_value(value, should_callback=False)
if value != 0:
self._hass.components.light.turn_on(
self._entity_id, brightness_pct=value)
else:
self._hass.components.light.turn_off(self._entity_id)
def set_saturation(self, value):
"""Set saturation if call came from HomeKit."""
_LOGGER.debug('%s: Set saturation to %d', self._entity_id, value)
self._flag[CHAR_SATURATION] = True
self.char_saturation.set_value(value, should_callback=False)
self._saturation = value
self.set_color()
def set_hue(self, value):
"""Set hue if call came from HomeKit."""
_LOGGER.debug('%s: Set hue to %d', self._entity_id, value)
self._flag[CHAR_HUE] = True
self.char_hue.set_value(value, should_callback=False)
self._hue = value
self.set_color()
def set_color(self):
"""Set color if call came from HomeKit."""
# Handle Color
if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \
self._flag[CHAR_SATURATION]:
color = (self._hue, self._saturation)
_LOGGER.debug('%s: Set hs_color to %s', self._entity_id, color)
self._flag.update({
CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True})
self._hass.components.light.turn_on(
self._entity_id, hs_color=color)
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update light after state change."""
if not new_state:
return
# Handle State
state = new_state.state
if state in (STATE_ON, STATE_OFF):
self._state = 1 if state == STATE_ON else 0
if not self._flag[CHAR_ON] and self.char_on.value != self._state:
self.char_on.set_value(self._state, should_callback=False)
self._flag[CHAR_ON] = False
# Handle Brightness
if CHAR_BRIGHTNESS in self.chars:
brightness = new_state.attributes.get(ATTR_BRIGHTNESS)
if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int):
brightness = round(brightness / 255 * 100, 0)
if self.char_brightness.value != brightness:
self.char_brightness.set_value(brightness,
should_callback=False)
self._flag[CHAR_BRIGHTNESS] = False
# Handle Color
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
hue, saturation = new_state.attributes.get(
ATTR_HS_COLOR, (None, None))
if not self._flag[RGB_COLOR] and (
hue != self._hue or saturation != self._saturation):
self.char_hue.set_value(hue, should_callback=False)
self.char_saturation.set_value(saturation,
should_callback=False)
self._flag[RGB_COLOR] = False

View file

@ -5,7 +5,6 @@ from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
ATTR_ENTITY_ID, ATTR_CODE) ATTR_ENTITY_ID, ATTR_CODE)
from homeassistant.helpers.event import async_track_state_change
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, add_preload_service from .accessories import HomeAccessory, add_preload_service
@ -28,9 +27,11 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
class SecuritySystem(HomeAccessory): class SecuritySystem(HomeAccessory):
"""Generate an SecuritySystem accessory for an alarm control panel.""" """Generate an SecuritySystem accessory for an alarm control panel."""
def __init__(self, hass, entity_id, display_name, alarm_code=None): def __init__(self, hass, entity_id, display_name,
alarm_code, *args, **kwargs):
"""Initialize a SecuritySystem accessory object.""" """Initialize a SecuritySystem accessory object."""
super().__init__(display_name, entity_id, 'ALARM_SYSTEM') super().__init__(display_name, entity_id, 'ALARM_SYSTEM',
*args, **kwargs)
self._hass = hass self._hass = hass
self._entity_id = entity_id self._entity_id = entity_id
@ -38,39 +39,31 @@ class SecuritySystem(HomeAccessory):
self.flag_target_state = False self.flag_target_state = False
self.service_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
self.char_current_state = self.service_alarm. \ self.char_current_state = serv_alarm. \
get_characteristic(CHAR_CURRENT_SECURITY_STATE) get_characteristic(CHAR_CURRENT_SECURITY_STATE)
self.char_current_state.value = 3 self.char_current_state.value = 3
self.char_target_state = self.service_alarm. \ self.char_target_state = serv_alarm. \
get_characteristic(CHAR_TARGET_SECURITY_STATE) get_characteristic(CHAR_TARGET_SECURITY_STATE)
self.char_target_state.value = 3 self.char_target_state.value = 3
self.char_target_state.setter_callback = self.set_security_state 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): def set_security_state(self, value):
"""Move security state to value if call came from HomeKit.""" """Move security state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set security state to %d", _LOGGER.debug('%s: Set security state to %d',
self._entity_id, value) self._entity_id, value)
self.flag_target_state = True self.flag_target_state = True
self.char_target_state.set_value(value, should_callback=False)
hass_value = HOMEKIT_TO_HASS[value] hass_value = HOMEKIT_TO_HASS[value]
service = STATE_TO_SERVICE[hass_value] service = STATE_TO_SERVICE[hass_value]
params = {ATTR_ENTITY_ID: self._entity_id} params = {ATTR_ENTITY_ID: self._entity_id}
if self._alarm_code is not None: if self._alarm_code:
params[ATTR_CODE] = self._alarm_code params[ATTR_CODE] = self._alarm_code
self._hass.services.call('alarm_control_panel', service, params) self._hass.services.call('alarm_control_panel', service, params)
def update_security_state(self, entity_id=None, def update_state(self, entity_id=None, old_state=None, new_state=None):
old_state=None, new_state=None):
"""Update security state after state changed.""" """Update security state after state changed."""
if new_state is None: if new_state is None:
return return
@ -78,15 +71,15 @@ class SecuritySystem(HomeAccessory):
hass_state = new_state.state hass_state = new_state.state
if hass_state not in HASS_TO_HOMEKIT: if hass_state not in HASS_TO_HOMEKIT:
return return
current_security_state = HASS_TO_HOMEKIT[hass_state] current_security_state = HASS_TO_HOMEKIT[hass_state]
self.char_current_state.set_value(current_security_state) self.char_current_state.set_value(current_security_state,
_LOGGER.debug("%s: Updated current state to %s (%d)", should_callback=False)
self._entity_id, hass_state, _LOGGER.debug('%s: Updated current state to %s (%d)',
current_security_state) self._entity_id, hass_state, current_security_state)
if not self.flag_target_state: if not self.flag_target_state:
self.char_target_state.set_value(current_security_state, self.char_target_state.set_value(current_security_state,
should_callback=False) should_callback=False)
elif self.char_target_state.get_value() \ if self.char_target_state.value == self.char_current_state.value:
== self.char_current_state.get_value():
self.flag_target_state = False self.flag_target_state = False

View file

@ -0,0 +1,78 @@
"""Class to hold all sensor accessories."""
import logging
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
from . import TYPES
from .accessories import (
HomeAccessory, add_preload_service, override_properties)
from .const import (
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
from .util import convert_to_float, temperature_to_homekit
_LOGGER = logging.getLogger(__name__)
@TYPES.register('TemperatureSensor')
class TemperatureSensor(HomeAccessory):
"""Generate a TemperatureSensor accessory for a temperature sensor.
Sensor entity must return temperature in °C, °F.
"""
def __init__(self, hass, entity_id, name, *args, **kwargs):
"""Initialize a TemperatureSensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
self._hass = hass
self._entity_id = entity_id
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE)
override_properties(self.char_temp, PROP_CELSIUS)
self.char_temp.value = 0
self.unit = None
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update temperature after state changed."""
if new_state is None:
return
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
temperature = convert_to_float(new_state.state)
if temperature:
temperature = temperature_to_homekit(temperature, unit)
self.char_temp.set_value(temperature, should_callback=False)
_LOGGER.debug('%s: Current temperature set to %d°C',
self._entity_id, temperature)
@TYPES.register('HumiditySensor')
class HumiditySensor(HomeAccessory):
"""Generate a HumiditySensor accessory as humidity sensor."""
def __init__(self, hass, entity_id, name, *args, **kwargs):
"""Initialize a HumiditySensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
self._hass = hass
self._entity_id = entity_id
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
self.char_humidity = serv_humidity \
.get_characteristic(CHAR_CURRENT_HUMIDITY)
self.char_humidity.value = 0
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update accessory after state change."""
if new_state is None:
return
humidity = convert_to_float(new_state.state)
if humidity:
self.char_humidity.set_value(humidity, should_callback=False)
_LOGGER.debug('%s: Percent set to %d%%',
self._entity_id, humidity)

View file

@ -1,9 +1,9 @@
"""Class to hold all switch accessories.""" """Class to hold all switch accessories."""
import logging import logging
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON)
from homeassistant.core import split_entity_id from homeassistant.core import split_entity_id
from homeassistant.helpers.event import async_track_state_change
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, add_preload_service from .accessories import HomeAccessory, add_preload_service
@ -16,9 +16,9 @@ _LOGGER = logging.getLogger(__name__)
class Switch(HomeAccessory): class Switch(HomeAccessory):
"""Generate a Switch accessory.""" """Generate a Switch accessory."""
def __init__(self, hass, entity_id, display_name): def __init__(self, hass, entity_id, display_name, *args, **kwargs):
"""Initialize a Switch accessory object to represent a remote.""" """Initialize a Switch accessory object to represent a remote."""
super().__init__(display_name, entity_id, 'SWITCH') super().__init__(display_name, entity_id, 'SWITCH', *args, **kwargs)
self._hass = hass self._hass = hass
self._entity_id = entity_id self._entity_id = entity_id
@ -26,25 +26,18 @@ class Switch(HomeAccessory):
self.flag_target_state = False self.flag_target_state = False
self.service_switch = add_preload_service(self, SERV_SWITCH) serv_switch = add_preload_service(self, SERV_SWITCH)
self.char_on = self.service_switch.get_characteristic(CHAR_ON) self.char_on = serv_switch.get_characteristic(CHAR_ON)
self.char_on.value = False self.char_on.value = False
self.char_on.setter_callback = self.set_state 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): def set_state(self, value):
"""Move switch state to value if call came from HomeKit.""" """Move switch state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set switch state to %s", _LOGGER.debug('%s: Set switch state to %s',
self._entity_id, value) self._entity_id, value)
self.flag_target_state = True self.flag_target_state = True
service = 'turn_on' if value else 'turn_off' self.char_on.set_value(value, should_callback=False)
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self._hass.services.call(self._domain, service, self._hass.services.call(self._domain, service,
{ATTR_ENTITY_ID: self._entity_id}) {ATTR_ENTITY_ID: self._entity_id})
@ -53,10 +46,10 @@ class Switch(HomeAccessory):
if new_state is None: if new_state is None:
return return
current_state = (new_state.state == 'on') current_state = (new_state.state == STATE_ON)
if not self.flag_target_state: if not self.flag_target_state:
_LOGGER.debug("%s: Set current state to %s", _LOGGER.debug('%s: Set current state to %s',
self._entity_id, current_state) self._entity_id, current_state)
self.char_on.set_value(current_state, should_callback=False) self.char_on.set_value(current_state, should_callback=False)
else:
self.flag_target_state = False self.flag_target_state = False

View file

@ -7,9 +7,7 @@ from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
STATE_HEAT, STATE_COOL, STATE_AUTO) STATE_HEAT, STATE_COOL, STATE_AUTO)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT)
TEMP_CELSIUS, TEMP_FAHRENHEIT)
from homeassistant.helpers.event import async_track_state_change
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, add_preload_service from .accessories import HomeAccessory, add_preload_service
@ -18,6 +16,7 @@ from .const import (
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
from .util import temperature_to_homekit, temperature_to_states
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -33,61 +32,63 @@ HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
class Thermostat(HomeAccessory): class Thermostat(HomeAccessory):
"""Generate a Thermostat accessory for a climate.""" """Generate a Thermostat accessory for a climate."""
def __init__(self, hass, entity_id, display_name, support_auto=False): def __init__(self, hass, entity_id, display_name,
support_auto, *args, **kwargs):
"""Initialize a Thermostat accessory object.""" """Initialize a Thermostat accessory object."""
super().__init__(display_name, entity_id, 'THERMOSTAT') super().__init__(display_name, entity_id, 'THERMOSTAT',
*args, **kwargs)
self._hass = hass self._hass = hass
self._entity_id = entity_id self._entity_id = entity_id
self._call_timer = None self._call_timer = None
self._unit = TEMP_CELSIUS
self.heat_cool_flag_target_state = False self.heat_cool_flag_target_state = False
self.temperature_flag_target_state = False self.temperature_flag_target_state = False
self.coolingthresh_flag_target_state = False self.coolingthresh_flag_target_state = False
self.heatingthresh_flag_target_state = False self.heatingthresh_flag_target_state = False
extra_chars = None
# Add additional characteristics if auto mode is supported # Add additional characteristics if auto mode is supported
if support_auto: extra_chars = [
extra_chars = [CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_COOLING_THRESHOLD_TEMPERATURE,
CHAR_HEATING_THRESHOLD_TEMPERATURE] CHAR_HEATING_THRESHOLD_TEMPERATURE] if support_auto else None
# Preload the thermostat service # Preload the thermostat service
self.service_thermostat = add_preload_service(self, SERV_THERMOSTAT, serv_thermostat = add_preload_service(self, SERV_THERMOSTAT,
extra_chars) extra_chars)
# Current and target mode characteristics # Current and target mode characteristics
self.char_current_heat_cool = self.service_thermostat. \ self.char_current_heat_cool = serv_thermostat. \
get_characteristic(CHAR_CURRENT_HEATING_COOLING) get_characteristic(CHAR_CURRENT_HEATING_COOLING)
self.char_current_heat_cool.value = 0 self.char_current_heat_cool.value = 0
self.char_target_heat_cool = self.service_thermostat. \ self.char_target_heat_cool = serv_thermostat. \
get_characteristic(CHAR_TARGET_HEATING_COOLING) get_characteristic(CHAR_TARGET_HEATING_COOLING)
self.char_target_heat_cool.value = 0 self.char_target_heat_cool.value = 0
self.char_target_heat_cool.setter_callback = self.set_heat_cool self.char_target_heat_cool.setter_callback = self.set_heat_cool
# Current and target temperature characteristics # Current and target temperature characteristics
self.char_current_temp = self.service_thermostat. \ self.char_current_temp = serv_thermostat. \
get_characteristic(CHAR_CURRENT_TEMPERATURE) get_characteristic(CHAR_CURRENT_TEMPERATURE)
self.char_current_temp.value = 21.0 self.char_current_temp.value = 21.0
self.char_target_temp = self.service_thermostat. \ self.char_target_temp = serv_thermostat. \
get_characteristic(CHAR_TARGET_TEMPERATURE) get_characteristic(CHAR_TARGET_TEMPERATURE)
self.char_target_temp.value = 21.0 self.char_target_temp.value = 21.0
self.char_target_temp.setter_callback = self.set_target_temperature self.char_target_temp.setter_callback = self.set_target_temperature
# Display units characteristic # Display units characteristic
self.char_display_units = self.service_thermostat. \ self.char_display_units = serv_thermostat. \
get_characteristic(CHAR_TEMP_DISPLAY_UNITS) get_characteristic(CHAR_TEMP_DISPLAY_UNITS)
self.char_display_units.value = 0 self.char_display_units.value = 0
# If the device supports it: high and low temperature characteristics # If the device supports it: high and low temperature characteristics
if support_auto: if support_auto:
self.char_cooling_thresh_temp = self.service_thermostat. \ self.char_cooling_thresh_temp = serv_thermostat. \
get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE) get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE)
self.char_cooling_thresh_temp.value = 23.0 self.char_cooling_thresh_temp.value = 23.0
self.char_cooling_thresh_temp.setter_callback = \ self.char_cooling_thresh_temp.setter_callback = \
self.set_cooling_threshold self.set_cooling_threshold
self.char_heating_thresh_temp = self.service_thermostat. \ self.char_heating_thresh_temp = serv_thermostat. \
get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE) get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE)
self.char_heating_thresh_temp.value = 19.0 self.char_heating_thresh_temp.value = 19.0
self.char_heating_thresh_temp.setter_callback = \ self.char_heating_thresh_temp.setter_callback = \
@ -96,132 +97,127 @@ class Thermostat(HomeAccessory):
self.char_cooling_thresh_temp = None self.char_cooling_thresh_temp = None
self.char_heating_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): def set_heat_cool(self, value):
"""Move operation mode to value if call came from HomeKit.""" """Move operation mode to value if call came from HomeKit."""
self.char_target_heat_cool.set_value(value, should_callback=False)
if value in HC_HOMEKIT_TO_HASS: if value in HC_HOMEKIT_TO_HASS:
_LOGGER.debug("%s: Set heat-cool to %d", self._entity_id, value) _LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value)
self.heat_cool_flag_target_state = True self.heat_cool_flag_target_state = True
hass_value = HC_HOMEKIT_TO_HASS[value] hass_value = HC_HOMEKIT_TO_HASS[value]
self._hass.services.call('climate', 'set_operation_mode', self._hass.components.climate.set_operation_mode(
{ATTR_ENTITY_ID: self._entity_id, operation_mode=hass_value, entity_id=self._entity_id)
ATTR_OPERATION_MODE: hass_value})
def set_cooling_threshold(self, value): def set_cooling_threshold(self, value):
"""Set cooling threshold temp to value if call came from HomeKit.""" """Set cooling threshold temp to value if call came from HomeKit."""
_LOGGER.debug("%s: Set cooling threshold temperature to %.2f", _LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
self._entity_id, value) self._entity_id, value)
self.coolingthresh_flag_target_state = True self.coolingthresh_flag_target_state = True
low = self.char_heating_thresh_temp.get_value() self.char_cooling_thresh_temp.set_value(value, should_callback=False)
self._hass.services.call( low = self.char_heating_thresh_temp.value
'climate', 'set_temperature', low = temperature_to_states(low, self._unit)
{ATTR_ENTITY_ID: self._entity_id, value = temperature_to_states(value, self._unit)
ATTR_TARGET_TEMP_HIGH: value, self._hass.components.climate.set_temperature(
ATTR_TARGET_TEMP_LOW: low}) entity_id=self._entity_id, target_temp_high=value,
target_temp_low=low)
def set_heating_threshold(self, value): def set_heating_threshold(self, value):
"""Set heating threshold temp to value if call came from HomeKit.""" """Set heating threshold temp to value if call came from HomeKit."""
_LOGGER.debug("%s: Set heating threshold temperature to %.2f", _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
self._entity_id, value) self._entity_id, value)
self.heatingthresh_flag_target_state = True self.heatingthresh_flag_target_state = True
self.char_heating_thresh_temp.set_value(value, should_callback=False)
# Home assistant always wants to set low and high at the same time # Home assistant always wants to set low and high at the same time
high = self.char_cooling_thresh_temp.get_value() high = self.char_cooling_thresh_temp.value
self._hass.services.call( high = temperature_to_states(high, self._unit)
'climate', 'set_temperature', value = temperature_to_states(value, self._unit)
{ATTR_ENTITY_ID: self._entity_id, self._hass.components.climate.set_temperature(
ATTR_TARGET_TEMP_LOW: value, entity_id=self._entity_id, target_temp_high=high,
ATTR_TARGET_TEMP_HIGH: high}) target_temp_low=value)
def set_target_temperature(self, value): def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit.""" """Set target temperature to value if call came from HomeKit."""
_LOGGER.debug("%s: Set target temperature to %.2f", _LOGGER.debug('%s: Set target temperature to %.2f°C',
self._entity_id, value) self._entity_id, value)
self.temperature_flag_target_state = True self.temperature_flag_target_state = True
self._hass.services.call( self.char_target_temp.set_value(value, should_callback=False)
'climate', 'set_temperature', value = temperature_to_states(value, self._unit)
{ATTR_ENTITY_ID: self._entity_id, self._hass.components.climate.set_temperature(
ATTR_TEMPERATURE: value}) temperature=value, entity_id=self._entity_id)
def update_thermostat(self, entity_id=None, def update_state(self, entity_id=None, old_state=None, new_state=None):
old_state=None, new_state=None):
"""Update security state after state changed.""" """Update security state after state changed."""
if new_state is None: if new_state is None:
return return
self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS)
# Update current temperature # Update current temperature
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
if isinstance(current_temp, (int, float)): if isinstance(current_temp, (int, float)):
current_temp = temperature_to_homekit(current_temp, self._unit)
self.char_current_temp.set_value(current_temp) self.char_current_temp.set_value(current_temp)
# Update target temperature # Update target temperature
target_temp = new_state.attributes.get(ATTR_TEMPERATURE) target_temp = new_state.attributes.get(ATTR_TEMPERATURE)
if isinstance(target_temp, (int, float)): if isinstance(target_temp, (int, float)):
target_temp = temperature_to_homekit(target_temp, self._unit)
if not self.temperature_flag_target_state: if not self.temperature_flag_target_state:
self.char_target_temp.set_value(target_temp, self.char_target_temp.set_value(target_temp,
should_callback=False) should_callback=False)
else: self.temperature_flag_target_state = False
self.temperature_flag_target_state = False
# Update cooling threshold temperature if characteristic exists # Update cooling threshold temperature if characteristic exists
if self.char_cooling_thresh_temp is not None: if self.char_cooling_thresh_temp:
cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH)
if cooling_thresh is not None: if isinstance(cooling_thresh, (int, float)):
cooling_thresh = temperature_to_homekit(cooling_thresh,
self._unit)
if not self.coolingthresh_flag_target_state: if not self.coolingthresh_flag_target_state:
self.char_cooling_thresh_temp.set_value( self.char_cooling_thresh_temp.set_value(
cooling_thresh, should_callback=False) cooling_thresh, should_callback=False)
else: self.coolingthresh_flag_target_state = False
self.coolingthresh_flag_target_state = False
# Update heating threshold temperature if characteristic exists # Update heating threshold temperature if characteristic exists
if self.char_heating_thresh_temp is not None: if self.char_heating_thresh_temp:
heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW)
if heating_thresh is not None: if isinstance(heating_thresh, (int, float)):
heating_thresh = temperature_to_homekit(heating_thresh,
self._unit)
if not self.heatingthresh_flag_target_state: if not self.heatingthresh_flag_target_state:
self.char_heating_thresh_temp.set_value( self.char_heating_thresh_temp.set_value(
heating_thresh, should_callback=False) heating_thresh, should_callback=False)
else: self.heatingthresh_flag_target_state = False
self.heatingthresh_flag_target_state = False
# Update display units # Update display units
display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:
if display_units is not None \ self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit])
and display_units in UNIT_HASS_TO_HOMEKIT:
self.char_display_units.set_value(
UNIT_HASS_TO_HOMEKIT[display_units])
# Update target operation mode # Update target operation mode
operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE)
if operation_mode is not None \ if operation_mode \
and operation_mode in HC_HASS_TO_HOMEKIT: and operation_mode in HC_HASS_TO_HOMEKIT:
if not self.heat_cool_flag_target_state: if not self.heat_cool_flag_target_state:
self.char_target_heat_cool.set_value( self.char_target_heat_cool.set_value(
HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False) HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False)
else: self.heat_cool_flag_target_state = False
self.heat_cool_flag_target_state = False
# Set current operation mode based on temperatures and target mode # Set current operation mode based on temperatures and target mode
if operation_mode == STATE_HEAT: if operation_mode == STATE_HEAT:
if current_temp < target_temp: if isinstance(target_temp, float) and current_temp < target_temp:
current_operation_mode = STATE_HEAT current_operation_mode = STATE_HEAT
else: else:
current_operation_mode = STATE_OFF current_operation_mode = STATE_OFF
elif operation_mode == STATE_COOL: elif operation_mode == STATE_COOL:
if current_temp > target_temp: if isinstance(target_temp, float) and current_temp > target_temp:
current_operation_mode = STATE_COOL current_operation_mode = STATE_COOL
else: else:
current_operation_mode = STATE_OFF current_operation_mode = STATE_OFF
elif operation_mode == STATE_AUTO: elif operation_mode == STATE_AUTO:
# Check if auto is supported # Check if auto is supported
if self.char_cooling_thresh_temp is not None: if self.char_cooling_thresh_temp:
lower_temp = self.char_heating_thresh_temp.get_value() lower_temp = self.char_heating_thresh_temp.value
upper_temp = self.char_cooling_thresh_temp.get_value() upper_temp = self.char_cooling_thresh_temp.value
if current_temp < lower_temp: if current_temp < lower_temp:
current_operation_mode = STATE_HEAT current_operation_mode = STATE_HEAT
elif current_temp > upper_temp: elif current_temp > upper_temp:
@ -232,9 +228,11 @@ class Thermostat(HomeAccessory):
# Check if heating or cooling are supported # Check if heating or cooling are supported
heat = STATE_HEAT in new_state.attributes[ATTR_OPERATION_LIST] heat = STATE_HEAT in new_state.attributes[ATTR_OPERATION_LIST]
cool = STATE_COOL in new_state.attributes[ATTR_OPERATION_LIST] cool = STATE_COOL in new_state.attributes[ATTR_OPERATION_LIST]
if current_temp < target_temp and heat: if isinstance(target_temp, float) and \
current_temp < target_temp and heat:
current_operation_mode = STATE_HEAT current_operation_mode = STATE_HEAT
elif current_temp > target_temp and cool: elif isinstance(target_temp, float) and \
current_temp > target_temp and cool:
current_operation_mode = STATE_COOL current_operation_mode = STATE_COOL
else: else:
current_operation_mode = STATE_OFF current_operation_mode = STATE_OFF

View file

@ -0,0 +1,65 @@
"""Collection of useful functions for the HomeKit component."""
import logging
import voluptuous as vol
from homeassistant.core import split_entity_id
from homeassistant.const import (
ATTR_CODE, TEMP_CELSIUS)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.temperature as temp_util
from .const import HOMEKIT_NOTIFY_ID
_LOGGER = logging.getLogger(__name__)
def validate_entity_config(values):
"""Validate config entry for CONF_ENTITY."""
entities = {}
for key, config in values.items():
entity = cv.entity_id(key)
params = {}
if not isinstance(config, dict):
raise vol.Invalid('The configuration for "{}" must be '
' an dictionary.'.format(entity))
domain, _ = split_entity_id(entity)
if domain == 'alarm_control_panel':
code = config.get(ATTR_CODE)
params[ATTR_CODE] = cv.string(code) if code else None
entities[entity] = params
return entities
def show_setup_message(bridge, hass):
"""Display persistent notification with setup information."""
pin = bridge.pincode.decode()
message = 'To setup Home Assistant in the Home App, enter the ' \
'following code:\n### {}'.format(pin)
hass.components.persistent_notification.create(
message, 'HomeKit Setup', HOMEKIT_NOTIFY_ID)
def dismiss_setup_message(hass):
"""Dismiss persistent notification and remove QR code."""
hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID)
def convert_to_float(state):
"""Return float of state, catch errors."""
try:
return float(state)
except (ValueError, TypeError):
return None
def temperature_to_homekit(temperature, unit):
"""Convert temperature to Celsius for HomeKit."""
return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1)
def temperature_to_states(temperature, unit):
"""Convert temperature back from Celsius to Home Assistant unit."""
return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1)

View file

@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['pyhomematic==0.1.39'] REQUIREMENTS = ['pyhomematic==0.1.40']
DOMAIN = 'homematic' DOMAIN = 'homematic'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -33,6 +33,7 @@ DISCOVER_SENSORS = 'homematic.sensor'
DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor' DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor'
DISCOVER_COVER = 'homematic.cover' DISCOVER_COVER = 'homematic.cover'
DISCOVER_CLIMATE = 'homematic.climate' DISCOVER_CLIMATE = 'homematic.climate'
DISCOVER_LOCKS = 'homematic.locks'
ATTR_DISCOVER_DEVICES = 'devices' ATTR_DISCOVER_DEVICES = 'devices'
ATTR_PARAM = 'param' ATTR_PARAM = 'param'
@ -59,7 +60,7 @@ SERVICE_SET_INSTALL_MODE = 'set_install_mode'
HM_DEVICE_TYPES = { HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: [ DISCOVER_SWITCHES: [
'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren', 'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren',
'IPSwitchPowermeter', 'KeyMatic', 'HMWIOSwitch', 'Rain', 'EcoLogic'], 'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic'],
DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'], DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'],
DISCOVER_SENSORS: [ DISCOVER_SENSORS: [
'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP', 'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP',
@ -68,7 +69,7 @@ HM_DEVICE_TYPES = {
'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor', 'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor',
'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch', 'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch',
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
'IPSmoke', 'RFSiren', 'PresenceIP'], 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat'],
DISCOVER_CLIMATE: [ DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
@ -78,7 +79,8 @@ HM_DEVICE_TYPES = {
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
'WiredSensor', 'PresenceIP'], 'WiredSensor', 'PresenceIP'],
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'] DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'],
DISCOVER_LOCKS: ['KeyMatic']
} }
HM_IGNORE_DISCOVERY_NODE = [ HM_IGNORE_DISCOVERY_NODE = [
@ -86,6 +88,10 @@ HM_IGNORE_DISCOVERY_NODE = [
'ACTUAL_HUMIDITY' 'ACTUAL_HUMIDITY'
] ]
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = {
'ACTUAL_TEMPERATURE': ['IPAreaThermostat'],
}
HM_ATTRIBUTE_SUPPORT = { HM_ATTRIBUTE_SUPPORT = {
'LOWBAT': ['battery', {0: 'High', 1: 'Low'}], 'LOWBAT': ['battery', {0: 'High', 1: 'Low'}],
'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}], 'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}],
@ -460,7 +466,8 @@ def _system_callback_handler(hass, config, src, *args):
('cover', DISCOVER_COVER), ('cover', DISCOVER_COVER),
('binary_sensor', DISCOVER_BINARY_SENSORS), ('binary_sensor', DISCOVER_BINARY_SENSORS),
('sensor', DISCOVER_SENSORS), ('sensor', DISCOVER_SENSORS),
('climate', DISCOVER_CLIMATE)): ('climate', DISCOVER_CLIMATE),
('lock', DISCOVER_LOCKS)):
# Get all devices of a specific type # Get all devices of a specific type
found_devices = _get_devices( found_devices = _get_devices(
hass, discovery_type, addresses, interface) hass, discovery_type, addresses, interface)
@ -505,7 +512,8 @@ def _get_devices(hass, discovery_type, keys, interface):
# Generate options for 1...n elements with 1...n parameters # Generate options for 1...n elements with 1...n parameters
for param, channels in metadata.items(): for param, channels in metadata.items():
if param in HM_IGNORE_DISCOVERY_NODE: if param in HM_IGNORE_DISCOVERY_NODE and class_name not in \
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS.get(param, []):
continue continue
# Add devices # Add devices

View file

@ -0,0 +1,176 @@
"""
Support for HomematicIP components.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematicip_cloud/
"""
import logging
from socket import timeout
import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (dispatcher_send,
async_dispatcher_connect)
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['homematicip==0.8']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'homematicip_cloud'
CONF_NAME = 'name'
CONF_ACCESSPOINT = 'accesspoint'
CONF_AUTHTOKEN = 'authtoken'
CONFIG_SCHEMA = vol.Schema({
vol.Optional(DOMAIN): [vol.Schema({
vol.Optional(CONF_NAME, default=''): cv.string,
vol.Required(CONF_ACCESSPOINT): cv.string,
vol.Required(CONF_AUTHTOKEN): cv.string,
})],
}, extra=vol.ALLOW_EXTRA)
EVENT_HOME_CHANGED = 'homematicip_home_changed'
EVENT_DEVICE_CHANGED = 'homematicip_device_changed'
EVENT_GROUP_CHANGED = 'homematicip_group_changed'
EVENT_SECURITY_CHANGED = 'homematicip_security_changed'
EVENT_JOURNAL_CHANGED = 'homematicip_journal_changed'
ATTR_HOME_ID = 'home_id'
ATTR_HOME_LABEL = 'home_label'
ATTR_DEVICE_ID = 'device_id'
ATTR_DEVICE_LABEL = 'device_label'
ATTR_STATUS_UPDATE = 'status_update'
ATTR_FIRMWARE_STATE = 'firmware_state'
ATTR_LOW_BATTERY = 'low_battery'
ATTR_SABOTAGE = 'sabotage'
ATTR_RSSI = 'rssi'
ATTR_TYPE = 'type'
def setup(hass, config):
"""Set up the HomematicIP component."""
# pylint: disable=import-error, no-name-in-module
from homematicip.home import Home
hass.data.setdefault(DOMAIN, {})
homes = hass.data[DOMAIN]
accesspoints = config.get(DOMAIN, [])
def _update_event(events):
"""Handle incoming HomeMaticIP events."""
for event in events:
etype = event['eventType']
edata = event['data']
if etype == 'DEVICE_CHANGED':
dispatcher_send(hass, EVENT_DEVICE_CHANGED, edata.id)
elif etype == 'GROUP_CHANGED':
dispatcher_send(hass, EVENT_GROUP_CHANGED, edata.id)
elif etype == 'HOME_CHANGED':
dispatcher_send(hass, EVENT_HOME_CHANGED, edata.id)
elif etype == 'JOURNAL_CHANGED':
dispatcher_send(hass, EVENT_SECURITY_CHANGED, edata.id)
return True
for device in accesspoints:
name = device.get(CONF_NAME)
accesspoint = device.get(CONF_ACCESSPOINT)
authtoken = device.get(CONF_AUTHTOKEN)
home = Home()
if name.lower() == 'none':
name = ''
home.label = name
try:
home.set_auth_token(authtoken)
home.init(accesspoint)
if home.get_current_state():
_LOGGER.info("Connection to HMIP established")
else:
_LOGGER.warning("Connection to HMIP could not be established")
return False
except timeout:
_LOGGER.warning("Connection to HMIP could not be established")
return False
homes[home.id] = home
home.onEvent += _update_event
home.enable_events()
_LOGGER.info('HUB name: %s, id: %s', home.label, home.id)
for component in ['sensor']:
load_platform(hass, component, DOMAIN, {'homeid': home.id}, config)
return True
class HomematicipGenericDevice(Entity):
"""Representation of an HomematicIP generic device."""
def __init__(self, home, device):
"""Initialize the generic device."""
self._home = home
self._device = device
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, EVENT_DEVICE_CHANGED, self._device_changed)
@callback
def _device_changed(self, deviceid):
"""Handle device state changes."""
if deviceid is None or deviceid == self._device.id:
_LOGGER.debug('Event device %s', self._device.label)
self.async_schedule_update_ha_state()
def _name(self, addon=''):
"""Return the name of the device."""
name = ''
if self._home.label != '':
name += self._home.label + ' '
name += self._device.label
if addon != '':
name += ' ' + addon
return name
@property
def name(self):
"""Return the name of the generic device."""
return self._name()
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def available(self):
"""Device available."""
return not self._device.unreach
def _generic_state_attributes(self):
"""Return the state attributes of the generic device."""
laststatus = ''
if self._device.lastStatusUpdate is not None:
laststatus = self._device.lastStatusUpdate.isoformat()
return {
ATTR_HOME_LABEL: self._home.label,
ATTR_DEVICE_LABEL: self._device.label,
ATTR_HOME_ID: self._device.homeId,
ATTR_DEVICE_ID: self._device.id.lower(),
ATTR_STATUS_UPDATE: laststatus,
ATTR_FIRMWARE_STATE: self._device.updateState.lower(),
ATTR_LOW_BATTERY: self._device.lowBat,
ATTR_RSSI: self._device.rssiDeviceValue,
ATTR_TYPE: self._device.modelType
}
@property
def device_state_attributes(self):
"""Return the state attributes of the generic device."""
return self._generic_state_attributes()

View file

@ -4,7 +4,6 @@ This module provides WSGI application to serve the Home Assistant API.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/http/ https://home-assistant.io/components/http/
""" """
from ipaddress import ip_network from ipaddress import ip_network
import logging import logging
import os import os
@ -32,7 +31,7 @@ from .static import (
from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa
from .view import HomeAssistantView # noqa from .view import HomeAssistantView # noqa
REQUIREMENTS = ['aiohttp_cors==0.6.0'] REQUIREMENTS = ['aiohttp_cors==0.7.0']
DOMAIN = 'http' DOMAIN = 'http'

View file

@ -9,7 +9,7 @@ import json
import logging import logging
from aiohttp import web from aiohttp import web
from aiohttp.web_exceptions import HTTPUnauthorized from aiohttp.web_exceptions import HTTPUnauthorized, HTTPInternalServerError
import homeassistant.remote as rem import homeassistant.remote as rem
from homeassistant.core import is_callback from homeassistant.core import is_callback
@ -31,8 +31,12 @@ class HomeAssistantView(object):
# pylint: disable=no-self-use # pylint: disable=no-self-use
def json(self, result, status_code=200, headers=None): def json(self, result, status_code=200, headers=None):
"""Return a JSON response.""" """Return a JSON response."""
msg = json.dumps( try:
result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8') msg = json.dumps(
result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8')
except TypeError as err:
_LOGGER.error('Unable to serialize to JSON: %s\n%s', err, result)
raise HTTPInternalServerError
response = web.Response( response = web.Response(
body=msg, content_type=CONTENT_TYPE_JSON, status=status_code, body=msg, content_type=CONTENT_TYPE_JSON, status=status_code,
headers=headers) headers=headers)

View file

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert",
"discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken",
"no_bridges": "Philips Hue Bridges entdeckt"
},
"error": {
"linking": "Unbekannter Link-Fehler aufgetreten.",
"register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut"
},
"step": {
"init": {
"data": {
"host": "Host"
},
"title": "W\u00e4hle eine Hue Bridge"
},
"link": {
"description": "Dr\u00fccke den Knopf auf der Bridge, um Philips Hue mit Home Assistant zu registrieren.\n\n![Position des Buttons auf der Bridge](/static/images/config_philips_hue.jpg)",
"title": "Hub verbinden"
}
},
"title": "Philips Hue Bridge"
}
}

View file

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"all_configured": "All Philips Hue bridges are already configured",
"discover_timeout": "Unable to discover Hue bridges",
"no_bridges": "No Philips Hue bridges discovered"
},
"error": {
"linking": "Unknown linking error occurred.",
"register_failed": "Failed to register, please try again"
},
"step": {
"init": {
"data": {
"host": "Host"
},
"title": "Pick Hue bridge"
},
"link": {
"description": "Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)",
"title": "Link Hub"
}
},
"title": "Philips Hue Bridge"
}
}

View file

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
"discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
"no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4"
},
"error": {
"linking": "\uc54c \uc218\uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
"register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694"
},
"step": {
"init": {
"data": {
"host": "\ud638\uc2a4\ud2b8"
},
"title": "Hue \ube0c\ub9bf\uc9c0 \uc120\ud0dd"
},
"link": {
"description": "\ube0c\ub9bf\uc9c0\uc758 \ubc84\ud2bc\uc744 \ub20c\ub7ec \ud544\ub9bd\uc2a4 Hue\ub97c Home Assistant\uc5d0 \ub4f1\ub85d\ud558\uc138\uc694.\n\n![\ube0c\ub9bf\uc9c0 \ubc84\ud2bc \uc704\uce58](/static/images/config_philips_hue.jpg)",
"title": "\ud5c8\ube0c \uc5f0\uacb0"
}
},
"title": "\ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0"
}
}

View file

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"all_configured": "Alle Philips Hue bridges zijn al geconfigureerd",
"discover_timeout": "Hue bridges kunnen niet worden gevonden",
"no_bridges": "Geen Philips Hue bridges ontdekt"
},
"error": {
"linking": "Er is een onbekende verbindingsfout opgetreden.",
"register_failed": "Registratie is mislukt, probeer het opnieuw"
},
"step": {
"init": {
"data": {
"host": "Host"
},
"title": "Kies Hue bridge"
},
"link": {
"description": "Druk op de knop van de bridge om Philips Hue te registreren met de Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)",
"title": "Link Hub"
}
},
"title": "Philips Hue Bridge"
}
}

View file

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"all_configured": "Alle Philips Hue Bridger er allerede konfigurert",
"discover_timeout": "Kunne ikke oppdage Hue Bridger",
"no_bridges": "Ingen Philips Hue Bridger oppdaget"
},
"error": {
"linking": "Ukjent koblingsfeil oppstod.",
"register_failed": "Registrering feilet, vennligst pr\u00f8v igjen"
},
"step": {
"init": {
"data": {
"host": "Vert"
},
"title": "Velg Hue Bridge"
},
"link": {
"description": "Trykk p\u00e5 knappen p\u00e5 Bridgen for \u00e5 registrere Philips Hue med Home Assistant. \n\n ![Knappens plassering p\u00e5 Bridgen](/static/images/config_philips_hue.jpg)",
"title": "Link Hub"
}
},
"title": "Philips Hue Bridge"
}
}

View file

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane",
"discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue",
"no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue"
},
"error": {
"linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.",
"register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Prosz\u0119 spr\u00f3bowa\u0107 ponownie."
},
"step": {
"init": {
"data": {
"host": "Host"
},
"title": "Wybierz mostek Hue"
},
"link": {
"description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant.",
"title": "Hub Link"
}
},
"title": "Mostek Philips Hue"
}
}

View file

@ -0,0 +1,18 @@
{
"config": {
"error": {
"linking": "A ap\u0103rut o eroare de leg\u0103tur\u0103 necunoscut\u0103.",
"register_failed": "Nu a reu\u0219it \u00eenregistrarea, \u00eencerca\u021bi din nou"
},
"step": {
"init": {
"data": {
"host": "Gazd\u0103"
}
},
"link": {
"description": "Ap\u0103sa\u021bi butonul de pe pod pentru a \u00eenregistra Philips Hue cu Home Assistant. \n\n ! [Loca\u021bia butonului pe pod] (/ static / images / config_philips_hue.jpg)"
}
}
}
}

View file

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"all_configured": "Vsi mostovi Philips Hue so \u017ee konfigurirani",
"discover_timeout": "Ni bilo mogo\u010de odkriti Hue mostov",
"no_bridges": "Ni odkritih mostov Philips Hue"
},
"error": {
"linking": "Pri\u0161lo je do neznane napake pri povezavi.",
"register_failed": "Registracija ni uspela, poskusite znova"
},
"step": {
"init": {
"data": {
"host": "Host"
},
"title": "Izberite Hue most"
},
"link": {
"description": "Pritisnite gumb na mostu, da registrirate Philips Hue s Home Assistentom. \n\n ! [Polo\u017eaj gumba na mostu] (/static/images/config_philips_hue.jpg)",
"title": "Link Hub"
}
},
"title": "Philips Hue Bridge"
}
}

View file

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"all_configured": "\u5168\u90e8\u98de\u5229\u6d66 Hue \u6865\u63a5\u5668\u5df2\u914d\u7f6e",
"discover_timeout": "\u65e0\u6cd5\u55c5\u63a2 Hue \u6865\u63a5\u5668",
"no_bridges": "\u672a\u53d1\u73b0\u98de\u5229\u6d66 Hue Bridge"
},
"error": {
"linking": "\u53d1\u751f\u672a\u77e5\u7684\u8fde\u63a5\u9519\u8bef\u3002",
"register_failed": "\u6ce8\u518c\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5"
},
"step": {
"init": {
"data": {
"host": "\u4e3b\u673a"
},
"title": "\u9009\u62e9 Hue Bridge"
},
"link": {
"description": "\u8bf7\u6309\u4e0b\u6865\u63a5\u5668\u4e0a\u7684\u6309\u94ae\uff0c\u5728 Home Assistant \u4e0a\u6ce8\u518c\u98de\u5229\u6d66 Hue ![\u6865\u63a5\u5668\u6309\u94ae\u4f4d\u7f6e](/static/images/config_philips_hue.jpg)",
"title": "\u8fde\u63a5\u4e2d\u67a2"
}
},
"title": "\u98de\u5229\u6d66 Hue Bridge"
}
}

View file

@ -6,22 +6,22 @@ https://home-assistant.io/components/hue/
""" """
import asyncio import asyncio
import json import json
from functools import partial import ipaddress
import logging import logging
import os import os
import socket
import async_timeout import async_timeout
import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.discovery import SERVICE_HUE from homeassistant.components.discovery import SERVICE_HUE
from homeassistant.const import CONF_FILENAME, CONF_HOST from homeassistant.const import CONF_FILENAME, CONF_HOST
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery, aiohttp_client from homeassistant.helpers import discovery, aiohttp_client
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.util.json import save_json
REQUIREMENTS = ['phue==1.0', 'aiohue==0.3.0'] REQUIREMENTS = ['aiohue==1.3.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -36,26 +36,23 @@ DEFAULT_ALLOW_UNREACHABLE = False
PHUE_CONFIG_FILE = 'phue.conf' PHUE_CONFIG_FILE = 'phue.conf'
CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue"
DEFAULT_ALLOW_IN_EMULATED_HUE = True
CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" CONF_ALLOW_HUE_GROUPS = "allow_hue_groups"
DEFAULT_ALLOW_HUE_GROUPS = True DEFAULT_ALLOW_HUE_GROUPS = True
BRIDGE_CONFIG_SCHEMA = vol.Schema([{ BRIDGE_CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_HOST): cv.string, # Validate as IP address and then convert back to a string.
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string, vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string,
vol.Optional(CONF_ALLOW_UNREACHABLE, vol.Optional(CONF_ALLOW_UNREACHABLE,
default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean, default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean,
vol.Optional(CONF_ALLOW_IN_EMULATED_HUE,
default=DEFAULT_ALLOW_IN_EMULATED_HUE): cv.boolean,
vol.Optional(CONF_ALLOW_HUE_GROUPS, vol.Optional(CONF_ALLOW_HUE_GROUPS,
default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean, default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean,
}]) })
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_BRIDGES): BRIDGE_CONFIG_SCHEMA, vol.Optional(CONF_BRIDGES):
vol.All(cv.ensure_list, [BRIDGE_CONFIG_SCHEMA]),
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -73,7 +70,7 @@ Press the button on the bridge to register Philips Hue with Home Assistant.
""" """
def setup(hass, config): async def async_setup(hass, config):
"""Set up the Hue platform.""" """Set up the Hue platform."""
conf = config.get(DOMAIN) conf = config.get(DOMAIN)
if conf is None: if conf is None:
@ -82,196 +79,212 @@ def setup(hass, config):
if DOMAIN not in hass.data: if DOMAIN not in hass.data:
hass.data[DOMAIN] = {} hass.data[DOMAIN] = {}
discovery.listen( async def async_bridge_discovered(service, discovery_info):
hass, """Dispatcher for Hue discovery events."""
SERVICE_HUE, # Ignore emulated hue
lambda service, discovery_info: if "HASS Bridge" in discovery_info.get('name', ''):
bridge_discovered(hass, service, discovery_info)) return
await async_setup_bridge(
hass, discovery_info['host'],
'phue-{}.conf'.format(discovery_info['serial']))
discovery.async_listen(hass, SERVICE_HUE, async_bridge_discovered)
# User has configured bridges # User has configured bridges
if CONF_BRIDGES in conf: if CONF_BRIDGES in conf:
bridges = conf[CONF_BRIDGES] bridges = conf[CONF_BRIDGES]
# Component is part of config but no bridges specified, discover. # Component is part of config but no bridges specified, discover.
elif DOMAIN in config: elif DOMAIN in config:
# discover from nupnp # discover from nupnp
hosts = requests.get(API_NUPNP).json() websession = aiohttp_client.async_get_clientsession(hass)
bridges = [{
async with websession.get(API_NUPNP) as req:
hosts = await req.json()
# Run through config schema to populate defaults
bridges = [BRIDGE_CONFIG_SCHEMA({
CONF_HOST: entry['internalipaddress'], CONF_HOST: entry['internalipaddress'],
CONF_FILENAME: '.hue_{}.conf'.format(entry['id']), CONF_FILENAME: '.hue_{}.conf'.format(entry['id']),
} for entry in hosts] }) for entry in hosts]
else: else:
# Component not specified in config, we're loaded via discovery # Component not specified in config, we're loaded via discovery
bridges = [] bridges = []
for bridge in bridges: if not bridges:
filename = bridge.get(CONF_FILENAME) return True
allow_unreachable = bridge.get(CONF_ALLOW_UNREACHABLE)
allow_in_emulated_hue = bridge.get(CONF_ALLOW_IN_EMULATED_HUE)
allow_hue_groups = bridge.get(CONF_ALLOW_HUE_GROUPS)
host = bridge.get(CONF_HOST) await asyncio.wait([
async_setup_bridge(
if host is None: hass, bridge[CONF_HOST], bridge[CONF_FILENAME],
host = _find_host_from_config(hass, filename) bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS]
) for bridge in bridges
if host is None: ])
_LOGGER.error("No host found in configuration")
return False
setup_bridge(host, hass, filename, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups)
return True return True
def bridge_discovered(hass, service, discovery_info): async def async_setup_bridge(
"""Dispatcher for Hue discovery events.""" hass, host, filename=None,
if "HASS Bridge" in discovery_info.get('name', ''): allow_unreachable=DEFAULT_ALLOW_UNREACHABLE,
return allow_hue_groups=DEFAULT_ALLOW_HUE_GROUPS,
username=None):
host = discovery_info.get('host')
serial = discovery_info.get('serial')
filename = 'phue-{}.conf'.format(serial)
setup_bridge(host, hass, filename)
def setup_bridge(host, hass, filename=None, allow_unreachable=False,
allow_in_emulated_hue=True, allow_hue_groups=True,
username=None):
"""Set up a given Hue bridge.""" """Set up a given Hue bridge."""
assert filename or username, 'Need to pass at least a username or filename'
# Only register a device once # Only register a device once
if socket.gethostbyname(host) in hass.data[DOMAIN]: if host in hass.data[DOMAIN]:
return return
if username is None:
username = await hass.async_add_job(
_find_username_from_config, hass, filename)
bridge = HueBridge(host, hass, filename, username, allow_unreachable, bridge = HueBridge(host, hass, filename, username, allow_unreachable,
allow_in_emulated_hue, allow_hue_groups) allow_hue_groups)
bridge.setup() await bridge.async_setup()
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE): def _find_username_from_config(hass, filename):
"""Attempt to detect host based on existing configuration.""" """Load username from config."""
path = hass.config.path(filename) path = hass.config.path(filename)
if not os.path.isfile(path): if not os.path.isfile(path):
return None return None
try: with open(path) as inp:
with open(path) as inp: return list(json.load(inp).values())[0]['username']
return next(iter(json.load(inp).keys()))
except (ValueError, AttributeError, StopIteration):
# ValueError if can't parse as JSON
# AttributeError if JSON value is not a dict
# StopIteration if no keys
return None
class HueBridge(object): class HueBridge(object):
"""Manages a single Hue bridge.""" """Manages a single Hue bridge."""
def __init__(self, host, hass, filename, username, allow_unreachable=False, def __init__(self, host, hass, filename, username,
allow_in_emulated_hue=True, allow_hue_groups=True): allow_unreachable=False, allow_groups=True):
"""Initialize the system.""" """Initialize the system."""
self.host = host self.host = host
self.bridge_id = socket.gethostbyname(host)
self.hass = hass self.hass = hass
self.filename = filename self.filename = filename
self.username = username self.username = username
self.allow_unreachable = allow_unreachable self.allow_unreachable = allow_unreachable
self.allow_in_emulated_hue = allow_in_emulated_hue self.allow_groups = allow_groups
self.allow_hue_groups = allow_hue_groups
self.available = True self.available = True
self.bridge = None
self.lights = {}
self.lightgroups = {}
self.configured = False
self.config_request_id = None self.config_request_id = None
self.api = None
hass.data[DOMAIN][self.bridge_id] = self async def async_setup(self):
def setup(self):
"""Set up a phue bridge based on host parameter.""" """Set up a phue bridge based on host parameter."""
import phue import aiohue
api = aiohue.Bridge(
self.host,
username=self.username,
websession=aiohttp_client.async_get_clientsession(self.hass)
)
try: try:
kwargs = {} with async_timeout.timeout(5):
if self.username is not None: # Initialize bridge and validate our username
kwargs['username'] = self.username if not self.username:
if self.filename is not None: await api.create_user('home-assistant')
kwargs['config_file_path'] = \ await api.initialize()
self.hass.config.path(self.filename) except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized):
self.bridge = phue.Bridge(self.host, **kwargs) _LOGGER.warning("Connected to Hue at %s but not registered.",
except OSError: # Wrong host was given self.host)
self.async_request_configuration()
return
except (asyncio.TimeoutError, aiohue.RequestError):
_LOGGER.error("Error connecting to the Hue bridge at %s", _LOGGER.error("Error connecting to the Hue bridge at %s",
self.host) self.host)
return return
except phue.PhueRegistrationException: except aiohue.AiohueException:
_LOGGER.warning("Connected to Hue at %s but not registered.", _LOGGER.exception('Unknown Hue linking error occurred')
self.host) self.async_request_configuration()
self.request_configuration()
return return
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unknown error connecting with Hue bridge at %s", _LOGGER.exception("Unknown error connecting with Hue bridge at %s",
self.host) self.host)
return return
self.hass.data[DOMAIN][self.host] = self
# If we came here and configuring this host, mark as done # If we came here and configuring this host, mark as done
if self.config_request_id: if self.config_request_id:
request_id = self.config_request_id request_id = self.config_request_id
self.config_request_id = None self.config_request_id = None
configurator = self.hass.components.configurator self.hass.components.configurator.async_request_done(request_id)
configurator.request_done(request_id)
self.configured = True self.username = api.username
discovery.load_platform( # Save config file
await self.hass.async_add_job(
save_json, self.hass.config.path(self.filename),
{self.host: {'username': api.username}})
self.api = api
self.hass.async_add_job(discovery.async_load_platform(
self.hass, 'light', DOMAIN, self.hass, 'light', DOMAIN,
{'bridge_id': self.bridge_id}) {'host': self.host}))
# create a service for calling run_scene directly on the bridge, self.hass.services.async_register(
# used to simplify automation rules. DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
def hue_activate_scene(call):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
self.bridge.run_scene(group_name, scene_name)
self.hass.services.register(
DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene,
schema=SCENE_SCHEMA) schema=SCENE_SCHEMA)
def request_configuration(self): @callback
def async_request_configuration(self):
"""Request configuration steps from the user.""" """Request configuration steps from the user."""
configurator = self.hass.components.configurator configurator = self.hass.components.configurator
# We got an error if this method is called while we are configuring # We got an error if this method is called while we are configuring
if self.config_request_id: if self.config_request_id:
configurator.notify_errors( configurator.async_notify_errors(
self.config_request_id, self.config_request_id,
"Failed to register, please try again.") "Failed to register, please try again.")
return return
self.config_request_id = configurator.request_config( async def config_callback(data):
"Philips Hue", """Callback for configurator data."""
lambda data: self.setup(), await self.async_setup()
self.config_request_id = configurator.async_request_config(
"Philips Hue", config_callback,
description=CONFIG_INSTRUCTIONS, description=CONFIG_INSTRUCTIONS,
entity_picture="/static/images/logo_philips_hue.png", entity_picture="/static/images/logo_philips_hue.png",
submit_caption="I have pressed the button" submit_caption="I have pressed the button"
) )
def get_api(self): async def hue_activate_scene(self, call, updated=False):
"""Return the full api dictionary from phue.""" """Service to call directly into bridge to set scenes."""
return self.bridge.get_api() group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
def set_light(self, light_id, command): group = next(
"""Adjust properties of one or more lights. See phue for details.""" (group for group in self.api.groups.values()
return self.bridge.set_light(light_id, command) if group.name == group_name), None)
def set_group(self, light_id, command): scene_id = next(
"""Change light settings for a group. See phue for detail.""" (scene.id for scene in self.api.scenes.values()
return self.bridge.set_group(light_id, command) if scene.name == scene_name), None)
# If we can't find it, fetch latest info.
if not updated and (group is None or scene_id is None):
await self.api.groups.update()
await self.api.scenes.update()
await self.hue_activate_scene(call, updated=True)
return
if group is None:
_LOGGER.warning('Unable to find group %s', group_name)
return
if scene_id is None:
_LOGGER.warning('Unable to find scene %s', scene_name)
return
await group.set_action(scene=scene_id)
@config_entries.HANDLERS.register(DOMAIN) @config_entries.HANDLERS.register(DOMAIN)
@ -305,12 +318,12 @@ class HueFlowHandler(config_entries.ConfigFlowHandler):
bridges = await discover_nupnp(websession=self._websession) bridges = await discover_nupnp(websession=self._websession)
except asyncio.TimeoutError: except asyncio.TimeoutError:
return self.async_abort( return self.async_abort(
reason='Unable to discover Hue bridges.' reason='discover_timeout'
) )
if not bridges: if not bridges:
return self.async_abort( return self.async_abort(
reason='No Philips Hue bridges discovered.' reason='no_bridges'
) )
# Find already configured hosts # Find already configured hosts
@ -323,7 +336,7 @@ class HueFlowHandler(config_entries.ConfigFlowHandler):
if not hosts: if not hosts:
return self.async_abort( return self.async_abort(
reason='All Philips Hue bridges are already configured.' reason='all_configured'
) )
elif len(hosts) == 1: elif len(hosts) == 1:
@ -332,7 +345,6 @@ class HueFlowHandler(config_entries.ConfigFlowHandler):
return self.async_show_form( return self.async_show_form(
step_id='init', step_id='init',
title='Pick Hue Bridge',
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Required('host'): vol.In(hosts) vol.Required('host'): vol.In(hosts)
}) })
@ -353,10 +365,10 @@ class HueFlowHandler(config_entries.ConfigFlowHandler):
await bridge.initialize() await bridge.initialize()
except (asyncio.TimeoutError, aiohue.RequestError, except (asyncio.TimeoutError, aiohue.RequestError,
aiohue.LinkButtonNotPressed): aiohue.LinkButtonNotPressed):
errors['base'] = 'Failed to register, please try again.' errors['base'] = 'register_failed'
except aiohue.AiohueException: except aiohue.AiohueException:
errors['base'] = 'Unknown linking error occurred.' errors['base'] = 'linking'
_LOGGER.exception('Uknown Hue linking error occurred') _LOGGER.exception('Unknown Hue linking error occurred')
else: else:
return self.async_create_entry( return self.async_create_entry(
title=bridge.config.name, title=bridge.config.name,
@ -369,15 +381,12 @@ class HueFlowHandler(config_entries.ConfigFlowHandler):
return self.async_show_form( return self.async_show_form(
step_id='link', step_id='link',
title='Link Hub',
description=CONFIG_INSTRUCTIONS,
errors=errors, errors=errors,
) )
async def async_setup_entry(hass, entry): async def async_setup_entry(hass, entry):
"""Set up a bridge for a config entry.""" """Set up a bridge for a config entry."""
await hass.async_add_job(partial( await async_setup_bridge(hass, entry.data['host'],
setup_bridge, entry.data['host'], hass, username=entry.data['username'])
username=entry.data['username']))
return True return True

View file

@ -0,0 +1,26 @@
{
"config": {
"title": "Philips Hue Bridge",
"step": {
"init": {
"title": "Pick Hue bridge",
"data": {
"host": "Host"
}
},
"link": {
"title": "Link Hub",
"description": "Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)"
}
},
"error": {
"register_failed": "Failed to register, please try again",
"linking": "Unknown linking error occurred."
},
"abort": {
"discover_timeout": "Unable to discover Hue bridges",
"no_bridges": "No Philips Hue bridges discovered",
"all_configured": "All Philips Hue bridges are already configured"
}
}
}

View file

@ -17,7 +17,7 @@ from homeassistant.components.image_processing import (
PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE,
CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE) CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_callback_threadsafe from homeassistant.util.async_ import run_callback_threadsafe
DEPENDENCIES = ['microsoft_face'] DEPENDENCIES = ['microsoft_face']

View file

@ -17,7 +17,7 @@ from homeassistant.const import STATE_UNKNOWN, CONF_REGION
from homeassistant.components.image_processing import ( from homeassistant.components.image_processing import (
PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE,
CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE) CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE)
from homeassistant.util.async import run_callback_threadsafe from homeassistant.util.async_ import run_callback_threadsafe
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -16,7 +16,7 @@ from homeassistant.components.image_processing import (
from homeassistant.core import split_entity_id from homeassistant.core import split_entity_id
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['numpy==1.14.0'] REQUIREMENTS = ['numpy==1.14.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['insteonplm==0.8.2'] REQUIREMENTS = ['insteonplm==0.8.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -64,19 +64,20 @@ def async_setup(hass, config):
"""Detect device from transport to be delegated to platform.""" """Detect device from transport to be delegated to platform."""
for state_key in device.states: for state_key in device.states:
platform_info = ipdb[device.states[state_key]] platform_info = ipdb[device.states[state_key]]
platform = platform_info.platform if platform_info:
if platform is not None: platform = platform_info.platform
_LOGGER.info("New INSTEON PLM device: %s (%s) %s", if platform:
device.address, _LOGGER.info("New INSTEON PLM device: %s (%s) %s",
device.states[state_key].name, device.address,
platform) device.states[state_key].name,
platform)
hass.async_add_job( hass.async_add_job(
discovery.async_load_platform( discovery.async_load_platform(
hass, platform, DOMAIN, hass, platform, DOMAIN,
discovered={'address': device.address.hex, discovered={'address': device.address.hex,
'state_key': state_key}, 'state_key': state_key},
hass_config=config)) hass_config=config))
_LOGGER.info("Looking for PLM on %s", port) _LOGGER.info("Looking for PLM on %s", port)
conn = yield from insteonplm.Connection.create( conn = yield from insteonplm.Connection.create(
@ -127,13 +128,15 @@ class IPDB(object):
from insteonplm.states.sensor import (VariableSensor, from insteonplm.states.sensor import (VariableSensor,
OnOffSensor, OnOffSensor,
SmokeCO2Sensor, SmokeCO2Sensor,
IoLincSensor) IoLincSensor,
LeakSensorDryWet)
self.states = [State(OnOffSwitch_OutletTop, 'switch'), self.states = [State(OnOffSwitch_OutletTop, 'switch'),
State(OnOffSwitch_OutletBottom, 'switch'), State(OnOffSwitch_OutletBottom, 'switch'),
State(OpenClosedRelay, 'switch'), State(OpenClosedRelay, 'switch'),
State(OnOffSwitch, 'switch'), State(OnOffSwitch, 'switch'),
State(LeakSensorDryWet, 'binary_sensor'),
State(IoLincSensor, 'binary_sensor'), State(IoLincSensor, 'binary_sensor'),
State(SmokeCO2Sensor, 'sensor'), State(SmokeCO2Sensor, 'sensor'),
State(OnOffSensor, 'binary_sensor'), State(OnOffSensor, 'binary_sensor'),

View file

@ -40,9 +40,8 @@ SUPPORT_BRIGHTNESS = 1
SUPPORT_COLOR_TEMP = 2 SUPPORT_COLOR_TEMP = 2
SUPPORT_EFFECT = 4 SUPPORT_EFFECT = 4
SUPPORT_FLASH = 8 SUPPORT_FLASH = 8
SUPPORT_RGB_COLOR = 16 SUPPORT_COLOR = 16
SUPPORT_TRANSITION = 32 SUPPORT_TRANSITION = 32
SUPPORT_XY_COLOR = 64
SUPPORT_WHITE_VALUE = 128 SUPPORT_WHITE_VALUE = 128
# Integer that represents transition time in seconds to make change. # Integer that represents transition time in seconds to make change.
@ -51,6 +50,7 @@ ATTR_TRANSITION = "transition"
# Lists holding color values # Lists holding color values
ATTR_RGB_COLOR = "rgb_color" ATTR_RGB_COLOR = "rgb_color"
ATTR_XY_COLOR = "xy_color" ATTR_XY_COLOR = "xy_color"
ATTR_HS_COLOR = "hs_color"
ATTR_COLOR_TEMP = "color_temp" ATTR_COLOR_TEMP = "color_temp"
ATTR_KELVIN = "kelvin" ATTR_KELVIN = "kelvin"
ATTR_MIN_MIREDS = "min_mireds" ATTR_MIN_MIREDS = "min_mireds"
@ -86,8 +86,9 @@ LIGHT_PROFILES_FILE = "light_profiles.csv"
PROP_TO_ATTR = { PROP_TO_ATTR = {
'brightness': ATTR_BRIGHTNESS, 'brightness': ATTR_BRIGHTNESS,
'color_temp': ATTR_COLOR_TEMP, 'color_temp': ATTR_COLOR_TEMP,
'rgb_color': ATTR_RGB_COLOR, 'min_mireds': ATTR_MIN_MIREDS,
'xy_color': ATTR_XY_COLOR, 'max_mireds': ATTR_MAX_MIREDS,
'hs_color': ATTR_HS_COLOR,
'white_value': ATTR_WHITE_VALUE, 'white_value': ATTR_WHITE_VALUE,
'effect_list': ATTR_EFFECT_LIST, 'effect_list': ATTR_EFFECT_LIST,
'effect': ATTR_EFFECT, 'effect': ATTR_EFFECT,
@ -111,6 +112,11 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({
vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP):
vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
vol.Coerce(tuple)), vol.Coerce(tuple)),
vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP):
vol.All(vol.ExactSequence(
(vol.All(vol.Coerce(float), vol.Range(min=0, max=360)),
vol.All(vol.Coerce(float), vol.Range(min=0, max=100)))),
vol.Coerce(tuple)),
vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP):
vol.All(vol.Coerce(int), vol.Range(min=1)), vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): vol.Exclusive(ATTR_KELVIN, COLOR_GROUP):
@ -149,13 +155,13 @@ def is_on(hass, entity_id=None):
@bind_hass @bind_hass
def turn_on(hass, entity_id=None, transition=None, brightness=None, def turn_on(hass, entity_id=None, transition=None, brightness=None,
brightness_pct=None, rgb_color=None, xy_color=None, brightness_pct=None, rgb_color=None, xy_color=None, hs_color=None,
color_temp=None, kelvin=None, white_value=None, color_temp=None, kelvin=None, white_value=None,
profile=None, flash=None, effect=None, color_name=None): profile=None, flash=None, effect=None, color_name=None):
"""Turn all or specified light on.""" """Turn all or specified light on."""
hass.add_job( hass.add_job(
async_turn_on, hass, entity_id, transition, brightness, brightness_pct, async_turn_on, hass, entity_id, transition, brightness, brightness_pct,
rgb_color, xy_color, color_temp, kelvin, white_value, rgb_color, xy_color, hs_color, color_temp, kelvin, white_value,
profile, flash, effect, color_name) profile, flash, effect, color_name)
@ -163,8 +169,9 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
@bind_hass @bind_hass
def async_turn_on(hass, entity_id=None, transition=None, brightness=None, def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
brightness_pct=None, rgb_color=None, xy_color=None, brightness_pct=None, rgb_color=None, xy_color=None,
color_temp=None, kelvin=None, white_value=None, hs_color=None, color_temp=None, kelvin=None,
profile=None, flash=None, effect=None, color_name=None): white_value=None, profile=None, flash=None, effect=None,
color_name=None):
"""Turn all or specified light on.""" """Turn all or specified light on."""
data = { data = {
key: value for key, value in [ key: value for key, value in [
@ -175,6 +182,7 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
(ATTR_BRIGHTNESS_PCT, brightness_pct), (ATTR_BRIGHTNESS_PCT, brightness_pct),
(ATTR_RGB_COLOR, rgb_color), (ATTR_RGB_COLOR, rgb_color),
(ATTR_XY_COLOR, xy_color), (ATTR_XY_COLOR, xy_color),
(ATTR_HS_COLOR, hs_color),
(ATTR_COLOR_TEMP, color_temp), (ATTR_COLOR_TEMP, color_temp),
(ATTR_KELVIN, kelvin), (ATTR_KELVIN, kelvin),
(ATTR_WHITE_VALUE, white_value), (ATTR_WHITE_VALUE, white_value),
@ -254,6 +262,14 @@ def preprocess_turn_on_alternatives(params):
if brightness_pct is not None: if brightness_pct is not None:
params[ATTR_BRIGHTNESS] = int(255 * brightness_pct/100) params[ATTR_BRIGHTNESS] = int(255 * brightness_pct/100)
xy_color = params.pop(ATTR_XY_COLOR, None)
if xy_color is not None:
params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color)
rgb_color = params.pop(ATTR_RGB_COLOR, None)
if rgb_color is not None:
params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
class SetIntentHandler(intent.IntentHandler): class SetIntentHandler(intent.IntentHandler):
"""Handle set color intents.""" """Handle set color intents."""
@ -281,7 +297,7 @@ class SetIntentHandler(intent.IntentHandler):
if 'color' in slots: if 'color' in slots:
intent.async_test_feature( intent.async_test_feature(
state, SUPPORT_RGB_COLOR, 'changing colors') state, SUPPORT_COLOR, 'changing colors')
service_data[ATTR_RGB_COLOR] = slots['color']['value'] service_data[ATTR_RGB_COLOR] = slots['color']['value']
# Use original passed in value of the color because we don't have # Use original passed in value of the color because we don't have
# human readable names for that internally. # human readable names for that internally.
@ -428,13 +444,8 @@ class Light(ToggleEntity):
return None return None
@property @property
def xy_color(self): def hs_color(self):
"""Return the XY color value [float, float].""" """Return the hue and saturation color value [float, float]."""
return None
@property
def rgb_color(self):
"""Return the RGB color value [int, int, int]."""
return None return None
@property @property
@ -484,11 +495,16 @@ class Light(ToggleEntity):
if value is not None: if value is not None:
data[attr] = value data[attr] = value
if ATTR_RGB_COLOR not in data and ATTR_XY_COLOR in data and \ # Expose current color also as RGB and XY
ATTR_BRIGHTNESS in data: if ATTR_HS_COLOR in data:
data[ATTR_RGB_COLOR] = color_util.color_xy_brightness_to_RGB( data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(
data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1], *data[ATTR_HS_COLOR])
data[ATTR_BRIGHTNESS]) data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(
*data[ATTR_HS_COLOR])
data[ATTR_HS_COLOR] = (
round(data[ATTR_HS_COLOR][0], 3),
round(data[ATTR_HS_COLOR][1], 3),
)
return data return data

View file

@ -8,8 +8,9 @@ import logging
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_BRIGHTNESS, ATTR_HS_COLOR,
SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light) SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light)
import homeassistant.util.color as color_util
DEPENDENCIES = ['abode'] DEPENDENCIES = ['abode']
@ -44,10 +45,12 @@ class AbodeLight(AbodeDevice, Light):
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn on the light.""" """Turn on the light."""
if (ATTR_RGB_COLOR in kwargs and if (ATTR_HS_COLOR in kwargs and
self._device.is_dimmable and self._device.has_color): self._device.is_dimmable and self._device.has_color):
self._device.set_color(kwargs[ATTR_RGB_COLOR]) self._device.set_color(color_util.color_hs_to_RGB(
elif ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: *kwargs[ATTR_HS_COLOR]))
if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable:
self._device.set_level(kwargs[ATTR_BRIGHTNESS]) self._device.set_level(kwargs[ATTR_BRIGHTNESS])
else: else:
self._device.switch_on() self._device.switch_on()
@ -68,16 +71,16 @@ class AbodeLight(AbodeDevice, Light):
return self._device.brightness return self._device.brightness
@property @property
def rgb_color(self): def hs_color(self):
"""Return the color of the light.""" """Return the color of the light."""
if self._device.is_dimmable and self._device.has_color: if self._device.is_dimmable and self._device.has_color:
return self._device.color return color_util.color_RGB_to_hs(*self._device.color)
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
if self._device.is_dimmable and self._device.has_color: if self._device.is_dimmable and self._device.has_color:
return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR return SUPPORT_BRIGHTNESS | SUPPORT_COLOR
elif self._device.is_dimmable: elif self._device.is_dimmable:
return SUPPORT_BRIGHTNESS return SUPPORT_BRIGHTNESS

View file

@ -9,9 +9,11 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, Light, PLATFORM_SCHEMA) ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light,
PLATFORM_SCHEMA)
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
REQUIREMENTS = ['blinkstick==1.1.8'] REQUIREMENTS = ['blinkstick==1.1.8']
@ -21,7 +23,7 @@ CONF_SERIAL = 'serial'
DEFAULT_NAME = 'Blinkstick' DEFAULT_NAME = 'Blinkstick'
SUPPORT_BLINKSTICK = SUPPORT_RGB_COLOR SUPPORT_BLINKSTICK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SERIAL): cv.string, vol.Required(CONF_SERIAL): cv.string,
@ -39,7 +41,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
stick = blinkstick.find_by_serial(serial) stick = blinkstick.find_by_serial(serial)
add_devices([BlinkStickLight(stick, name)]) add_devices([BlinkStickLight(stick, name)], True)
class BlinkStickLight(Light): class BlinkStickLight(Light):
@ -50,7 +52,8 @@ class BlinkStickLight(Light):
self._stick = stick self._stick = stick
self._name = name self._name = name
self._serial = stick.get_serial() self._serial = stick.get_serial()
self._rgb_color = stick.get_color() self._hs_color = None
self._brightness = None
@property @property
def should_poll(self): def should_poll(self):
@ -63,14 +66,19 @@ class BlinkStickLight(Light):
return self._name return self._name
@property @property
def rgb_color(self): def brightness(self):
"""Read back the brightness of the light."""
return self._brightness
@property
def hs_color(self):
"""Read back the color of the light.""" """Read back the color of the light."""
return self._rgb_color return self._hs_color
@property @property
def is_on(self): def is_on(self):
"""Check whether any of the LEDs colors are non-zero.""" """Return True if entity is on."""
return sum(self._rgb_color) > 0 return self._brightness > 0
@property @property
def supported_features(self): def supported_features(self):
@ -79,18 +87,24 @@ class BlinkStickLight(Light):
def update(self): def update(self):
"""Read back the device state.""" """Read back the device state."""
self._rgb_color = self._stick.get_color() rgb_color = self._stick.get_color()
hsv = color_util.color_RGB_to_hsv(*rgb_color)
self._hs_color = hsv[:2]
self._brightness = hsv[2]
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the device on.""" """Turn the device on."""
if ATTR_RGB_COLOR in kwargs: if ATTR_HS_COLOR in kwargs:
self._rgb_color = kwargs[ATTR_RGB_COLOR] self._hs_color = kwargs[ATTR_HS_COLOR]
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
else: else:
self._rgb_color = [255, 255, 255] self._brightness = 255
self._stick.set_color(red=self._rgb_color[0], rgb_color = color_util.color_hsv_to_RGB(
green=self._rgb_color[1], self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100)
blue=self._rgb_color[2]) self._stick.set_color(
red=rgb_color[0], green=rgb_color[1], blue=rgb_color[2])
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the device off.""" """Turn the device off."""

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