commit
0d62f472cb
393 changed files with 11384 additions and 5321 deletions
26
.coveragerc
26
.coveragerc
|
@ -109,6 +109,9 @@ omit =
|
|||
homeassistant/components/homematic/__init__.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/homematicip_cloud.py
|
||||
homeassistant/components/*/homematicip_cloud.py
|
||||
|
||||
homeassistant/components/ihc/*
|
||||
homeassistant/components/*/ihc.py
|
||||
|
||||
|
@ -309,6 +312,7 @@ omit =
|
|||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.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/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
|
@ -332,6 +336,7 @@ omit =
|
|||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/onvif.py
|
||||
homeassistant/components/camera/proxy.py
|
||||
homeassistant/components/camera/ring.py
|
||||
homeassistant/components/camera/rpi_camera.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_identify.py
|
||||
homeassistant/components/image_processing/seven_segments.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/avion.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/light/blinkt.py
|
||||
homeassistant/components/light/decora.py
|
||||
homeassistant/components/light/decora_wifi.py
|
||||
homeassistant/components/light/decora.py
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/greenwave.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/iglo.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/lifx_legacy.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
|
@ -442,6 +447,7 @@ omit =
|
|||
homeassistant/components/media_player/bluesound.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/channels.py
|
||||
homeassistant/components/media_player/clementine.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
|
@ -482,8 +488,8 @@ omit =
|
|||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.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.py
|
||||
homeassistant/components/media_player/ziggo_mediabox_xl.py
|
||||
homeassistant/components/mycroft.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
|
@ -491,8 +497,8 @@ omit =
|
|||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.py
|
||||
homeassistant/components/notify/clickatell.py
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/clicksend_tts.py
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
|
@ -517,6 +523,7 @@ omit =
|
|||
homeassistant/components/notify/sendgrid.py
|
||||
homeassistant/components/notify/simplepush.py
|
||||
homeassistant/components/notify/slack.py
|
||||
homeassistant/components/notify/stride.py
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/synology_chat.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
|
@ -554,7 +561,6 @@ omit =
|
|||
homeassistant/components/sensor/crimereports.py
|
||||
homeassistant/components/sensor/cups.py
|
||||
homeassistant/components/sensor/currencylayer.py
|
||||
homeassistant/components/sensor/darksky.py
|
||||
homeassistant/components/sensor/deluge.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
|
@ -576,6 +582,7 @@ omit =
|
|||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/folder.py
|
||||
homeassistant/components/sensor/foobot.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
homeassistant/components/sensor/fritzbox_netmonitor.py
|
||||
homeassistant/components/sensor/gearbest.py
|
||||
|
@ -588,8 +595,8 @@ omit =
|
|||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/htu21d.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
homeassistant/components/sensor/irish_rail_transport.py
|
||||
homeassistant/components/sensor/kwb.py
|
||||
|
@ -632,8 +639,8 @@ omit =
|
|||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sense.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/serial.py
|
||||
homeassistant/components/sensor/shodan.py
|
||||
homeassistant/components/sensor/simulated.py
|
||||
homeassistant/components/sensor/skybeacon.py
|
||||
|
@ -647,6 +654,7 @@ omit =
|
|||
homeassistant/components/sensor/supervisord.py
|
||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/syncthru.py
|
||||
homeassistant/components/sensor/synologydsm.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/sytadin.py
|
||||
|
@ -656,6 +664,7 @@ omit =
|
|||
homeassistant/components/sensor/tibber.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/torque.py
|
||||
homeassistant/components/sensor/trafikverket_weatherstation.py
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/travisci.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
|
@ -697,6 +706,7 @@ omit =
|
|||
homeassistant/components/switch/telnet.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/vesync.py
|
||||
homeassistant/components/switch/xiaomi_miio.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
|
|
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -12,19 +12,18 @@
|
|||
|
||||
## Checklist:
|
||||
- [ ] 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:
|
||||
- [ ] 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:
|
||||
- [ ] 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 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 files were added to `.coveragerc`.
|
||||
|
||||
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.
|
||||
|
||||
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -21,6 +21,7 @@ Icon
|
|||
*.iml
|
||||
|
||||
# pytest
|
||||
.pytest_cache
|
||||
.cache
|
||||
|
||||
# GITHUB Proposed Python stuff:
|
||||
|
|
0
.gitmodules
vendored
0
.gitmodules
vendored
|
@ -49,6 +49,7 @@ homeassistant/components/camera/yi.py @bachya
|
|||
homeassistant/components/climate/ephember.py @ttroy50
|
||||
homeassistant/components/climate/eq3btsmart.py @rytilahti
|
||||
homeassistant/components/climate/sensibo.py @andrey-git
|
||||
homeassistant/components/cover/group.py @cdce8p
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/device_tracker/tile.py @bachya
|
||||
|
|
|
@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot
|
|||
|
||||
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).
|
||||
- Write the code for your device, notification service, sensor, or IoT thing.
|
||||
- Ensure tests work.
|
||||
|
|
|
@ -4,10 +4,10 @@ homeassistant.util package
|
|||
Submodules
|
||||
----------
|
||||
|
||||
homeassistant.util.async module
|
||||
homeassistant.util.async_ module
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: homeassistant.util.async
|
||||
.. automodule:: homeassistant.util.async_
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
|
|
@ -272,7 +272,7 @@ def setup_and_run_hass(config_dir: str,
|
|||
|
||||
if args.open_ui:
|
||||
# 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):
|
||||
"""Open the webinterface in a browser."""
|
||||
|
@ -335,7 +335,8 @@ def main() -> int:
|
|||
"""Start Home Assistant."""
|
||||
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):
|
||||
monkey_patch.disable_c_asyncio()
|
||||
monkey_patch.patch_weakref_tasks()
|
||||
|
|
|
@ -86,14 +86,6 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||
if enable_log:
|
||||
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, {})
|
||||
|
||||
try:
|
||||
|
|
|
@ -12,13 +12,14 @@ import requests
|
|||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.const import (
|
||||
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 (
|
||||
EGARDIA_DEVICE, EGARDIA_SERVER,
|
||||
REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES,
|
||||
CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT
|
||||
)
|
||||
REQUIREMENTS = ['pythonegardia==1.0.38']
|
||||
DEPENDENCIES = ['egardia']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -27,6 +28,8 @@ STATES = {
|
|||
'DAY HOME': STATE_ALARM_ARMED_HOME,
|
||||
'DISARM': STATE_ALARM_DISARMED,
|
||||
'ARMHOME': STATE_ALARM_ARMED_HOME,
|
||||
'HOME': STATE_ALARM_ARMED_HOME,
|
||||
'NIGHT HOME': STATE_ALARM_ARMED_NIGHT,
|
||||
'TRIGGERED': STATE_ALARM_TRIGGERED
|
||||
}
|
||||
|
||||
|
|
170
homeassistant/components/alarm_control_panel/ifttt.py
Normal file
170
homeassistant/components/alarm_control_panel/ifttt.py
Normal 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
|
|
@ -69,3 +69,13 @@ alarmdecoder_alarm_toggle_chime:
|
|||
code:
|
||||
description: A required code to toggle the alarm control panel chime with.
|
||||
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'
|
||||
|
|
|
@ -438,9 +438,7 @@ class _LightCapabilities(_AlexaEntity):
|
|||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & light.SUPPORT_BRIGHTNESS:
|
||||
yield _AlexaBrightnessController(self.entity)
|
||||
if supported & light.SUPPORT_RGB_COLOR:
|
||||
yield _AlexaColorController(self.entity)
|
||||
if supported & light.SUPPORT_XY_COLOR:
|
||||
if supported & light.SUPPORT_COLOR:
|
||||
yield _AlexaColorController(self.entity)
|
||||
if supported & light.SUPPORT_COLOR_TEMP:
|
||||
yield _AlexaColorTemperatureController(self.entity)
|
||||
|
@ -842,25 +840,16 @@ def async_api_adjust_brightness(hass, config, request, entity):
|
|||
@asyncio.coroutine
|
||||
def async_api_set_color(hass, config, request, entity):
|
||||
"""Process a set color request."""
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
rgb = color_util.color_hsb_to_RGB(
|
||||
float(request[API_PAYLOAD]['color']['hue']),
|
||||
float(request[API_PAYLOAD]['color']['saturation']),
|
||||
float(request[API_PAYLOAD]['color']['brightness'])
|
||||
)
|
||||
|
||||
if supported & light.SUPPORT_RGB_COLOR > 0:
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_RGB_COLOR: rgb,
|
||||
}, 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)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_RGB_COLOR: rgb,
|
||||
}, blocking=False)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
|
118
homeassistant/components/binary_sensor/bmw_connected_drive.py
Normal file
118
homeassistant/components/binary_sensor/bmw_connected_drive.py
Normal 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)
|
|
@ -4,8 +4,6 @@ Support for deCONZ binary sensor.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.deconz/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz import (
|
||||
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
|
||||
|
@ -15,8 +13,8 @@ from homeassistant.core import callback
|
|||
DEPENDENCIES = ['deconz']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the deCONZ binary sensor."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
@ -25,8 +23,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
sensors = hass.data[DATA_DECONZ].sensors
|
||||
entities = []
|
||||
|
||||
for key in sorted(sensors.keys(), key=int):
|
||||
sensor = sensors[key]
|
||||
for sensor in sensors.values():
|
||||
if sensor and sensor.type in DECONZ_BINARY_SENSOR:
|
||||
entities.append(DeconzBinarySensor(sensor))
|
||||
async_add_devices(entities, True)
|
||||
|
@ -39,8 +36,7 @@ class DeconzBinarySensor(BinarySensorDevice):
|
|||
"""Set up sensor and add update callback to get data from websocket."""
|
||||
self._sensor = sensor
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe sensors events."""
|
||||
self._sensor.register_async_callback(self.async_update_callback)
|
||||
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):
|
||||
"""Return the state attributes of the sensor."""
|
||||
from pydeconz.sensor import PRESENCE
|
||||
attr = {
|
||||
ATTR_BATTERY_LEVEL: self._sensor.battery,
|
||||
}
|
||||
if self._sensor.type in PRESENCE:
|
||||
attr = {}
|
||||
if self._sensor.battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
|
||||
if self._sensor.type in PRESENCE and self._sensor.dark:
|
||||
attr['dark'] = self._sensor.dark
|
||||
return attr
|
||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.const import STATE_ON, STATE_OFF
|
|||
from homeassistant.components.egardia import (
|
||||
EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['egardia']
|
||||
EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion',
|
||||
'Door Contact': 'opening',
|
||||
'IR': 'motion'}
|
||||
|
|
|
@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
SENSOR_TYPES = {'openClosedSensor': 'opening',
|
||||
'motionSensor': 'motion',
|
||||
'doorSensor': 'door',
|
||||
'leakSensor': 'moisture'}
|
||||
'wetLeakSensor': 'moisture'}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -28,13 +28,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
address = discovery_info['address']
|
||||
device = plm.devices[address]
|
||||
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',
|
||||
device.address.hex, device.states[state_key].name)
|
||||
new_entity = InsteonPLMBinarySensor(device, state_key)
|
||||
|
||||
new_entity = InsteonPLMBinarySensor(device, state_key)
|
||||
|
||||
async_add_devices([new_entity])
|
||||
async_add_devices([new_entity])
|
||||
|
||||
|
||||
class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
|
||||
|
@ -53,5 +54,4 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
|
|||
@property
|
||||
def is_on(self):
|
||||
"""Return the boolean response if the node is on."""
|
||||
sensorstate = self._insteon_device_state.value
|
||||
return bool(sensorstate)
|
||||
return bool(self._insteon_device_state.value)
|
||||
|
|
|
@ -9,6 +9,17 @@ from homeassistant.components.binary_sensor import (
|
|||
DEVICE_CLASSES, DOMAIN, BinarySensorDevice)
|
||||
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):
|
||||
"""Set up the MySensors platform for binary sensors."""
|
||||
|
@ -29,18 +40,7 @@ class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
|
|||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
pres = self.gateway.const.Presentation
|
||||
class_map = {
|
||||
pres.S_DOOR: 'opening',
|
||||
pres.S_MOTION: 'motion',
|
||||
pres.S_SMOKE: 'smoke',
|
||||
}
|
||||
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)
|
||||
device_class = SENSORS.get(pres(self.child_type).name)
|
||||
if device_class in DEVICE_CLASSES:
|
||||
return device_class
|
||||
return None
|
||||
|
|
|
@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id
|
|||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
REQUIREMENTS = ['numpy==1.14.0']
|
||||
REQUIREMENTS = ['numpy==1.14.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['holidays==0.9.3']
|
||||
REQUIREMENTS = ['holidays==0.9.4']
|
||||
|
||||
# List of all countries currently supported by holidays
|
||||
# There seems to be no way to get the list out at runtime
|
||||
|
|
|
@ -37,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
BMW_COMPONENTS = ['device_tracker', 'sensor']
|
||||
BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor']
|
||||
UPDATE_INTERVAL = 5 # in minutes
|
||||
|
||||
|
||||
|
|
|
@ -194,7 +194,9 @@ class WebDavCalendarData(object):
|
|||
@staticmethod
|
||||
def is_over(vevent):
|
||||
"""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
|
||||
def get_hass_date(obj):
|
||||
|
@ -230,4 +232,4 @@ class WebDavCalendarData(object):
|
|||
else:
|
||||
enddate = obj.dtstart.value + timedelta(days=1)
|
||||
|
||||
return WebDavCalendarData.to_datetime(enddate)
|
||||
return enddate
|
||||
|
|
|
@ -62,7 +62,14 @@ class GoogleCalendarData(object):
|
|||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""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['timeMin'] = dt.now().isoformat('T')
|
||||
params['calendarId'] = self.calendar_id
|
||||
|
|
|
@ -496,6 +496,10 @@ class TodoistProjectData(object):
|
|||
# We had no valid tasks
|
||||
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
|
||||
# they have, organized)
|
||||
while project_tasks:
|
||||
|
|
|
@ -21,7 +21,7 @@ from homeassistant.components.camera import (
|
|||
PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, Camera)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
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__)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ https://home-assistant.io/components/camera.onvif/
|
|||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -103,92 +104,128 @@ class ONVIFHassCamera(Camera):
|
|||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a ONVIF camera."""
|
||||
from onvif import ONVIFCamera, exceptions
|
||||
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._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
||||
self._profile_index = config.get(CONF_PROFILE)
|
||||
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:
|
||||
_LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
|
||||
config.get(CONF_HOST), config.get(CONF_PORT))
|
||||
camera = ONVIFCamera(
|
||||
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):
|
||||
profiles = self._media_service.GetProfiles()
|
||||
|
||||
if self._profile_index >= len(profiles):
|
||||
_LOGGER.warning("ONVIF Camera '%s' doesn't provide profile %d."
|
||||
" Using the last profile.",
|
||||
self._name, self._profile_index)
|
||||
self._profile_index = -1
|
||||
req = media_service.create_type('GetStreamUri')
|
||||
|
||||
req = self._media_service.create_type('GetStreamUri')
|
||||
|
||||
# pylint: disable=protected-access
|
||||
req.ProfileToken = self._profiles[self._profile_index]._token
|
||||
self._input = media_service.GetStreamUri(req).Uri.replace(
|
||||
'rtsp://', 'rtsp://{}:{}@'.format(
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD)), 1)
|
||||
req.ProfileToken = profiles[self._profile_index]._token
|
||||
uri_no_auth = self._media_service.GetStreamUri(req).Uri
|
||||
uri_for_log = uri_no_auth.replace(
|
||||
'rtsp://', 'rtsp://<user>:<password>@', 1)
|
||||
self._input = uri_no_auth.replace(
|
||||
'rtsp://', 'rtsp://{}:{}@'.format(self._username,
|
||||
self._password), 1)
|
||||
_LOGGER.debug(
|
||||
"ONVIF Camera Using the following URL for %s: %s",
|
||||
self._name, self._input)
|
||||
except Exception as err:
|
||||
_LOGGER.error("Unable to communicate with ONVIF Camera: %s", err)
|
||||
raise
|
||||
try:
|
||||
self._ptz = camera.create_ptz_service()
|
||||
self._name, uri_for_log)
|
||||
# we won't need the media service anymore
|
||||
self._media_service = None
|
||||
except exceptions.ONVIFError as err:
|
||||
self._ptz = None
|
||||
_LOGGER.warning("Unable to setup PTZ for ONVIF Camera: %s", err)
|
||||
_LOGGER.debug("Couldn't setup camera '%s'. Error: %s",
|
||||
self._name, err)
|
||||
return
|
||||
|
||||
def perform_ptz(self, pan, tilt, zoom):
|
||||
"""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
|
||||
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
|
||||
req = {"Velocity": {
|
||||
"PanTilt": {"_x": pan_val, "_y": tilt_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
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
if ONVIF_DATA not in self.hass.data:
|
||||
self.hass.data[ONVIF_DATA] = {}
|
||||
self.hass.data[ONVIF_DATA][ENTITIES] = []
|
||||
self.hass.data[ONVIF_DATA][ENTITIES].append(self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
|
||||
if not self._input:
|
||||
await self.hass.async_add_job(self.obtain_input_uri)
|
||||
if not self._input:
|
||||
return None
|
||||
|
||||
ffmpeg = ImageFrame(
|
||||
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,
|
||||
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
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,
|
||||
loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
await stream.open_camera(
|
||||
self._input, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
await stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
|
@ -11,7 +11,7 @@ import async_timeout
|
|||
|
||||
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
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
|
|
@ -4,7 +4,6 @@ Support for Xeoma Cameras.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.xeoma/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
@ -14,7 +13,7 @@ from homeassistant.const import (
|
|||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyxeoma==1.3']
|
||||
REQUIREMENTS = ['pyxeoma==1.4.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -41,8 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Discover and setup Xeoma Cameras."""
|
||||
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)
|
||||
|
||||
try:
|
||||
yield from xeoma.async_test_connection()
|
||||
discovered_image_names = yield from xeoma.async_get_image_names()
|
||||
await xeoma.async_test_connection()
|
||||
discovered_image_names = await xeoma.async_get_image_names()
|
||||
discovered_cameras = [
|
||||
{
|
||||
CONF_IMAGE_NAME: image_name,
|
||||
|
@ -103,12 +102,11 @@ class XeomaCamera(Camera):
|
|||
self._password = password
|
||||
self._last_image = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from pyxeoma.xeoma import XeomaError
|
||||
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._last_image = image
|
||||
except XeomaError as err:
|
||||
|
|
|
@ -14,10 +14,10 @@ from homeassistant.components.climate import (
|
|||
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH,
|
||||
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF)
|
||||
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
|
||||
|
||||
_CONFIGURING = {}
|
||||
|
@ -50,7 +50,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
|
|||
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
|
||||
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_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):
|
||||
|
@ -122,6 +122,7 @@ class Thermostat(ClimateDevice):
|
|||
self._climate_list = self.climate_list
|
||||
self._operation_list = ['auto', 'auxHeatOnly', 'cool',
|
||||
'heat', 'off']
|
||||
self._fan_list = ['auto', 'on']
|
||||
self.update_without_throttle = False
|
||||
|
||||
def update(self):
|
||||
|
@ -180,24 +181,29 @@ class Thermostat(ClimateDevice):
|
|||
return self.thermostat['runtime']['desiredCool'] / 10.0
|
||||
return None
|
||||
|
||||
@property
|
||||
def desired_fan_mode(self):
|
||||
"""Return the desired fan mode of operation."""
|
||||
return self.thermostat['runtime']['desiredFanMode']
|
||||
|
||||
@property
|
||||
def fan(self):
|
||||
"""Return the current fan state."""
|
||||
"""Return the current fan status."""
|
||||
if 'fan' in self.thermostat['equipmentStatus']:
|
||||
return STATE_ON
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self.thermostat['runtime']['desiredFanMode']
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return current hold mode."""
|
||||
mode = self._current_hold_mode
|
||||
return None if mode == AWAY_MODE else mode
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""Return the available fan modes."""
|
||||
return self._fan_list
|
||||
|
||||
@property
|
||||
def _current_hold_mode(self):
|
||||
events = self.thermostat['events']
|
||||
|
@ -206,7 +212,7 @@ class Thermostat(ClimateDevice):
|
|||
if event['type'] == 'hold':
|
||||
if event['holdClimateRef'] == 'away':
|
||||
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
|
||||
return 'away'
|
||||
# A permanent hold from away climate
|
||||
|
@ -228,7 +234,7 @@ class Thermostat(ClimateDevice):
|
|||
def current_operation(self):
|
||||
"""Return current operation."""
|
||||
if self.operation_mode == 'auxHeatOnly' or \
|
||||
self.operation_mode == 'heatPump':
|
||||
self.operation_mode == 'heatPump':
|
||||
return STATE_HEAT
|
||||
return self.operation_mode
|
||||
|
||||
|
@ -271,10 +277,11 @@ class Thermostat(ClimateDevice):
|
|||
operation = STATE_HEAT
|
||||
else:
|
||||
operation = status
|
||||
|
||||
return {
|
||||
"actual_humidity": self.thermostat['runtime']['actualHumidity'],
|
||||
"fan": self.fan,
|
||||
"mode": self.mode,
|
||||
"climate_mode": self.mode,
|
||||
"operation": operation,
|
||||
"climate_list": self.climate_list,
|
||||
"fan_min_on_time": self.fan_min_on_time
|
||||
|
@ -342,25 +349,46 @@ class Thermostat(ClimateDevice):
|
|||
cool_temp_setpoint, heat_temp_setpoint,
|
||||
self.hold_preference())
|
||||
_LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, "
|
||||
"cool=%s, is=%s", heat_temp, isinstance(
|
||||
heat_temp, (int, float)), cool_temp,
|
||||
"cool=%s, is=%s", heat_temp,
|
||||
isinstance(heat_temp, (int, float)), cool_temp,
|
||||
isinstance(cool_temp, (int, float)))
|
||||
|
||||
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):
|
||||
"""Set temperature hold in modes other than auto."""
|
||||
# Set arbitrary range when not in auto mode
|
||||
if self.current_operation == STATE_HEAT:
|
||||
"""Set temperature hold in modes other than auto.
|
||||
|
||||
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
|
||||
cool_temp = temp + 20
|
||||
elif self.current_operation == STATE_COOL:
|
||||
heat_temp = temp - 20
|
||||
cool_temp = temp
|
||||
else:
|
||||
# In auto mode set temperature between
|
||||
heat_temp = temp - 10
|
||||
cool_temp = temp + 10
|
||||
delta = self.thermostat['settings']['heatCoolMinDelta'] / 10
|
||||
heat_temp = temp - delta
|
||||
cool_temp = temp + delta
|
||||
self.set_auto_temp_hold(heat_temp, cool_temp)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
|
@ -369,8 +397,8 @@ class Thermostat(ClimateDevice):
|
|||
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
if self.current_operation == STATE_AUTO and (low_temp is not None or
|
||||
high_temp is not None):
|
||||
if self.current_operation == STATE_AUTO and \
|
||||
(low_temp is not None or high_temp is not None):
|
||||
self.set_auto_temp_hold(low_temp, high_temp)
|
||||
elif temp is not None:
|
||||
self.set_temp_hold(temp)
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
|
||||
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__)
|
||||
|
||||
|
|
|
@ -248,6 +248,11 @@ class SensiboClimate(ClimateDevice):
|
|||
return self._temperatures_list[-1] \
|
||||
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
|
||||
def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
|
|
|
@ -37,6 +37,7 @@ CONF_FILTER = 'filter'
|
|||
CONF_GOOGLE_ACTIONS = 'google_actions'
|
||||
CONF_RELAYER = 'relayer'
|
||||
CONF_USER_POOL_ID = 'user_pool_id'
|
||||
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
|
||||
|
||||
DEFAULT_MODE = 'production'
|
||||
DEPENDENCIES = ['http']
|
||||
|
@ -75,6 +76,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
vol.Optional(CONF_USER_POOL_ID): str,
|
||||
vol.Optional(CONF_REGION): str,
|
||||
vol.Optional(CONF_RELAYER): str,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
|
||||
}),
|
||||
|
@ -110,7 +112,7 @@ class Cloud:
|
|||
|
||||
def __init__(self, hass, mode, alexa, google_actions,
|
||||
cognito_client_id=None, user_pool_id=None, region=None,
|
||||
relayer=None):
|
||||
relayer=None, google_actions_sync_url=None):
|
||||
"""Create an instance of Cloud."""
|
||||
self.hass = hass
|
||||
self.mode = mode
|
||||
|
@ -128,6 +130,7 @@ class Cloud:
|
|||
self.user_pool_id = user_pool_id
|
||||
self.region = region
|
||||
self.relayer = relayer
|
||||
self.google_actions_sync_url = google_actions_sync_url
|
||||
|
||||
else:
|
||||
info = SERVERS[mode]
|
||||
|
@ -136,6 +139,7 @@ class Cloud:
|
|||
self.user_pool_id = info['user_pool_id']
|
||||
self.region = info['region']
|
||||
self.relayer = info['relayer']
|
||||
self.google_actions_sync_url = info['google_actions_sync_url']
|
||||
|
||||
@property
|
||||
def is_logged_in(self):
|
||||
|
|
|
@ -8,7 +8,9 @@ SERVERS = {
|
|||
'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
|
||||
'user_pool_id': 'us-east-1_87ll5WOP8',
|
||||
'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'),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,9 +16,9 @@ from .const import DOMAIN, REQUEST_TIMEOUT
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
async def async_setup(hass):
|
||||
"""Initialize the HTTP API."""
|
||||
hass.http.register_view(GoogleActionsSyncView)
|
||||
hass.http.register_view(CloudLoginView)
|
||||
hass.http.register_view(CloudLogoutView)
|
||||
hass.http.register_view(CloudAccountView)
|
||||
|
@ -38,12 +38,11 @@ _CLOUD_ERRORS = {
|
|||
|
||||
def _handle_cloud_errors(handler):
|
||||
"""Handle auth errors."""
|
||||
@asyncio.coroutine
|
||||
@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."""
|
||||
try:
|
||||
result = yield from handler(view, request, *args, **kwargs)
|
||||
result = await handler(view, request, *args, **kwargs)
|
||||
return result
|
||||
|
||||
except (auth_api.CloudError, asyncio.TimeoutError) as err:
|
||||
|
@ -57,6 +56,31 @@ def _handle_cloud_errors(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):
|
||||
"""Login to Home Assistant cloud."""
|
||||
|
||||
|
@ -68,19 +92,18 @@ class CloudLoginView(HomeAssistantView):
|
|||
vol.Required('email'): str,
|
||||
vol.Required('password'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Handle login request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(auth_api.login, cloud, data['email'],
|
||||
data['password'])
|
||||
await hass.async_add_job(auth_api.login, cloud, data['email'],
|
||||
data['password'])
|
||||
|
||||
hass.async_add_job(cloud.iot.connect)
|
||||
# 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))
|
||||
|
||||
|
||||
|
@ -91,14 +114,13 @@ class CloudLogoutView(HomeAssistantView):
|
|||
name = 'api:cloud:logout'
|
||||
|
||||
@_handle_cloud_errors
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
"""Handle logout request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from cloud.logout()
|
||||
await cloud.logout()
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
@ -109,8 +131,7 @@ class CloudAccountView(HomeAssistantView):
|
|||
url = '/api/cloud/account'
|
||||
name = 'api:cloud:account'
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
"""Get account info."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
@ -132,14 +153,13 @@ class CloudRegisterView(HomeAssistantView):
|
|||
vol.Required('email'): str,
|
||||
vol.Required('password'): vol.All(str, vol.Length(min=6)),
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Handle registration request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
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'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
@ -155,14 +175,13 @@ class CloudResendConfirmView(HomeAssistantView):
|
|||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('email'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Handle resending confirm email code request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
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'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
@ -178,14 +197,13 @@ class CloudForgotPasswordView(HomeAssistantView):
|
|||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('email'): str,
|
||||
}))
|
||||
@asyncio.coroutine
|
||||
def post(self, request, data):
|
||||
async def post(self, request, data):
|
||||
"""Handle forgot password request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
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'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"name": {
|
||||
"data": {
|
||||
"name": "Nimi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -62,13 +62,11 @@ class ExampleConfigFlow(config_entries.ConfigFlowHandler):
|
|||
return (yield from self.async_step_name())
|
||||
|
||||
errors = {
|
||||
'object_id': 'Invalid object id.'
|
||||
'object_id': 'invalid_object_id'
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
title='Pick object id',
|
||||
step_id='init',
|
||||
description="Please enter an object_id for the test entity.",
|
||||
data_schema=vol.Schema({
|
||||
'object_id': str
|
||||
}),
|
||||
|
@ -92,9 +90,7 @@ class ExampleConfigFlow(config_entries.ConfigFlowHandler):
|
|||
)
|
||||
|
||||
return self.async_show_form(
|
||||
title='Name of the entity',
|
||||
step_id='name',
|
||||
description="Please enter a name for the test entity.",
|
||||
data_schema=vol.Schema({
|
||||
'name': str
|
||||
}),
|
24
homeassistant/components/config_entry_example/strings.json
Normal file
24
homeassistant/components/config_entry_example/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
|
|||
ATTR_ENTITY_PICTURE
|
||||
from homeassistant.loader import bind_hass
|
||||
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__)
|
||||
_KEY_INSTANCE = 'configurator'
|
||||
|
|
271
homeassistant/components/cover/group.py
Executable file
271
homeassistant/components/cover/group.py
Executable 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
|
|
@ -234,7 +234,9 @@ class CoverTemplate(CoverDevice):
|
|||
|
||||
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
|
||||
def current_cover_tilt_position(self):
|
||||
|
|
|
@ -4,8 +4,6 @@ Support for deCONZ devices.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/deconz/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
@ -19,7 +17,7 @@ from homeassistant.helpers import discovery
|
|||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
REQUIREMENTS = ['pydeconz==30']
|
||||
REQUIREMENTS = ['pydeconz==32']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -57,30 +55,28 @@ Unlock your deCONZ gateway to register with Home Assistant.
|
|||
"""
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up services and configuration for deCONZ component."""
|
||||
result = False
|
||||
config_file = yield from hass.async_add_job(
|
||||
config_file = await hass.async_add_job(
|
||||
load_json, hass.config.path(CONFIG_FILE))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_deconz_discovered(service, discovery_info):
|
||||
async def async_deconz_discovered(service, discovery_info):
|
||||
"""Call when deCONZ gateway has been found."""
|
||||
deconz_config = {}
|
||||
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
|
||||
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:
|
||||
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]:
|
||||
deconz_config = config[DOMAIN]
|
||||
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:
|
||||
yield from async_request_configuration(hass, config, deconz_config)
|
||||
await async_request_configuration(hass, config, deconz_config)
|
||||
return True
|
||||
|
||||
if not result:
|
||||
|
@ -89,8 +85,7 @@ def async_setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_deconz(hass, config, deconz_config):
|
||||
async def async_setup_deconz(hass, config, deconz_config):
|
||||
"""Set up a deCONZ session.
|
||||
|
||||
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
|
||||
websession = async_get_clientsession(hass)
|
||||
deconz = DeconzSession(hass.loop, websession, **deconz_config)
|
||||
result = yield from deconz.async_load_parameters()
|
||||
result = await deconz.async_load_parameters()
|
||||
if result is False:
|
||||
_LOGGER.error("Failed to communicate with deCONZ")
|
||||
return False
|
||||
|
@ -113,8 +108,7 @@ def async_setup_deconz(hass, config, deconz_config):
|
|||
hass, component, DOMAIN, {}, config))
|
||||
deconz.start()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_configure(call):
|
||||
async def async_configure(call):
|
||||
"""Set attribute of 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:
|
||||
_LOGGER.error('Could not find the entity %s', entity_id)
|
||||
return
|
||||
yield from deconz.async_put_state(field, data)
|
||||
await deconz.async_put_state(field, data)
|
||||
hass.services.async_register(
|
||||
DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA)
|
||||
|
||||
|
@ -159,21 +153,19 @@ def async_setup_deconz(hass, config, deconz_config):
|
|||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_request_configuration(hass, config, deconz_config):
|
||||
async def async_request_configuration(hass, config, deconz_config):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_configuration_callback(data):
|
||||
async def async_configuration_callback(data):
|
||||
"""Set up actions to do when our configuration callback is called."""
|
||||
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:
|
||||
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:
|
||||
yield from hass.async_add_job(
|
||||
await hass.async_add_job(
|
||||
save_json, hass.config.path(CONFIG_FILE), deconz_config)
|
||||
configurator.async_request_done(request_id)
|
||||
return
|
||||
|
|
|
@ -28,7 +28,7 @@ from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.loader import get_component
|
||||
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
|
||||
from homeassistant.util.yaml import dump
|
||||
|
||||
|
|
|
@ -73,7 +73,8 @@ class MikrotikScanner(DeviceScanner):
|
|||
self.host,
|
||||
self.username,
|
||||
self.password,
|
||||
port=int(self.port)
|
||||
port=int(self.port),
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
|
@ -98,7 +98,8 @@ class UnifiScanner(DeviceScanner):
|
|||
# Filter clients to provided SSID list
|
||||
if self._ssid_filter:
|
||||
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 = {
|
||||
client['mac']: client
|
||||
|
|
|
@ -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
|
||||
loaded before the EVENT_PLATFORM_DISCOVERED is fired.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import timedelta
|
||||
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
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['netdisco==1.2.4']
|
||||
REQUIREMENTS = ['netdisco==1.3.0']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
|
@ -39,6 +38,7 @@ SERVICE_TELLDUSLIVE = 'tellstick'
|
|||
SERVICE_HUE = 'philips_hue'
|
||||
SERVICE_DECONZ = 'deconz'
|
||||
SERVICE_DAIKIN = 'daikin'
|
||||
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||
|
@ -54,6 +54,7 @@ SERVICE_HANDLERS = {
|
|||
SERVICE_HUE: ('hue', None),
|
||||
SERVICE_DECONZ: ('deconz', None),
|
||||
SERVICE_DAIKIN: ('daikin', None),
|
||||
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
'plex_mediaserver': ('media_player', 'plex'),
|
||||
|
@ -84,8 +85,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Start a discovery service."""
|
||||
from netdisco.discovery import NetworkDiscovery
|
||||
|
||||
|
@ -99,8 +99,7 @@ def async_setup(hass, config):
|
|||
# Platforms ignore by config
|
||||
ignored_platforms = config[DOMAIN][CONF_IGNORE]
|
||||
|
||||
@asyncio.coroutine
|
||||
def new_service_found(service, info):
|
||||
async def new_service_found(service, info):
|
||||
"""Handle a new service if one is found."""
|
||||
if service in ignored_platforms:
|
||||
logger.info("Ignoring service: %s %s", service, info)
|
||||
|
@ -124,15 +123,14 @@ def async_setup(hass, config):
|
|||
component, platform = comp_plat
|
||||
|
||||
if platform is None:
|
||||
yield from async_discover(hass, service, info, component, config)
|
||||
await async_discover(hass, service, info, component, config)
|
||||
else:
|
||||
yield from async_load_platform(
|
||||
await async_load_platform(
|
||||
hass, component, platform, info, config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def scan_devices(now):
|
||||
async def scan_devices(now):
|
||||
"""Scan for devices."""
|
||||
results = yield from hass.async_add_job(_discover, netdisco)
|
||||
results = await hass.async_add_job(_discover, netdisco)
|
||||
|
||||
for result in results:
|
||||
hass.async_add_job(new_service_found(*result))
|
||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
|||
from homeassistant.components.http import HomeAssistantView
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['DoorBirdPy==0.1.2']
|
||||
REQUIREMENTS = ['DoorBirdPy==0.1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ ATTR_OVERWRITE = 'overwrite'
|
|||
CONF_DOWNLOAD_DIR = 'download_dir'
|
||||
|
||||
DOMAIN = 'downloader'
|
||||
DOWNLOAD_FAILED_EVENT = 'download_failed'
|
||||
DOWNLOAD_COMPLETED_EVENT = 'download_completed'
|
||||
|
||||
SERVICE_DOWNLOAD_FILE = 'download_file'
|
||||
|
||||
|
@ -133,9 +135,19 @@ def setup(hass, config):
|
|||
fil.write(chunk)
|
||||
|
||||
_LOGGER.debug("Downloading of %s done", url)
|
||||
hass.bus.fire(
|
||||
"{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), {
|
||||
'url': url,
|
||||
'filename': filename
|
||||
})
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
_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
|
||||
if final_path and os.path.isfile(final_path):
|
||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY
|
|||
from homeassistant.util import Throttle
|
||||
from homeassistant.util.json import save_json
|
||||
|
||||
REQUIREMENTS = ['python-ecobee-api==0.0.15']
|
||||
REQUIREMENTS = ['python-ecobee-api==0.0.17']
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||
CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
REQUIREMENTS = ['pythonegardia==1.0.38']
|
||||
REQUIREMENTS = ['pythonegardia==1.0.39']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
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_HUMIDITY = 'humidity'
|
||||
|
|
114
homeassistant/components/fan/zha.py
Normal file
114
homeassistant/components/fan/zha.py
Normal 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
|
|
@ -24,7 +24,7 @@ from homeassistant.core import callback
|
|||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180310.0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180330.0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||
|
|
|
@ -243,7 +243,7 @@ class ColorSpectrumTrait(_Trait):
|
|||
if domain != light.DOMAIN:
|
||||
return False
|
||||
|
||||
return features & (light.SUPPORT_RGB_COLOR | light.SUPPORT_XY_COLOR)
|
||||
return features & light.SUPPORT_COLOR
|
||||
|
||||
def sync_attributes(self):
|
||||
"""Return color spectrum attributes for a sync request."""
|
||||
|
@ -254,13 +254,11 @@ class ColorSpectrumTrait(_Trait):
|
|||
"""Return color spectrum query attributes."""
|
||||
response = {}
|
||||
|
||||
# No need to handle XY color because light component will always
|
||||
# convert XY to RGB if possible (which is when brightness is available)
|
||||
color_rgb = self.state.attributes.get(light.ATTR_RGB_COLOR)
|
||||
if color_rgb is not None:
|
||||
color_hs = self.state.attributes.get(light.ATTR_HS_COLOR)
|
||||
if color_hs is not None:
|
||||
response['color'] = {
|
||||
'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
|
||||
|
@ -274,11 +272,12 @@ class ColorSpectrumTrait(_Trait):
|
|||
"""Execute a color spectrum command."""
|
||||
# Convert integer to hex format and left pad with 0's till length 6
|
||||
hex_value = "{0:06x}".format(params['color']['spectrumRGB'])
|
||||
color = color_util.rgb_hex_to_rgb_list(hex_value)
|
||||
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, {
|
||||
ATTR_ENTITY_ID: self.state.entity_id,
|
||||
light.ATTR_RGB_COLOR: color
|
||||
light.ATTR_HS_COLOR: color
|
||||
}, blocking=True)
|
||||
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
|||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
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'
|
||||
|
||||
|
|
|
@ -156,7 +156,7 @@ def async_setup(hass, config):
|
|||
|
||||
if 'frontend' in hass.config.components:
|
||||
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:
|
||||
yield from hassio.update_hass_api(config['http'])
|
||||
|
|
|
@ -239,15 +239,16 @@ def get_state(hass, utc_point_in_time, entity_id, run=None):
|
|||
def async_setup(hass, config):
|
||||
"""Set up the history hooks."""
|
||||
filters = Filters()
|
||||
exclude = config[DOMAIN].get(CONF_EXCLUDE)
|
||||
conf = config.get(DOMAIN, {})
|
||||
exclude = conf.get(CONF_EXCLUDE)
|
||||
if exclude:
|
||||
filters.excluded_entities = exclude.get(CONF_ENTITIES, [])
|
||||
filters.excluded_domains = exclude.get(CONF_DOMAINS, [])
|
||||
include = config[DOMAIN].get(CONF_INCLUDE)
|
||||
include = conf.get(CONF_INCLUDE)
|
||||
if include:
|
||||
filters.included_entities = include.get(CONF_ENTITIES, [])
|
||||
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))
|
||||
yield from hass.components.frontend.async_register_built_in_panel(
|
||||
|
@ -308,7 +309,7 @@ class HistoryPeriodView(HomeAssistantView):
|
|||
result = yield from hass.async_add_job(
|
||||
get_significant_states, hass, start_time, end_time,
|
||||
entity_ids, self.filters, include_start_time_state)
|
||||
result = result.values()
|
||||
result = list(result.values())
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
elapsed = time.perf_counter() - timer_start
|
||||
_LOGGER.debug(
|
||||
|
@ -318,7 +319,6 @@ class HistoryPeriodView(HomeAssistantView):
|
|||
# by any entities explicitly included in the configuration.
|
||||
|
||||
if self.use_include_order:
|
||||
result = list(result)
|
||||
sorted_result = []
|
||||
for order_entity in self.filters.included_entities:
|
||||
for state_list in result:
|
||||
|
|
|
@ -3,154 +3,202 @@
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/homekit/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from zlib import adler32
|
||||
|
||||
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 (
|
||||
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.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()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_RE_VALID_PINCODE = r"^(\d{3}-\d{2}-\d{3})$"
|
||||
|
||||
DOMAIN = 'homekit'
|
||||
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({
|
||||
DOMAIN: vol.All({
|
||||
vol.Optional(CONF_PORT, default=51826): vol.Coerce(int),
|
||||
vol.Optional(CONF_PIN_CODE, default='123-45-678'): valid_pin,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
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)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Setup the HomeKit component."""
|
||||
_LOGGER.debug("Begin setup HomeKit")
|
||||
_LOGGER.debug('Begin setup HomeKit')
|
||||
|
||||
conf = config[DOMAIN]
|
||||
port = conf.get(CONF_PORT)
|
||||
pin = str.encode(conf.get(CONF_PIN_CODE))
|
||||
port = conf[CONF_PORT]
|
||||
auto_start = conf[CONF_AUTO_START]
|
||||
entity_filter = conf[CONF_FILTER]
|
||||
entity_config = conf[CONF_ENTITY_CONFIG]
|
||||
|
||||
homekit = HomeKit(hass, port)
|
||||
homekit.setup_bridge(pin)
|
||||
homekit = HomeKit(hass, port, entity_filter, entity_config)
|
||||
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
|
||||
|
||||
|
||||
def import_types():
|
||||
"""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):
|
||||
def get_accessory(hass, state, aid, config):
|
||||
"""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':
|
||||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT:
|
||||
_LOGGER.debug("Add \"%s\" as \"%s\"",
|
||||
_LOGGER.debug('Add "%s" as "%s"',
|
||||
state.entity_id, 'TemperatureSensor')
|
||||
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':
|
||||
# Only add covers that support set_cover_position
|
||||
if state.attributes.get(ATTR_SUPPORTED_FEATURES) & 4:
|
||||
_LOGGER.debug("Add \"%s\" as \"%s\"",
|
||||
state.entity_id, 'Window')
|
||||
return TYPES['Window'](hass, state.entity_id, state.name)
|
||||
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if features & SUPPORT_SET_POSITION:
|
||||
_LOGGER.debug('Add "%s" as "%s"',
|
||||
state.entity_id, 'WindowCovering')
|
||||
return TYPES['WindowCovering'](hass, state.entity_id, state.name,
|
||||
aid=aid)
|
||||
|
||||
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')
|
||||
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':
|
||||
support_auto = False
|
||||
features = state.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH
|
||||
# Check if climate device supports auto mode
|
||||
if (features & SUPPORT_TARGET_TEMPERATURE_HIGH) \
|
||||
and (features & SUPPORT_TARGET_TEMPERATURE_LOW):
|
||||
support_auto = True
|
||||
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Thermostat')
|
||||
support_auto = bool(features & support_temp_range)
|
||||
|
||||
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat')
|
||||
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' \
|
||||
or state.domain == 'input_boolean':
|
||||
_LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Switch')
|
||||
return TYPES['Switch'](hass, state.entity_id, state.name)
|
||||
or state.domain == 'input_boolean' or state.domain == 'script':
|
||||
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch')
|
||||
return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid)
|
||||
|
||||
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 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."""
|
||||
self._hass = hass
|
||||
self._port = port
|
||||
self._filter = entity_filter
|
||||
self._config = entity_config
|
||||
self.started = False
|
||||
|
||||
self.bridge = None
|
||||
self.driver = None
|
||||
|
||||
def setup_bridge(self, pin):
|
||||
"""Setup the bridge component to track all accessories."""
|
||||
from .accessories import HomeBridge
|
||||
self.bridge = HomeBridge(BRIDGE_NAME, 'homekit.bridge', pin)
|
||||
def setup(self):
|
||||
"""Setup bridge and accessory driver."""
|
||||
from .accessories import HomeBridge, HomeDriver
|
||||
|
||||
def start_driver(self, event):
|
||||
"""Start the accessory driver."""
|
||||
from pyhap.accessory_driver import AccessoryDriver
|
||||
self._hass.bus.listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, self.stop_driver)
|
||||
self._hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, self.stop)
|
||||
|
||||
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)
|
||||
self.driver = AccessoryDriver(self.bridge, self._port,
|
||||
ip_address, path)
|
||||
_LOGGER.debug("Driver started")
|
||||
self.bridge = HomeBridge(self._hass)
|
||||
self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path)
|
||||
|
||||
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()
|
||||
|
||||
def stop_driver(self, event):
|
||||
def stop(self, *args):
|
||||
"""Stop the accessory driver."""
|
||||
_LOGGER.debug("Driver stop")
|
||||
if self.driver is not None:
|
||||
if not self.started:
|
||||
return
|
||||
|
||||
_LOGGER.debug('Driver stop')
|
||||
if self.driver and self.driver.run_sentinel:
|
||||
self.driver.stop()
|
||||
|
|
|
@ -2,15 +2,33 @@
|
|||
import logging
|
||||
|
||||
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 (
|
||||
SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER,
|
||||
CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER)
|
||||
|
||||
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
|
||||
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__)
|
||||
|
||||
|
||||
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,
|
||||
serial_number='0000'):
|
||||
"""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)
|
||||
|
||||
|
||||
def add_preload_service(acc, service, chars=None, opt_chars=None):
|
||||
"""Define and return a service to be available for the accessory."""
|
||||
from pyhap.loader import get_serv_loader, get_char_loader
|
||||
service = get_serv_loader().get(service)
|
||||
if chars:
|
||||
chars = chars if isinstance(chars, list) else [chars]
|
||||
for char_name in chars:
|
||||
char = get_char_loader().get(char_name)
|
||||
service.add_characteristic(char)
|
||||
if opt_chars:
|
||||
opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars]
|
||||
for opt_char_name in opt_chars:
|
||||
opt_char = get_char_loader().get(opt_char_name)
|
||||
service.add_opt_characteristic(opt_char)
|
||||
acc.add_service(service)
|
||||
return service
|
||||
def override_properties(char, properties=None, valid_values=None):
|
||||
"""Override characteristic property values and valid values."""
|
||||
if properties:
|
||||
char.properties.update(properties)
|
||||
|
||||
|
||||
def override_properties(char, new_properties):
|
||||
"""Override characteristic property values."""
|
||||
char.properties.update(new_properties)
|
||||
if valid_values:
|
||||
char.properties['ValidValues'].update(valid_values)
|
||||
|
||||
|
||||
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."""
|
||||
super().__init__(display_name, **kwargs)
|
||||
set_accessory_info(self, display_name, model)
|
||||
super().__init__(name, **kwargs)
|
||||
set_accessory_info(self, name, model)
|
||||
self.category = getattr(Category, category, Category.OTHER)
|
||||
|
||||
def _set_services(self):
|
||||
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 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."""
|
||||
super().__init__(display_name, pincode=pincode, **kwargs)
|
||||
set_accessory_info(self, display_name, model)
|
||||
super().__init__(name, **kwargs)
|
||||
set_accessory_info(self, name, model)
|
||||
self._hass = hass
|
||||
|
||||
def _set_services(self):
|
||||
add_preload_service(self, SERV_ACCESSORY_INFO)
|
||||
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)
|
||||
|
|
|
@ -1,31 +1,67 @@
|
|||
"""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'
|
||||
|
||||
# Services
|
||||
# #### Categories ####
|
||||
CATEGORY_LIGHT = 'LIGHTBULB'
|
||||
CATEGORY_SENSOR = 'SENSOR'
|
||||
|
||||
|
||||
# #### Services ####
|
||||
SERV_ACCESSORY_INFO = 'AccessoryInformation'
|
||||
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_SWITCH = 'Switch'
|
||||
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
|
||||
SERV_THERMOSTAT = 'Thermostat'
|
||||
SERV_WINDOW_COVERING = 'WindowCovering'
|
||||
|
||||
# Characteristics
|
||||
|
||||
# #### Characteristics ####
|
||||
CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier'
|
||||
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
|
||||
CHAR_CATEGORY = 'Category'
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
|
||||
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
|
||||
CHAR_CURRENT_POSITION = 'CurrentPosition'
|
||||
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
|
||||
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
|
||||
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
|
||||
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
|
||||
CHAR_LINK_QUALITY = 'LinkQuality'
|
||||
CHAR_MANUFACTURER = 'Manufacturer'
|
||||
CHAR_MODEL = 'Model'
|
||||
CHAR_NAME = 'Name'
|
||||
CHAR_ON = 'On'
|
||||
CHAR_ON = 'On' # boolean
|
||||
CHAR_POSITION_STATE = 'PositionState'
|
||||
CHAR_REACHABLE = 'Reachable'
|
||||
CHAR_SATURATION = 'Saturation' # percent
|
||||
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
||||
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
|
||||
CHAR_TARGET_POSITION = 'TargetPosition'
|
||||
|
@ -33,5 +69,5 @@ CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
|
|||
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
|
||||
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
|
||||
|
||||
# Properties
|
||||
# #### Properties ####
|
||||
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}
|
||||
|
|
|
@ -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)
|
4
homeassistant/components/homekit/services.yaml
Normal file
4
homeassistant/components/homekit/services.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Describes the format for available HomeKit services
|
||||
|
||||
start:
|
||||
description: Starts the HomeKit component driver.
|
|
@ -2,7 +2,6 @@
|
|||
import logging
|
||||
|
||||
from homeassistant.components.cover import ATTR_CURRENT_POSITION
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
|
@ -14,16 +13,17 @@ from .const import (
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@TYPES.register('Window')
|
||||
class Window(HomeAccessory):
|
||||
@TYPES.register('WindowCovering')
|
||||
class WindowCovering(HomeAccessory):
|
||||
"""Generate a Window accessory for a cover entity.
|
||||
|
||||
The cover entity must support: set_cover_position.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity_id, display_name):
|
||||
"""Initialize a Window accessory object."""
|
||||
super().__init__(display_name, entity_id, 'WINDOW')
|
||||
def __init__(self, hass, entity_id, display_name, *args, **kwargs):
|
||||
"""Initialize a WindowCovering accessory object."""
|
||||
super().__init__(display_name, entity_id, 'WINDOW_COVERING',
|
||||
*args, **kwargs)
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
|
@ -31,12 +31,12 @@ class Window(HomeAccessory):
|
|||
self.current_position = None
|
||||
self.homekit_target = None
|
||||
|
||||
self.serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
|
||||
self.char_current_position = self.serv_cover. \
|
||||
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
|
||||
self.char_current_position = serv_cover. \
|
||||
get_characteristic(CHAR_CURRENT_POSITION)
|
||||
self.char_target_position = self.serv_cover. \
|
||||
self.char_target_position = serv_cover. \
|
||||
get_characteristic(CHAR_TARGET_POSITION)
|
||||
self.char_position_state = self.serv_cover. \
|
||||
self.char_position_state = serv_cover. \
|
||||
get_characteristic(CHAR_POSITION_STATE)
|
||||
self.char_current_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
|
||||
|
||||
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):
|
||||
"""Move cover to value if call came from HomeKit."""
|
||||
self.char_target_position.set_value(value, should_callback=False)
|
||||
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
|
||||
if value > self.current_position:
|
||||
self.char_position_state.set_value(1)
|
||||
elif value < self.current_position:
|
||||
self.char_position_state.set_value(0)
|
||||
self._hass.services.call(
|
||||
'cover', 'set_cover_position',
|
||||
{'entity_id': self._entity_id, 'position': value})
|
||||
self._hass.components.cover.set_cover_position(
|
||||
value, self._entity_id)
|
||||
|
||||
def update_cover_position(self, entity_id=None, old_state=None,
|
||||
new_state=None):
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
"""Update cover position after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
current_position = new_state.attributes[ATTR_CURRENT_POSITION]
|
||||
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
if current_position is None:
|
||||
return
|
||||
|
||||
self.current_position = int(current_position)
|
||||
self.char_current_position.set_value(self.current_position)
|
||||
|
153
homeassistant/components/homekit/type_lights.py
Normal file
153
homeassistant/components/homekit/type_lights.py
Normal 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
|
|
@ -5,7 +5,6 @@ from homeassistant.const import (
|
|||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
ATTR_ENTITY_ID, ATTR_CODE)
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
|
@ -28,9 +27,11 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
|
|||
class SecuritySystem(HomeAccessory):
|
||||
"""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."""
|
||||
super().__init__(display_name, entity_id, 'ALARM_SYSTEM')
|
||||
super().__init__(display_name, entity_id, 'ALARM_SYSTEM',
|
||||
*args, **kwargs)
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
|
@ -38,39 +39,31 @@ class SecuritySystem(HomeAccessory):
|
|||
|
||||
self.flag_target_state = False
|
||||
|
||||
self.service_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
|
||||
self.char_current_state = self.service_alarm. \
|
||||
serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
|
||||
self.char_current_state = serv_alarm. \
|
||||
get_characteristic(CHAR_CURRENT_SECURITY_STATE)
|
||||
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)
|
||||
self.char_target_state.value = 3
|
||||
|
||||
self.char_target_state.setter_callback = self.set_security_state
|
||||
|
||||
def run(self):
|
||||
"""Method called be object after driver is started."""
|
||||
state = self._hass.states.get(self._entity_id)
|
||||
self.update_security_state(new_state=state)
|
||||
|
||||
async_track_state_change(self._hass, self._entity_id,
|
||||
self.update_security_state)
|
||||
|
||||
def set_security_state(self, value):
|
||||
"""Move security state to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set security state to %d",
|
||||
_LOGGER.debug('%s: Set security state to %d',
|
||||
self._entity_id, value)
|
||||
self.flag_target_state = True
|
||||
self.char_target_state.set_value(value, should_callback=False)
|
||||
hass_value = HOMEKIT_TO_HASS[value]
|
||||
service = STATE_TO_SERVICE[hass_value]
|
||||
|
||||
params = {ATTR_ENTITY_ID: self._entity_id}
|
||||
if self._alarm_code is not None:
|
||||
if self._alarm_code:
|
||||
params[ATTR_CODE] = self._alarm_code
|
||||
self._hass.services.call('alarm_control_panel', service, params)
|
||||
|
||||
def update_security_state(self, entity_id=None,
|
||||
old_state=None, new_state=None):
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
"""Update security state after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
@ -78,15 +71,15 @@ class SecuritySystem(HomeAccessory):
|
|||
hass_state = new_state.state
|
||||
if hass_state not in HASS_TO_HOMEKIT:
|
||||
return
|
||||
|
||||
current_security_state = HASS_TO_HOMEKIT[hass_state]
|
||||
self.char_current_state.set_value(current_security_state)
|
||||
_LOGGER.debug("%s: Updated current state to %s (%d)",
|
||||
self._entity_id, hass_state,
|
||||
current_security_state)
|
||||
self.char_current_state.set_value(current_security_state,
|
||||
should_callback=False)
|
||||
_LOGGER.debug('%s: Updated current state to %s (%d)',
|
||||
self._entity_id, hass_state, current_security_state)
|
||||
|
||||
if not self.flag_target_state:
|
||||
self.char_target_state.set_value(current_security_state,
|
||||
should_callback=False)
|
||||
elif self.char_target_state.get_value() \
|
||||
== self.char_current_state.get_value():
|
||||
if self.char_target_state.value == self.char_current_state.value:
|
||||
self.flag_target_state = False
|
78
homeassistant/components/homekit/type_sensors.py
Normal file
78
homeassistant/components/homekit/type_sensors.py
Normal 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)
|
|
@ -1,9 +1,9 @@
|
|||
"""Class to hold all switch accessories."""
|
||||
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.helpers.event import async_track_state_change
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
|
@ -16,9 +16,9 @@ _LOGGER = logging.getLogger(__name__)
|
|||
class Switch(HomeAccessory):
|
||||
"""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."""
|
||||
super().__init__(display_name, entity_id, 'SWITCH')
|
||||
super().__init__(display_name, entity_id, 'SWITCH', *args, **kwargs)
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
|
@ -26,25 +26,18 @@ class Switch(HomeAccessory):
|
|||
|
||||
self.flag_target_state = False
|
||||
|
||||
self.service_switch = add_preload_service(self, SERV_SWITCH)
|
||||
self.char_on = self.service_switch.get_characteristic(CHAR_ON)
|
||||
serv_switch = add_preload_service(self, SERV_SWITCH)
|
||||
self.char_on = serv_switch.get_characteristic(CHAR_ON)
|
||||
self.char_on.value = False
|
||||
self.char_on.setter_callback = self.set_state
|
||||
|
||||
def run(self):
|
||||
"""Method called be object after driver is started."""
|
||||
state = self._hass.states.get(self._entity_id)
|
||||
self.update_state(new_state=state)
|
||||
|
||||
async_track_state_change(self._hass, self._entity_id,
|
||||
self.update_state)
|
||||
|
||||
def set_state(self, value):
|
||||
"""Move switch state to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set switch state to %s",
|
||||
_LOGGER.debug('%s: Set switch state to %s',
|
||||
self._entity_id, value)
|
||||
self.flag_target_state = True
|
||||
service = 'turn_on' if value else 'turn_off'
|
||||
self.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,
|
||||
{ATTR_ENTITY_ID: self._entity_id})
|
||||
|
||||
|
@ -53,10 +46,10 @@ class Switch(HomeAccessory):
|
|||
if new_state is None:
|
||||
return
|
||||
|
||||
current_state = (new_state.state == 'on')
|
||||
current_state = (new_state.state == STATE_ON)
|
||||
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.char_on.set_value(current_state, should_callback=False)
|
||||
else:
|
||||
self.flag_target_state = False
|
||||
|
||||
self.flag_target_state = False
|
|
@ -7,9 +7,7 @@ from homeassistant.components.climate import (
|
|||
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
|
||||
STATE_HEAT, STATE_COOL, STATE_AUTO)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT,
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, add_preload_service
|
||||
|
@ -18,6 +16,7 @@ from .const import (
|
|||
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
|
||||
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
||||
from .util import temperature_to_homekit, temperature_to_states
|
||||
|
||||
_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):
|
||||
"""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."""
|
||||
super().__init__(display_name, entity_id, 'THERMOSTAT')
|
||||
super().__init__(display_name, entity_id, 'THERMOSTAT',
|
||||
*args, **kwargs)
|
||||
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
self._call_timer = None
|
||||
self._unit = TEMP_CELSIUS
|
||||
|
||||
self.heat_cool_flag_target_state = False
|
||||
self.temperature_flag_target_state = False
|
||||
self.coolingthresh_flag_target_state = False
|
||||
self.heatingthresh_flag_target_state = False
|
||||
|
||||
extra_chars = None
|
||||
# Add additional characteristics if auto mode is supported
|
||||
if support_auto:
|
||||
extra_chars = [CHAR_COOLING_THRESHOLD_TEMPERATURE,
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE]
|
||||
extra_chars = [
|
||||
CHAR_COOLING_THRESHOLD_TEMPERATURE,
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE] if support_auto else None
|
||||
|
||||
# Preload the thermostat service
|
||||
self.service_thermostat = add_preload_service(self, SERV_THERMOSTAT,
|
||||
extra_chars)
|
||||
serv_thermostat = add_preload_service(self, SERV_THERMOSTAT,
|
||||
extra_chars)
|
||||
|
||||
# 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)
|
||||
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)
|
||||
self.char_target_heat_cool.value = 0
|
||||
self.char_target_heat_cool.setter_callback = self.set_heat_cool
|
||||
|
||||
# Current and target temperature characteristics
|
||||
self.char_current_temp = self.service_thermostat. \
|
||||
self.char_current_temp = serv_thermostat. \
|
||||
get_characteristic(CHAR_CURRENT_TEMPERATURE)
|
||||
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)
|
||||
self.char_target_temp.value = 21.0
|
||||
self.char_target_temp.setter_callback = self.set_target_temperature
|
||||
|
||||
# Display units characteristic
|
||||
self.char_display_units = self.service_thermostat. \
|
||||
self.char_display_units = serv_thermostat. \
|
||||
get_characteristic(CHAR_TEMP_DISPLAY_UNITS)
|
||||
self.char_display_units.value = 0
|
||||
|
||||
# If the device supports it: high and low temperature characteristics
|
||||
if support_auto:
|
||||
self.char_cooling_thresh_temp = self.service_thermostat. \
|
||||
self.char_cooling_thresh_temp = serv_thermostat. \
|
||||
get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE)
|
||||
self.char_cooling_thresh_temp.value = 23.0
|
||||
self.char_cooling_thresh_temp.setter_callback = \
|
||||
self.set_cooling_threshold
|
||||
|
||||
self.char_heating_thresh_temp = self.service_thermostat. \
|
||||
self.char_heating_thresh_temp = serv_thermostat. \
|
||||
get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
||||
self.char_heating_thresh_temp.value = 19.0
|
||||
self.char_heating_thresh_temp.setter_callback = \
|
||||
|
@ -96,132 +97,127 @@ class Thermostat(HomeAccessory):
|
|||
self.char_cooling_thresh_temp = None
|
||||
self.char_heating_thresh_temp = None
|
||||
|
||||
def run(self):
|
||||
"""Method called be object after driver is started."""
|
||||
state = self._hass.states.get(self._entity_id)
|
||||
self.update_thermostat(new_state=state)
|
||||
|
||||
async_track_state_change(self._hass, self._entity_id,
|
||||
self.update_thermostat)
|
||||
|
||||
def set_heat_cool(self, value):
|
||||
"""Move operation mode to value if call came from HomeKit."""
|
||||
self.char_target_heat_cool.set_value(value, should_callback=False)
|
||||
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
|
||||
hass_value = HC_HOMEKIT_TO_HASS[value]
|
||||
self._hass.services.call('climate', 'set_operation_mode',
|
||||
{ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_OPERATION_MODE: hass_value})
|
||||
self._hass.components.climate.set_operation_mode(
|
||||
operation_mode=hass_value, entity_id=self._entity_id)
|
||||
|
||||
def set_cooling_threshold(self, value):
|
||||
"""Set cooling threshold temp to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set cooling threshold temperature to %.2f",
|
||||
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
|
||||
self._entity_id, value)
|
||||
self.coolingthresh_flag_target_state = True
|
||||
low = self.char_heating_thresh_temp.get_value()
|
||||
self._hass.services.call(
|
||||
'climate', 'set_temperature',
|
||||
{ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_TARGET_TEMP_HIGH: value,
|
||||
ATTR_TARGET_TEMP_LOW: low})
|
||||
self.char_cooling_thresh_temp.set_value(value, should_callback=False)
|
||||
low = self.char_heating_thresh_temp.value
|
||||
low = temperature_to_states(low, self._unit)
|
||||
value = temperature_to_states(value, self._unit)
|
||||
self._hass.components.climate.set_temperature(
|
||||
entity_id=self._entity_id, target_temp_high=value,
|
||||
target_temp_low=low)
|
||||
|
||||
def set_heating_threshold(self, value):
|
||||
"""Set heating threshold temp to value if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set heating threshold temperature to %.2f",
|
||||
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
|
||||
self._entity_id, value)
|
||||
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
|
||||
high = self.char_cooling_thresh_temp.get_value()
|
||||
self._hass.services.call(
|
||||
'climate', 'set_temperature',
|
||||
{ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_TARGET_TEMP_LOW: value,
|
||||
ATTR_TARGET_TEMP_HIGH: high})
|
||||
high = self.char_cooling_thresh_temp.value
|
||||
high = temperature_to_states(high, self._unit)
|
||||
value = temperature_to_states(value, self._unit)
|
||||
self._hass.components.climate.set_temperature(
|
||||
entity_id=self._entity_id, target_temp_high=high,
|
||||
target_temp_low=value)
|
||||
|
||||
def set_target_temperature(self, value):
|
||||
"""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.temperature_flag_target_state = True
|
||||
self._hass.services.call(
|
||||
'climate', 'set_temperature',
|
||||
{ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_TEMPERATURE: value})
|
||||
self.char_target_temp.set_value(value, should_callback=False)
|
||||
value = temperature_to_states(value, self._unit)
|
||||
self._hass.components.climate.set_temperature(
|
||||
temperature=value, entity_id=self._entity_id)
|
||||
|
||||
def update_thermostat(self, entity_id=None,
|
||||
old_state=None, new_state=None):
|
||||
def update_state(self, entity_id=None, old_state=None, new_state=None):
|
||||
"""Update security state after state changed."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT,
|
||||
TEMP_CELSIUS)
|
||||
|
||||
# Update current temperature
|
||||
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
|
||||
if isinstance(current_temp, (int, float)):
|
||||
current_temp = temperature_to_homekit(current_temp, self._unit)
|
||||
self.char_current_temp.set_value(current_temp)
|
||||
|
||||
# Update target temperature
|
||||
target_temp = new_state.attributes.get(ATTR_TEMPERATURE)
|
||||
if isinstance(target_temp, (int, float)):
|
||||
target_temp = temperature_to_homekit(target_temp, self._unit)
|
||||
if not self.temperature_flag_target_state:
|
||||
self.char_target_temp.set_value(target_temp,
|
||||
should_callback=False)
|
||||
else:
|
||||
self.temperature_flag_target_state = False
|
||||
self.temperature_flag_target_state = False
|
||||
|
||||
# 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)
|
||||
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:
|
||||
self.char_cooling_thresh_temp.set_value(
|
||||
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
|
||||
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)
|
||||
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:
|
||||
self.char_heating_thresh_temp.set_value(
|
||||
heating_thresh, should_callback=False)
|
||||
else:
|
||||
self.heatingthresh_flag_target_state = False
|
||||
self.heatingthresh_flag_target_state = False
|
||||
|
||||
# Update display units
|
||||
display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
if display_units is not None \
|
||||
and display_units in UNIT_HASS_TO_HOMEKIT:
|
||||
self.char_display_units.set_value(
|
||||
UNIT_HASS_TO_HOMEKIT[display_units])
|
||||
if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT:
|
||||
self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit])
|
||||
|
||||
# Update target 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:
|
||||
if not self.heat_cool_flag_target_state:
|
||||
self.char_target_heat_cool.set_value(
|
||||
HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False)
|
||||
else:
|
||||
self.heat_cool_flag_target_state = False
|
||||
self.heat_cool_flag_target_state = False
|
||||
|
||||
# Set current operation mode based on temperatures and target mode
|
||||
if operation_mode == STATE_HEAT:
|
||||
if current_temp < target_temp:
|
||||
if isinstance(target_temp, float) and current_temp < target_temp:
|
||||
current_operation_mode = STATE_HEAT
|
||||
else:
|
||||
current_operation_mode = STATE_OFF
|
||||
elif operation_mode == STATE_COOL:
|
||||
if current_temp > target_temp:
|
||||
if isinstance(target_temp, float) and current_temp > target_temp:
|
||||
current_operation_mode = STATE_COOL
|
||||
else:
|
||||
current_operation_mode = STATE_OFF
|
||||
elif operation_mode == STATE_AUTO:
|
||||
# Check if auto is supported
|
||||
if self.char_cooling_thresh_temp is not None:
|
||||
lower_temp = self.char_heating_thresh_temp.get_value()
|
||||
upper_temp = self.char_cooling_thresh_temp.get_value()
|
||||
if self.char_cooling_thresh_temp:
|
||||
lower_temp = self.char_heating_thresh_temp.value
|
||||
upper_temp = self.char_cooling_thresh_temp.value
|
||||
if current_temp < lower_temp:
|
||||
current_operation_mode = STATE_HEAT
|
||||
elif current_temp > upper_temp:
|
||||
|
@ -232,9 +228,11 @@ class Thermostat(HomeAccessory):
|
|||
# Check if heating or cooling are supported
|
||||
heat = STATE_HEAT in new_state.attributes[ATTR_OPERATION_LIST]
|
||||
cool = STATE_COOL in new_state.attributes[ATTR_OPERATION_LIST]
|
||||
if current_temp < target_temp and heat:
|
||||
if isinstance(target_temp, float) and \
|
||||
current_temp < target_temp and 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
|
||||
else:
|
||||
current_operation_mode = STATE_OFF
|
65
homeassistant/components/homekit/util.py
Normal file
65
homeassistant/components/homekit/util.py
Normal 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)
|
|
@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.39']
|
||||
REQUIREMENTS = ['pyhomematic==0.1.40']
|
||||
DOMAIN = 'homematic'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -33,6 +33,7 @@ DISCOVER_SENSORS = 'homematic.sensor'
|
|||
DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor'
|
||||
DISCOVER_COVER = 'homematic.cover'
|
||||
DISCOVER_CLIMATE = 'homematic.climate'
|
||||
DISCOVER_LOCKS = 'homematic.locks'
|
||||
|
||||
ATTR_DISCOVER_DEVICES = 'devices'
|
||||
ATTR_PARAM = 'param'
|
||||
|
@ -59,7 +60,7 @@ SERVICE_SET_INSTALL_MODE = 'set_install_mode'
|
|||
HM_DEVICE_TYPES = {
|
||||
DISCOVER_SWITCHES: [
|
||||
'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren',
|
||||
'IPSwitchPowermeter', 'KeyMatic', 'HMWIOSwitch', 'Rain', 'EcoLogic'],
|
||||
'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic'],
|
||||
DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'],
|
||||
DISCOVER_SENSORS: [
|
||||
'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP',
|
||||
|
@ -68,7 +69,7 @@ HM_DEVICE_TYPES = {
|
|||
'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor',
|
||||
'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch',
|
||||
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
|
||||
'IPSmoke', 'RFSiren', 'PresenceIP'],
|
||||
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat'],
|
||||
DISCOVER_CLIMATE: [
|
||||
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
|
||||
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
|
||||
|
@ -78,7 +79,8 @@ HM_DEVICE_TYPES = {
|
|||
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
|
||||
'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
|
||||
'WiredSensor', 'PresenceIP'],
|
||||
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt']
|
||||
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'],
|
||||
DISCOVER_LOCKS: ['KeyMatic']
|
||||
}
|
||||
|
||||
HM_IGNORE_DISCOVERY_NODE = [
|
||||
|
@ -86,6 +88,10 @@ HM_IGNORE_DISCOVERY_NODE = [
|
|||
'ACTUAL_HUMIDITY'
|
||||
]
|
||||
|
||||
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = {
|
||||
'ACTUAL_TEMPERATURE': ['IPAreaThermostat'],
|
||||
}
|
||||
|
||||
HM_ATTRIBUTE_SUPPORT = {
|
||||
'LOWBAT': ['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),
|
||||
('binary_sensor', DISCOVER_BINARY_SENSORS),
|
||||
('sensor', DISCOVER_SENSORS),
|
||||
('climate', DISCOVER_CLIMATE)):
|
||||
('climate', DISCOVER_CLIMATE),
|
||||
('lock', DISCOVER_LOCKS)):
|
||||
# Get all devices of a specific type
|
||||
found_devices = _get_devices(
|
||||
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
|
||||
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
|
||||
|
||||
# Add devices
|
||||
|
|
176
homeassistant/components/homematicip_cloud.py
Normal file
176
homeassistant/components/homematicip_cloud.py
Normal 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()
|
|
@ -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
|
||||
https://home-assistant.io/components/http/
|
||||
"""
|
||||
|
||||
from ipaddress import ip_network
|
||||
import logging
|
||||
import os
|
||||
|
@ -32,7 +31,7 @@ from .static import (
|
|||
from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa
|
||||
from .view import HomeAssistantView # noqa
|
||||
|
||||
REQUIREMENTS = ['aiohttp_cors==0.6.0']
|
||||
REQUIREMENTS = ['aiohttp_cors==0.7.0']
|
||||
|
||||
DOMAIN = 'http'
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import json
|
|||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPUnauthorized
|
||||
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPInternalServerError
|
||||
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant.core import is_callback
|
||||
|
@ -31,8 +31,12 @@ class HomeAssistantView(object):
|
|||
# pylint: disable=no-self-use
|
||||
def json(self, result, status_code=200, headers=None):
|
||||
"""Return a JSON response."""
|
||||
msg = json.dumps(
|
||||
result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8')
|
||||
try:
|
||||
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(
|
||||
body=msg, content_type=CONTENT_TYPE_JSON, status=status_code,
|
||||
headers=headers)
|
||||
|
|
26
homeassistant/components/hue/.translations/de.json
Normal file
26
homeassistant/components/hue/.translations/de.json
Normal 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",
|
||||
"title": "Hub verbinden"
|
||||
}
|
||||
},
|
||||
"title": "Philips Hue Bridge"
|
||||
}
|
||||
}
|
26
homeassistant/components/hue/.translations/en.json
Normal file
26
homeassistant/components/hue/.translations/en.json
Normal 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",
|
||||
"title": "Link Hub"
|
||||
}
|
||||
},
|
||||
"title": "Philips Hue Bridge"
|
||||
}
|
||||
}
|
26
homeassistant/components/hue/.translations/ko.json
Normal file
26
homeassistant/components/hue/.translations/ko.json
Normal 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",
|
||||
"title": "\ud5c8\ube0c \uc5f0\uacb0"
|
||||
}
|
||||
},
|
||||
"title": "\ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0"
|
||||
}
|
||||
}
|
26
homeassistant/components/hue/.translations/nl.json
Normal file
26
homeassistant/components/hue/.translations/nl.json
Normal 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"
|
||||
}
|
||||
}
|
26
homeassistant/components/hue/.translations/no.json
Normal file
26
homeassistant/components/hue/.translations/no.json
Normal 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 ",
|
||||
"title": "Link Hub"
|
||||
}
|
||||
},
|
||||
"title": "Philips Hue Bridge"
|
||||
}
|
||||
}
|
26
homeassistant/components/hue/.translations/pl.json
Normal file
26
homeassistant/components/hue/.translations/pl.json
Normal 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"
|
||||
}
|
||||
}
|
18
homeassistant/components/hue/.translations/ro.json
Normal file
18
homeassistant/components/hue/.translations/ro.json
Normal 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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
homeassistant/components/hue/.translations/sl.json
Normal file
26
homeassistant/components/hue/.translations/sl.json
Normal 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"
|
||||
}
|
||||
}
|
26
homeassistant/components/hue/.translations/zh-Hans.json
Normal file
26
homeassistant/components/hue/.translations/zh-Hans.json
Normal 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 ",
|
||||
"title": "\u8fde\u63a5\u4e2d\u67a2"
|
||||
}
|
||||
},
|
||||
"title": "\u98de\u5229\u6d66 Hue Bridge"
|
||||
}
|
||||
}
|
|
@ -6,22 +6,22 @@ https://home-assistant.io/components/hue/
|
|||
"""
|
||||
import asyncio
|
||||
import json
|
||||
from functools import partial
|
||||
import ipaddress
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
|
||||
import async_timeout
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.discovery import SERVICE_HUE
|
||||
from homeassistant.const import CONF_FILENAME, CONF_HOST
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import discovery, aiohttp_client
|
||||
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__)
|
||||
|
||||
|
@ -36,26 +36,23 @@ DEFAULT_ALLOW_UNREACHABLE = False
|
|||
|
||||
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"
|
||||
DEFAULT_ALLOW_HUE_GROUPS = True
|
||||
|
||||
BRIDGE_CONFIG_SCHEMA = vol.Schema([{
|
||||
vol.Optional(CONF_HOST): cv.string,
|
||||
BRIDGE_CONFIG_SCHEMA = vol.Schema({
|
||||
# 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_ALLOW_UNREACHABLE,
|
||||
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,
|
||||
default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean,
|
||||
}])
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = 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)
|
||||
|
||||
|
@ -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."""
|
||||
conf = config.get(DOMAIN)
|
||||
if conf is None:
|
||||
|
@ -82,196 +79,212 @@ def setup(hass, config):
|
|||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
discovery.listen(
|
||||
hass,
|
||||
SERVICE_HUE,
|
||||
lambda service, discovery_info:
|
||||
bridge_discovered(hass, service, discovery_info))
|
||||
async def async_bridge_discovered(service, discovery_info):
|
||||
"""Dispatcher for Hue discovery events."""
|
||||
# Ignore emulated hue
|
||||
if "HASS Bridge" in discovery_info.get('name', ''):
|
||||
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
|
||||
if CONF_BRIDGES in conf:
|
||||
bridges = conf[CONF_BRIDGES]
|
||||
|
||||
# Component is part of config but no bridges specified, discover.
|
||||
elif DOMAIN in config:
|
||||
# discover from nupnp
|
||||
hosts = requests.get(API_NUPNP).json()
|
||||
bridges = [{
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
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_FILENAME: '.hue_{}.conf'.format(entry['id']),
|
||||
} for entry in hosts]
|
||||
}) for entry in hosts]
|
||||
|
||||
else:
|
||||
# Component not specified in config, we're loaded via discovery
|
||||
bridges = []
|
||||
|
||||
for bridge in bridges:
|
||||
filename = bridge.get(CONF_FILENAME)
|
||||
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)
|
||||
if not bridges:
|
||||
return True
|
||||
|
||||
host = bridge.get(CONF_HOST)
|
||||
|
||||
if host is None:
|
||||
host = _find_host_from_config(hass, filename)
|
||||
|
||||
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)
|
||||
await asyncio.wait([
|
||||
async_setup_bridge(
|
||||
hass, bridge[CONF_HOST], bridge[CONF_FILENAME],
|
||||
bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS]
|
||||
) for bridge in bridges
|
||||
])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def bridge_discovered(hass, service, discovery_info):
|
||||
"""Dispatcher for Hue discovery events."""
|
||||
if "HASS Bridge" in discovery_info.get('name', ''):
|
||||
return
|
||||
|
||||
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):
|
||||
async def async_setup_bridge(
|
||||
hass, host, filename=None,
|
||||
allow_unreachable=DEFAULT_ALLOW_UNREACHABLE,
|
||||
allow_hue_groups=DEFAULT_ALLOW_HUE_GROUPS,
|
||||
username=None):
|
||||
"""Set up a given Hue bridge."""
|
||||
assert filename or username, 'Need to pass at least a username or filename'
|
||||
|
||||
# Only register a device once
|
||||
if socket.gethostbyname(host) in hass.data[DOMAIN]:
|
||||
if host in hass.data[DOMAIN]:
|
||||
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,
|
||||
allow_in_emulated_hue, allow_hue_groups)
|
||||
bridge.setup()
|
||||
allow_hue_groups)
|
||||
await bridge.async_setup()
|
||||
|
||||
|
||||
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
|
||||
"""Attempt to detect host based on existing configuration."""
|
||||
def _find_username_from_config(hass, filename):
|
||||
"""Load username from config."""
|
||||
path = hass.config.path(filename)
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(path) as inp:
|
||||
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
|
||||
with open(path) as inp:
|
||||
return list(json.load(inp).values())[0]['username']
|
||||
|
||||
|
||||
class HueBridge(object):
|
||||
"""Manages a single Hue bridge."""
|
||||
|
||||
def __init__(self, host, hass, filename, username, allow_unreachable=False,
|
||||
allow_in_emulated_hue=True, allow_hue_groups=True):
|
||||
def __init__(self, host, hass, filename, username,
|
||||
allow_unreachable=False, allow_groups=True):
|
||||
"""Initialize the system."""
|
||||
self.host = host
|
||||
self.bridge_id = socket.gethostbyname(host)
|
||||
self.hass = hass
|
||||
self.filename = filename
|
||||
self.username = username
|
||||
self.allow_unreachable = allow_unreachable
|
||||
self.allow_in_emulated_hue = allow_in_emulated_hue
|
||||
self.allow_hue_groups = allow_hue_groups
|
||||
|
||||
self.allow_groups = allow_groups
|
||||
self.available = True
|
||||
self.bridge = None
|
||||
self.lights = {}
|
||||
self.lightgroups = {}
|
||||
|
||||
self.configured = False
|
||||
self.config_request_id = None
|
||||
self.api = None
|
||||
|
||||
hass.data[DOMAIN][self.bridge_id] = self
|
||||
|
||||
def setup(self):
|
||||
async def async_setup(self):
|
||||
"""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:
|
||||
kwargs = {}
|
||||
if self.username is not None:
|
||||
kwargs['username'] = self.username
|
||||
if self.filename is not None:
|
||||
kwargs['config_file_path'] = \
|
||||
self.hass.config.path(self.filename)
|
||||
self.bridge = phue.Bridge(self.host, **kwargs)
|
||||
except OSError: # Wrong host was given
|
||||
with async_timeout.timeout(5):
|
||||
# Initialize bridge and validate our username
|
||||
if not self.username:
|
||||
await api.create_user('home-assistant')
|
||||
await api.initialize()
|
||||
except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized):
|
||||
_LOGGER.warning("Connected to Hue at %s but not registered.",
|
||||
self.host)
|
||||
self.async_request_configuration()
|
||||
return
|
||||
except (asyncio.TimeoutError, aiohue.RequestError):
|
||||
_LOGGER.error("Error connecting to the Hue bridge at %s",
|
||||
self.host)
|
||||
return
|
||||
except phue.PhueRegistrationException:
|
||||
_LOGGER.warning("Connected to Hue at %s but not registered.",
|
||||
self.host)
|
||||
self.request_configuration()
|
||||
except aiohue.AiohueException:
|
||||
_LOGGER.exception('Unknown Hue linking error occurred')
|
||||
self.async_request_configuration()
|
||||
return
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unknown error connecting with Hue bridge at %s",
|
||||
self.host)
|
||||
return
|
||||
|
||||
self.hass.data[DOMAIN][self.host] = self
|
||||
|
||||
# If we came here and configuring this host, mark as done
|
||||
if self.config_request_id:
|
||||
request_id = self.config_request_id
|
||||
self.config_request_id = None
|
||||
configurator = self.hass.components.configurator
|
||||
configurator.request_done(request_id)
|
||||
self.hass.components.configurator.async_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,
|
||||
{'bridge_id': self.bridge_id})
|
||||
{'host': self.host}))
|
||||
|
||||
# create a service for calling run_scene directly on the bridge,
|
||||
# used to simplify automation rules.
|
||||
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,
|
||||
self.hass.services.async_register(
|
||||
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
|
||||
schema=SCENE_SCHEMA)
|
||||
|
||||
def request_configuration(self):
|
||||
@callback
|
||||
def async_request_configuration(self):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = self.hass.components.configurator
|
||||
|
||||
# We got an error if this method is called while we are configuring
|
||||
if self.config_request_id:
|
||||
configurator.notify_errors(
|
||||
configurator.async_notify_errors(
|
||||
self.config_request_id,
|
||||
"Failed to register, please try again.")
|
||||
return
|
||||
|
||||
self.config_request_id = configurator.request_config(
|
||||
"Philips Hue",
|
||||
lambda data: self.setup(),
|
||||
async def config_callback(data):
|
||||
"""Callback for configurator data."""
|
||||
await self.async_setup()
|
||||
|
||||
self.config_request_id = configurator.async_request_config(
|
||||
"Philips Hue", config_callback,
|
||||
description=CONFIG_INSTRUCTIONS,
|
||||
entity_picture="/static/images/logo_philips_hue.png",
|
||||
submit_caption="I have pressed the button"
|
||||
)
|
||||
|
||||
def get_api(self):
|
||||
"""Return the full api dictionary from phue."""
|
||||
return self.bridge.get_api()
|
||||
async def hue_activate_scene(self, call, updated=False):
|
||||
"""Service to call directly into bridge to set scenes."""
|
||||
group_name = call.data[ATTR_GROUP_NAME]
|
||||
scene_name = call.data[ATTR_SCENE_NAME]
|
||||
|
||||
def set_light(self, light_id, command):
|
||||
"""Adjust properties of one or more lights. See phue for details."""
|
||||
return self.bridge.set_light(light_id, command)
|
||||
group = next(
|
||||
(group for group in self.api.groups.values()
|
||||
if group.name == group_name), None)
|
||||
|
||||
def set_group(self, light_id, command):
|
||||
"""Change light settings for a group. See phue for detail."""
|
||||
return self.bridge.set_group(light_id, command)
|
||||
scene_id = next(
|
||||
(scene.id for scene in self.api.scenes.values()
|
||||
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)
|
||||
|
@ -305,12 +318,12 @@ class HueFlowHandler(config_entries.ConfigFlowHandler):
|
|||
bridges = await discover_nupnp(websession=self._websession)
|
||||
except asyncio.TimeoutError:
|
||||
return self.async_abort(
|
||||
reason='Unable to discover Hue bridges.'
|
||||
reason='discover_timeout'
|
||||
)
|
||||
|
||||
if not bridges:
|
||||
return self.async_abort(
|
||||
reason='No Philips Hue bridges discovered.'
|
||||
reason='no_bridges'
|
||||
)
|
||||
|
||||
# Find already configured hosts
|
||||
|
@ -323,7 +336,7 @@ class HueFlowHandler(config_entries.ConfigFlowHandler):
|
|||
|
||||
if not hosts:
|
||||
return self.async_abort(
|
||||
reason='All Philips Hue bridges are already configured.'
|
||||
reason='all_configured'
|
||||
)
|
||||
|
||||
elif len(hosts) == 1:
|
||||
|
@ -332,7 +345,6 @@ class HueFlowHandler(config_entries.ConfigFlowHandler):
|
|||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
title='Pick Hue Bridge',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required('host'): vol.In(hosts)
|
||||
})
|
||||
|
@ -353,10 +365,10 @@ class HueFlowHandler(config_entries.ConfigFlowHandler):
|
|||
await bridge.initialize()
|
||||
except (asyncio.TimeoutError, aiohue.RequestError,
|
||||
aiohue.LinkButtonNotPressed):
|
||||
errors['base'] = 'Failed to register, please try again.'
|
||||
errors['base'] = 'register_failed'
|
||||
except aiohue.AiohueException:
|
||||
errors['base'] = 'Unknown linking error occurred.'
|
||||
_LOGGER.exception('Uknown Hue linking error occurred')
|
||||
errors['base'] = 'linking'
|
||||
_LOGGER.exception('Unknown Hue linking error occurred')
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=bridge.config.name,
|
||||
|
@ -369,15 +381,12 @@ class HueFlowHandler(config_entries.ConfigFlowHandler):
|
|||
|
||||
return self.async_show_form(
|
||||
step_id='link',
|
||||
title='Link Hub',
|
||||
description=CONFIG_INSTRUCTIONS,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up a bridge for a config entry."""
|
||||
await hass.async_add_job(partial(
|
||||
setup_bridge, entry.data['host'], hass,
|
||||
username=entry.data['username']))
|
||||
await async_setup_bridge(hass, entry.data['host'],
|
||||
username=entry.data['username'])
|
||||
return True
|
26
homeassistant/components/hue/strings.json
Normal file
26
homeassistant/components/hue/strings.json
Normal 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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ from homeassistant.components.image_processing import (
|
|||
PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE,
|
||||
CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE)
|
||||
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']
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ from homeassistant.const import STATE_UNKNOWN, CONF_REGION
|
|||
from homeassistant.components.image_processing import (
|
||||
PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE,
|
||||
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__)
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.components.image_processing import (
|
|||
from homeassistant.core import split_entity_id
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['numpy==1.14.0']
|
||||
REQUIREMENTS = ['numpy==1.14.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv
|
|||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['insteonplm==0.8.2']
|
||||
REQUIREMENTS = ['insteonplm==0.8.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -64,19 +64,20 @@ def async_setup(hass, config):
|
|||
"""Detect device from transport to be delegated to platform."""
|
||||
for state_key in device.states:
|
||||
platform_info = ipdb[device.states[state_key]]
|
||||
platform = platform_info.platform
|
||||
if platform is not None:
|
||||
_LOGGER.info("New INSTEON PLM device: %s (%s) %s",
|
||||
device.address,
|
||||
device.states[state_key].name,
|
||||
platform)
|
||||
if platform_info:
|
||||
platform = platform_info.platform
|
||||
if platform:
|
||||
_LOGGER.info("New INSTEON PLM device: %s (%s) %s",
|
||||
device.address,
|
||||
device.states[state_key].name,
|
||||
platform)
|
||||
|
||||
hass.async_add_job(
|
||||
discovery.async_load_platform(
|
||||
hass, platform, DOMAIN,
|
||||
discovered={'address': device.address.hex,
|
||||
'state_key': state_key},
|
||||
hass_config=config))
|
||||
hass.async_add_job(
|
||||
discovery.async_load_platform(
|
||||
hass, platform, DOMAIN,
|
||||
discovered={'address': device.address.hex,
|
||||
'state_key': state_key},
|
||||
hass_config=config))
|
||||
|
||||
_LOGGER.info("Looking for PLM on %s", port)
|
||||
conn = yield from insteonplm.Connection.create(
|
||||
|
@ -127,13 +128,15 @@ class IPDB(object):
|
|||
from insteonplm.states.sensor import (VariableSensor,
|
||||
OnOffSensor,
|
||||
SmokeCO2Sensor,
|
||||
IoLincSensor)
|
||||
IoLincSensor,
|
||||
LeakSensorDryWet)
|
||||
|
||||
self.states = [State(OnOffSwitch_OutletTop, 'switch'),
|
||||
State(OnOffSwitch_OutletBottom, 'switch'),
|
||||
State(OpenClosedRelay, 'switch'),
|
||||
State(OnOffSwitch, 'switch'),
|
||||
|
||||
State(LeakSensorDryWet, 'binary_sensor'),
|
||||
State(IoLincSensor, 'binary_sensor'),
|
||||
State(SmokeCO2Sensor, 'sensor'),
|
||||
State(OnOffSensor, 'binary_sensor'),
|
||||
|
|
|
@ -40,9 +40,8 @@ SUPPORT_BRIGHTNESS = 1
|
|||
SUPPORT_COLOR_TEMP = 2
|
||||
SUPPORT_EFFECT = 4
|
||||
SUPPORT_FLASH = 8
|
||||
SUPPORT_RGB_COLOR = 16
|
||||
SUPPORT_COLOR = 16
|
||||
SUPPORT_TRANSITION = 32
|
||||
SUPPORT_XY_COLOR = 64
|
||||
SUPPORT_WHITE_VALUE = 128
|
||||
|
||||
# Integer that represents transition time in seconds to make change.
|
||||
|
@ -51,6 +50,7 @@ ATTR_TRANSITION = "transition"
|
|||
# Lists holding color values
|
||||
ATTR_RGB_COLOR = "rgb_color"
|
||||
ATTR_XY_COLOR = "xy_color"
|
||||
ATTR_HS_COLOR = "hs_color"
|
||||
ATTR_COLOR_TEMP = "color_temp"
|
||||
ATTR_KELVIN = "kelvin"
|
||||
ATTR_MIN_MIREDS = "min_mireds"
|
||||
|
@ -86,8 +86,9 @@ LIGHT_PROFILES_FILE = "light_profiles.csv"
|
|||
PROP_TO_ATTR = {
|
||||
'brightness': ATTR_BRIGHTNESS,
|
||||
'color_temp': ATTR_COLOR_TEMP,
|
||||
'rgb_color': ATTR_RGB_COLOR,
|
||||
'xy_color': ATTR_XY_COLOR,
|
||||
'min_mireds': ATTR_MIN_MIREDS,
|
||||
'max_mireds': ATTR_MAX_MIREDS,
|
||||
'hs_color': ATTR_HS_COLOR,
|
||||
'white_value': ATTR_WHITE_VALUE,
|
||||
'effect_list': ATTR_EFFECT_LIST,
|
||||
'effect': ATTR_EFFECT,
|
||||
|
@ -111,6 +112,11 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({
|
|||
vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP):
|
||||
vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
||||
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.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Exclusive(ATTR_KELVIN, COLOR_GROUP):
|
||||
|
@ -149,13 +155,13 @@ def is_on(hass, entity_id=None):
|
|||
|
||||
@bind_hass
|
||||
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,
|
||||
profile=None, flash=None, effect=None, color_name=None):
|
||||
"""Turn all or specified light on."""
|
||||
hass.add_job(
|
||||
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)
|
||||
|
||||
|
||||
|
@ -163,8 +169,9 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
|
|||
@bind_hass
|
||||
def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
|
||||
brightness_pct=None, rgb_color=None, xy_color=None,
|
||||
color_temp=None, kelvin=None, white_value=None,
|
||||
profile=None, flash=None, effect=None, color_name=None):
|
||||
hs_color=None, color_temp=None, kelvin=None,
|
||||
white_value=None, profile=None, flash=None, effect=None,
|
||||
color_name=None):
|
||||
"""Turn all or specified light on."""
|
||||
data = {
|
||||
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_RGB_COLOR, rgb_color),
|
||||
(ATTR_XY_COLOR, xy_color),
|
||||
(ATTR_HS_COLOR, hs_color),
|
||||
(ATTR_COLOR_TEMP, color_temp),
|
||||
(ATTR_KELVIN, kelvin),
|
||||
(ATTR_WHITE_VALUE, white_value),
|
||||
|
@ -254,6 +262,14 @@ def preprocess_turn_on_alternatives(params):
|
|||
if brightness_pct is not None:
|
||||
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):
|
||||
"""Handle set color intents."""
|
||||
|
@ -281,7 +297,7 @@ class SetIntentHandler(intent.IntentHandler):
|
|||
|
||||
if 'color' in slots:
|
||||
intent.async_test_feature(
|
||||
state, SUPPORT_RGB_COLOR, 'changing colors')
|
||||
state, SUPPORT_COLOR, 'changing colors')
|
||||
service_data[ATTR_RGB_COLOR] = slots['color']['value']
|
||||
# Use original passed in value of the color because we don't have
|
||||
# human readable names for that internally.
|
||||
|
@ -428,13 +444,8 @@ class Light(ToggleEntity):
|
|||
return None
|
||||
|
||||
@property
|
||||
def xy_color(self):
|
||||
"""Return the XY color value [float, float]."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""Return the RGB color value [int, int, int]."""
|
||||
def hs_color(self):
|
||||
"""Return the hue and saturation color value [float, float]."""
|
||||
return None
|
||||
|
||||
@property
|
||||
|
@ -484,11 +495,16 @@ class Light(ToggleEntity):
|
|||
if value is not None:
|
||||
data[attr] = value
|
||||
|
||||
if ATTR_RGB_COLOR not in data and ATTR_XY_COLOR in data and \
|
||||
ATTR_BRIGHTNESS in data:
|
||||
data[ATTR_RGB_COLOR] = color_util.color_xy_brightness_to_RGB(
|
||||
data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1],
|
||||
data[ATTR_BRIGHTNESS])
|
||||
# Expose current color also as RGB and XY
|
||||
if ATTR_HS_COLOR in data:
|
||||
data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(
|
||||
*data[ATTR_HS_COLOR])
|
||||
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
|
||||
|
||||
|
|
|
@ -8,8 +8,9 @@ import logging
|
|||
|
||||
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light)
|
||||
ATTR_BRIGHTNESS, ATTR_HS_COLOR,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light)
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
|
@ -44,10 +45,12 @@ class AbodeLight(AbodeDevice, Light):
|
|||
|
||||
def turn_on(self, **kwargs):
|
||||
"""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.set_color(kwargs[ATTR_RGB_COLOR])
|
||||
elif ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable:
|
||||
self._device.set_color(color_util.color_hs_to_RGB(
|
||||
*kwargs[ATTR_HS_COLOR]))
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable:
|
||||
self._device.set_level(kwargs[ATTR_BRIGHTNESS])
|
||||
else:
|
||||
self._device.switch_on()
|
||||
|
@ -68,16 +71,16 @@ class AbodeLight(AbodeDevice, Light):
|
|||
return self._device.brightness
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
def hs_color(self):
|
||||
"""Return the color of the light."""
|
||||
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
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
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:
|
||||
return SUPPORT_BRIGHTNESS
|
||||
|
||||
|
|
|
@ -9,9 +9,11 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
REQUIREMENTS = ['blinkstick==1.1.8']
|
||||
|
||||
|
@ -21,7 +23,7 @@ CONF_SERIAL = 'serial'
|
|||
|
||||
DEFAULT_NAME = 'Blinkstick'
|
||||
|
||||
SUPPORT_BLINKSTICK = SUPPORT_RGB_COLOR
|
||||
SUPPORT_BLINKSTICK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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)
|
||||
|
||||
add_devices([BlinkStickLight(stick, name)])
|
||||
add_devices([BlinkStickLight(stick, name)], True)
|
||||
|
||||
|
||||
class BlinkStickLight(Light):
|
||||
|
@ -50,7 +52,8 @@ class BlinkStickLight(Light):
|
|||
self._stick = stick
|
||||
self._name = name
|
||||
self._serial = stick.get_serial()
|
||||
self._rgb_color = stick.get_color()
|
||||
self._hs_color = None
|
||||
self._brightness = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -63,14 +66,19 @@ class BlinkStickLight(Light):
|
|||
return self._name
|
||||
|
||||
@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."""
|
||||
return self._rgb_color
|
||||
return self._hs_color
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Check whether any of the LEDs colors are non-zero."""
|
||||
return sum(self._rgb_color) > 0
|
||||
"""Return True if entity is on."""
|
||||
return self._brightness > 0
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
@ -79,18 +87,24 @@ class BlinkStickLight(Light):
|
|||
|
||||
def update(self):
|
||||
"""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):
|
||||
"""Turn the device on."""
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
self._rgb_color = kwargs[ATTR_RGB_COLOR]
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
self._hs_color = kwargs[ATTR_HS_COLOR]
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
else:
|
||||
self._rgb_color = [255, 255, 255]
|
||||
self._brightness = 255
|
||||
|
||||
self._stick.set_color(red=self._rgb_color[0],
|
||||
green=self._rgb_color[1],
|
||||
blue=self._rgb_color[2])
|
||||
rgb_color = color_util.color_hsv_to_RGB(
|
||||
self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100)
|
||||
self._stick.set_color(
|
||||
red=rgb_color[0], green=rgb_color[1], blue=rgb_color[2])
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue