mirror of
https://github.com/home-assistant/core
synced 2024-09-06 10:29:55 +02:00
commit
fb91b05051
25
.coveragerc
25
.coveragerc
@ -160,9 +160,6 @@ omit =
|
|||||||
homeassistant/components/maxcube.py
|
homeassistant/components/maxcube.py
|
||||||
homeassistant/components/*/maxcube.py
|
homeassistant/components/*/maxcube.py
|
||||||
|
|
||||||
homeassistant/components/mercedesme.py
|
|
||||||
homeassistant/components/*/mercedesme.py
|
|
||||||
|
|
||||||
homeassistant/components/mochad.py
|
homeassistant/components/mochad.py
|
||||||
homeassistant/components/*/mochad.py
|
homeassistant/components/*/mochad.py
|
||||||
|
|
||||||
@ -289,11 +286,9 @@ omit =
|
|||||||
homeassistant/components/*/wink.py
|
homeassistant/components/*/wink.py
|
||||||
|
|
||||||
homeassistant/components/xiaomi_aqara.py
|
homeassistant/components/xiaomi_aqara.py
|
||||||
homeassistant/components/binary_sensor/xiaomi_aqara.py
|
homeassistant/components/*/xiaomi_aqara.py
|
||||||
homeassistant/components/cover/xiaomi_aqara.py
|
|
||||||
homeassistant/components/light/xiaomi_aqara.py
|
homeassistant/components/*/xiaomi_miio.py
|
||||||
homeassistant/components/sensor/xiaomi_aqara.py
|
|
||||||
homeassistant/components/switch/xiaomi_aqara.py
|
|
||||||
|
|
||||||
homeassistant/components/zabbix.py
|
homeassistant/components/zabbix.py
|
||||||
homeassistant/components/*/zabbix.py
|
homeassistant/components/*/zabbix.py
|
||||||
@ -357,6 +352,7 @@ omit =
|
|||||||
homeassistant/components/climate/touchline.py
|
homeassistant/components/climate/touchline.py
|
||||||
homeassistant/components/climate/venstar.py
|
homeassistant/components/climate/venstar.py
|
||||||
homeassistant/components/cover/garadget.py
|
homeassistant/components/cover/garadget.py
|
||||||
|
homeassistant/components/cover/gogogate2.py
|
||||||
homeassistant/components/cover/homematic.py
|
homeassistant/components/cover/homematic.py
|
||||||
homeassistant/components/cover/knx.py
|
homeassistant/components/cover/knx.py
|
||||||
homeassistant/components/cover/myq.py
|
homeassistant/components/cover/myq.py
|
||||||
@ -374,6 +370,7 @@ omit =
|
|||||||
homeassistant/components/device_tracker/cisco_ios.py
|
homeassistant/components/device_tracker/cisco_ios.py
|
||||||
homeassistant/components/device_tracker/ddwrt.py
|
homeassistant/components/device_tracker/ddwrt.py
|
||||||
homeassistant/components/device_tracker/fritz.py
|
homeassistant/components/device_tracker/fritz.py
|
||||||
|
homeassistant/components/device_tracker/google_maps.py
|
||||||
homeassistant/components/device_tracker/gpslogger.py
|
homeassistant/components/device_tracker/gpslogger.py
|
||||||
homeassistant/components/device_tracker/hitron_coda.py
|
homeassistant/components/device_tracker/hitron_coda.py
|
||||||
homeassistant/components/device_tracker/huawei_router.py
|
homeassistant/components/device_tracker/huawei_router.py
|
||||||
@ -400,8 +397,8 @@ omit =
|
|||||||
homeassistant/components/emoncms_history.py
|
homeassistant/components/emoncms_history.py
|
||||||
homeassistant/components/emulated_hue/upnp.py
|
homeassistant/components/emulated_hue/upnp.py
|
||||||
homeassistant/components/fan/mqtt.py
|
homeassistant/components/fan/mqtt.py
|
||||||
homeassistant/components/fan/xiaomi_miio.py
|
|
||||||
homeassistant/components/feedreader.py
|
homeassistant/components/feedreader.py
|
||||||
|
homeassistant/components/folder_watcher.py
|
||||||
homeassistant/components/foursquare.py
|
homeassistant/components/foursquare.py
|
||||||
homeassistant/components/goalfeed.py
|
homeassistant/components/goalfeed.py
|
||||||
homeassistant/components/ifttt.py
|
homeassistant/components/ifttt.py
|
||||||
@ -424,6 +421,7 @@ omit =
|
|||||||
homeassistant/components/light/lifx.py
|
homeassistant/components/light/lifx.py
|
||||||
homeassistant/components/light/limitlessled.py
|
homeassistant/components/light/limitlessled.py
|
||||||
homeassistant/components/light/mystrom.py
|
homeassistant/components/light/mystrom.py
|
||||||
|
homeassistant/components/light/nanoleaf_aurora.py
|
||||||
homeassistant/components/light/osramlightify.py
|
homeassistant/components/light/osramlightify.py
|
||||||
homeassistant/components/light/piglow.py
|
homeassistant/components/light/piglow.py
|
||||||
homeassistant/components/light/rpi_gpio_pwm.py
|
homeassistant/components/light/rpi_gpio_pwm.py
|
||||||
@ -432,7 +430,6 @@ omit =
|
|||||||
homeassistant/components/light/tplink.py
|
homeassistant/components/light/tplink.py
|
||||||
homeassistant/components/light/tradfri.py
|
homeassistant/components/light/tradfri.py
|
||||||
homeassistant/components/light/x10.py
|
homeassistant/components/light/x10.py
|
||||||
homeassistant/components/light/xiaomi_miio.py
|
|
||||||
homeassistant/components/light/yeelight.py
|
homeassistant/components/light/yeelight.py
|
||||||
homeassistant/components/light/yeelightsunflower.py
|
homeassistant/components/light/yeelightsunflower.py
|
||||||
homeassistant/components/light/zengge.py
|
homeassistant/components/light/zengge.py
|
||||||
@ -441,6 +438,7 @@ omit =
|
|||||||
homeassistant/components/lock/nello.py
|
homeassistant/components/lock/nello.py
|
||||||
homeassistant/components/lock/nuki.py
|
homeassistant/components/lock/nuki.py
|
||||||
homeassistant/components/lock/sesame.py
|
homeassistant/components/lock/sesame.py
|
||||||
|
homeassistant/components/map.py
|
||||||
homeassistant/components/media_extractor.py
|
homeassistant/components/media_extractor.py
|
||||||
homeassistant/components/media_player/anthemav.py
|
homeassistant/components/media_player/anthemav.py
|
||||||
homeassistant/components/media_player/aquostv.py
|
homeassistant/components/media_player/aquostv.py
|
||||||
@ -508,6 +506,7 @@ omit =
|
|||||||
homeassistant/components/notify/kodi.py
|
homeassistant/components/notify/kodi.py
|
||||||
homeassistant/components/notify/lannouncer.py
|
homeassistant/components/notify/lannouncer.py
|
||||||
homeassistant/components/notify/llamalab_automate.py
|
homeassistant/components/notify/llamalab_automate.py
|
||||||
|
homeassistant/components/notify/mastodon.py
|
||||||
homeassistant/components/notify/matrix.py
|
homeassistant/components/notify/matrix.py
|
||||||
homeassistant/components/notify/message_bird.py
|
homeassistant/components/notify/message_bird.py
|
||||||
homeassistant/components/notify/mycroft.py
|
homeassistant/components/notify/mycroft.py
|
||||||
@ -523,8 +522,8 @@ omit =
|
|||||||
homeassistant/components/notify/sendgrid.py
|
homeassistant/components/notify/sendgrid.py
|
||||||
homeassistant/components/notify/simplepush.py
|
homeassistant/components/notify/simplepush.py
|
||||||
homeassistant/components/notify/slack.py
|
homeassistant/components/notify/slack.py
|
||||||
homeassistant/components/notify/stride.py
|
|
||||||
homeassistant/components/notify/smtp.py
|
homeassistant/components/notify/smtp.py
|
||||||
|
homeassistant/components/notify/stride.py
|
||||||
homeassistant/components/notify/synology_chat.py
|
homeassistant/components/notify/synology_chat.py
|
||||||
homeassistant/components/notify/syslog.py
|
homeassistant/components/notify/syslog.py
|
||||||
homeassistant/components/notify/telegram.py
|
homeassistant/components/notify/telegram.py
|
||||||
@ -538,7 +537,6 @@ omit =
|
|||||||
homeassistant/components/remember_the_milk/__init__.py
|
homeassistant/components/remember_the_milk/__init__.py
|
||||||
homeassistant/components/remote/harmony.py
|
homeassistant/components/remote/harmony.py
|
||||||
homeassistant/components/remote/itach.py
|
homeassistant/components/remote/itach.py
|
||||||
homeassistant/components/remote/xiaomi_miio.py
|
|
||||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||||
homeassistant/components/scene/lifx_cloud.py
|
homeassistant/components/scene/lifx_cloud.py
|
||||||
homeassistant/components/sensor/airvisual.py
|
homeassistant/components/sensor/airvisual.py
|
||||||
@ -674,6 +672,7 @@ omit =
|
|||||||
homeassistant/components/sensor/vasttrafik.py
|
homeassistant/components/sensor/vasttrafik.py
|
||||||
homeassistant/components/sensor/viaggiatreno.py
|
homeassistant/components/sensor/viaggiatreno.py
|
||||||
homeassistant/components/sensor/waqi.py
|
homeassistant/components/sensor/waqi.py
|
||||||
|
homeassistant/components/sensor/waze_travel_time.py
|
||||||
homeassistant/components/sensor/whois.py
|
homeassistant/components/sensor/whois.py
|
||||||
homeassistant/components/sensor/worldtidesinfo.py
|
homeassistant/components/sensor/worldtidesinfo.py
|
||||||
homeassistant/components/sensor/worxlandroid.py
|
homeassistant/components/sensor/worxlandroid.py
|
||||||
@ -707,7 +706,6 @@ omit =
|
|||||||
homeassistant/components/switch/tplink.py
|
homeassistant/components/switch/tplink.py
|
||||||
homeassistant/components/switch/transmission.py
|
homeassistant/components/switch/transmission.py
|
||||||
homeassistant/components/switch/vesync.py
|
homeassistant/components/switch/vesync.py
|
||||||
homeassistant/components/switch/xiaomi_miio.py
|
|
||||||
homeassistant/components/telegram_bot/*
|
homeassistant/components/telegram_bot/*
|
||||||
homeassistant/components/thingspeak.py
|
homeassistant/components/thingspeak.py
|
||||||
homeassistant/components/tts/amazon_polly.py
|
homeassistant/components/tts/amazon_polly.py
|
||||||
@ -716,7 +714,6 @@ omit =
|
|||||||
homeassistant/components/tts/picotts.py
|
homeassistant/components/tts/picotts.py
|
||||||
homeassistant/components/vacuum/mqtt.py
|
homeassistant/components/vacuum/mqtt.py
|
||||||
homeassistant/components/vacuum/roomba.py
|
homeassistant/components/vacuum/roomba.py
|
||||||
homeassistant/components/vacuum/xiaomi_miio.py
|
|
||||||
homeassistant/components/weather/bom.py
|
homeassistant/components/weather/bom.py
|
||||||
homeassistant/components/weather/buienradar.py
|
homeassistant/components/weather/buienradar.py
|
||||||
homeassistant/components/weather/darksky.py
|
homeassistant/components/weather/darksky.py
|
||||||
|
38
.github/ISSUE_TEMPLATE.md
vendored
38
.github/ISSUE_TEMPLATE.md
vendored
@ -1,35 +1,45 @@
|
|||||||
Make sure you are running the latest version of Home Assistant before reporting an issue.
|
<!-- READ THIS FIRST:
|
||||||
|
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||||
|
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||||
|
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
|
||||||
|
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||||
|
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
||||||
|
-->
|
||||||
|
|
||||||
You should only file an issue if you found a bug. Feature and enhancement requests should go in [the Feature Requests section](https://community.home-assistant.io/c/feature-requests) of our community forum:
|
**Home Assistant release with the issue:**
|
||||||
|
<!--
|
||||||
**Home Assistant release (`hass --version`):**
|
- Frontend -> Developer tools -> Info
|
||||||
|
- Or use this command: hass --version
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
**Python release (`python3 --version`):**
|
**Last working Home Assistant release (if known):**
|
||||||
|
|
||||||
|
|
||||||
|
**Operating environment (Hass.io/Docker/Windows/etc.):**
|
||||||
|
<!--
|
||||||
|
Please provide details about your environment.
|
||||||
|
-->
|
||||||
|
|
||||||
**Component/platform:**
|
**Component/platform:**
|
||||||
|
<!--
|
||||||
|
Please add the link to the documentation at https://www.home-assistant.io/components/ of the component/platform in question.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
**Description of problem:**
|
**Description of problem:**
|
||||||
|
|
||||||
|
|
||||||
**Expected:**
|
|
||||||
|
|
||||||
|
**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):**
|
||||||
**Problem-relevant `configuration.yaml` entries and steps to reproduce:**
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
|
|
||||||
**Traceback (if applicable):**
|
**Traceback (if applicable):**
|
||||||
```bash
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Additional info:**
|
**Additional information:**
|
||||||
|
|
||||||
|
20
CODEOWNERS
20
CODEOWNERS
@ -29,9 +29,6 @@ homeassistant/components/weblink.py @home-assistant/core
|
|||||||
homeassistant/components/websocket_api.py @home-assistant/core
|
homeassistant/components/websocket_api.py @home-assistant/core
|
||||||
homeassistant/components/zone.py @home-assistant/core
|
homeassistant/components/zone.py @home-assistant/core
|
||||||
|
|
||||||
# To monitor non-pypi additions
|
|
||||||
requirements_all.txt @andrey-git
|
|
||||||
|
|
||||||
# HomeAssistant developer Teams
|
# HomeAssistant developer Teams
|
||||||
Dockerfile @home-assistant/docker
|
Dockerfile @home-assistant/docker
|
||||||
virtualization/Docker/* @home-assistant/docker
|
virtualization/Docker/* @home-assistant/docker
|
||||||
@ -43,6 +40,7 @@ homeassistant/components/hassio.py @home-assistant/hassio
|
|||||||
|
|
||||||
# Individual components
|
# Individual components
|
||||||
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
||||||
|
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
|
||||||
homeassistant/components/binary_sensor/hikvision.py @mezz64
|
homeassistant/components/binary_sensor/hikvision.py @mezz64
|
||||||
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
|
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
|
||||||
homeassistant/components/camera/yi.py @bachya
|
homeassistant/components/camera/yi.py @bachya
|
||||||
@ -69,8 +67,10 @@ homeassistant/components/sensor/gearbest.py @HerrHofrat
|
|||||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||||
homeassistant/components/sensor/pollen.py @bachya
|
homeassistant/components/sensor/pollen.py @bachya
|
||||||
homeassistant/components/sensor/sytadin.py @gautric
|
homeassistant/components/sensor/qnap.py @colinodell
|
||||||
|
homeassistant/components/sensor/sma.py @kellerza
|
||||||
homeassistant/components/sensor/sql.py @dgomes
|
homeassistant/components/sensor/sql.py @dgomes
|
||||||
|
homeassistant/components/sensor/sytadin.py @gautric
|
||||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||||
homeassistant/components/sensor/waqi.py @andrey-git
|
homeassistant/components/sensor/waqi.py @andrey-git
|
||||||
homeassistant/components/switch/rainmachine.py @bachya
|
homeassistant/components/switch/rainmachine.py @bachya
|
||||||
@ -80,17 +80,17 @@ homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
|||||||
homeassistant/components/*/axis.py @kane610
|
homeassistant/components/*/axis.py @kane610
|
||||||
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
|
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
|
||||||
homeassistant/components/*/broadlink.py @danielhiversen
|
homeassistant/components/*/broadlink.py @danielhiversen
|
||||||
|
homeassistant/components/*/deconz.py @kane610
|
||||||
homeassistant/components/eight_sleep.py @mezz64
|
homeassistant/components/eight_sleep.py @mezz64
|
||||||
homeassistant/components/*/eight_sleep.py @mezz64
|
homeassistant/components/*/eight_sleep.py @mezz64
|
||||||
homeassistant/components/hive.py @Rendili @KJonline
|
homeassistant/components/hive.py @Rendili @KJonline
|
||||||
homeassistant/components/*/hive.py @Rendili @KJonline
|
homeassistant/components/*/hive.py @Rendili @KJonline
|
||||||
homeassistant/components/homekit/* @cdce8p
|
homeassistant/components/homekit/* @cdce8p
|
||||||
homeassistant/components/*/deconz.py @kane610
|
|
||||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
|
||||||
homeassistant/components/velux.py @Julius2342
|
|
||||||
homeassistant/components/*/velux.py @Julius2342
|
|
||||||
homeassistant/components/knx.py @Julius2342
|
homeassistant/components/knx.py @Julius2342
|
||||||
homeassistant/components/*/knx.py @Julius2342
|
homeassistant/components/*/knx.py @Julius2342
|
||||||
|
homeassistant/components/qwikswitch.py @kellerza
|
||||||
|
homeassistant/components/*/qwikswitch.py @kellerza
|
||||||
|
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||||
homeassistant/components/tahoma.py @philklei
|
homeassistant/components/tahoma.py @philklei
|
||||||
homeassistant/components/*/tahoma.py @philklei
|
homeassistant/components/*/tahoma.py @philklei
|
||||||
homeassistant/components/tesla.py @zabuldon
|
homeassistant/components/tesla.py @zabuldon
|
||||||
@ -98,5 +98,9 @@ homeassistant/components/*/tesla.py @zabuldon
|
|||||||
homeassistant/components/tellduslive.py @molobrakos @fredrike
|
homeassistant/components/tellduslive.py @molobrakos @fredrike
|
||||||
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
|
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
|
||||||
homeassistant/components/*/tradfri.py @ggravlingen
|
homeassistant/components/*/tradfri.py @ggravlingen
|
||||||
|
homeassistant/components/velux.py @Julius2342
|
||||||
|
homeassistant/components/*/velux.py @Julius2342
|
||||||
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
|
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
|
||||||
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
|
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
|
||||||
|
|
||||||
|
homeassistant/scripts/check_config.py @kellerza
|
||||||
|
@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
REQUIREMENTS = ['abodepy==0.12.2']
|
REQUIREMENTS = ['abodepy==0.12.3']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
|
|
||||||
class IFTTTAlarmPanel(alarm.AlarmControlPanel):
|
class IFTTTAlarmPanel(alarm.AlarmControlPanel):
|
||||||
"""Representation of an alarm control panel controlled throught IFTTT."""
|
"""Representation of an alarm control panel controlled through IFTTT."""
|
||||||
|
|
||||||
def __init__(self, name, code, event_away, event_home, event_night,
|
def __init__(self, name, code, event_away, event_home, event_night,
|
||||||
event_disarm, optimistic):
|
event_disarm, optimistic):
|
||||||
|
@ -18,7 +18,7 @@ from homeassistant.const import (
|
|||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||||
|
|
||||||
|
|
||||||
REQUIREMENTS = ['total_connect_client==0.16']
|
REQUIREMENTS = ['total_connect_client==0.17']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -6,18 +6,20 @@ from datetime import datetime
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
alert, automation, cover, fan, group, input_boolean, light, lock,
|
alert, automation, cover, climate, fan, group, input_boolean, light, lock,
|
||||||
media_player, scene, script, switch, http, sensor)
|
media_player, scene, script, switch, http, sensor)
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
import homeassistant.util.color as color_util
|
import homeassistant.util.color as color_util
|
||||||
|
from homeassistant.util.temperature import convert as convert_temperature
|
||||||
from homeassistant.util.decorator import Registry
|
from homeassistant.util.decorator import Registry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK,
|
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME,
|
||||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
|
||||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
||||||
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
|
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
|
||||||
|
|
||||||
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
|
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -34,6 +36,16 @@ API_TEMP_UNITS = {
|
|||||||
TEMP_CELSIUS: 'CELSIUS',
|
TEMP_CELSIUS: 'CELSIUS',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
API_THERMOSTAT_MODES = {
|
||||||
|
climate.STATE_HEAT: 'HEAT',
|
||||||
|
climate.STATE_COOL: 'COOL',
|
||||||
|
climate.STATE_AUTO: 'AUTO',
|
||||||
|
climate.STATE_ECO: 'ECO',
|
||||||
|
climate.STATE_IDLE: 'OFF',
|
||||||
|
climate.STATE_FAN_ONLY: 'OFF',
|
||||||
|
climate.STATE_DRY: 'OFF',
|
||||||
|
}
|
||||||
|
|
||||||
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
|
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
|
||||||
|
|
||||||
CONF_DESCRIPTION = 'description'
|
CONF_DESCRIPTION = 'description'
|
||||||
@ -383,8 +395,60 @@ class _AlexaTemperatureSensor(_AlexaInterface):
|
|||||||
raise _UnsupportedProperty(name)
|
raise _UnsupportedProperty(name)
|
||||||
|
|
||||||
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||||
|
temp = self.entity.state
|
||||||
|
if self.entity.domain == climate.DOMAIN:
|
||||||
|
temp = self.entity.attributes.get(
|
||||||
|
climate.ATTR_CURRENT_TEMPERATURE)
|
||||||
return {
|
return {
|
||||||
'value': float(self.entity.state),
|
'value': float(temp),
|
||||||
|
'scale': API_TEMP_UNITS[unit],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _AlexaThermostatController(_AlexaInterface):
|
||||||
|
def name(self):
|
||||||
|
return 'Alexa.ThermostatController'
|
||||||
|
|
||||||
|
def properties_supported(self):
|
||||||
|
properties = []
|
||||||
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
if supported & climate.SUPPORT_TARGET_TEMPERATURE:
|
||||||
|
properties.append({'name': 'targetSetpoint'})
|
||||||
|
if supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW:
|
||||||
|
properties.append({'name': 'lowerSetpoint'})
|
||||||
|
if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH:
|
||||||
|
properties.append({'name': 'upperSetpoint'})
|
||||||
|
if supported & climate.SUPPORT_OPERATION_MODE:
|
||||||
|
properties.append({'name': 'thermostatMode'})
|
||||||
|
return properties
|
||||||
|
|
||||||
|
def properties_retrievable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_property(self, name):
|
||||||
|
if name == 'thermostatMode':
|
||||||
|
ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE)
|
||||||
|
mode = API_THERMOSTAT_MODES.get(ha_mode)
|
||||||
|
if mode is None:
|
||||||
|
_LOGGER.error("%s (%s) has unsupported %s value '%s'",
|
||||||
|
self.entity.entity_id, type(self.entity),
|
||||||
|
climate.ATTR_OPERATION_MODE, ha_mode)
|
||||||
|
raise _UnsupportedProperty(name)
|
||||||
|
return mode
|
||||||
|
|
||||||
|
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||||
|
temp = None
|
||||||
|
if name == 'targetSetpoint':
|
||||||
|
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
|
||||||
|
elif name == 'lowerSetpoint':
|
||||||
|
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
|
||||||
|
elif name == 'upperSetpoint':
|
||||||
|
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
|
||||||
|
if temp is None:
|
||||||
|
raise _UnsupportedProperty(name)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'value': float(temp),
|
||||||
'scale': API_TEMP_UNITS[unit],
|
'scale': API_TEMP_UNITS[unit],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,6 +479,16 @@ class _SwitchCapabilities(_AlexaEntity):
|
|||||||
return [_AlexaPowerController(self.entity)]
|
return [_AlexaPowerController(self.entity)]
|
||||||
|
|
||||||
|
|
||||||
|
@ENTITY_ADAPTERS.register(climate.DOMAIN)
|
||||||
|
class _ClimateCapabilities(_AlexaEntity):
|
||||||
|
def default_display_categories(self):
|
||||||
|
return [_DisplayCategory.THERMOSTAT]
|
||||||
|
|
||||||
|
def interfaces(self):
|
||||||
|
yield _AlexaThermostatController(self.entity)
|
||||||
|
yield _AlexaTemperatureSensor(self.entity)
|
||||||
|
|
||||||
|
|
||||||
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
||||||
class _CoverCapabilities(_AlexaEntity):
|
class _CoverCapabilities(_AlexaEntity):
|
||||||
def default_display_categories(self):
|
def default_display_categories(self):
|
||||||
@ -682,17 +756,26 @@ def api_message(request,
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
|
def api_error(request,
|
||||||
|
namespace='Alexa',
|
||||||
|
error_type='INTERNAL_ERROR',
|
||||||
|
error_message="",
|
||||||
|
payload=None):
|
||||||
"""Create a API formatted error response.
|
"""Create a API formatted error response.
|
||||||
|
|
||||||
Async friendly.
|
Async friendly.
|
||||||
"""
|
"""
|
||||||
payload = {
|
payload = payload or {}
|
||||||
'type': error_type,
|
payload['type'] = error_type
|
||||||
'message': error_message,
|
payload['message'] = error_message
|
||||||
}
|
|
||||||
|
|
||||||
return api_message(request, name='ErrorResponse', payload=payload)
|
_LOGGER.info("Request %s/%s error %s: %s",
|
||||||
|
request[API_HEADER]['namespace'],
|
||||||
|
request[API_HEADER]['name'],
|
||||||
|
error_type, error_message)
|
||||||
|
|
||||||
|
return api_message(
|
||||||
|
request, name='ErrorResponse', namespace=namespace, payload=payload)
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
|
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
|
||||||
@ -1104,7 +1187,6 @@ def async_api_select_input(hass, config, request, entity):
|
|||||||
else:
|
else:
|
||||||
msg = 'failed to map input {} to a media source on {}'.format(
|
msg = 'failed to map input {} to a media source on {}'.format(
|
||||||
media_input, entity.entity_id)
|
media_input, entity.entity_id)
|
||||||
_LOGGER.error(msg)
|
|
||||||
return api_error(
|
return api_error(
|
||||||
request, error_type='INVALID_VALUE', error_message=msg)
|
request, error_type='INVALID_VALUE', error_message=msg)
|
||||||
|
|
||||||
@ -1276,6 +1358,149 @@ def async_api_previous(hass, config, request, entity):
|
|||||||
return api_message(request)
|
return api_message(request)
|
||||||
|
|
||||||
|
|
||||||
|
def api_error_temp_range(request, temp, min_temp, max_temp, unit):
|
||||||
|
"""Create temperature value out of range API error response.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
|
temp_range = {
|
||||||
|
'minimumValue': {
|
||||||
|
'value': min_temp,
|
||||||
|
'scale': API_TEMP_UNITS[unit],
|
||||||
|
},
|
||||||
|
'maximumValue': {
|
||||||
|
'value': max_temp,
|
||||||
|
'scale': API_TEMP_UNITS[unit],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = 'The requested temperature {} is out of range'.format(temp)
|
||||||
|
return api_error(
|
||||||
|
request,
|
||||||
|
error_type='TEMPERATURE_VALUE_OUT_OF_RANGE',
|
||||||
|
error_message=msg,
|
||||||
|
payload={'validRange': temp_range},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def temperature_from_object(temp_obj, to_unit, interval=False):
|
||||||
|
"""Get temperature from Temperature object in requested unit."""
|
||||||
|
from_unit = TEMP_CELSIUS
|
||||||
|
temp = float(temp_obj['value'])
|
||||||
|
|
||||||
|
if temp_obj['scale'] == 'FAHRENHEIT':
|
||||||
|
from_unit = TEMP_FAHRENHEIT
|
||||||
|
elif temp_obj['scale'] == 'KELVIN':
|
||||||
|
# convert to Celsius if absolute temperature
|
||||||
|
if not interval:
|
||||||
|
temp -= 273.15
|
||||||
|
|
||||||
|
return convert_temperature(temp, from_unit, to_unit, interval)
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
|
||||||
|
@extract_entity
|
||||||
|
async def async_api_set_target_temp(hass, config, request, entity):
|
||||||
|
"""Process a set target temperature request."""
|
||||||
|
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||||
|
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||||
|
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
ATTR_ENTITY_ID: entity.entity_id
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = request[API_PAYLOAD]
|
||||||
|
if 'targetSetpoint' in payload:
|
||||||
|
temp = temperature_from_object(
|
||||||
|
payload['targetSetpoint'], unit)
|
||||||
|
if temp < min_temp or temp > max_temp:
|
||||||
|
return api_error_temp_range(
|
||||||
|
request, temp, min_temp, max_temp, unit)
|
||||||
|
data[ATTR_TEMPERATURE] = temp
|
||||||
|
if 'lowerSetpoint' in payload:
|
||||||
|
temp_low = temperature_from_object(
|
||||||
|
payload['lowerSetpoint'], unit)
|
||||||
|
if temp_low < min_temp or temp_low > max_temp:
|
||||||
|
return api_error_temp_range(
|
||||||
|
request, temp_low, min_temp, max_temp, unit)
|
||||||
|
data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
|
||||||
|
if 'upperSetpoint' in payload:
|
||||||
|
temp_high = temperature_from_object(
|
||||||
|
payload['upperSetpoint'], unit)
|
||||||
|
if temp_high < min_temp or temp_high > max_temp:
|
||||||
|
return api_error_temp_range(
|
||||||
|
request, temp_high, min_temp, max_temp, unit)
|
||||||
|
data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
|
||||||
|
|
||||||
|
return api_message(request)
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
|
||||||
|
@extract_entity
|
||||||
|
async def async_api_adjust_target_temp(hass, config, request, entity):
|
||||||
|
"""Process an adjust target temperature request."""
|
||||||
|
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||||
|
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||||
|
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||||
|
|
||||||
|
temp_delta = temperature_from_object(
|
||||||
|
request[API_PAYLOAD]['targetSetpointDelta'], unit, interval=True)
|
||||||
|
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
|
||||||
|
|
||||||
|
if target_temp < min_temp or target_temp > max_temp:
|
||||||
|
return api_error_temp_range(
|
||||||
|
request, target_temp, min_temp, max_temp, unit)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
ATTR_ENTITY_ID: entity.entity_id,
|
||||||
|
ATTR_TEMPERATURE: target_temp,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
|
||||||
|
|
||||||
|
return api_message(request)
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
|
||||||
|
@extract_entity
|
||||||
|
async def async_api_set_thermostat_mode(hass, config, request, entity):
|
||||||
|
"""Process a set thermostat mode request."""
|
||||||
|
mode = request[API_PAYLOAD]['thermostatMode']
|
||||||
|
|
||||||
|
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
|
||||||
|
# Work around a pylint false positive due to
|
||||||
|
# https://github.com/PyCQA/pylint/issues/1830
|
||||||
|
# pylint: disable=stop-iteration-return
|
||||||
|
ha_mode = next(
|
||||||
|
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if ha_mode not in operation_list:
|
||||||
|
msg = 'The requested thermostat mode {} is not supported'.format(mode)
|
||||||
|
return api_error(
|
||||||
|
request,
|
||||||
|
namespace='Alexa.ThermostatController',
|
||||||
|
error_type='UNSUPPORTED_THERMOSTAT_MODE',
|
||||||
|
error_message=msg
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
ATTR_ENTITY_ID: entity.entity_id,
|
||||||
|
climate.ATTR_OPERATION_MODE: ha_mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
entity.domain, climate.SERVICE_SET_OPERATION_MODE, data,
|
||||||
|
blocking=False)
|
||||||
|
|
||||||
|
return api_message(request)
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register(('Alexa', 'ReportState'))
|
@HANDLERS.register(('Alexa', 'ReportState'))
|
||||||
@extract_entity
|
@extract_entity
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -10,14 +10,15 @@ from datetime import timedelta
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from requests.exceptions import HTTPError, ConnectTimeout
|
from requests.exceptions import HTTPError, ConnectTimeout
|
||||||
|
from requests.exceptions import ConnectionError as ConnectError
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||||
CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
|
CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['amcrest==1.2.1']
|
REQUIREMENTS = ['amcrest==1.2.2']
|
||||||
DEPENDENCIES = ['ffmpeg']
|
DEPENDENCIES = ['ffmpeg']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -63,6 +64,12 @@ SENSORS = {
|
|||||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Switch types are defined like: Name, icon
|
||||||
|
SWITCHES = {
|
||||||
|
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
|
||||||
|
'motion_recording': ['Motion Recording', 'mdi:record-rec']
|
||||||
|
}
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
@ -81,6 +88,8 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
cv.time_period,
|
cv.time_period,
|
||||||
vol.Optional(CONF_SENSORS):
|
vol.Optional(CONF_SENSORS):
|
||||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||||
|
vol.Optional(CONF_SWITCHES):
|
||||||
|
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||||
})])
|
})])
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
@ -93,14 +102,15 @@ def setup(hass, config):
|
|||||||
amcrest_cams = config[DOMAIN]
|
amcrest_cams = config[DOMAIN]
|
||||||
|
|
||||||
for device in amcrest_cams:
|
for device in amcrest_cams:
|
||||||
|
try:
|
||||||
camera = AmcrestCamera(device.get(CONF_HOST),
|
camera = AmcrestCamera(device.get(CONF_HOST),
|
||||||
device.get(CONF_PORT),
|
device.get(CONF_PORT),
|
||||||
device.get(CONF_USERNAME),
|
device.get(CONF_USERNAME),
|
||||||
device.get(CONF_PASSWORD)).camera
|
device.get(CONF_PASSWORD)).camera
|
||||||
try:
|
# pylint: disable=pointless-statement
|
||||||
camera.current_time
|
camera.current_time
|
||||||
|
|
||||||
except (ConnectTimeout, HTTPError) as ex:
|
except (ConnectError, ConnectTimeout, HTTPError) as ex:
|
||||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||||
hass.components.persistent_notification.create(
|
hass.components.persistent_notification.create(
|
||||||
'Error: {}<br />'
|
'Error: {}<br />'
|
||||||
@ -108,12 +118,13 @@ def setup(hass, config):
|
|||||||
''.format(ex),
|
''.format(ex),
|
||||||
title=NOTIFICATION_TITLE,
|
title=NOTIFICATION_TITLE,
|
||||||
notification_id=NOTIFICATION_ID)
|
notification_id=NOTIFICATION_ID)
|
||||||
return False
|
continue
|
||||||
|
|
||||||
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
|
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
|
||||||
name = device.get(CONF_NAME)
|
name = device.get(CONF_NAME)
|
||||||
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
|
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
|
||||||
sensors = device.get(CONF_SENSORS)
|
sensors = device.get(CONF_SENSORS)
|
||||||
|
switches = device.get(CONF_SWITCHES)
|
||||||
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
|
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
|
||||||
|
|
||||||
username = device.get(CONF_USERNAME)
|
username = device.get(CONF_USERNAME)
|
||||||
@ -143,6 +154,13 @@ def setup(hass, config):
|
|||||||
CONF_SENSORS: sensors,
|
CONF_SENSORS: sensors,
|
||||||
}, config)
|
}, config)
|
||||||
|
|
||||||
|
if switches:
|
||||||
|
discovery.load_platform(
|
||||||
|
hass, 'switch', DOMAIN, {
|
||||||
|
CONF_NAME: name,
|
||||||
|
CONF_SWITCHES: switches
|
||||||
|
}, config)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,9 +52,8 @@ def setup(hass, config):
|
|||||||
hass.http.register_view(APIComponentsView)
|
hass.http.register_view(APIComponentsView)
|
||||||
hass.http.register_view(APITemplateView)
|
hass.http.register_view(APITemplateView)
|
||||||
|
|
||||||
log_path = hass.data.get(DATA_LOGGING, None)
|
if DATA_LOGGING in hass.data:
|
||||||
if log_path:
|
hass.http.register_view(APIErrorLog)
|
||||||
hass.http.register_static_path(URL_API_ERROR_LOG, log_path, False)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -356,6 +355,17 @@ class APITemplateView(HomeAssistantView):
|
|||||||
HTTP_BAD_REQUEST)
|
HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class APIErrorLog(HomeAssistantView):
|
||||||
|
"""View to fetch the error log."""
|
||||||
|
|
||||||
|
url = URL_API_ERROR_LOG
|
||||||
|
name = "api:error_log"
|
||||||
|
|
||||||
|
async def get(self, request):
|
||||||
|
"""Retrieve API error log."""
|
||||||
|
return await self.file(request, request.app['hass'].data[DATA_LOGGING])
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_services_json(hass):
|
def async_services_json(hass):
|
||||||
"""Generate services data to JSONify."""
|
"""Generate services data to JSONify."""
|
||||||
|
@ -7,8 +7,8 @@ https://home-assistant.io/components/binary_sensor.bmw_connected_drive/
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
|
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
|
||||||
|
|
||||||
DEPENDENCIES = ['bmw_connected_drive']
|
DEPENDENCIES = ['bmw_connected_drive']
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
|||||||
self._account = account
|
self._account = account
|
||||||
self._vehicle = vehicle
|
self._vehicle = vehicle
|
||||||
self._attribute = attribute
|
self._attribute = attribute
|
||||||
self._name = '{} {}'.format(self._vehicle.modelName, self._attribute)
|
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
|
||||||
self._sensor_name = sensor_name
|
self._sensor_name = sensor_name
|
||||||
self._device_class = device_class
|
self._device_class = device_class
|
||||||
self._state = None
|
self._state = None
|
||||||
@ -75,7 +75,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
|||||||
"""Return the state attributes of the binary sensor."""
|
"""Return the state attributes of the binary sensor."""
|
||||||
vehicle_state = self._vehicle.state
|
vehicle_state = self._vehicle.state
|
||||||
result = {
|
result = {
|
||||||
'car': self._vehicle.modelName
|
'car': self._vehicle.name
|
||||||
}
|
}
|
||||||
|
|
||||||
if self._attribute == 'lids':
|
if self._attribute == 'lids':
|
||||||
@ -91,6 +91,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
|||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Read new state data from the library."""
|
"""Read new state data from the library."""
|
||||||
|
from bimmer_connected.state import LockState
|
||||||
vehicle_state = self._vehicle.state
|
vehicle_state = self._vehicle.state
|
||||||
|
|
||||||
# device class opening: On means open, Off means closed
|
# device class opening: On means open, Off means closed
|
||||||
@ -101,9 +102,9 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
|||||||
self._state = not vehicle_state.all_windows_closed
|
self._state = not vehicle_state.all_windows_closed
|
||||||
# device class safety: On means unsafe, Off means safe
|
# device class safety: On means unsafe, Off means safe
|
||||||
if self._attribute == 'door_lock_state':
|
if self._attribute == 'door_lock_state':
|
||||||
# Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED
|
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
||||||
self._state = bool(vehicle_state.door_lock_state.value
|
self._state = vehicle_state.door_lock_state not in \
|
||||||
in ('SELECTIVELOCKED', 'UNLOCKED'))
|
[LockState.LOCKED, LockState.SECURED]
|
||||||
|
|
||||||
def update_callback(self):
|
def update_callback(self):
|
||||||
"""Schedule a state update."""
|
"""Schedule a state update."""
|
||||||
|
@ -1,97 +0,0 @@
|
|||||||
"""
|
|
||||||
Support for Mercedes cars with Mercedes ME.
|
|
||||||
|
|
||||||
For more details about this component, please refer to the documentation at
|
|
||||||
https://home-assistant.io/components/binary_sensor.mercedesme/
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
|
||||||
from homeassistant.components.mercedesme import (
|
|
||||||
DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, BINARY_SENSORS)
|
|
||||||
|
|
||||||
DEPENDENCIES = ['mercedesme']
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
|
||||||
"""Setup the sensor platform."""
|
|
||||||
data = hass.data[DATA_MME].data
|
|
||||||
|
|
||||||
if not data.cars:
|
|
||||||
_LOGGER.error("No cars found. Check component log.")
|
|
||||||
return
|
|
||||||
|
|
||||||
devices = []
|
|
||||||
for car in data.cars:
|
|
||||||
for key, value in sorted(BINARY_SENSORS.items()):
|
|
||||||
if car['availabilities'].get(key, 'INVALID') == 'VALID':
|
|
||||||
devices.append(MercedesMEBinarySensor(
|
|
||||||
data, key, value[0], car["vin"], None))
|
|
||||||
else:
|
|
||||||
_LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"])
|
|
||||||
|
|
||||||
add_devices(devices, True)
|
|
||||||
|
|
||||||
|
|
||||||
class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice):
|
|
||||||
"""Representation of a Sensor."""
|
|
||||||
|
|
||||||
@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."""
|
|
||||||
if self._internal_name == "windowsClosed":
|
|
||||||
return {
|
|
||||||
"window_front_left": self._car["windowStatusFrontLeft"],
|
|
||||||
"window_front_right": self._car["windowStatusFrontRight"],
|
|
||||||
"window_rear_left": self._car["windowStatusRearLeft"],
|
|
||||||
"window_rear_right": self._car["windowStatusRearRight"],
|
|
||||||
"original_value": self._car[self._internal_name],
|
|
||||||
"last_update": datetime.datetime.fromtimestamp(
|
|
||||||
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
|
|
||||||
"car": self._car["license"]
|
|
||||||
}
|
|
||||||
elif self._internal_name == "tireWarningLight":
|
|
||||||
return {
|
|
||||||
"front_right_tire_pressure_kpa":
|
|
||||||
self._car["frontRightTirePressureKpa"],
|
|
||||||
"front_left_tire_pressure_kpa":
|
|
||||||
self._car["frontLeftTirePressureKpa"],
|
|
||||||
"rear_right_tire_pressure_kpa":
|
|
||||||
self._car["rearRightTirePressureKpa"],
|
|
||||||
"rear_left_tire_pressure_kpa":
|
|
||||||
self._car["rearLeftTirePressureKpa"],
|
|
||||||
"original_value": self._car[self._internal_name],
|
|
||||||
"last_update": datetime.datetime.fromtimestamp(
|
|
||||||
self._car["lastUpdate"]
|
|
||||||
).strftime('%Y-%m-%d %H:%M:%S'),
|
|
||||||
"car": self._car["license"],
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"original_value": self._car[self._internal_name],
|
|
||||||
"last_update": datetime.datetime.fromtimestamp(
|
|
||||||
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
|
|
||||||
"car": self._car["license"]
|
|
||||||
}
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""Fetch new state data for the sensor."""
|
|
||||||
self._car = next(
|
|
||||||
car for car in self._data.cars if car["vin"] == self._vin)
|
|
||||||
|
|
||||||
if self._internal_name == "windowsClosed":
|
|
||||||
self._state = bool(self._car[self._internal_name] == "CLOSED")
|
|
||||||
elif self._internal_name == "tireWarningLight":
|
|
||||||
self._state = bool(self._car[self._internal_name] != "INACTIVE")
|
|
||||||
else:
|
|
||||||
self._state = self._car[self._internal_name] is True
|
|
||||||
|
|
||||||
_LOGGER.debug("Updated %s Value: %s IsOn: %s",
|
|
||||||
self._internal_name, self._state, self.is_on)
|
|
@ -21,11 +21,12 @@ SENSORS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
async def async_setup_platform(
|
||||||
"""Set up the MySensors platform for binary sensors."""
|
hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Set up the mysensors platform for binary sensors."""
|
||||||
mysensors.setup_mysensors_platform(
|
mysensors.setup_mysensors_platform(
|
||||||
hass, DOMAIN, discovery_info, MySensorsBinarySensor,
|
hass, DOMAIN, discovery_info, MySensorsBinarySensor,
|
||||||
add_devices=add_devices)
|
async_add_devices=async_add_devices)
|
||||||
|
|
||||||
|
|
||||||
class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
|
class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):
|
||||||
|
@ -30,8 +30,8 @@ ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada',
|
|||||||
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
|
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
|
||||||
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
|
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
|
||||||
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
|
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
|
||||||
'Sweden', 'SE', 'UnitedKingdom', 'UK', 'UnitedStates', 'US',
|
'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK',
|
||||||
'Wales']
|
'UnitedStates', 'US', 'Wales']
|
||||||
CONF_COUNTRY = 'country'
|
CONF_COUNTRY = 'country'
|
||||||
CONF_PROVINCE = 'province'
|
CONF_PROVINCE = 'province'
|
||||||
CONF_WORKDAYS = 'workdays'
|
CONF_WORKDAYS = 'workdays'
|
||||||
@ -47,13 +47,13 @@ DEFAULT_OFFSET = 0
|
|||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
|
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
|
||||||
vol.Optional(CONF_PROVINCE): cv.string,
|
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
|
||||||
|
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int),
|
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int),
|
||||||
|
vol.Optional(CONF_PROVINCE): cv.string,
|
||||||
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
|
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
|
||||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||||
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
|
|
||||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -74,14 +74,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if province:
|
if province:
|
||||||
# 'state' and 'prov' are not interchangeable, so need to make
|
# 'state' and 'prov' are not interchangeable, so need to make
|
||||||
# sure we use the right one
|
# sure we use the right one
|
||||||
if (hasattr(obj_holidays, "PROVINCES") and
|
if (hasattr(obj_holidays, 'PROVINCES') and
|
||||||
province in obj_holidays.PROVINCES):
|
province in obj_holidays.PROVINCES):
|
||||||
obj_holidays = getattr(holidays, country)(prov=province,
|
obj_holidays = getattr(holidays, country)(
|
||||||
years=year)
|
prov=province, years=year)
|
||||||
elif (hasattr(obj_holidays, "STATES") and
|
elif (hasattr(obj_holidays, 'STATES') and
|
||||||
province in obj_holidays.STATES):
|
province in obj_holidays.STATES):
|
||||||
obj_holidays = getattr(holidays, country)(state=province,
|
obj_holidays = getattr(holidays, country)(
|
||||||
years=year)
|
state=province, years=year)
|
||||||
else:
|
else:
|
||||||
_LOGGER.error("There is no province/state %s in country %s",
|
_LOGGER.error("There is no province/state %s in country %s",
|
||||||
province, country)
|
province, country)
|
||||||
|
@ -4,30 +4,29 @@ Reads vehicle status from BMW connected drive portal.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/bmw_connected_drive/
|
https://home-assistant.io/components/bmw_connected_drive/
|
||||||
"""
|
"""
|
||||||
import logging
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.helpers.event import track_utc_time_change
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.const import (
|
|
||||||
CONF_USERNAME, CONF_PASSWORD
|
|
||||||
)
|
|
||||||
|
|
||||||
REQUIREMENTS = ['bimmer_connected==0.4.1']
|
REQUIREMENTS = ['bimmer_connected==0.5.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'bmw_connected_drive'
|
DOMAIN = 'bmw_connected_drive'
|
||||||
CONF_VALUES = 'values'
|
CONF_REGION = 'region'
|
||||||
CONF_COUNTRY = 'country'
|
|
||||||
|
|
||||||
ACCOUNT_SCHEMA = vol.Schema({
|
ACCOUNT_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Required(CONF_COUNTRY): cv.string,
|
vol.Required(CONF_REGION): vol.Any('north_america', 'china',
|
||||||
|
'rest_of_world'),
|
||||||
})
|
})
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
@ -47,9 +46,9 @@ def setup(hass, config):
|
|||||||
for name, account_config in config[DOMAIN].items():
|
for name, account_config in config[DOMAIN].items():
|
||||||
username = account_config[CONF_USERNAME]
|
username = account_config[CONF_USERNAME]
|
||||||
password = account_config[CONF_PASSWORD]
|
password = account_config[CONF_PASSWORD]
|
||||||
country = account_config[CONF_COUNTRY]
|
region = account_config[CONF_REGION]
|
||||||
_LOGGER.debug('Adding new account %s', name)
|
_LOGGER.debug('Adding new account %s', name)
|
||||||
bimmer = BMWConnectedDriveAccount(username, password, country, name)
|
bimmer = BMWConnectedDriveAccount(username, password, region, name)
|
||||||
accounts.append(bimmer)
|
accounts.append(bimmer)
|
||||||
|
|
||||||
# update every UPDATE_INTERVAL minutes, starting now
|
# update every UPDATE_INTERVAL minutes, starting now
|
||||||
@ -75,12 +74,15 @@ def setup(hass, config):
|
|||||||
class BMWConnectedDriveAccount(object):
|
class BMWConnectedDriveAccount(object):
|
||||||
"""Representation of a BMW vehicle."""
|
"""Representation of a BMW vehicle."""
|
||||||
|
|
||||||
def __init__(self, username: str, password: str, country: str,
|
def __init__(self, username: str, password: str, region_str: str,
|
||||||
name: str) -> None:
|
name: str) -> None:
|
||||||
"""Constructor."""
|
"""Constructor."""
|
||||||
from bimmer_connected.account import ConnectedDriveAccount
|
from bimmer_connected.account import ConnectedDriveAccount
|
||||||
|
from bimmer_connected.country_selector import get_region_from_name
|
||||||
|
|
||||||
self.account = ConnectedDriveAccount(username, password, country)
|
region = get_region_from_name(region_str)
|
||||||
|
|
||||||
|
self.account = ConnectedDriveAccount(username, password, region)
|
||||||
self.name = name
|
self.name = name
|
||||||
self._update_listeners = []
|
self._update_listeners = []
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_TOPIC = 'topic'
|
CONF_TOPIC = 'topic'
|
||||||
|
|
||||||
DEFAULT_NAME = 'MQTT Camera'
|
DEFAULT_NAME = 'MQTT Camera'
|
||||||
|
|
||||||
DEPENDENCIES = ['mqtt']
|
DEPENDENCIES = ['mqtt']
|
||||||
@ -33,9 +32,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Set up the MQTT Camera."""
|
"""Set up the MQTT Camera."""
|
||||||
topic = config[CONF_TOPIC]
|
if discovery_info is not None:
|
||||||
|
config = PLATFORM_SCHEMA(discovery_info)
|
||||||
|
|
||||||
async_add_devices([MqttCamera(config[CONF_NAME], topic)])
|
async_add_devices([MqttCamera(
|
||||||
|
config.get(CONF_NAME),
|
||||||
|
config.get(CONF_TOPIC)
|
||||||
|
)])
|
||||||
|
|
||||||
|
|
||||||
class MqttCamera(Camera):
|
class MqttCamera(Camera):
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
|
|||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
REQUIREMENTS = ['py-canary==0.4.1']
|
REQUIREMENTS = ['py-canary==0.5.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -31,10 +31,12 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
|
|||||||
SUPPORT_OPERATION_MODE)
|
SUPPORT_OPERATION_MODE)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
async def async_setup_platform(
|
||||||
"""Set up the MySensors climate."""
|
hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Set up the mysensors climate."""
|
||||||
mysensors.setup_mysensors_platform(
|
mysensors.setup_mysensors_platform(
|
||||||
hass, DOMAIN, discovery_info, MySensorsHVAC, add_devices=add_devices)
|
hass, DOMAIN, discovery_info, MySensorsHVAC,
|
||||||
|
async_add_devices=async_add_devices)
|
||||||
|
|
||||||
|
|
||||||
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||||
@ -163,8 +165,8 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
|||||||
self._values[self.value_type] = operation_mode
|
self._values[self.value_type] = operation_mode
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def update(self):
|
async def async_update(self):
|
||||||
"""Update the controller with the latest value from a sensor."""
|
"""Update the controller with the latest value from a sensor."""
|
||||||
super().update()
|
await super().async_update()
|
||||||
self._values[self.value_type] = DICT_MYS_TO_HA[
|
self._values[self.value_type] = DICT_MYS_TO_HA[
|
||||||
self._values[self.value_type]]
|
self._values[self.value_type]]
|
||||||
|
@ -179,7 +179,7 @@ class NestThermostat(ClimateDevice):
|
|||||||
try:
|
try:
|
||||||
self.device.target = temp
|
self.device.target = temp
|
||||||
except nest.nest.APIError:
|
except nest.nest.APIError:
|
||||||
_LOGGER.error("An error occured while setting the temperature")
|
_LOGGER.error("An error occurred while setting the temperature")
|
||||||
|
|
||||||
def set_operation_mode(self, operation_mode):
|
def set_operation_mode(self, operation_mode):
|
||||||
"""Set operation mode."""
|
"""Set operation mode."""
|
||||||
|
@ -14,24 +14,16 @@ from homeassistant.util.yaml import load_yaml, dump
|
|||||||
DOMAIN = 'config'
|
DOMAIN = 'config'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
|
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
|
||||||
'entity_registry')
|
'entity_registry', 'config_entries')
|
||||||
ON_DEMAND = ('zwave',)
|
ON_DEMAND = ('zwave',)
|
||||||
FEATURE_FLAGS = ('config_entries',)
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup(hass, config):
|
def async_setup(hass, config):
|
||||||
"""Set up the config component."""
|
"""Set up the config component."""
|
||||||
global SECTIONS
|
|
||||||
|
|
||||||
yield from hass.components.frontend.async_register_built_in_panel(
|
yield from hass.components.frontend.async_register_built_in_panel(
|
||||||
'config', 'config', 'mdi:settings')
|
'config', 'config', 'mdi:settings')
|
||||||
|
|
||||||
# Temporary way of allowing people to opt-in for unreleased config sections
|
|
||||||
for key, value in config.get(DOMAIN, {}).items():
|
|
||||||
if key in FEATURE_FLAGS and value:
|
|
||||||
SECTIONS += (key,)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def setup_panel(panel_name):
|
def setup_panel(panel_name):
|
||||||
"""Set up a panel."""
|
"""Set up a panel."""
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"step": {
|
|
||||||
"name": {
|
|
||||||
"data": {
|
|
||||||
"name": "Nimi"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"step": {
|
|
||||||
"init": {
|
|
||||||
"description": "Introduce\u021bi un obiect_id pentru entitatea testat\u0103.",
|
|
||||||
"title": "Alege\u021bi id-ul obiectului"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"data": {
|
|
||||||
"name": "Nume"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
"""Example component to show how config entries work."""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant import config_entries
|
|
||||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
|
||||||
from homeassistant.util import slugify
|
|
||||||
|
|
||||||
|
|
||||||
DOMAIN = 'config_entry_example'
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def async_setup(hass, config):
|
|
||||||
"""Setup for our example component."""
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def async_setup_entry(hass, entry):
|
|
||||||
"""Initialize an entry."""
|
|
||||||
entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
|
|
||||||
hass.states.async_set(entity_id, 'loaded', {
|
|
||||||
ATTR_FRIENDLY_NAME: entry.data['name']
|
|
||||||
})
|
|
||||||
|
|
||||||
# Indicate setup was successful.
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def async_unload_entry(hass, entry):
|
|
||||||
"""Unload an entry."""
|
|
||||||
entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
|
|
||||||
hass.states.async_remove(entity_id)
|
|
||||||
|
|
||||||
# Indicate unload was successful.
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@config_entries.HANDLERS.register(DOMAIN)
|
|
||||||
class ExampleConfigFlow(config_entries.ConfigFlowHandler):
|
|
||||||
"""Handle an example configuration flow."""
|
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize a Hue config handler."""
|
|
||||||
self.object_id = None
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def async_step_init(self, user_input=None):
|
|
||||||
"""Start config flow."""
|
|
||||||
errors = None
|
|
||||||
if user_input is not None:
|
|
||||||
object_id = user_input['object_id']
|
|
||||||
|
|
||||||
if object_id != '' and object_id == slugify(object_id):
|
|
||||||
self.object_id = user_input['object_id']
|
|
||||||
return (yield from self.async_step_name())
|
|
||||||
|
|
||||||
errors = {
|
|
||||||
'object_id': 'invalid_object_id'
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id='init',
|
|
||||||
data_schema=vol.Schema({
|
|
||||||
'object_id': str
|
|
||||||
}),
|
|
||||||
errors=errors
|
|
||||||
)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def async_step_name(self, user_input=None):
|
|
||||||
"""Ask user to enter the name."""
|
|
||||||
errors = None
|
|
||||||
if user_input is not None:
|
|
||||||
name = user_input['name']
|
|
||||||
|
|
||||||
if name != '':
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=name,
|
|
||||||
data={
|
|
||||||
'name': name,
|
|
||||||
'object_id': self.object_id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id='name',
|
|
||||||
data_schema=vol.Schema({
|
|
||||||
'name': str
|
|
||||||
}),
|
|
||||||
errors=errors
|
|
||||||
)
|
|
@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,10 +13,14 @@ from homeassistant import core
|
|||||||
from homeassistant.components import http
|
from homeassistant.components import http
|
||||||
from homeassistant.components.http.data_validator import (
|
from homeassistant.components.http.data_validator import (
|
||||||
RequestDataValidator)
|
RequestDataValidator)
|
||||||
|
from homeassistant.components.cover import (INTENT_OPEN_COVER,
|
||||||
|
INTENT_CLOSE_COVER)
|
||||||
|
from homeassistant.const import EVENT_COMPONENT_LOADED
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers import intent
|
from homeassistant.helpers import intent
|
||||||
|
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
|
from homeassistant.setup import (ATTR_COMPONENT)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -28,6 +32,13 @@ DOMAIN = 'conversation'
|
|||||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||||
REGEX_TYPE = type(re.compile(''))
|
REGEX_TYPE = type(re.compile(''))
|
||||||
|
|
||||||
|
UTTERANCES = {
|
||||||
|
'cover': {
|
||||||
|
INTENT_OPEN_COVER: ['Open [the] [a] [an] {name}[s]'],
|
||||||
|
INTENT_CLOSE_COVER: ['Close [the] [a] [an] {name}[s]']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SERVICE_PROCESS = 'process'
|
SERVICE_PROCESS = 'process'
|
||||||
|
|
||||||
SERVICE_PROCESS_SCHEMA = vol.Schema({
|
SERVICE_PROCESS_SCHEMA = vol.Schema({
|
||||||
@ -112,6 +123,25 @@ async def async_setup(hass, config):
|
|||||||
'[the] [a] [an] {name}[s] toggle',
|
'[the] [a] [an] {name}[s] toggle',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def register_utterances(component):
|
||||||
|
"""Register utterances for a component."""
|
||||||
|
if component not in UTTERANCES:
|
||||||
|
return
|
||||||
|
for intent_type, sentences in UTTERANCES[component].items():
|
||||||
|
async_register(hass, intent_type, sentences)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def component_loaded(event):
|
||||||
|
"""Handle a new component loaded."""
|
||||||
|
register_utterances(event.data[ATTR_COMPONENT])
|
||||||
|
|
||||||
|
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
|
||||||
|
|
||||||
|
# Check already loaded components.
|
||||||
|
for component in hass.config.components:
|
||||||
|
register_utterances(component)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ from homeassistant.helpers.entity import Entity
|
|||||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components import group
|
from homeassistant.components import group
|
||||||
|
from homeassistant.helpers import intent
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_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, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
|
||||||
@ -55,6 +56,9 @@ ATTR_CURRENT_TILT_POSITION = 'current_tilt_position'
|
|||||||
ATTR_POSITION = 'position'
|
ATTR_POSITION = 'position'
|
||||||
ATTR_TILT_POSITION = 'tilt_position'
|
ATTR_TILT_POSITION = 'tilt_position'
|
||||||
|
|
||||||
|
INTENT_OPEN_COVER = 'HassOpenCover'
|
||||||
|
INTENT_CLOSE_COVER = 'HassCloseCover'
|
||||||
|
|
||||||
COVER_SERVICE_SCHEMA = vol.Schema({
|
COVER_SERVICE_SCHEMA = vol.Schema({
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
})
|
})
|
||||||
@ -181,6 +185,12 @@ async def async_setup(hass, config):
|
|||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, service_name, async_handle_cover_service,
|
DOMAIN, service_name, async_handle_cover_service,
|
||||||
schema=schema)
|
schema=schema)
|
||||||
|
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||||
|
INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER,
|
||||||
|
"Opened {}"))
|
||||||
|
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||||
|
INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER,
|
||||||
|
"Closed {}"))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
120
homeassistant/components/cover/gogogate2.py
Normal file
120
homeassistant/components/cover/gogogate2.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
"""
|
||||||
|
Support for Gogogate2 Garage Doors.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation
|
||||||
|
https://home-assistant.io/components/cover.gogogate2/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.cover import (
|
||||||
|
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, STATE_UNKNOWN,
|
||||||
|
CONF_IP_ADDRESS, CONF_NAME)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
REQUIREMENTS = ['pygogogate2==0.0.3']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'gogogate2'
|
||||||
|
|
||||||
|
NOTIFICATION_ID = 'gogogate2_notification'
|
||||||
|
NOTIFICATION_TITLE = 'Gogogate2 Cover Setup'
|
||||||
|
|
||||||
|
COVER_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
vol.Required(CONF_IP_ADDRESS): cv.string,
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Set up the Gogogate2 component."""
|
||||||
|
from pygogogate2 import Gogogate2API as pygogogate2
|
||||||
|
|
||||||
|
username = config.get(CONF_USERNAME)
|
||||||
|
password = config.get(CONF_PASSWORD)
|
||||||
|
ip_address = config.get(CONF_IP_ADDRESS)
|
||||||
|
name = config.get(CONF_NAME)
|
||||||
|
mygogogate2 = pygogogate2(username, password, ip_address)
|
||||||
|
|
||||||
|
try:
|
||||||
|
devices = mygogogate2.get_devices()
|
||||||
|
if devices is False:
|
||||||
|
raise ValueError(
|
||||||
|
"Username or Password is incorrect or no devices found")
|
||||||
|
|
||||||
|
add_devices(MyGogogate2Device(
|
||||||
|
mygogogate2, door, name) for door in devices)
|
||||||
|
return
|
||||||
|
|
||||||
|
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||||
|
_LOGGER.error("%s", ex)
|
||||||
|
hass.components.persistent_notification.create(
|
||||||
|
'Error: {}<br />'
|
||||||
|
'You will need to restart hass after fixing.'
|
||||||
|
''.format(ex),
|
||||||
|
title=NOTIFICATION_TITLE,
|
||||||
|
notification_id=NOTIFICATION_ID)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class MyGogogate2Device(CoverDevice):
|
||||||
|
"""Representation of a Gogogate2 cover."""
|
||||||
|
|
||||||
|
def __init__(self, mygogogate2, device, name):
|
||||||
|
"""Initialize with API object, device id."""
|
||||||
|
self.mygogogate2 = mygogogate2
|
||||||
|
self.device_id = device['door']
|
||||||
|
self._name = name or device['name']
|
||||||
|
self._status = device['status']
|
||||||
|
self.available = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the garage door if any."""
|
||||||
|
return self._name if self._name else DEFAULT_NAME
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
"""Return true if cover is closed, else False."""
|
||||||
|
return self._status == STATE_CLOSED
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self):
|
||||||
|
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||||
|
return 'garage'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag supported features."""
|
||||||
|
return SUPPORT_OPEN | SUPPORT_CLOSE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Could the device be accessed during the last update call."""
|
||||||
|
return self.available
|
||||||
|
|
||||||
|
def close_cover(self, **kwargs):
|
||||||
|
"""Issue close command to cover."""
|
||||||
|
self.mygogogate2.close_device(self.device_id)
|
||||||
|
self.schedule_update_ha_state(True)
|
||||||
|
|
||||||
|
def open_cover(self, **kwargs):
|
||||||
|
"""Issue open command to cover."""
|
||||||
|
self.mygogogate2.open_device(self.device_id)
|
||||||
|
self.schedule_update_ha_state(True)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Update status of cover."""
|
||||||
|
try:
|
||||||
|
self._status = self.mygogogate2.get_status(self.device_id)
|
||||||
|
self.available = True
|
||||||
|
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||||
|
_LOGGER.error("%s", ex)
|
||||||
|
self._status = STATE_UNKNOWN
|
||||||
|
self.available = False
|
@ -9,10 +9,12 @@ from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice
|
|||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
async def async_setup_platform(
|
||||||
"""Set up the MySensors platform for covers."""
|
hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Set up the mysensors platform for covers."""
|
||||||
mysensors.setup_mysensors_platform(
|
mysensors.setup_mysensors_platform(
|
||||||
hass, DOMAIN, discovery_info, MySensorsCover, add_devices=add_devices)
|
hass, DOMAIN, discovery_info, MySensorsCover,
|
||||||
|
async_add_devices=async_add_devices)
|
||||||
|
|
||||||
|
|
||||||
class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||||
|
@ -87,7 +87,7 @@ class RPiGPIOCover(CoverDevice):
|
|||||||
self._invert_relay = invert_relay
|
self._invert_relay = invert_relay
|
||||||
rpi_gpio.setup_output(self._relay_pin)
|
rpi_gpio.setup_output(self._relay_pin)
|
||||||
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
|
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
|
||||||
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
|
rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -105,9 +105,9 @@ class RPiGPIOCover(CoverDevice):
|
|||||||
|
|
||||||
def _trigger(self):
|
def _trigger(self):
|
||||||
"""Trigger the cover."""
|
"""Trigger the cover."""
|
||||||
rpi_gpio.write_output(self._relay_pin, self._invert_relay)
|
rpi_gpio.write_output(self._relay_pin, 1 if self._invert_relay else 0)
|
||||||
sleep(self._relay_time)
|
sleep(self._relay_time)
|
||||||
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
|
rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1)
|
||||||
|
|
||||||
def close_cover(self, **kwargs):
|
def close_cover(self, **kwargs):
|
||||||
"""Close the cover."""
|
"""Close the cover."""
|
||||||
|
25
homeassistant/components/deconz/.translations/en.json
Normal file
25
homeassistant/components/deconz/.translations/en.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "deCONZ",
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Define deCONZ gateway",
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port (default value: '80')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"title": "Link with deCONZ",
|
||||||
|
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "Couldn't get an API key"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"no_bridges": "No deCONZ bridges discovered",
|
||||||
|
"one_instance_only": "Component only supports one deCONZ instance"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,16 +8,17 @@ import logging
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.discovery import SERVICE_DECONZ
|
from homeassistant.components.discovery import SERVICE_DECONZ
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
|
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery, aiohttp_client
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.util.json import load_json, save_json
|
from homeassistant.util.json import load_json, save_json
|
||||||
|
|
||||||
REQUIREMENTS = ['pydeconz==32']
|
REQUIREMENTS = ['pydeconz==35']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -160,7 +161,8 @@ async def async_request_configuration(hass, config, deconz_config):
|
|||||||
async def async_configuration_callback(data):
|
async def async_configuration_callback(data):
|
||||||
"""Set up actions to do when our configuration callback is called."""
|
"""Set up actions to do when our configuration callback is called."""
|
||||||
from pydeconz.utils import async_get_api_key
|
from pydeconz.utils import async_get_api_key
|
||||||
api_key = await async_get_api_key(hass.loop, **deconz_config)
|
websession = async_get_clientsession(hass)
|
||||||
|
api_key = await async_get_api_key(websession, **deconz_config)
|
||||||
if api_key:
|
if api_key:
|
||||||
deconz_config[CONF_API_KEY] = api_key
|
deconz_config[CONF_API_KEY] = api_key
|
||||||
result = await async_setup_deconz(hass, config, deconz_config)
|
result = await async_setup_deconz(hass, config, deconz_config)
|
||||||
@ -186,3 +188,85 @@ async def async_request_configuration(hass, config, deconz_config):
|
|||||||
entity_picture="/static/images/logo_deconz.jpeg",
|
entity_picture="/static/images/logo_deconz.jpeg",
|
||||||
submit_caption="I have unlocked the gateway",
|
submit_caption="I have unlocked the gateway",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@config_entries.HANDLERS.register(DOMAIN)
|
||||||
|
class DeconzFlowHandler(config_entries.ConfigFlowHandler):
|
||||||
|
"""Handle a deCONZ config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the deCONZ flow."""
|
||||||
|
self.bridges = []
|
||||||
|
self.deconz_config = {}
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input=None):
|
||||||
|
"""Handle a flow start."""
|
||||||
|
from pydeconz.utils import async_discovery
|
||||||
|
|
||||||
|
if DOMAIN in self.hass.data:
|
||||||
|
return self.async_abort(
|
||||||
|
reason='one_instance_only'
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
for bridge in self.bridges:
|
||||||
|
if bridge[CONF_HOST] == user_input[CONF_HOST]:
|
||||||
|
self.deconz_config = bridge
|
||||||
|
return await self.async_step_link()
|
||||||
|
|
||||||
|
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
|
self.bridges = await async_discovery(session)
|
||||||
|
|
||||||
|
if len(self.bridges) == 1:
|
||||||
|
self.deconz_config = self.bridges[0]
|
||||||
|
return await self.async_step_link()
|
||||||
|
elif len(self.bridges) > 1:
|
||||||
|
hosts = []
|
||||||
|
for bridge in self.bridges:
|
||||||
|
hosts.append(bridge[CONF_HOST])
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id='init',
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required(CONF_HOST): vol.In(hosts)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_abort(
|
||||||
|
reason='no_bridges'
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_link(self, user_input=None):
|
||||||
|
"""Attempt to link with the deCONZ bridge."""
|
||||||
|
from pydeconz.utils import async_get_api_key
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
|
api_key = await async_get_api_key(session, **self.deconz_config)
|
||||||
|
if api_key:
|
||||||
|
self.deconz_config[CONF_API_KEY] = api_key
|
||||||
|
return self.async_create_entry(
|
||||||
|
title='deCONZ',
|
||||||
|
data=self.deconz_config
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
errors['base'] = 'no_key'
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id='link',
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry):
|
||||||
|
"""Set up a bridge for a config entry."""
|
||||||
|
if DOMAIN in hass.data:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Config entry failed since one deCONZ instance already exists")
|
||||||
|
return False
|
||||||
|
result = await async_setup_deconz(hass, None, entry.data)
|
||||||
|
if result:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
25
homeassistant/components/deconz/strings.json
Normal file
25
homeassistant/components/deconz/strings.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "deCONZ",
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Define deCONZ gateway",
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"port": "Port (default value: '80')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"title": "Link with deCONZ",
|
||||||
|
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_key": "Couldn't get an API key"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"no_bridges": "No deCONZ bridges discovered",
|
||||||
|
"one_instance_only": "Component only supports one deCONZ instance"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,8 +9,6 @@ from datetime import timedelta
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, List, Sequence, Callable
|
from typing import Any, List, Sequence, Callable
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
import async_timeout
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.setup import async_prepare_setup_platform
|
from homeassistant.setup import async_prepare_setup_platform
|
||||||
@ -19,7 +17,6 @@ from homeassistant.loader import bind_hass
|
|||||||
from homeassistant.components import group, zone
|
from homeassistant.components import group, zone
|
||||||
from homeassistant.config import load_yaml_config_file, async_log_exception
|
from homeassistant.config import load_yaml_config_file, async_log_exception
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.helpers import config_per_platform, discovery
|
from homeassistant.helpers import config_per_platform, discovery
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
@ -76,7 +73,6 @@ ATTR_LOCATION_NAME = 'location_name'
|
|||||||
ATTR_MAC = 'mac'
|
ATTR_MAC = 'mac'
|
||||||
ATTR_NAME = 'name'
|
ATTR_NAME = 'name'
|
||||||
ATTR_SOURCE_TYPE = 'source_type'
|
ATTR_SOURCE_TYPE = 'source_type'
|
||||||
ATTR_VENDOR = 'vendor'
|
|
||||||
ATTR_CONSIDER_HOME = 'consider_home'
|
ATTR_CONSIDER_HOME = 'consider_home'
|
||||||
|
|
||||||
SOURCE_TYPE_GPS = 'gps'
|
SOURCE_TYPE_GPS = 'gps'
|
||||||
@ -328,14 +324,10 @@ class DeviceTracker(object):
|
|||||||
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
|
||||||
name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id])
|
name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id])
|
||||||
|
|
||||||
# lookup mac vendor string to be stored in config
|
|
||||||
yield from device.set_vendor_for_mac()
|
|
||||||
|
|
||||||
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
||||||
ATTR_ENTITY_ID: device.entity_id,
|
ATTR_ENTITY_ID: device.entity_id,
|
||||||
ATTR_HOST_NAME: device.host_name,
|
ATTR_HOST_NAME: device.host_name,
|
||||||
ATTR_MAC: device.mac,
|
ATTR_MAC: device.mac,
|
||||||
ATTR_VENDOR: device.vendor,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# update known_devices.yaml
|
# update known_devices.yaml
|
||||||
@ -413,7 +405,6 @@ class Device(Entity):
|
|||||||
consider_home = None # type: dt_util.dt.timedelta
|
consider_home = None # type: dt_util.dt.timedelta
|
||||||
battery = None # type: int
|
battery = None # type: int
|
||||||
attributes = None # type: dict
|
attributes = None # type: dict
|
||||||
vendor = None # type: str
|
|
||||||
icon = None # type: str
|
icon = None # type: str
|
||||||
|
|
||||||
# Track if the last update of this device was HOME.
|
# Track if the last update of this device was HOME.
|
||||||
@ -423,7 +414,7 @@ class Device(Entity):
|
|||||||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||||
track: bool, dev_id: str, mac: str, name: str = None,
|
track: bool, dev_id: str, mac: str, name: str = None,
|
||||||
picture: str = None, gravatar: str = None, icon: str = None,
|
picture: str = None, gravatar: str = None, icon: str = None,
|
||||||
hide_if_away: bool = False, vendor: str = None) -> None:
|
hide_if_away: bool = False) -> None:
|
||||||
"""Initialize a device."""
|
"""Initialize a device."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||||
@ -451,7 +442,6 @@ class Device(Entity):
|
|||||||
self.icon = icon
|
self.icon = icon
|
||||||
|
|
||||||
self.away_hide = hide_if_away
|
self.away_hide = hide_if_away
|
||||||
self.vendor = vendor
|
|
||||||
|
|
||||||
self.source_type = None
|
self.source_type = None
|
||||||
|
|
||||||
@ -567,51 +557,6 @@ class Device(Entity):
|
|||||||
self._state = STATE_HOME
|
self._state = STATE_HOME
|
||||||
self.last_update_home = True
|
self.last_update_home = True
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def set_vendor_for_mac(self):
|
|
||||||
"""Set vendor string using api.macvendors.com."""
|
|
||||||
self.vendor = yield from self.get_vendor_for_mac()
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def get_vendor_for_mac(self):
|
|
||||||
"""Try to find the vendor string for a given MAC address."""
|
|
||||||
if not self.mac:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if '_' in self.mac:
|
|
||||||
_, mac = self.mac.split('_', 1)
|
|
||||||
else:
|
|
||||||
mac = self.mac
|
|
||||||
|
|
||||||
if not len(mac.split(':')) == 6:
|
|
||||||
return 'unknown'
|
|
||||||
|
|
||||||
# We only need the first 3 bytes of the MAC for a lookup
|
|
||||||
# this improves somewhat on privacy
|
|
||||||
oui_bytes = mac.split(':')[0:3]
|
|
||||||
# bytes like 00 get truncates to 0, API needs full bytes
|
|
||||||
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
|
|
||||||
url = 'http://api.macvendors.com/' + oui
|
|
||||||
try:
|
|
||||||
websession = async_get_clientsession(self.hass)
|
|
||||||
|
|
||||||
with async_timeout.timeout(5, loop=self.hass.loop):
|
|
||||||
resp = yield from websession.get(url)
|
|
||||||
# mac vendor found, response is the string
|
|
||||||
if resp.status == 200:
|
|
||||||
vendor_string = yield from resp.text()
|
|
||||||
return vendor_string
|
|
||||||
# If vendor is not known to the API (404) or there
|
|
||||||
# was a failure during the lookup (500); set vendor
|
|
||||||
# to something other then None to prevent retry
|
|
||||||
# as the value is only relevant when it is to be stored
|
|
||||||
# in the 'known_devices.yaml' file which only happens
|
|
||||||
# the first time the device is seen.
|
|
||||||
return 'unknown'
|
|
||||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
|
||||||
# Same as above
|
|
||||||
return 'unknown'
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Add an entity."""
|
"""Add an entity."""
|
||||||
@ -685,7 +630,6 @@ def async_load_config(path: str, hass: HomeAssistantType,
|
|||||||
vol.Optional('picture', default=None): vol.Any(None, cv.string),
|
vol.Optional('picture', default=None): vol.Any(None, cv.string),
|
||||||
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
|
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
|
||||||
cv.time_period, cv.positive_timedelta),
|
cv.time_period, cv.positive_timedelta),
|
||||||
vol.Optional('vendor', default=None): vol.Any(None, cv.string),
|
|
||||||
})
|
})
|
||||||
try:
|
try:
|
||||||
result = []
|
result = []
|
||||||
@ -697,6 +641,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
for dev_id, device in devices.items():
|
for dev_id, device in devices.items():
|
||||||
|
# Deprecated option. We just ignore it to avoid breaking change
|
||||||
|
device.pop('vendor', None)
|
||||||
try:
|
try:
|
||||||
device = dev_schema(device)
|
device = dev_schema(device)
|
||||||
device['dev_id'] = cv.slugify(dev_id)
|
device['dev_id'] = cv.slugify(dev_id)
|
||||||
@ -772,7 +718,6 @@ def update_config(path: str, dev_id: str, device: Device):
|
|||||||
'picture': device.config_picture,
|
'picture': device.config_picture,
|
||||||
'track': device.track,
|
'track': device.track,
|
||||||
CONF_AWAY_HIDE: device.away_hide,
|
CONF_AWAY_HIDE: device.away_hide,
|
||||||
'vendor': device.vendor,
|
|
||||||
}}
|
}}
|
||||||
out.write('\n')
|
out.write('\n')
|
||||||
out.write(dump(device))
|
out.write(dump(device))
|
||||||
|
@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
CONF_PUB_KEY = 'pub_key'
|
CONF_PUB_KEY = 'pub_key'
|
||||||
CONF_SSH_KEY = 'ssh_key'
|
CONF_SSH_KEY = 'ssh_key'
|
||||||
|
CONF_REQUIRE_IP = 'require_ip'
|
||||||
DEFAULT_SSH_PORT = 22
|
DEFAULT_SSH_PORT = 22
|
||||||
SECRET_GROUP = 'Password or SSH Key'
|
SECRET_GROUP = 'Password or SSH Key'
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ PLATFORM_SCHEMA = vol.All(
|
|||||||
vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']),
|
vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']),
|
||||||
vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']),
|
vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']),
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
|
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
|
||||||
|
vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
|
||||||
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
|
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
|
||||||
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
|
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
|
||||||
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile
|
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile
|
||||||
@ -115,6 +117,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
|||||||
self.protocol = config[CONF_PROTOCOL]
|
self.protocol = config[CONF_PROTOCOL]
|
||||||
self.mode = config[CONF_MODE]
|
self.mode = config[CONF_MODE]
|
||||||
self.port = config[CONF_PORT]
|
self.port = config[CONF_PORT]
|
||||||
|
self.require_ip = config[CONF_REQUIRE_IP]
|
||||||
|
|
||||||
if self.protocol == 'ssh':
|
if self.protocol == 'ssh':
|
||||||
self.connection = SshConnection(
|
self.connection = SshConnection(
|
||||||
@ -172,7 +175,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
ret_devices = {}
|
ret_devices = {}
|
||||||
for key in devices:
|
for key in devices:
|
||||||
if devices[key].ip is not None:
|
if not self.require_ip or devices[key].ip is not None:
|
||||||
ret_devices[key] = devices[key]
|
ret_devices[key] = devices[key]
|
||||||
return ret_devices
|
return ret_devices
|
||||||
|
|
||||||
|
@ -17,12 +17,15 @@ import homeassistant.util.dt as dt_util
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['pybluez==0.22']
|
REQUIREMENTS = ['pybluez==0.22', 'bt_proximity==0.1.2']
|
||||||
|
|
||||||
BT_PREFIX = 'BT_'
|
BT_PREFIX = 'BT_'
|
||||||
|
|
||||||
|
CONF_REQUEST_RSSI = 'request_rssi'
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_TRACK_NEW): cv.boolean
|
vol.Optional(CONF_TRACK_NEW): cv.boolean,
|
||||||
|
vol.Optional(CONF_REQUEST_RSSI): cv.boolean
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -30,11 +33,15 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
"""Set up the Bluetooth Scanner."""
|
"""Set up the Bluetooth Scanner."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
import bluetooth
|
import bluetooth
|
||||||
|
from bt_proximity import BluetoothRSSI
|
||||||
|
|
||||||
def see_device(device):
|
def see_device(mac, name, rssi=None):
|
||||||
"""Mark a device as seen."""
|
"""Mark a device as seen."""
|
||||||
see(mac=BT_PREFIX + device[0], host_name=device[1],
|
attributes = {}
|
||||||
source_type=SOURCE_TYPE_BLUETOOTH)
|
if rssi is not None:
|
||||||
|
attributes['rssi'] = rssi
|
||||||
|
see(mac="{}_{}".format(BT_PREFIX, mac), host_name=name,
|
||||||
|
attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH)
|
||||||
|
|
||||||
def discover_devices():
|
def discover_devices():
|
||||||
"""Discover Bluetooth devices."""
|
"""Discover Bluetooth devices."""
|
||||||
@ -66,10 +73,12 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
if dev[0] not in devs_to_track and \
|
if dev[0] not in devs_to_track and \
|
||||||
dev[0] not in devs_donot_track:
|
dev[0] not in devs_donot_track:
|
||||||
devs_to_track.append(dev[0])
|
devs_to_track.append(dev[0])
|
||||||
see_device(dev)
|
see_device(dev[0], dev[1])
|
||||||
|
|
||||||
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||||
|
|
||||||
|
request_rssi = config.get(CONF_REQUEST_RSSI, False)
|
||||||
|
|
||||||
def update_bluetooth(now):
|
def update_bluetooth(now):
|
||||||
"""Lookup Bluetooth device and update status."""
|
"""Lookup Bluetooth device and update status."""
|
||||||
try:
|
try:
|
||||||
@ -81,10 +90,13 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
for mac in devs_to_track:
|
for mac in devs_to_track:
|
||||||
_LOGGER.debug("Scanning %s", mac)
|
_LOGGER.debug("Scanning %s", mac)
|
||||||
result = bluetooth.lookup_name(mac, timeout=5)
|
result = bluetooth.lookup_name(mac, timeout=5)
|
||||||
if not result:
|
rssi = None
|
||||||
|
if request_rssi:
|
||||||
|
rssi = BluetoothRSSI(mac).request_rssi()
|
||||||
|
if result is None:
|
||||||
# Could not lookup device name
|
# Could not lookup device name
|
||||||
continue
|
continue
|
||||||
see_device((mac, result))
|
see_device(mac, result, rssi)
|
||||||
except bluetooth.BluetoothError:
|
except bluetooth.BluetoothError:
|
||||||
_LOGGER.exception("Error looking up Bluetooth device")
|
_LOGGER.exception("Error looking up Bluetooth device")
|
||||||
track_point_in_utc_time(
|
track_point_in_utc_time(
|
||||||
|
@ -36,16 +36,20 @@ class BMWDeviceTracker(object):
|
|||||||
self.vehicle = vehicle
|
self.vehicle = vehicle
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
"""Update the device info."""
|
"""Update the device info.
|
||||||
dev_id = slugify(self.vehicle.modelName)
|
|
||||||
|
Only update the state in home assistant if tracking in
|
||||||
|
the car is enabled.
|
||||||
|
"""
|
||||||
|
dev_id = slugify(self.vehicle.name)
|
||||||
|
|
||||||
|
if not self.vehicle.state.is_vehicle_tracking_enabled:
|
||||||
|
_LOGGER.debug('Tracking is disabled for vehicle %s', dev_id)
|
||||||
|
return
|
||||||
|
|
||||||
_LOGGER.debug('Updating %s', dev_id)
|
_LOGGER.debug('Updating %s', dev_id)
|
||||||
attrs = {
|
|
||||||
'trackr_id': dev_id,
|
|
||||||
'id': dev_id,
|
|
||||||
'name': self.vehicle.modelName
|
|
||||||
}
|
|
||||||
self._see(
|
self._see(
|
||||||
dev_id=dev_id, host_name=self.vehicle.modelName,
|
dev_id=dev_id, host_name=self.vehicle.name,
|
||||||
gps=self.vehicle.state.gps_position, attributes=attrs,
|
gps=self.vehicle.state.gps_position, icon='mdi:car'
|
||||||
icon='mdi:car'
|
|
||||||
)
|
)
|
||||||
|
83
homeassistant/components/device_tracker/google_maps.py
Normal file
83
homeassistant/components/device_tracker/google_maps.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
"""
|
||||||
|
Support for Google Maps location sharing.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/device_tracker.google_maps/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.device_tracker import (
|
||||||
|
PLATFORM_SCHEMA, SOURCE_TYPE_GPS)
|
||||||
|
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||||
|
from homeassistant.helpers.event import track_time_interval
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
REQUIREMENTS = ['locationsharinglib==0.4.0']
|
||||||
|
|
||||||
|
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_scanner(hass, config: ConfigType, see, discovery_info=None):
|
||||||
|
"""Set up the scanner."""
|
||||||
|
scanner = GoogleMapsScanner(hass, config, see)
|
||||||
|
return scanner.success_init
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleMapsScanner(object):
|
||||||
|
"""Representation of an Google Maps location sharing account."""
|
||||||
|
|
||||||
|
def __init__(self, hass, config: ConfigType, see) -> None:
|
||||||
|
"""Initialize the scanner."""
|
||||||
|
from locationsharinglib import Service
|
||||||
|
from locationsharinglib.locationsharinglibexceptions import InvalidUser
|
||||||
|
|
||||||
|
self.see = see
|
||||||
|
self.username = config[CONF_USERNAME]
|
||||||
|
self.password = config[CONF_PASSWORD]
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.service = Service(self.username, self.password,
|
||||||
|
hass.config.path(CREDENTIALS_FILE))
|
||||||
|
self._update_info()
|
||||||
|
|
||||||
|
track_time_interval(
|
||||||
|
hass, self._update_info, MIN_TIME_BETWEEN_SCANS)
|
||||||
|
|
||||||
|
self.success_init = True
|
||||||
|
|
||||||
|
except InvalidUser:
|
||||||
|
_LOGGER.error('You have specified invalid login credentials')
|
||||||
|
self.success_init = False
|
||||||
|
|
||||||
|
def _update_info(self, now=None):
|
||||||
|
for person in self.service.get_all_people():
|
||||||
|
dev_id = 'google_maps_{0}'.format(slugify(person.id))
|
||||||
|
|
||||||
|
attrs = {
|
||||||
|
'id': person.id,
|
||||||
|
'nickname': person.nickname,
|
||||||
|
'full_name': person.full_name,
|
||||||
|
'last_seen': person.datetime,
|
||||||
|
'address': person.address
|
||||||
|
}
|
||||||
|
self.see(
|
||||||
|
dev_id=dev_id,
|
||||||
|
gps=(person.latitude, person.longitude),
|
||||||
|
picture=person.picture_url,
|
||||||
|
source_type=SOURCE_TYPE_GPS,
|
||||||
|
attributes=attrs
|
||||||
|
)
|
@ -1,74 +0,0 @@
|
|||||||
"""
|
|
||||||
Support for Mercedes cars with Mercedes ME.
|
|
||||||
|
|
||||||
For more details about this component, please refer to the documentation at
|
|
||||||
https://home-assistant.io/components/device_tracker.mercedesme/
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from homeassistant.components.mercedesme import DATA_MME
|
|
||||||
from homeassistant.helpers.event import track_time_interval
|
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
DEPENDENCIES = ['mercedesme']
|
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_scanner(hass, config, see, discovery_info=None):
|
|
||||||
"""Set up the Mercedes ME tracker."""
|
|
||||||
if discovery_info is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
data = hass.data[DATA_MME].data
|
|
||||||
|
|
||||||
if not data.cars:
|
|
||||||
return False
|
|
||||||
|
|
||||||
MercedesMEDeviceTracker(hass, config, see, data)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class MercedesMEDeviceTracker(object):
|
|
||||||
"""A class representing a Mercedes ME device tracker."""
|
|
||||||
|
|
||||||
def __init__(self, hass, config, see, data):
|
|
||||||
"""Initialize the Mercedes ME device tracker."""
|
|
||||||
self.see = see
|
|
||||||
self.data = data
|
|
||||||
self.update_info()
|
|
||||||
|
|
||||||
track_time_interval(
|
|
||||||
hass, self.update_info, MIN_TIME_BETWEEN_SCANS)
|
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
|
||||||
def update_info(self, now=None):
|
|
||||||
"""Update the device info."""
|
|
||||||
for device in self.data.cars:
|
|
||||||
if not device['services'].get('VEHICLE_FINDER', False):
|
|
||||||
continue
|
|
||||||
|
|
||||||
location = self.data.get_location(device["vin"])
|
|
||||||
if location is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
dev_id = device["vin"]
|
|
||||||
name = device["license"]
|
|
||||||
|
|
||||||
lat = location['positionLat']['value']
|
|
||||||
lon = location['positionLong']['value']
|
|
||||||
attrs = {
|
|
||||||
'trackr_id': dev_id,
|
|
||||||
'id': dev_id,
|
|
||||||
'name': name
|
|
||||||
}
|
|
||||||
self.see(
|
|
||||||
dev_id=dev_id, host_name=name,
|
|
||||||
gps=(lat, lon), attributes=attrs
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
@ -6,15 +6,15 @@ https://home-assistant.io/components/device_tracker.mysensors/
|
|||||||
"""
|
"""
|
||||||
from homeassistant.components import mysensors
|
from homeassistant.components import mysensors
|
||||||
from homeassistant.components.device_tracker import DOMAIN
|
from homeassistant.components.device_tracker import DOMAIN
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
|
|
||||||
def setup_scanner(hass, config, see, discovery_info=None):
|
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||||
"""Set up the MySensors device scanner."""
|
"""Set up the MySensors device scanner."""
|
||||||
new_devices = mysensors.setup_mysensors_platform(
|
new_devices = mysensors.setup_mysensors_platform(
|
||||||
hass, DOMAIN, discovery_info, MySensorsDeviceScanner,
|
hass, DOMAIN, discovery_info, MySensorsDeviceScanner,
|
||||||
device_args=(see, ))
|
device_args=(async_see, ))
|
||||||
if not new_devices:
|
if not new_devices:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -22,9 +22,9 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
dev_id = (
|
dev_id = (
|
||||||
id(device.gateway), device.node_id, device.child_id,
|
id(device.gateway), device.node_id, device.child_id,
|
||||||
device.value_type)
|
device.value_type)
|
||||||
dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
hass, mysensors.SIGNAL_CALLBACK.format(*dev_id),
|
hass, mysensors.SIGNAL_CALLBACK.format(*dev_id),
|
||||||
device.update_callback)
|
device.async_update_callback)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -32,20 +32,20 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||||||
class MySensorsDeviceScanner(mysensors.MySensorsDevice):
|
class MySensorsDeviceScanner(mysensors.MySensorsDevice):
|
||||||
"""Represent a MySensors scanner."""
|
"""Represent a MySensors scanner."""
|
||||||
|
|
||||||
def __init__(self, see, *args):
|
def __init__(self, async_see, *args):
|
||||||
"""Set up instance."""
|
"""Set up instance."""
|
||||||
super().__init__(*args)
|
super().__init__(*args)
|
||||||
self.see = see
|
self.async_see = async_see
|
||||||
|
|
||||||
def update_callback(self):
|
async def async_update_callback(self):
|
||||||
"""Update the device."""
|
"""Update the device."""
|
||||||
self.update()
|
await self.async_update()
|
||||||
node = self.gateway.sensors[self.node_id]
|
node = self.gateway.sensors[self.node_id]
|
||||||
child = node.children[self.child_id]
|
child = node.children[self.child_id]
|
||||||
position = child.values[self.value_type]
|
position = child.values[self.value_type]
|
||||||
latitude, longitude, _ = position.split(',')
|
latitude, longitude, _ = position.split(',')
|
||||||
|
|
||||||
self.see(
|
await self.async_see(
|
||||||
dev_id=slugify(self.name),
|
dev_id=slugify(self.name),
|
||||||
host_name=self.name,
|
host_name=self.name,
|
||||||
gps=(latitude, longitude),
|
gps=(latitude, longitude),
|
||||||
|
@ -95,7 +95,7 @@ class UbusDeviceScanner(DeviceScanner):
|
|||||||
return self.last_results
|
return self.last_results
|
||||||
|
|
||||||
def _generate_mac2name(self):
|
def _generate_mac2name(self):
|
||||||
"""Return empty MAC to name dict. Overriden if DHCP server is set."""
|
"""Return empty MAC to name dict. Overridden if DHCP server is set."""
|
||||||
self.mac2name = dict()
|
self.mac2name = dict()
|
||||||
|
|
||||||
@_refresh_on_access_denied
|
@_refresh_on_access_denied
|
||||||
|
77
homeassistant/components/device_tracker/xiaomi_miio.py
Normal file
77
homeassistant/components/device_tracker/xiaomi_miio.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
"""
|
||||||
|
Support for Xiaomi Mi WiFi Repeater 2.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation
|
||||||
|
https://home-assistant.io/components/device_tracker.xiaomi_miio/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA,
|
||||||
|
DeviceScanner)
|
||||||
|
from homeassistant.const import (CONF_HOST, CONF_TOKEN)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
|
||||||
|
})
|
||||||
|
|
||||||
|
REQUIREMENTS = ['python-miio==0.3.9']
|
||||||
|
|
||||||
|
|
||||||
|
def get_scanner(hass, config):
|
||||||
|
"""Return a Xiaomi MiIO device scanner."""
|
||||||
|
from miio import WifiRepeater, DeviceException
|
||||||
|
|
||||||
|
scanner = None
|
||||||
|
host = config[DOMAIN].get(CONF_HOST)
|
||||||
|
token = config[DOMAIN].get(CONF_TOKEN)
|
||||||
|
|
||||||
|
_LOGGER.info(
|
||||||
|
"Initializing with host %s (token %s...)", host, token[:5])
|
||||||
|
|
||||||
|
try:
|
||||||
|
device = WifiRepeater(host, token)
|
||||||
|
device_info = device.info()
|
||||||
|
_LOGGER.info("%s %s %s detected",
|
||||||
|
device_info.model,
|
||||||
|
device_info.firmware_version,
|
||||||
|
device_info.hardware_version)
|
||||||
|
scanner = XiaomiMiioDeviceScanner(hass, device)
|
||||||
|
except DeviceException as ex:
|
||||||
|
_LOGGER.error("Device unavailable or token incorrect: %s", ex)
|
||||||
|
|
||||||
|
return scanner
|
||||||
|
|
||||||
|
|
||||||
|
class XiaomiMiioDeviceScanner(DeviceScanner):
|
||||||
|
"""This class queries a Xiaomi Mi WiFi Repeater."""
|
||||||
|
|
||||||
|
def __init__(self, hass, device):
|
||||||
|
"""Initialize the scanner."""
|
||||||
|
self.device = device
|
||||||
|
|
||||||
|
async def async_scan_devices(self):
|
||||||
|
"""Scan for devices and return a list containing found device ids."""
|
||||||
|
from miio import DeviceException
|
||||||
|
|
||||||
|
devices = []
|
||||||
|
try:
|
||||||
|
station_info = await self.hass.async_add_job(self.device.status)
|
||||||
|
_LOGGER.debug("Got new station info: %s", station_info)
|
||||||
|
|
||||||
|
for device in station_info['mat']:
|
||||||
|
devices.append(device['mac'])
|
||||||
|
|
||||||
|
except DeviceException as ex:
|
||||||
|
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||||
|
|
||||||
|
return devices
|
||||||
|
|
||||||
|
async def async_get_device_name(self, device):
|
||||||
|
"""The repeater doesn't provide the name of the associated device."""
|
||||||
|
return None
|
@ -13,6 +13,7 @@ import os
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -40,6 +41,10 @@ SERVICE_DECONZ = 'deconz'
|
|||||||
SERVICE_DAIKIN = 'daikin'
|
SERVICE_DAIKIN = 'daikin'
|
||||||
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
|
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
|
||||||
|
|
||||||
|
CONFIG_ENTRY_HANDLERS = {
|
||||||
|
SERVICE_HUE: 'hue',
|
||||||
|
}
|
||||||
|
|
||||||
SERVICE_HANDLERS = {
|
SERVICE_HANDLERS = {
|
||||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||||
SERVICE_NETGEAR: ('device_tracker', None),
|
SERVICE_NETGEAR: ('device_tracker', None),
|
||||||
@ -51,7 +56,6 @@ SERVICE_HANDLERS = {
|
|||||||
SERVICE_WINK: ('wink', None),
|
SERVICE_WINK: ('wink', None),
|
||||||
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
|
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
|
||||||
SERVICE_TELLDUSLIVE: ('tellduslive', None),
|
SERVICE_TELLDUSLIVE: ('tellduslive', None),
|
||||||
SERVICE_HUE: ('hue', None),
|
|
||||||
SERVICE_DECONZ: ('deconz', None),
|
SERVICE_DECONZ: ('deconz', None),
|
||||||
SERVICE_DAIKIN: ('daikin', None),
|
SERVICE_DAIKIN: ('daikin', None),
|
||||||
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
|
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
|
||||||
@ -105,6 +109,20 @@ async def async_setup(hass, config):
|
|||||||
logger.info("Ignoring service: %s %s", service, info)
|
logger.info("Ignoring service: %s %s", service, info)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
discovery_hash = json.dumps([service, info], sort_keys=True)
|
||||||
|
if discovery_hash in already_discovered:
|
||||||
|
return
|
||||||
|
|
||||||
|
already_discovered.add(discovery_hash)
|
||||||
|
|
||||||
|
if service in CONFIG_ENTRY_HANDLERS:
|
||||||
|
await hass.config_entries.flow.async_init(
|
||||||
|
CONFIG_ENTRY_HANDLERS[service],
|
||||||
|
source=config_entries.SOURCE_DISCOVERY,
|
||||||
|
data=info
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
comp_plat = SERVICE_HANDLERS.get(service)
|
comp_plat = SERVICE_HANDLERS.get(service)
|
||||||
|
|
||||||
# We do not know how to handle this service.
|
# We do not know how to handle this service.
|
||||||
@ -112,12 +130,6 @@ async def async_setup(hass, config):
|
|||||||
logger.info("Unknown service discovered: %s %s", service, info)
|
logger.info("Unknown service discovered: %s %s", service, info)
|
||||||
return
|
return
|
||||||
|
|
||||||
discovery_hash = json.dumps([service, info], sort_keys=True)
|
|
||||||
if discovery_hash in already_discovered:
|
|
||||||
return
|
|
||||||
|
|
||||||
already_discovered.add(discovery_hash)
|
|
||||||
|
|
||||||
logger.info("Found new service: %s %s", service, info)
|
logger.info("Found new service: %s %s", service, info)
|
||||||
|
|
||||||
component, platform = comp_plat
|
component, platform = comp_plat
|
||||||
|
@ -22,6 +22,7 @@ DOMAIN = 'doorbird'
|
|||||||
API_URL = '/api/{}'.format(DOMAIN)
|
API_URL = '/api/{}'.format(DOMAIN)
|
||||||
|
|
||||||
CONF_DOORBELL_EVENTS = 'doorbell_events'
|
CONF_DOORBELL_EVENTS = 'doorbell_events'
|
||||||
|
CONF_CUSTOM_URL = 'hass_url_override'
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
@ -29,6 +30,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean,
|
vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean,
|
||||||
|
vol.Optional(CONF_CUSTOM_URL): cv.string,
|
||||||
})
|
})
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
@ -61,9 +63,17 @@ def setup(hass, config):
|
|||||||
# Provide an endpoint for the device to call to trigger events
|
# Provide an endpoint for the device to call to trigger events
|
||||||
hass.http.register_view(DoorbirdRequestView())
|
hass.http.register_view(DoorbirdRequestView())
|
||||||
|
|
||||||
|
# Get the URL of this server
|
||||||
|
hass_url = hass.config.api.base_url
|
||||||
|
|
||||||
|
# Override it if another is specified in the component configuration
|
||||||
|
if config[DOMAIN].get(CONF_CUSTOM_URL):
|
||||||
|
hass_url = config[DOMAIN].get(CONF_CUSTOM_URL)
|
||||||
|
_LOGGER.info("DoorBird will connect to this instance via %s",
|
||||||
|
hass_url)
|
||||||
|
|
||||||
# This will make HA the only service that gets doorbell events
|
# This will make HA the only service that gets doorbell events
|
||||||
url = '{}{}/{}'.format(
|
url = '{}{}/{}'.format(hass_url, API_URL, SENSOR_DOORBELL)
|
||||||
hass.config.api.base_url, API_URL, SENSOR_DOORBELL)
|
|
||||||
device.reset_notifications()
|
device.reset_notifications()
|
||||||
device.subscribe_notification(SENSOR_DOORBELL, url)
|
device.subscribe_notification(SENSOR_DOORBELL, url)
|
||||||
|
|
||||||
|
@ -158,10 +158,6 @@ class Config(object):
|
|||||||
"Listen port not specified, defaulting to %s",
|
"Listen port not specified, defaulting to %s",
|
||||||
self.listen_port)
|
self.listen_port)
|
||||||
|
|
||||||
if self.type == TYPE_GOOGLE and self.listen_port != 80:
|
|
||||||
_LOGGER.warning("When targeting Google Home, listening port has "
|
|
||||||
"to be port 80")
|
|
||||||
|
|
||||||
# Get whether or not UPNP binds to multicast address (239.255.255.250)
|
# Get whether or not UPNP binds to multicast address (239.255.255.250)
|
||||||
# or to the unicast address (host_ip_addr)
|
# or to the unicast address (host_ip_addr)
|
||||||
self.upnp_bind_multicast = conf.get(
|
self.upnp_bind_multicast = conf.get(
|
||||||
|
@ -68,50 +68,50 @@ xiaomi_miio_set_buzzer_on:
|
|||||||
description: Turn the buzzer on.
|
description: Turn the buzzer on.
|
||||||
fields:
|
fields:
|
||||||
entity_id:
|
entity_id:
|
||||||
description: Name of the air purifier entity.
|
description: Name of the xiaomi miio entity.
|
||||||
example: 'fan.xiaomi_air_purifier'
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
|
||||||
xiaomi_miio_set_buzzer_off:
|
xiaomi_miio_set_buzzer_off:
|
||||||
description: Turn the buzzer off.
|
description: Turn the buzzer off.
|
||||||
fields:
|
fields:
|
||||||
entity_id:
|
entity_id:
|
||||||
description: Name of the air purifier entity.
|
description: Name of the xiaomi miio entity.
|
||||||
example: 'fan.xiaomi_air_purifier'
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
|
||||||
xiaomi_miio_set_led_on:
|
xiaomi_miio_set_led_on:
|
||||||
description: Turn the led on.
|
description: Turn the led on.
|
||||||
fields:
|
fields:
|
||||||
entity_id:
|
entity_id:
|
||||||
description: Name of the air purifier entity.
|
description: Name of the xiaomi miio entity.
|
||||||
example: 'fan.xiaomi_air_purifier'
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
|
||||||
xiaomi_miio_set_led_off:
|
xiaomi_miio_set_led_off:
|
||||||
description: Turn the led off.
|
description: Turn the led off.
|
||||||
fields:
|
fields:
|
||||||
entity_id:
|
entity_id:
|
||||||
description: Name of the air purifier entity.
|
description: Name of the xiaomi miio entity.
|
||||||
example: 'fan.xiaomi_air_purifier'
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
|
||||||
xiaomi_miio_set_child_lock_on:
|
xiaomi_miio_set_child_lock_on:
|
||||||
description: Turn the child lock on.
|
description: Turn the child lock on.
|
||||||
fields:
|
fields:
|
||||||
entity_id:
|
entity_id:
|
||||||
description: Name of the air purifier entity.
|
description: Name of the xiaomi miio entity.
|
||||||
example: 'fan.xiaomi_air_purifier'
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
|
||||||
xiaomi_miio_set_child_lock_off:
|
xiaomi_miio_set_child_lock_off:
|
||||||
description: Turn the child lock off.
|
description: Turn the child lock off.
|
||||||
fields:
|
fields:
|
||||||
entity_id:
|
entity_id:
|
||||||
description: Name of the air purifier entity.
|
description: Name of the xiaomi miio entity.
|
||||||
example: 'fan.xiaomi_air_purifier'
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
|
||||||
xiaomi_miio_set_favorite_level:
|
xiaomi_miio_set_favorite_level:
|
||||||
description: Set the favorite level.
|
description: Set the favorite level.
|
||||||
fields:
|
fields:
|
||||||
entity_id:
|
entity_id:
|
||||||
description: Name of the air purifier entity.
|
description: Name of the xiaomi miio entity.
|
||||||
example: 'fan.xiaomi_air_purifier'
|
example: 'fan.xiaomi_miio_device'
|
||||||
level:
|
level:
|
||||||
description: Level, between 0 and 16.
|
description: Level, between 0 and 16.
|
||||||
example: 1
|
example: 1
|
||||||
@ -120,8 +120,87 @@ xiaomi_miio_set_led_brightness:
|
|||||||
description: Set the led brightness.
|
description: Set the led brightness.
|
||||||
fields:
|
fields:
|
||||||
entity_id:
|
entity_id:
|
||||||
description: Name of the air purifier entity.
|
description: Name of the xiaomi miio entity.
|
||||||
example: 'fan.xiaomi_air_purifier'
|
example: 'fan.xiaomi_miio_device'
|
||||||
brightness:
|
brightness:
|
||||||
description: Brightness (0 = Bright, 1 = Dim, 2 = Off)
|
description: Brightness (0 = Bright, 1 = Dim, 2 = Off)
|
||||||
example: 1
|
example: 1
|
||||||
|
|
||||||
|
xiaomi_miio_set_auto_detect_on:
|
||||||
|
description: Turn the auto detect on.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the xiaomi miio entity.
|
||||||
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
|
||||||
|
xiaomi_miio_set_auto_detect_off:
|
||||||
|
description: Turn the auto detect off.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the xiaomi miio entity.
|
||||||
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
|
||||||
|
xiaomi_miio_set_learn_mode_on:
|
||||||
|
description: Turn the learn mode on.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the xiaomi miio entity.
|
||||||
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
|
||||||
|
xiaomi_miio_set_learn_mode_off:
|
||||||
|
description: Turn the learn mode off.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the xiaomi miio entity.
|
||||||
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
|
||||||
|
xiaomi_miio_set_volume:
|
||||||
|
description: Set the sound volume.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the xiaomi miio entity.
|
||||||
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
volume:
|
||||||
|
description: Volume, between 0 and 100.
|
||||||
|
example: 50
|
||||||
|
|
||||||
|
xiaomi_miio_reset_filter:
|
||||||
|
description: Reset the filter lifetime and usage.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the xiaomi miio entity.
|
||||||
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
|
||||||
|
xiaomi_miio_set_extra_features:
|
||||||
|
description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the xiaomi miio entity.
|
||||||
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
features:
|
||||||
|
description: Integer, known values are 0 (default) and 1 (turbo mode).
|
||||||
|
example: 1
|
||||||
|
|
||||||
|
xiaomi_miio_set_target_humidity:
|
||||||
|
description: Set the target humidity.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the xiaomi miio entity.
|
||||||
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
humidity:
|
||||||
|
description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80.
|
||||||
|
example: 50
|
||||||
|
|
||||||
|
xiaomi_miio_set_dry_on:
|
||||||
|
description: Turn the dry mode on.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the xiaomi miio entity.
|
||||||
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
|
||||||
|
xiaomi_miio_set_dry_off:
|
||||||
|
description: Turn the dry mode off.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the xiaomi miio entity.
|
||||||
|
example: 'fan.xiaomi_miio_device'
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
Support for Xiaomi Mi Air Purifier 2.
|
Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation
|
For more details about this platform, please refer to the documentation
|
||||||
https://home-assistant.io/components/fan.xiaomi_miio/
|
https://home-assistant.io/components/fan.xiaomi_miio/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from enum import Enum
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.helpers.entity import ToggleEntity
|
|
||||||
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA,
|
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA,
|
||||||
SUPPORT_SET_SPEED, DOMAIN, )
|
SUPPORT_SET_SPEED, DOMAIN, )
|
||||||
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
|
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
|
||||||
@ -20,17 +20,40 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = 'Xiaomi Air Purifier'
|
DEFAULT_NAME = 'Xiaomi Miio Device'
|
||||||
PLATFORM = 'xiaomi_miio'
|
DATA_KEY = 'fan.xiaomi_miio'
|
||||||
|
|
||||||
|
CONF_MODEL = 'model'
|
||||||
|
MODEL_AIRPURIFIER_PRO = 'zhimi.airpurifier.v6'
|
||||||
|
MODEL_AIRPURIFIER_V3 = 'zhimi.airpurifier.v3'
|
||||||
|
MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1'
|
||||||
|
MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1'
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
|
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_MODEL): vol.In(
|
||||||
|
['zhimi.airpurifier.m1',
|
||||||
|
'zhimi.airpurifier.m2',
|
||||||
|
'zhimi.airpurifier.ma1',
|
||||||
|
'zhimi.airpurifier.ma2',
|
||||||
|
'zhimi.airpurifier.sa1',
|
||||||
|
'zhimi.airpurifier.sa2',
|
||||||
|
'zhimi.airpurifier.v1',
|
||||||
|
'zhimi.airpurifier.v2',
|
||||||
|
'zhimi.airpurifier.v3',
|
||||||
|
'zhimi.airpurifier.v5',
|
||||||
|
'zhimi.airpurifier.v6',
|
||||||
|
'zhimi.humidifier.v1',
|
||||||
|
'zhimi.humidifier.ca1']),
|
||||||
})
|
})
|
||||||
|
|
||||||
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
|
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
|
||||||
|
|
||||||
|
ATTR_MODEL = 'model'
|
||||||
|
|
||||||
|
# Air Purifier
|
||||||
ATTR_TEMPERATURE = 'temperature'
|
ATTR_TEMPERATURE = 'temperature'
|
||||||
ATTR_HUMIDITY = 'humidity'
|
ATTR_HUMIDITY = 'humidity'
|
||||||
ATTR_AIR_QUALITY_INDEX = 'aqi'
|
ATTR_AIR_QUALITY_INDEX = 'aqi'
|
||||||
@ -45,20 +68,190 @@ ATTR_LED_BRIGHTNESS = 'led_brightness'
|
|||||||
ATTR_MOTOR_SPEED = 'motor_speed'
|
ATTR_MOTOR_SPEED = 'motor_speed'
|
||||||
ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi'
|
ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi'
|
||||||
ATTR_PURIFY_VOLUME = 'purify_volume'
|
ATTR_PURIFY_VOLUME = 'purify_volume'
|
||||||
|
|
||||||
ATTR_BRIGHTNESS = 'brightness'
|
ATTR_BRIGHTNESS = 'brightness'
|
||||||
ATTR_LEVEL = 'level'
|
ATTR_LEVEL = 'level'
|
||||||
|
ATTR_MOTOR2_SPEED = 'motor2_speed'
|
||||||
|
ATTR_ILLUMINANCE = 'illuminance'
|
||||||
|
ATTR_FILTER_RFID_PRODUCT_ID = 'filter_rfid_product_id'
|
||||||
|
ATTR_FILTER_RFID_TAG = 'filter_rfid_tag'
|
||||||
|
ATTR_FILTER_TYPE = 'filter_type'
|
||||||
|
ATTR_LEARN_MODE = 'learn_mode'
|
||||||
|
ATTR_SLEEP_TIME = 'sleep_time'
|
||||||
|
ATTR_SLEEP_LEARN_COUNT = 'sleep_mode_learn_count'
|
||||||
|
ATTR_EXTRA_FEATURES = 'extra_features'
|
||||||
|
ATTR_FEATURES = 'features'
|
||||||
|
ATTR_TURBO_MODE_SUPPORTED = 'turbo_mode_supported'
|
||||||
|
ATTR_AUTO_DETECT = 'auto_detect'
|
||||||
|
ATTR_SLEEP_MODE = 'sleep_mode'
|
||||||
|
ATTR_VOLUME = 'volume'
|
||||||
|
ATTR_USE_TIME = 'use_time'
|
||||||
|
ATTR_BUTTON_PRESSED = 'button_pressed'
|
||||||
|
|
||||||
|
# Air Humidifier
|
||||||
|
ATTR_TARGET_HUMIDITY = 'target_humidity'
|
||||||
|
ATTR_TRANS_LEVEL = 'trans_level'
|
||||||
|
ATTR_HARDWARE_VERSION = 'hardware_version'
|
||||||
|
|
||||||
|
# Air Humidifier CA
|
||||||
|
ATTR_SPEED = 'speed'
|
||||||
|
ATTR_DEPTH = 'depth'
|
||||||
|
ATTR_DRY = 'dry'
|
||||||
|
|
||||||
|
# Map attributes to properties of the state object
|
||||||
|
AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = {
|
||||||
|
ATTR_TEMPERATURE: 'temperature',
|
||||||
|
ATTR_HUMIDITY: 'humidity',
|
||||||
|
ATTR_AIR_QUALITY_INDEX: 'aqi',
|
||||||
|
ATTR_MODE: 'mode',
|
||||||
|
ATTR_FILTER_HOURS_USED: 'filter_hours_used',
|
||||||
|
ATTR_FILTER_LIFE: 'filter_life_remaining',
|
||||||
|
ATTR_FAVORITE_LEVEL: 'favorite_level',
|
||||||
|
ATTR_CHILD_LOCK: 'child_lock',
|
||||||
|
ATTR_LED: 'led',
|
||||||
|
ATTR_MOTOR_SPEED: 'motor_speed',
|
||||||
|
ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi',
|
||||||
|
ATTR_PURIFY_VOLUME: 'purify_volume',
|
||||||
|
ATTR_LEARN_MODE: 'learn_mode',
|
||||||
|
ATTR_SLEEP_TIME: 'sleep_time',
|
||||||
|
ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
|
||||||
|
ATTR_EXTRA_FEATURES: 'extra_features',
|
||||||
|
ATTR_TURBO_MODE_SUPPORTED: 'turbo_mode_supported',
|
||||||
|
ATTR_AUTO_DETECT: 'auto_detect',
|
||||||
|
ATTR_USE_TIME: 'use_time',
|
||||||
|
ATTR_BUTTON_PRESSED: 'button_pressed',
|
||||||
|
}
|
||||||
|
|
||||||
|
AVAILABLE_ATTRIBUTES_AIRPURIFIER = {
|
||||||
|
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
|
||||||
|
ATTR_BUZZER: 'buzzer',
|
||||||
|
ATTR_LED_BRIGHTNESS: 'led_brightness',
|
||||||
|
ATTR_SLEEP_MODE: 'sleep_mode',
|
||||||
|
}
|
||||||
|
|
||||||
|
AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = {
|
||||||
|
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
|
||||||
|
ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
|
||||||
|
ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
|
||||||
|
ATTR_FILTER_TYPE: 'filter_type',
|
||||||
|
ATTR_ILLUMINANCE: 'illuminance',
|
||||||
|
ATTR_MOTOR2_SPEED: 'motor2_speed',
|
||||||
|
ATTR_VOLUME: 'volume',
|
||||||
|
}
|
||||||
|
|
||||||
|
AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = {
|
||||||
|
# Common set isn't used here. It's a very basic version of the device.
|
||||||
|
ATTR_AIR_QUALITY_INDEX: 'aqi',
|
||||||
|
ATTR_MODE: 'mode',
|
||||||
|
ATTR_LED: 'led',
|
||||||
|
ATTR_BUZZER: 'buzzer',
|
||||||
|
ATTR_CHILD_LOCK: 'child_lock',
|
||||||
|
ATTR_ILLUMINANCE: 'illuminance',
|
||||||
|
ATTR_FILTER_HOURS_USED: 'filter_hours_used',
|
||||||
|
ATTR_FILTER_LIFE: 'filter_life_remaining',
|
||||||
|
ATTR_MOTOR_SPEED: 'motor_speed',
|
||||||
|
# perhaps supported but unconfirmed
|
||||||
|
ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi',
|
||||||
|
ATTR_VOLUME: 'volume',
|
||||||
|
ATTR_MOTOR2_SPEED: 'motor2_speed',
|
||||||
|
ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
|
||||||
|
ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
|
||||||
|
ATTR_FILTER_TYPE: 'filter_type',
|
||||||
|
ATTR_PURIFY_VOLUME: 'purify_volume',
|
||||||
|
ATTR_LEARN_MODE: 'learn_mode',
|
||||||
|
ATTR_SLEEP_TIME: 'sleep_time',
|
||||||
|
ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
|
||||||
|
ATTR_EXTRA_FEATURES: 'extra_features',
|
||||||
|
ATTR_AUTO_DETECT: 'auto_detect',
|
||||||
|
ATTR_USE_TIME: 'use_time',
|
||||||
|
ATTR_BUTTON_PRESSED: 'button_pressed',
|
||||||
|
}
|
||||||
|
|
||||||
|
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = {
|
||||||
|
ATTR_TEMPERATURE: 'temperature',
|
||||||
|
ATTR_HUMIDITY: 'humidity',
|
||||||
|
ATTR_MODE: 'mode',
|
||||||
|
ATTR_BUZZER: 'buzzer',
|
||||||
|
ATTR_CHILD_LOCK: 'child_lock',
|
||||||
|
ATTR_TRANS_LEVEL: 'trans_level',
|
||||||
|
ATTR_TARGET_HUMIDITY: 'target_humidity',
|
||||||
|
ATTR_LED_BRIGHTNESS: 'led_brightness',
|
||||||
|
ATTR_BUTTON_PRESSED: 'button_pressed',
|
||||||
|
ATTR_USE_TIME: 'use_time',
|
||||||
|
ATTR_HARDWARE_VERSION: 'hardware_version',
|
||||||
|
}
|
||||||
|
|
||||||
|
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = {
|
||||||
|
**AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER,
|
||||||
|
ATTR_SPEED: 'speed',
|
||||||
|
ATTR_DEPTH: 'depth',
|
||||||
|
ATTR_DRY: 'dry',
|
||||||
|
}
|
||||||
|
|
||||||
|
OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle']
|
||||||
|
OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite']
|
||||||
|
OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle',
|
||||||
|
'Medium', 'High', 'Strong']
|
||||||
|
|
||||||
SUCCESS = ['ok']
|
SUCCESS = ['ok']
|
||||||
|
|
||||||
|
FEATURE_SET_BUZZER = 1
|
||||||
|
FEATURE_SET_LED = 2
|
||||||
|
FEATURE_SET_CHILD_LOCK = 4
|
||||||
|
FEATURE_SET_LED_BRIGHTNESS = 8
|
||||||
|
FEATURE_SET_FAVORITE_LEVEL = 16
|
||||||
|
FEATURE_SET_AUTO_DETECT = 32
|
||||||
|
FEATURE_SET_LEARN_MODE = 64
|
||||||
|
FEATURE_SET_VOLUME = 128
|
||||||
|
FEATURE_RESET_FILTER = 256
|
||||||
|
FEATURE_SET_EXTRA_FEATURES = 512
|
||||||
|
FEATURE_SET_TARGET_HUMIDITY = 1024
|
||||||
|
FEATURE_SET_DRY = 2048
|
||||||
|
|
||||||
|
FEATURE_FLAGS_GENERIC = (FEATURE_SET_BUZZER |
|
||||||
|
FEATURE_SET_CHILD_LOCK)
|
||||||
|
|
||||||
|
FEATURE_FLAGS_AIRPURIFIER = (FEATURE_FLAGS_GENERIC |
|
||||||
|
FEATURE_SET_LED |
|
||||||
|
FEATURE_SET_LED_BRIGHTNESS |
|
||||||
|
FEATURE_SET_FAVORITE_LEVEL |
|
||||||
|
FEATURE_SET_LEARN_MODE |
|
||||||
|
FEATURE_RESET_FILTER |
|
||||||
|
FEATURE_SET_EXTRA_FEATURES)
|
||||||
|
|
||||||
|
FEATURE_FLAGS_AIRPURIFIER_PRO = (FEATURE_SET_CHILD_LOCK |
|
||||||
|
FEATURE_SET_LED |
|
||||||
|
FEATURE_SET_FAVORITE_LEVEL |
|
||||||
|
FEATURE_SET_AUTO_DETECT |
|
||||||
|
FEATURE_SET_VOLUME)
|
||||||
|
|
||||||
|
FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_FLAGS_GENERIC |
|
||||||
|
FEATURE_SET_LED)
|
||||||
|
|
||||||
|
FEATURE_FLAGS_AIRHUMIDIFIER = (FEATURE_FLAGS_GENERIC |
|
||||||
|
FEATURE_SET_LED_BRIGHTNESS |
|
||||||
|
FEATURE_SET_TARGET_HUMIDITY)
|
||||||
|
|
||||||
|
FEATURE_FLAGS_AIRHUMIDIFIER_CA = (FEATURE_FLAGS_AIRHUMIDIFIER |
|
||||||
|
FEATURE_SET_DRY)
|
||||||
|
|
||||||
SERVICE_SET_BUZZER_ON = 'xiaomi_miio_set_buzzer_on'
|
SERVICE_SET_BUZZER_ON = 'xiaomi_miio_set_buzzer_on'
|
||||||
SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off'
|
SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off'
|
||||||
SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on'
|
SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on'
|
||||||
SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off'
|
SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off'
|
||||||
SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on'
|
SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on'
|
||||||
SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off'
|
SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off'
|
||||||
SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level'
|
|
||||||
SERVICE_SET_LED_BRIGHTNESS = 'xiaomi_miio_set_led_brightness'
|
SERVICE_SET_LED_BRIGHTNESS = 'xiaomi_miio_set_led_brightness'
|
||||||
|
SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level'
|
||||||
|
SERVICE_SET_AUTO_DETECT_ON = 'xiaomi_miio_set_auto_detect_on'
|
||||||
|
SERVICE_SET_AUTO_DETECT_OFF = 'xiaomi_miio_set_auto_detect_off'
|
||||||
|
SERVICE_SET_LEARN_MODE_ON = 'xiaomi_miio_set_learn_mode_on'
|
||||||
|
SERVICE_SET_LEARN_MODE_OFF = 'xiaomi_miio_set_learn_mode_off'
|
||||||
|
SERVICE_SET_VOLUME = 'xiaomi_miio_set_volume'
|
||||||
|
SERVICE_RESET_FILTER = 'xiaomi_miio_reset_filter'
|
||||||
|
SERVICE_SET_EXTRA_FEATURES = 'xiaomi_miio_set_extra_features'
|
||||||
|
SERVICE_SET_TARGET_HUMIDITY = 'xiaomi_miio_set_target_humidity'
|
||||||
|
SERVICE_SET_DRY_ON = 'xiaomi_miio_set_dry_on'
|
||||||
|
SERVICE_SET_DRY_OFF = 'xiaomi_miio_set_dry_off'
|
||||||
|
|
||||||
AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({
|
AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
@ -74,6 +267,21 @@ SERVICE_SCHEMA_FAVORITE_LEVEL = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
|||||||
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=16))
|
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=16))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
SERVICE_SCHEMA_VOLUME = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
||||||
|
vol.Required(ATTR_VOLUME):
|
||||||
|
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100))
|
||||||
|
})
|
||||||
|
|
||||||
|
SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
||||||
|
vol.Required(ATTR_FEATURES):
|
||||||
|
vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||||
|
})
|
||||||
|
|
||||||
|
SERVICE_SCHEMA_TARGET_HUMIDITY = AIRPURIFIER_SERVICE_SCHEMA.extend({
|
||||||
|
vol.Required(ATTR_HUMIDITY):
|
||||||
|
vol.All(vol.Coerce(int), vol.In([30, 40, 50, 60, 70, 80]))
|
||||||
|
})
|
||||||
|
|
||||||
SERVICE_TO_METHOD = {
|
SERVICE_TO_METHOD = {
|
||||||
SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'},
|
SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'},
|
||||||
SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'},
|
SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'},
|
||||||
@ -81,59 +289,99 @@ SERVICE_TO_METHOD = {
|
|||||||
SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'},
|
SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'},
|
||||||
SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'},
|
SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'},
|
||||||
SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'},
|
SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'},
|
||||||
SERVICE_SET_FAVORITE_LEVEL: {
|
SERVICE_SET_AUTO_DETECT_ON: {'method': 'async_set_auto_detect_on'},
|
||||||
'method': 'async_set_favorite_level',
|
SERVICE_SET_AUTO_DETECT_OFF: {'method': 'async_set_auto_detect_off'},
|
||||||
'schema': SERVICE_SCHEMA_FAVORITE_LEVEL},
|
SERVICE_SET_LEARN_MODE_ON: {'method': 'async_set_learn_mode_on'},
|
||||||
|
SERVICE_SET_LEARN_MODE_OFF: {'method': 'async_set_learn_mode_off'},
|
||||||
|
SERVICE_RESET_FILTER: {'method': 'async_reset_filter'},
|
||||||
SERVICE_SET_LED_BRIGHTNESS: {
|
SERVICE_SET_LED_BRIGHTNESS: {
|
||||||
'method': 'async_set_led_brightness',
|
'method': 'async_set_led_brightness',
|
||||||
'schema': SERVICE_SCHEMA_LED_BRIGHTNESS},
|
'schema': SERVICE_SCHEMA_LED_BRIGHTNESS},
|
||||||
|
SERVICE_SET_FAVORITE_LEVEL: {
|
||||||
|
'method': 'async_set_favorite_level',
|
||||||
|
'schema': SERVICE_SCHEMA_FAVORITE_LEVEL},
|
||||||
|
SERVICE_SET_VOLUME: {
|
||||||
|
'method': 'async_set_volume',
|
||||||
|
'schema': SERVICE_SCHEMA_VOLUME},
|
||||||
|
SERVICE_SET_EXTRA_FEATURES: {
|
||||||
|
'method': 'async_set_extra_features',
|
||||||
|
'schema': SERVICE_SCHEMA_EXTRA_FEATURES},
|
||||||
|
SERVICE_SET_TARGET_HUMIDITY: {
|
||||||
|
'method': 'async_set_target_humidity',
|
||||||
|
'schema': SERVICE_SCHEMA_TARGET_HUMIDITY},
|
||||||
|
SERVICE_SET_DRY_ON: {'method': 'async_set_dry_on'},
|
||||||
|
SERVICE_SET_DRY_OFF: {'method': 'async_set_dry_off'},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
@asyncio.coroutine
|
async def async_setup_platform(hass, config, async_add_devices,
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
discovery_info=None):
|
||||||
"""Set up the air purifier from config."""
|
"""Set up the miio fan device from config."""
|
||||||
from miio import AirPurifier, DeviceException
|
from miio import Device, DeviceException
|
||||||
if PLATFORM not in hass.data:
|
if DATA_KEY not in hass.data:
|
||||||
hass.data[PLATFORM] = {}
|
hass.data[DATA_KEY] = {}
|
||||||
|
|
||||||
host = config.get(CONF_HOST)
|
host = config.get(CONF_HOST)
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
token = config.get(CONF_TOKEN)
|
token = config.get(CONF_TOKEN)
|
||||||
|
model = config.get(CONF_MODEL)
|
||||||
|
|
||||||
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
|
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
|
||||||
|
unique_id = None
|
||||||
|
|
||||||
|
if model is None:
|
||||||
try:
|
try:
|
||||||
air_purifier = AirPurifier(host, token)
|
miio_device = Device(host, token)
|
||||||
|
device_info = miio_device.info()
|
||||||
xiaomi_air_purifier = XiaomiAirPurifier(name, air_purifier)
|
model = device_info.model
|
||||||
hass.data[PLATFORM][host] = xiaomi_air_purifier
|
unique_id = "{}-{}".format(model, device_info.mac_address)
|
||||||
|
_LOGGER.info("%s %s %s detected",
|
||||||
|
model,
|
||||||
|
device_info.firmware_version,
|
||||||
|
device_info.hardware_version)
|
||||||
except DeviceException:
|
except DeviceException:
|
||||||
raise PlatformNotReady
|
raise PlatformNotReady
|
||||||
|
|
||||||
async_add_devices([xiaomi_air_purifier], update_before_add=True)
|
if model.startswith('zhimi.airpurifier.'):
|
||||||
|
from miio import AirPurifier
|
||||||
|
air_purifier = AirPurifier(host, token)
|
||||||
|
device = XiaomiAirPurifier(name, air_purifier, model, unique_id)
|
||||||
|
elif model.startswith('zhimi.humidifier.'):
|
||||||
|
from miio import AirHumidifier
|
||||||
|
air_humidifier = AirHumidifier(host, token)
|
||||||
|
device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id)
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
'Unsupported device found! Please create an issue at '
|
||||||
|
'https://github.com/syssi/xiaomi_airpurifier/issues '
|
||||||
|
'and provide the following data: %s', model)
|
||||||
|
return False
|
||||||
|
|
||||||
@asyncio.coroutine
|
hass.data[DATA_KEY][host] = device
|
||||||
def async_service_handler(service):
|
async_add_devices([device], update_before_add=True)
|
||||||
|
|
||||||
|
async def async_service_handler(service):
|
||||||
"""Map services to methods on XiaomiAirPurifier."""
|
"""Map services to methods on XiaomiAirPurifier."""
|
||||||
method = SERVICE_TO_METHOD.get(service.service)
|
method = SERVICE_TO_METHOD.get(service.service)
|
||||||
params = {key: value for key, value in service.data.items()
|
params = {key: value for key, value in service.data.items()
|
||||||
if key != ATTR_ENTITY_ID}
|
if key != ATTR_ENTITY_ID}
|
||||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||||
if entity_ids:
|
if entity_ids:
|
||||||
devices = [device for device in hass.data[PLATFORM].values() if
|
devices = [device for device in hass.data[DATA_KEY].values() if
|
||||||
device.entity_id in entity_ids]
|
device.entity_id in entity_ids]
|
||||||
else:
|
else:
|
||||||
devices = hass.data[PLATFORM].values()
|
devices = hass.data[DATA_KEY].values()
|
||||||
|
|
||||||
update_tasks = []
|
update_tasks = []
|
||||||
for device in devices:
|
for device in devices:
|
||||||
yield from getattr(device, method['method'])(**params)
|
if not hasattr(device, method['method']):
|
||||||
|
continue
|
||||||
|
await getattr(device, method['method'])(**params)
|
||||||
update_tasks.append(device.async_update_ha_state(True))
|
update_tasks.append(device.async_update_ha_state(True))
|
||||||
|
|
||||||
if update_tasks:
|
if update_tasks:
|
||||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
await asyncio.wait(update_tasks, loop=hass.loop)
|
||||||
|
|
||||||
for air_purifier_service in SERVICE_TO_METHOD:
|
for air_purifier_service in SERVICE_TO_METHOD:
|
||||||
schema = SERVICE_TO_METHOD[air_purifier_service].get(
|
schema = SERVICE_TO_METHOD[air_purifier_service].get(
|
||||||
@ -142,31 +390,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
DOMAIN, air_purifier_service, async_service_handler, schema=schema)
|
DOMAIN, air_purifier_service, async_service_handler, schema=schema)
|
||||||
|
|
||||||
|
|
||||||
class XiaomiAirPurifier(FanEntity):
|
class XiaomiGenericDevice(FanEntity):
|
||||||
"""Representation of a Xiaomi Air Purifier."""
|
"""Representation of a generic Xiaomi device."""
|
||||||
|
|
||||||
def __init__(self, name, air_purifier):
|
def __init__(self, name, device, model, unique_id):
|
||||||
"""Initialize the air purifier."""
|
"""Initialize the generic Xiaomi device."""
|
||||||
self._name = name
|
self._name = name
|
||||||
|
self._device = device
|
||||||
|
self._model = model
|
||||||
|
self._unique_id = unique_id
|
||||||
|
|
||||||
self._air_purifier = air_purifier
|
self._available = False
|
||||||
self._state = None
|
self._state = None
|
||||||
self._state_attrs = {
|
self._state_attrs = {
|
||||||
ATTR_AIR_QUALITY_INDEX: None,
|
ATTR_MODEL: self._model,
|
||||||
ATTR_TEMPERATURE: None,
|
|
||||||
ATTR_HUMIDITY: None,
|
|
||||||
ATTR_MODE: None,
|
|
||||||
ATTR_FILTER_HOURS_USED: None,
|
|
||||||
ATTR_FILTER_LIFE: None,
|
|
||||||
ATTR_FAVORITE_LEVEL: None,
|
|
||||||
ATTR_BUZZER: None,
|
|
||||||
ATTR_CHILD_LOCK: None,
|
|
||||||
ATTR_LED: None,
|
|
||||||
ATTR_LED_BRIGHTNESS: None,
|
|
||||||
ATTR_MOTOR_SPEED: None,
|
|
||||||
ATTR_AVERAGE_AIR_QUALITY_INDEX: None,
|
|
||||||
ATTR_PURIFY_VOLUME: None,
|
|
||||||
}
|
}
|
||||||
|
self._device_features = FEATURE_FLAGS_GENERIC
|
||||||
self._skip_update = False
|
self._skip_update = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -176,9 +415,14 @@ class XiaomiAirPurifier(FanEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
"""Poll the fan."""
|
"""Poll the device."""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return an unique ID."""
|
||||||
|
return self._unique_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device if any."""
|
"""Return the name of the device if any."""
|
||||||
@ -187,7 +431,7 @@ class XiaomiAirPurifier(FanEntity):
|
|||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
"""Return true when state is known."""
|
"""Return true when state is known."""
|
||||||
return self._state is not None
|
return self._available
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
@ -196,50 +440,116 @@ class XiaomiAirPurifier(FanEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if fan is on."""
|
"""Return true if device is on."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@asyncio.coroutine
|
@staticmethod
|
||||||
def _try_command(self, mask_error, func, *args, **kwargs):
|
def _extract_value_from_attribute(state, attribute):
|
||||||
"""Call an air purifier command handling error messages."""
|
value = getattr(state, attribute)
|
||||||
|
if isinstance(value, Enum):
|
||||||
|
return value.value
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
async def _try_command(self, mask_error, func, *args, **kwargs):
|
||||||
|
"""Call a miio device command handling error messages."""
|
||||||
from miio import DeviceException
|
from miio import DeviceException
|
||||||
try:
|
try:
|
||||||
result = yield from self.hass.async_add_job(
|
result = await self.hass.async_add_job(
|
||||||
partial(func, *args, **kwargs))
|
partial(func, *args, **kwargs))
|
||||||
|
|
||||||
_LOGGER.debug("Response received from air purifier: %s", result)
|
_LOGGER.debug("Response received from miio device: %s", result)
|
||||||
|
|
||||||
return result == SUCCESS
|
return result == SUCCESS
|
||||||
except DeviceException as exc:
|
except DeviceException as exc:
|
||||||
_LOGGER.error(mask_error, exc)
|
_LOGGER.error(mask_error, exc)
|
||||||
|
self._available = False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_turn_on(self, speed: str = None,
|
||||||
def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None:
|
**kwargs) -> None:
|
||||||
"""Turn the fan on."""
|
"""Turn the device on."""
|
||||||
if speed:
|
if speed:
|
||||||
# If operation mode was set the device must not be turned on.
|
# If operation mode was set the device must not be turned on.
|
||||||
result = yield from self.async_set_speed(speed)
|
result = await self.async_set_speed(speed)
|
||||||
else:
|
else:
|
||||||
result = yield from self._try_command(
|
result = await self._try_command(
|
||||||
"Turning the air purifier on failed.", self._air_purifier.on)
|
"Turning the miio device on failed.", self._device.on)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
self._state = True
|
self._state = True
|
||||||
self._skip_update = True
|
self._skip_update = True
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_turn_off(self, **kwargs) -> None:
|
||||||
def async_turn_off(self: ToggleEntity, **kwargs) -> None:
|
"""Turn the device off."""
|
||||||
"""Turn the fan off."""
|
result = await self._try_command(
|
||||||
result = yield from self._try_command(
|
"Turning the miio device off failed.", self._device.off)
|
||||||
"Turning the air purifier off failed.", self._air_purifier.off)
|
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
self._state = False
|
self._state = False
|
||||||
self._skip_update = True
|
self._skip_update = True
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_set_buzzer_on(self):
|
||||||
def async_update(self):
|
"""Turn the buzzer on."""
|
||||||
|
if self._device_features & FEATURE_SET_BUZZER == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Turning the buzzer of the miio device on failed.",
|
||||||
|
self._device.set_buzzer, True)
|
||||||
|
|
||||||
|
async def async_set_buzzer_off(self):
|
||||||
|
"""Turn the buzzer off."""
|
||||||
|
if self._device_features & FEATURE_SET_BUZZER == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Turning the buzzer of the miio device off failed.",
|
||||||
|
self._device.set_buzzer, False)
|
||||||
|
|
||||||
|
async def async_set_child_lock_on(self):
|
||||||
|
"""Turn the child lock on."""
|
||||||
|
if self._device_features & FEATURE_SET_CHILD_LOCK == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Turning the child lock of the miio device on failed.",
|
||||||
|
self._device.set_child_lock, True)
|
||||||
|
|
||||||
|
async def async_set_child_lock_off(self):
|
||||||
|
"""Turn the child lock off."""
|
||||||
|
if self._device_features & FEATURE_SET_CHILD_LOCK == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Turning the child lock of the miio device off failed.",
|
||||||
|
self._device.set_child_lock, False)
|
||||||
|
|
||||||
|
|
||||||
|
class XiaomiAirPurifier(XiaomiGenericDevice):
|
||||||
|
"""Representation of a Xiaomi Air Purifier."""
|
||||||
|
|
||||||
|
def __init__(self, name, device, model, unique_id):
|
||||||
|
"""Initialize the plug switch."""
|
||||||
|
super().__init__(name, device, model, unique_id)
|
||||||
|
|
||||||
|
if self._model == MODEL_AIRPURIFIER_PRO:
|
||||||
|
self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO
|
||||||
|
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO
|
||||||
|
self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO
|
||||||
|
elif self._model == MODEL_AIRPURIFIER_V3:
|
||||||
|
self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3
|
||||||
|
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3
|
||||||
|
self._speed_list = OPERATION_MODES_AIRPURIFIER_V3
|
||||||
|
else:
|
||||||
|
self._device_features = FEATURE_FLAGS_AIRPURIFIER
|
||||||
|
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER
|
||||||
|
self._speed_list = OPERATION_MODES_AIRPURIFIER
|
||||||
|
|
||||||
|
self._state_attrs.update(
|
||||||
|
{attribute: None for attribute in self._available_attributes})
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
"""Fetch state from the device."""
|
"""Fetch state from the device."""
|
||||||
from miio import DeviceException
|
from miio import DeviceException
|
||||||
|
|
||||||
@ -249,40 +559,24 @@ class XiaomiAirPurifier(FanEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
state = yield from self.hass.async_add_job(
|
state = await self.hass.async_add_job(
|
||||||
self._air_purifier.status)
|
self._device.status)
|
||||||
_LOGGER.debug("Got new state: %s", state)
|
_LOGGER.debug("Got new state: %s", state)
|
||||||
|
|
||||||
|
self._available = True
|
||||||
self._state = state.is_on
|
self._state = state.is_on
|
||||||
self._state_attrs = {
|
self._state_attrs.update(
|
||||||
ATTR_TEMPERATURE: state.temperature,
|
{key: self._extract_value_from_attribute(state, value) for
|
||||||
ATTR_HUMIDITY: state.humidity,
|
key, value in self._available_attributes.items()})
|
||||||
ATTR_AIR_QUALITY_INDEX: state.aqi,
|
|
||||||
ATTR_MODE: state.mode.value,
|
|
||||||
ATTR_FILTER_HOURS_USED: state.filter_hours_used,
|
|
||||||
ATTR_FILTER_LIFE: state.filter_life_remaining,
|
|
||||||
ATTR_FAVORITE_LEVEL: state.favorite_level,
|
|
||||||
ATTR_BUZZER: state.buzzer,
|
|
||||||
ATTR_CHILD_LOCK: state.child_lock,
|
|
||||||
ATTR_LED: state.led,
|
|
||||||
ATTR_MOTOR_SPEED: state.motor_speed,
|
|
||||||
ATTR_AVERAGE_AIR_QUALITY_INDEX: state.average_aqi,
|
|
||||||
ATTR_PURIFY_VOLUME: state.purify_volume,
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.led_brightness:
|
|
||||||
self._state_attrs[
|
|
||||||
ATTR_LED_BRIGHTNESS] = state.led_brightness.value
|
|
||||||
|
|
||||||
except DeviceException as ex:
|
except DeviceException as ex:
|
||||||
self._state = None
|
self._available = False
|
||||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def speed_list(self: ToggleEntity) -> list:
|
def speed_list(self) -> list:
|
||||||
"""Get the list of available speeds."""
|
"""Get the list of available speeds."""
|
||||||
from miio.airpurifier import OperationMode
|
return self._speed_list
|
||||||
return [mode.name for mode in OperationMode]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def speed(self):
|
def speed(self):
|
||||||
@ -294,70 +588,227 @@ class XiaomiAirPurifier(FanEntity):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_set_speed(self, speed: str) -> None:
|
||||||
def async_set_speed(self: ToggleEntity, speed: str) -> None:
|
|
||||||
"""Set the speed of the fan."""
|
"""Set the speed of the fan."""
|
||||||
_LOGGER.debug("Setting the operation mode to: %s", speed)
|
if self.supported_features & SUPPORT_SET_SPEED == 0:
|
||||||
|
return
|
||||||
|
|
||||||
from miio.airpurifier import OperationMode
|
from miio.airpurifier import OperationMode
|
||||||
|
|
||||||
yield from self._try_command(
|
_LOGGER.debug("Setting the operation mode to: %s", speed)
|
||||||
"Setting operation mode of the air purifier failed.",
|
|
||||||
self._air_purifier.set_mode, OperationMode[speed.title()])
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
await self._try_command(
|
||||||
def async_set_buzzer_on(self):
|
"Setting operation mode of the miio device failed.",
|
||||||
"""Turn the buzzer on."""
|
self._device.set_mode, OperationMode[speed.title()])
|
||||||
yield from self._try_command(
|
|
||||||
"Turning the buzzer of the air purifier on failed.",
|
|
||||||
self._air_purifier.set_buzzer, True)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_set_led_on(self):
|
||||||
def async_set_buzzer_off(self):
|
|
||||||
"""Turn the buzzer off."""
|
|
||||||
yield from self._try_command(
|
|
||||||
"Turning the buzzer of the air purifier off failed.",
|
|
||||||
self._air_purifier.set_buzzer, False)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def async_set_led_on(self):
|
|
||||||
"""Turn the led on."""
|
"""Turn the led on."""
|
||||||
yield from self._try_command(
|
if self._device_features & FEATURE_SET_LED == 0:
|
||||||
"Turning the led of the air purifier off failed.",
|
return
|
||||||
self._air_purifier.set_led, True)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
await self._try_command(
|
||||||
def async_set_led_off(self):
|
"Turning the led of the miio device off failed.",
|
||||||
|
self._device.set_led, True)
|
||||||
|
|
||||||
|
async def async_set_led_off(self):
|
||||||
"""Turn the led off."""
|
"""Turn the led off."""
|
||||||
yield from self._try_command(
|
if self._device_features & FEATURE_SET_LED == 0:
|
||||||
"Turning the led of the air purifier off failed.",
|
return
|
||||||
self._air_purifier.set_led, False)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
await self._try_command(
|
||||||
def async_set_child_lock_on(self):
|
"Turning the led of the miio device off failed.",
|
||||||
"""Turn the child lock on."""
|
self._device.set_led, False)
|
||||||
yield from self._try_command(
|
|
||||||
"Turning the child lock of the air purifier on failed.",
|
|
||||||
self._air_purifier.set_child_lock, True)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_set_led_brightness(self, brightness: int = 2):
|
||||||
def async_set_child_lock_off(self):
|
|
||||||
"""Turn the child lock off."""
|
|
||||||
yield from self._try_command(
|
|
||||||
"Turning the child lock of the air purifier off failed.",
|
|
||||||
self._air_purifier.set_child_lock, False)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def async_set_led_brightness(self, brightness: int = 2):
|
|
||||||
"""Set the led brightness."""
|
"""Set the led brightness."""
|
||||||
|
if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
|
||||||
|
return
|
||||||
|
|
||||||
from miio.airpurifier import LedBrightness
|
from miio.airpurifier import LedBrightness
|
||||||
|
|
||||||
yield from self._try_command(
|
await self._try_command(
|
||||||
"Setting the led brightness of the air purifier failed.",
|
"Setting the led brightness of the miio device failed.",
|
||||||
self._air_purifier.set_led_brightness, LedBrightness(brightness))
|
self._device.set_led_brightness, LedBrightness(brightness))
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_set_favorite_level(self, level: int = 1):
|
||||||
def async_set_favorite_level(self, level: int = 1):
|
|
||||||
"""Set the favorite level."""
|
"""Set the favorite level."""
|
||||||
yield from self._try_command(
|
if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0:
|
||||||
"Setting the favorite level of the air purifier failed.",
|
return
|
||||||
self._air_purifier.set_favorite_level, level)
|
|
||||||
|
await self._try_command(
|
||||||
|
"Setting the favorite level of the miio device failed.",
|
||||||
|
self._device.set_favorite_level, level)
|
||||||
|
|
||||||
|
async def async_set_auto_detect_on(self):
|
||||||
|
"""Turn the auto detect on."""
|
||||||
|
if self._device_features & FEATURE_SET_AUTO_DETECT == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Turning the auto detect of the miio device on failed.",
|
||||||
|
self._device.set_auto_detect, True)
|
||||||
|
|
||||||
|
async def async_set_auto_detect_off(self):
|
||||||
|
"""Turn the auto detect off."""
|
||||||
|
if self._device_features & FEATURE_SET_AUTO_DETECT == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Turning the auto detect of the miio device off failed.",
|
||||||
|
self._device.set_auto_detect, False)
|
||||||
|
|
||||||
|
async def async_set_learn_mode_on(self):
|
||||||
|
"""Turn the learn mode on."""
|
||||||
|
if self._device_features & FEATURE_SET_LEARN_MODE == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Turning the learn mode of the miio device on failed.",
|
||||||
|
self._device.set_learn_mode, True)
|
||||||
|
|
||||||
|
async def async_set_learn_mode_off(self):
|
||||||
|
"""Turn the learn mode off."""
|
||||||
|
if self._device_features & FEATURE_SET_LEARN_MODE == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Turning the learn mode of the miio device off failed.",
|
||||||
|
self._device.set_learn_mode, False)
|
||||||
|
|
||||||
|
async def async_set_volume(self, volume: int = 50):
|
||||||
|
"""Set the sound volume."""
|
||||||
|
if self._device_features & FEATURE_SET_VOLUME == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Setting the sound volume of the miio device failed.",
|
||||||
|
self._device.set_volume, volume)
|
||||||
|
|
||||||
|
async def async_set_extra_features(self, features: int = 1):
|
||||||
|
"""Set the extra features."""
|
||||||
|
if self._device_features & FEATURE_SET_EXTRA_FEATURES == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Setting the extra features of the miio device failed.",
|
||||||
|
self._device.set_extra_features, features)
|
||||||
|
|
||||||
|
async def async_reset_filter(self):
|
||||||
|
"""Reset the filter lifetime and usage."""
|
||||||
|
if self._device_features & FEATURE_RESET_FILTER == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Resetting the filter lifetime of the miio device failed.",
|
||||||
|
self._device.reset_filter)
|
||||||
|
|
||||||
|
|
||||||
|
class XiaomiAirHumidifier(XiaomiGenericDevice):
|
||||||
|
"""Representation of a Xiaomi Air Humidifier."""
|
||||||
|
|
||||||
|
def __init__(self, name, device, model, unique_id):
|
||||||
|
"""Initialize the plug switch."""
|
||||||
|
from miio.airpurifier import OperationMode
|
||||||
|
|
||||||
|
super().__init__(name, device, model, unique_id)
|
||||||
|
|
||||||
|
if self._model == MODEL_AIRHUMIDIFIER_CA:
|
||||||
|
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA
|
||||||
|
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA
|
||||||
|
self._speed_list = [mode.name for mode in OperationMode]
|
||||||
|
else:
|
||||||
|
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER
|
||||||
|
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER
|
||||||
|
self._speed_list = [mode.name for mode in OperationMode if
|
||||||
|
mode.name != 'Auto']
|
||||||
|
|
||||||
|
self._state_attrs.update(
|
||||||
|
{attribute: None for attribute in self._available_attributes})
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Fetch state from the device."""
|
||||||
|
from miio import DeviceException
|
||||||
|
|
||||||
|
# On state change the device doesn't provide the new state immediately.
|
||||||
|
if self._skip_update:
|
||||||
|
self._skip_update = False
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
state = await self.hass.async_add_job(self._device.status)
|
||||||
|
_LOGGER.debug("Got new state: %s", state)
|
||||||
|
|
||||||
|
self._available = True
|
||||||
|
self._state = state.is_on
|
||||||
|
self._state_attrs.update(
|
||||||
|
{key: self._extract_value_from_attribute(state, value) for
|
||||||
|
key, value in self._available_attributes.items()})
|
||||||
|
|
||||||
|
except DeviceException as ex:
|
||||||
|
self._available = False
|
||||||
|
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||||
|
|
||||||
|
def speed_list(self) -> list:
|
||||||
|
"""Get the list of available speeds."""
|
||||||
|
return self._speed_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def speed(self):
|
||||||
|
"""Return the current speed."""
|
||||||
|
if self._state:
|
||||||
|
from miio.airhumidifier import OperationMode
|
||||||
|
|
||||||
|
return OperationMode(self._state_attrs[ATTR_MODE]).name
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_set_speed(self, speed: str) -> None:
|
||||||
|
"""Set the speed of the fan."""
|
||||||
|
if self.supported_features & SUPPORT_SET_SPEED == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
from miio.airhumidifier import OperationMode
|
||||||
|
|
||||||
|
_LOGGER.debug("Setting the operation mode to: %s", speed)
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Setting operation mode of the miio device failed.",
|
||||||
|
self._device.set_mode, OperationMode[speed.title()])
|
||||||
|
|
||||||
|
async def async_set_led_brightness(self, brightness: int = 2):
|
||||||
|
"""Set the led brightness."""
|
||||||
|
if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
from miio.airhumidifier import LedBrightness
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Setting the led brightness of the miio device failed.",
|
||||||
|
self._device.set_led_brightness, LedBrightness(brightness))
|
||||||
|
|
||||||
|
async def async_set_target_humidity(self, humidity: int = 40):
|
||||||
|
"""Set the target humidity."""
|
||||||
|
if self._device_features & FEATURE_SET_TARGET_HUMIDITY == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Setting the target humidity of the miio device failed.",
|
||||||
|
self._device.set_target_humidity, humidity)
|
||||||
|
|
||||||
|
async def async_set_dry_on(self):
|
||||||
|
"""Turn the dry mode on."""
|
||||||
|
if self._device_features & FEATURE_SET_DRY == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Turning the dry mode of the miio device off failed.",
|
||||||
|
self._device.set_dry, True)
|
||||||
|
|
||||||
|
async def async_set_dry_off(self):
|
||||||
|
"""Turn the dry mode off."""
|
||||||
|
if self._device_features & FEATURE_SET_DRY == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._try_command(
|
||||||
|
"Turning the dry mode of the miio device off failed.",
|
||||||
|
self._device.set_dry, False)
|
||||||
|
110
homeassistant/components/folder_watcher.py
Normal file
110
homeassistant/components/folder_watcher.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
"""
|
||||||
|
Component for monitoring activity on a folder.
|
||||||
|
|
||||||
|
For more details about this platform, refer to the documentation at
|
||||||
|
https://home-assistant.io/components/folder_watcher/
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import voluptuous as vol
|
||||||
|
from homeassistant.const import (
|
||||||
|
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
REQUIREMENTS = ['watchdog==0.8.3']
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_FOLDER = 'folder'
|
||||||
|
CONF_PATTERNS = 'patterns'
|
||||||
|
DEFAULT_PATTERN = '*'
|
||||||
|
DOMAIN = "folder_watcher"
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||||
|
vol.Required(CONF_FOLDER): cv.isdir,
|
||||||
|
vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]):
|
||||||
|
vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
})])
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Set up the folder watcher."""
|
||||||
|
conf = config[DOMAIN]
|
||||||
|
for watcher in conf:
|
||||||
|
path = watcher[CONF_FOLDER]
|
||||||
|
patterns = watcher[CONF_PATTERNS]
|
||||||
|
if not hass.config.is_allowed_path(path):
|
||||||
|
_LOGGER.error("folder %s is not valid or allowed", path)
|
||||||
|
return False
|
||||||
|
Watcher(path, patterns, hass)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def create_event_handler(patterns, hass):
|
||||||
|
""""Return the Watchdog EventHandler object."""
|
||||||
|
from watchdog.events import PatternMatchingEventHandler
|
||||||
|
|
||||||
|
class EventHandler(PatternMatchingEventHandler):
|
||||||
|
"""Class for handling Watcher events."""
|
||||||
|
|
||||||
|
def __init__(self, patterns, hass):
|
||||||
|
"""Initialise the EventHandler."""
|
||||||
|
super().__init__(patterns)
|
||||||
|
self.hass = hass
|
||||||
|
|
||||||
|
def process(self, event):
|
||||||
|
"""On Watcher event, fire HA event."""
|
||||||
|
_LOGGER.debug("process(%s)", event)
|
||||||
|
if not event.is_directory:
|
||||||
|
folder, file_name = os.path.split(event.src_path)
|
||||||
|
self.hass.bus.fire(
|
||||||
|
DOMAIN, {
|
||||||
|
"event_type": event.event_type,
|
||||||
|
'path': event.src_path,
|
||||||
|
'file': file_name,
|
||||||
|
'folder': folder,
|
||||||
|
})
|
||||||
|
|
||||||
|
def on_modified(self, event):
|
||||||
|
"""File modified."""
|
||||||
|
self.process(event)
|
||||||
|
|
||||||
|
def on_moved(self, event):
|
||||||
|
"""File moved."""
|
||||||
|
self.process(event)
|
||||||
|
|
||||||
|
def on_created(self, event):
|
||||||
|
"""File created."""
|
||||||
|
self.process(event)
|
||||||
|
|
||||||
|
def on_deleted(self, event):
|
||||||
|
"""File deleted."""
|
||||||
|
self.process(event)
|
||||||
|
|
||||||
|
return EventHandler(patterns, hass)
|
||||||
|
|
||||||
|
|
||||||
|
class Watcher():
|
||||||
|
"""Class for starting Watchdog."""
|
||||||
|
|
||||||
|
def __init__(self, path, patterns, hass):
|
||||||
|
"""Initialise the watchdog observer."""
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
self._observer = Observer()
|
||||||
|
self._observer.schedule(
|
||||||
|
create_event_handler(patterns, hass),
|
||||||
|
path,
|
||||||
|
recursive=True)
|
||||||
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.startup)
|
||||||
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown)
|
||||||
|
|
||||||
|
def startup(self, event):
|
||||||
|
"""Start the watcher."""
|
||||||
|
self._observer.start()
|
||||||
|
|
||||||
|
def shutdown(self, event):
|
||||||
|
"""Shutdown the watcher."""
|
||||||
|
self._observer.stop()
|
||||||
|
self._observer.join()
|
103
homeassistant/components/freedns.py
Normal file
103
homeassistant/components/freedns.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
"""
|
||||||
|
Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/freedns/
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import async_timeout
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DOMAIN = 'freedns'
|
||||||
|
|
||||||
|
DEFAULT_INTERVAL = timedelta(minutes=10)
|
||||||
|
|
||||||
|
TIMEOUT = 10
|
||||||
|
UPDATE_URL = 'https://freedns.afraid.org/dynamic/update.php'
|
||||||
|
|
||||||
|
CONF_UPDATE_INTERVAL = 'update_interval'
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
vol.Exclusive(CONF_URL, DOMAIN): cv.string,
|
||||||
|
vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string,
|
||||||
|
vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): vol.All(
|
||||||
|
cv.time_period, cv.positive_timedelta),
|
||||||
|
|
||||||
|
})
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup(hass, config):
|
||||||
|
"""Initialize the FreeDNS component."""
|
||||||
|
url = config[DOMAIN].get(CONF_URL)
|
||||||
|
auth_token = config[DOMAIN].get(CONF_ACCESS_TOKEN)
|
||||||
|
update_interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL)
|
||||||
|
|
||||||
|
session = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||||
|
|
||||||
|
result = yield from _update_freedns(
|
||||||
|
hass, session, url, auth_token)
|
||||||
|
|
||||||
|
if result is False:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def update_domain_callback(now):
|
||||||
|
"""Update the FreeDNS entry."""
|
||||||
|
yield from _update_freedns(hass, session, url, auth_token)
|
||||||
|
|
||||||
|
hass.helpers.event.async_track_time_interval(
|
||||||
|
update_domain_callback, update_interval)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _update_freedns(hass, session, url, auth_token):
|
||||||
|
"""Update FreeDNS."""
|
||||||
|
params = None
|
||||||
|
|
||||||
|
if url is None:
|
||||||
|
url = UPDATE_URL
|
||||||
|
|
||||||
|
if auth_token is not None:
|
||||||
|
params = {}
|
||||||
|
params[auth_token] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||||
|
resp = yield from session.get(url, params=params)
|
||||||
|
body = yield from resp.text()
|
||||||
|
|
||||||
|
if "has not changed" in body:
|
||||||
|
# IP has not changed.
|
||||||
|
_LOGGER.debug("FreeDNS update skipped: IP has not changed")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if "ERROR" not in body:
|
||||||
|
_LOGGER.debug("Updating FreeDNS was successful: %s", body)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if "Invalid update URL" in body:
|
||||||
|
_LOGGER.error("FreeDNS update token is invalid")
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Updating FreeDNS failed: %s", body)
|
||||||
|
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
_LOGGER.warning("Can't connect to FreeDNS API")
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.warning("Timeout from FreeDNS API at %s", url)
|
||||||
|
|
||||||
|
return False
|
@ -24,7 +24,7 @@ from homeassistant.core import callback
|
|||||||
from homeassistant.helpers.translation import async_get_translations
|
from homeassistant.helpers.translation import async_get_translations
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
|
|
||||||
REQUIREMENTS = ['home-assistant-frontend==20180401.0']
|
REQUIREMENTS = ['home-assistant-frontend==20180404.0']
|
||||||
|
|
||||||
DOMAIN = 'frontend'
|
DOMAIN = 'frontend'
|
||||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||||
|
@ -28,7 +28,7 @@ from .util import (
|
|||||||
TYPES = Registry()
|
TYPES = Registry()
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['HAP-python==1.1.7']
|
REQUIREMENTS = ['HAP-python==1.1.8']
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
@ -102,8 +102,7 @@ def get_accessory(hass, state, aid, config):
|
|||||||
aid=aid)
|
aid=aid)
|
||||||
|
|
||||||
elif state.domain == 'alarm_control_panel':
|
elif state.domain == 'alarm_control_panel':
|
||||||
_LOGGER.debug('Add "%s" as "%s"', state.entity_id,
|
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem')
|
||||||
'SecuritySystem')
|
|
||||||
return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
|
return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
|
||||||
alarm_code=config.get(ATTR_CODE),
|
alarm_code=config.get(ATTR_CODE),
|
||||||
aid=aid)
|
aid=aid)
|
||||||
@ -120,6 +119,7 @@ def get_accessory(hass, state, aid, config):
|
|||||||
state.name, support_auto, aid=aid)
|
state.name, support_auto, aid=aid)
|
||||||
|
|
||||||
elif state.domain == 'light':
|
elif state.domain == 'light':
|
||||||
|
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light')
|
||||||
return TYPES['Light'](hass, state.entity_id, state.name, aid=aid)
|
return TYPES['Light'](hass, state.entity_id, state.name, aid=aid)
|
||||||
|
|
||||||
elif state.domain == 'switch' or state.domain == 'remote' \
|
elif state.domain == 'switch' or state.domain == 'remote' \
|
||||||
|
@ -8,8 +8,8 @@ from homeassistant.helpers.event import async_track_state_change
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
|
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
|
||||||
MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE,
|
MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL,
|
||||||
CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
|
CHAR_NAME, CHAR_SERIAL_NUMBER)
|
||||||
from .util import (
|
from .util import (
|
||||||
show_setup_message, dismiss_setup_message)
|
show_setup_message, dismiss_setup_message)
|
||||||
|
|
||||||
@ -39,15 +39,6 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
|
|||||||
service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number)
|
service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number)
|
||||||
|
|
||||||
|
|
||||||
def override_properties(char, properties=None, valid_values=None):
|
|
||||||
"""Override characteristic property values and valid values."""
|
|
||||||
if properties:
|
|
||||||
char.properties.update(properties)
|
|
||||||
|
|
||||||
if valid_values:
|
|
||||||
char.properties['ValidValues'].update(valid_values)
|
|
||||||
|
|
||||||
|
|
||||||
class HomeAccessory(Accessory):
|
class HomeAccessory(Accessory):
|
||||||
"""Adapter class for Accessory."""
|
"""Adapter class for Accessory."""
|
||||||
|
|
||||||
@ -65,10 +56,10 @@ class HomeAccessory(Accessory):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Method called by accessory after driver is started."""
|
"""Method called by accessory after driver is started."""
|
||||||
state = self._hass.states.get(self._entity_id)
|
state = self.hass.states.get(self.entity_id)
|
||||||
self.update_state(new_state=state)
|
self.update_state(new_state=state)
|
||||||
async_track_state_change(
|
async_track_state_change(
|
||||||
self._hass, self._entity_id, self.update_state)
|
self.hass, self.entity_id, self.update_state)
|
||||||
|
|
||||||
|
|
||||||
class HomeBridge(Bridge):
|
class HomeBridge(Bridge):
|
||||||
@ -79,11 +70,10 @@ class HomeBridge(Bridge):
|
|||||||
"""Initialize a Bridge object."""
|
"""Initialize a Bridge object."""
|
||||||
super().__init__(name, **kwargs)
|
super().__init__(name, **kwargs)
|
||||||
set_accessory_info(self, name, model)
|
set_accessory_info(self, name, model)
|
||||||
self._hass = hass
|
self.hass = hass
|
||||||
|
|
||||||
def _set_services(self):
|
def _set_services(self):
|
||||||
add_preload_service(self, SERV_ACCESSORY_INFO)
|
add_preload_service(self, SERV_ACCESSORY_INFO)
|
||||||
add_preload_service(self, SERV_BRIDGING_STATE)
|
|
||||||
|
|
||||||
def setup_message(self):
|
def setup_message(self):
|
||||||
"""Prevent print of pyhap setup message to terminal."""
|
"""Prevent print of pyhap setup message to terminal."""
|
||||||
@ -92,12 +82,12 @@ class HomeBridge(Bridge):
|
|||||||
def add_paired_client(self, client_uuid, client_public):
|
def add_paired_client(self, client_uuid, client_public):
|
||||||
"""Override super function to dismiss setup message if paired."""
|
"""Override super function to dismiss setup message if paired."""
|
||||||
super().add_paired_client(client_uuid, client_public)
|
super().add_paired_client(client_uuid, client_public)
|
||||||
dismiss_setup_message(self._hass)
|
dismiss_setup_message(self.hass)
|
||||||
|
|
||||||
def remove_paired_client(self, client_uuid):
|
def remove_paired_client(self, client_uuid):
|
||||||
"""Override super function to show setup message if unpaired."""
|
"""Override super function to show setup message if unpaired."""
|
||||||
super().remove_paired_client(client_uuid)
|
super().remove_paired_client(client_uuid)
|
||||||
show_setup_message(self, self._hass)
|
show_setup_message(self, self.hass)
|
||||||
|
|
||||||
|
|
||||||
class HomeDriver(AccessoryDriver):
|
class HomeDriver(AccessoryDriver):
|
||||||
|
@ -24,13 +24,16 @@ BRIDGE_NAME = 'Home Assistant'
|
|||||||
MANUFACTURER = 'HomeAssistant'
|
MANUFACTURER = 'HomeAssistant'
|
||||||
|
|
||||||
# #### Categories ####
|
# #### Categories ####
|
||||||
|
CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
|
||||||
CATEGORY_LIGHT = 'LIGHTBULB'
|
CATEGORY_LIGHT = 'LIGHTBULB'
|
||||||
CATEGORY_SENSOR = 'SENSOR'
|
CATEGORY_SENSOR = 'SENSOR'
|
||||||
|
CATEGORY_SWITCH = 'SWITCH'
|
||||||
|
CATEGORY_THERMOSTAT = 'THERMOSTAT'
|
||||||
|
CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING'
|
||||||
|
|
||||||
|
|
||||||
# #### Services ####
|
# #### Services ####
|
||||||
SERV_ACCESSORY_INFO = 'AccessoryInformation'
|
SERV_ACCESSORY_INFO = 'AccessoryInformation'
|
||||||
SERV_BRIDGING_STATE = 'BridgingState'
|
|
||||||
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
|
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
|
||||||
# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered,
|
# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered,
|
||||||
# StatusLowBattery, Name
|
# StatusLowBattery, Name
|
||||||
@ -43,9 +46,8 @@ SERV_WINDOW_COVERING = 'WindowCovering'
|
|||||||
|
|
||||||
|
|
||||||
# #### Characteristics ####
|
# #### Characteristics ####
|
||||||
CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier'
|
|
||||||
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
|
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
|
||||||
CHAR_CATEGORY = 'Category'
|
CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
|
||||||
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
|
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
|
||||||
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
|
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
|
||||||
CHAR_CURRENT_POSITION = 'CurrentPosition'
|
CHAR_CURRENT_POSITION = 'CurrentPosition'
|
||||||
@ -54,13 +56,11 @@ CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
|
|||||||
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
|
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
|
||||||
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
|
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
|
||||||
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
|
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
|
||||||
CHAR_LINK_QUALITY = 'LinkQuality'
|
|
||||||
CHAR_MANUFACTURER = 'Manufacturer'
|
CHAR_MANUFACTURER = 'Manufacturer'
|
||||||
CHAR_MODEL = 'Model'
|
CHAR_MODEL = 'Model'
|
||||||
CHAR_NAME = 'Name'
|
CHAR_NAME = 'Name'
|
||||||
CHAR_ON = 'On' # boolean
|
CHAR_ON = 'On' # boolean
|
||||||
CHAR_POSITION_STATE = 'PositionState'
|
CHAR_POSITION_STATE = 'PositionState'
|
||||||
CHAR_REACHABLE = 'Reachable'
|
|
||||||
CHAR_SATURATION = 'Saturation' # percent
|
CHAR_SATURATION = 'Saturation' # percent
|
||||||
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
||||||
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
|
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
|
||||||
|
@ -6,8 +6,8 @@ from homeassistant.components.cover import ATTR_CURRENT_POSITION
|
|||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import HomeAccessory, add_preload_service
|
from .accessories import HomeAccessory, add_preload_service
|
||||||
from .const import (
|
from .const import (
|
||||||
SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION,
|
CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
|
||||||
CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
|
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
|
||||||
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -20,13 +20,13 @@ class WindowCovering(HomeAccessory):
|
|||||||
The cover entity must support: set_cover_position.
|
The cover entity must support: set_cover_position.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hass, entity_id, display_name, *args, **kwargs):
|
def __init__(self, hass, entity_id, display_name, **kwargs):
|
||||||
"""Initialize a WindowCovering accessory object."""
|
"""Initialize a WindowCovering accessory object."""
|
||||||
super().__init__(display_name, entity_id, 'WINDOW_COVERING',
|
super().__init__(display_name, entity_id,
|
||||||
*args, **kwargs)
|
CATEGORY_WINDOW_COVERING, **kwargs)
|
||||||
|
|
||||||
self._hass = hass
|
self.hass = hass
|
||||||
self._entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
|
|
||||||
self.current_position = None
|
self.current_position = None
|
||||||
self.homekit_target = None
|
self.homekit_target = None
|
||||||
@ -48,14 +48,14 @@ class WindowCovering(HomeAccessory):
|
|||||||
"""Move cover to value if call came from HomeKit."""
|
"""Move cover to value if call came from HomeKit."""
|
||||||
self.char_target_position.set_value(value, should_callback=False)
|
self.char_target_position.set_value(value, should_callback=False)
|
||||||
if value != self.current_position:
|
if value != self.current_position:
|
||||||
_LOGGER.debug('%s: Set position to %d', self._entity_id, value)
|
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
|
||||||
self.homekit_target = value
|
self.homekit_target = value
|
||||||
if value > self.current_position:
|
if value > self.current_position:
|
||||||
self.char_position_state.set_value(1)
|
self.char_position_state.set_value(1)
|
||||||
elif value < self.current_position:
|
elif value < self.current_position:
|
||||||
self.char_position_state.set_value(0)
|
self.char_position_state.set_value(0)
|
||||||
self._hass.components.cover.set_cover_position(
|
self.hass.components.cover.set_cover_position(
|
||||||
value, self._entity_id)
|
value, self.entity_id)
|
||||||
|
|
||||||
def update_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 cover position after state changed."""
|
"""Update cover position after state changed."""
|
||||||
@ -63,12 +63,9 @@ class WindowCovering(HomeAccessory):
|
|||||||
return
|
return
|
||||||
|
|
||||||
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
|
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
|
||||||
if current_position is None:
|
if isinstance(current_position, int):
|
||||||
return
|
self.current_position = current_position
|
||||||
|
|
||||||
self.current_position = int(current_position)
|
|
||||||
self.char_current_position.set_value(self.current_position)
|
self.char_current_position.set_value(self.current_position)
|
||||||
|
|
||||||
if self.homekit_target is None or \
|
if self.homekit_target is None or \
|
||||||
abs(self.current_position - self.homekit_target) < 6:
|
abs(self.current_position - self.homekit_target) < 6:
|
||||||
self.char_target_position.set_value(self.current_position)
|
self.char_target_position.set_value(self.current_position)
|
||||||
|
@ -2,13 +2,14 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_HS_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, SUPPORT_COLOR)
|
ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS,
|
||||||
|
ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS)
|
||||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
|
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import HomeAccessory, add_preload_service
|
from .accessories import HomeAccessory, add_preload_service
|
||||||
from .const import (
|
from .const import (
|
||||||
CATEGORY_LIGHT, SERV_LIGHTBULB,
|
CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
|
||||||
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
|
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -20,25 +21,27 @@ RGB_COLOR = 'rgb_color'
|
|||||||
class Light(HomeAccessory):
|
class Light(HomeAccessory):
|
||||||
"""Generate a Light accessory for a light entity.
|
"""Generate a Light accessory for a light entity.
|
||||||
|
|
||||||
Currently supports: state, brightness, rgb_color.
|
Currently supports: state, brightness, color temperature, rgb_color.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hass, entity_id, name, *args, **kwargs):
|
def __init__(self, hass, entity_id, name, **kwargs):
|
||||||
"""Initialize a new Light accessory object."""
|
"""Initialize a new Light accessory object."""
|
||||||
super().__init__(name, entity_id, CATEGORY_LIGHT, *args, **kwargs)
|
super().__init__(name, entity_id, CATEGORY_LIGHT, **kwargs)
|
||||||
|
|
||||||
self._hass = hass
|
self.hass = hass
|
||||||
self._entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
|
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
|
||||||
CHAR_HUE: False, CHAR_SATURATION: False,
|
CHAR_HUE: False, CHAR_SATURATION: False,
|
||||||
RGB_COLOR: False}
|
CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False}
|
||||||
self._state = 0
|
self._state = 0
|
||||||
|
|
||||||
self.chars = []
|
self.chars = []
|
||||||
self._features = self._hass.states.get(self._entity_id) \
|
self._features = self.hass.states.get(self.entity_id) \
|
||||||
.attributes.get(ATTR_SUPPORTED_FEATURES)
|
.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||||
if self._features & SUPPORT_BRIGHTNESS:
|
if self._features & SUPPORT_BRIGHTNESS:
|
||||||
self.chars.append(CHAR_BRIGHTNESS)
|
self.chars.append(CHAR_BRIGHTNESS)
|
||||||
|
if self._features & SUPPORT_COLOR_TEMP:
|
||||||
|
self.chars.append(CHAR_COLOR_TEMPERATURE)
|
||||||
if self._features & SUPPORT_COLOR:
|
if self._features & SUPPORT_COLOR:
|
||||||
self.chars.append(CHAR_HUE)
|
self.chars.append(CHAR_HUE)
|
||||||
self.chars.append(CHAR_SATURATION)
|
self.chars.append(CHAR_SATURATION)
|
||||||
@ -55,6 +58,18 @@ class Light(HomeAccessory):
|
|||||||
.get_characteristic(CHAR_BRIGHTNESS)
|
.get_characteristic(CHAR_BRIGHTNESS)
|
||||||
self.char_brightness.setter_callback = self.set_brightness
|
self.char_brightness.setter_callback = self.set_brightness
|
||||||
self.char_brightness.value = 0
|
self.char_brightness.value = 0
|
||||||
|
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||||
|
self.char_color_temperature = serv_light \
|
||||||
|
.get_characteristic(CHAR_COLOR_TEMPERATURE)
|
||||||
|
self.char_color_temperature.setter_callback = \
|
||||||
|
self.set_color_temperature
|
||||||
|
min_mireds = self.hass.states.get(self.entity_id) \
|
||||||
|
.attributes.get(ATTR_MIN_MIREDS, 153)
|
||||||
|
max_mireds = self.hass.states.get(self.entity_id) \
|
||||||
|
.attributes.get(ATTR_MAX_MIREDS, 500)
|
||||||
|
self.char_color_temperature.override_properties({
|
||||||
|
'minValue': min_mireds, 'maxValue': max_mireds})
|
||||||
|
self.char_color_temperature.value = min_mireds
|
||||||
if CHAR_HUE in self.chars:
|
if CHAR_HUE in self.chars:
|
||||||
self.char_hue = serv_light.get_characteristic(CHAR_HUE)
|
self.char_hue = serv_light.get_characteristic(CHAR_HUE)
|
||||||
self.char_hue.setter_callback = self.set_hue
|
self.char_hue.setter_callback = self.set_hue
|
||||||
@ -70,29 +85,36 @@ class Light(HomeAccessory):
|
|||||||
if self._state == value:
|
if self._state == value:
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.debug('%s: Set state to %d', self._entity_id, value)
|
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
|
||||||
self._flag[CHAR_ON] = True
|
self._flag[CHAR_ON] = True
|
||||||
self.char_on.set_value(value, should_callback=False)
|
self.char_on.set_value(value, should_callback=False)
|
||||||
|
|
||||||
if value == 1:
|
if value == 1:
|
||||||
self._hass.components.light.turn_on(self._entity_id)
|
self.hass.components.light.turn_on(self.entity_id)
|
||||||
elif value == 0:
|
elif value == 0:
|
||||||
self._hass.components.light.turn_off(self._entity_id)
|
self.hass.components.light.turn_off(self.entity_id)
|
||||||
|
|
||||||
def set_brightness(self, value):
|
def set_brightness(self, value):
|
||||||
"""Set brightness if call came from HomeKit."""
|
"""Set brightness if call came from HomeKit."""
|
||||||
_LOGGER.debug('%s: Set brightness to %d', self._entity_id, value)
|
_LOGGER.debug('%s: Set brightness to %d', self.entity_id, value)
|
||||||
self._flag[CHAR_BRIGHTNESS] = True
|
self._flag[CHAR_BRIGHTNESS] = True
|
||||||
self.char_brightness.set_value(value, should_callback=False)
|
self.char_brightness.set_value(value, should_callback=False)
|
||||||
if value != 0:
|
if value != 0:
|
||||||
self._hass.components.light.turn_on(
|
self.hass.components.light.turn_on(
|
||||||
self._entity_id, brightness_pct=value)
|
self.entity_id, brightness_pct=value)
|
||||||
else:
|
else:
|
||||||
self._hass.components.light.turn_off(self._entity_id)
|
self.hass.components.light.turn_off(self.entity_id)
|
||||||
|
|
||||||
|
def set_color_temperature(self, value):
|
||||||
|
"""Set color temperature if call came from HomeKit."""
|
||||||
|
_LOGGER.debug('%s: Set color temp to %s', self.entity_id, value)
|
||||||
|
self._flag[CHAR_COLOR_TEMPERATURE] = True
|
||||||
|
self.char_color_temperature.set_value(value, should_callback=False)
|
||||||
|
self.hass.components.light.turn_on(self.entity_id, color_temp=value)
|
||||||
|
|
||||||
def set_saturation(self, value):
|
def set_saturation(self, value):
|
||||||
"""Set saturation if call came from HomeKit."""
|
"""Set saturation if call came from HomeKit."""
|
||||||
_LOGGER.debug('%s: Set saturation to %d', self._entity_id, value)
|
_LOGGER.debug('%s: Set saturation to %d', self.entity_id, value)
|
||||||
self._flag[CHAR_SATURATION] = True
|
self._flag[CHAR_SATURATION] = True
|
||||||
self.char_saturation.set_value(value, should_callback=False)
|
self.char_saturation.set_value(value, should_callback=False)
|
||||||
self._saturation = value
|
self._saturation = value
|
||||||
@ -100,7 +122,7 @@ class Light(HomeAccessory):
|
|||||||
|
|
||||||
def set_hue(self, value):
|
def set_hue(self, value):
|
||||||
"""Set hue if call came from HomeKit."""
|
"""Set hue if call came from HomeKit."""
|
||||||
_LOGGER.debug('%s: Set hue to %d', self._entity_id, value)
|
_LOGGER.debug('%s: Set hue to %d', self.entity_id, value)
|
||||||
self._flag[CHAR_HUE] = True
|
self._flag[CHAR_HUE] = True
|
||||||
self.char_hue.set_value(value, should_callback=False)
|
self.char_hue.set_value(value, should_callback=False)
|
||||||
self._hue = value
|
self._hue = value
|
||||||
@ -112,11 +134,11 @@ class Light(HomeAccessory):
|
|||||||
if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \
|
if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \
|
||||||
self._flag[CHAR_SATURATION]:
|
self._flag[CHAR_SATURATION]:
|
||||||
color = (self._hue, self._saturation)
|
color = (self._hue, self._saturation)
|
||||||
_LOGGER.debug('%s: Set hs_color to %s', self._entity_id, color)
|
_LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color)
|
||||||
self._flag.update({
|
self._flag.update({
|
||||||
CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True})
|
CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True})
|
||||||
self._hass.components.light.turn_on(
|
self.hass.components.light.turn_on(
|
||||||
self._entity_id, hs_color=color)
|
self.entity_id, hs_color=color)
|
||||||
|
|
||||||
def update_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 light after state change."""
|
"""Update light after state change."""
|
||||||
@ -141,13 +163,25 @@ class Light(HomeAccessory):
|
|||||||
should_callback=False)
|
should_callback=False)
|
||||||
self._flag[CHAR_BRIGHTNESS] = False
|
self._flag[CHAR_BRIGHTNESS] = False
|
||||||
|
|
||||||
|
# Handle color temperature
|
||||||
|
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||||
|
color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP)
|
||||||
|
if not self._flag[CHAR_COLOR_TEMPERATURE] \
|
||||||
|
and isinstance(color_temperature, int):
|
||||||
|
self.char_color_temperature.set_value(color_temperature,
|
||||||
|
should_callback=False)
|
||||||
|
self._flag[CHAR_COLOR_TEMPERATURE] = False
|
||||||
|
|
||||||
# Handle Color
|
# Handle Color
|
||||||
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
|
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
|
||||||
hue, saturation = new_state.attributes.get(
|
hue, saturation = new_state.attributes.get(
|
||||||
ATTR_HS_COLOR, (None, None))
|
ATTR_HS_COLOR, (None, None))
|
||||||
if not self._flag[RGB_COLOR] and (
|
if not self._flag[RGB_COLOR] and (
|
||||||
hue != self._hue or saturation != self._saturation):
|
hue != self._hue or saturation != self._saturation) and \
|
||||||
|
isinstance(hue, (int, float)) and \
|
||||||
|
isinstance(saturation, (int, float)):
|
||||||
self.char_hue.set_value(hue, should_callback=False)
|
self.char_hue.set_value(hue, should_callback=False)
|
||||||
self.char_saturation.set_value(saturation,
|
self.char_saturation.set_value(saturation,
|
||||||
should_callback=False)
|
should_callback=False)
|
||||||
|
self._hue, self._saturation = (hue, saturation)
|
||||||
self._flag[RGB_COLOR] = False
|
self._flag[RGB_COLOR] = False
|
||||||
|
@ -9,8 +9,8 @@ from homeassistant.const import (
|
|||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import HomeAccessory, add_preload_service
|
from .accessories import HomeAccessory, add_preload_service
|
||||||
from .const import (
|
from .const import (
|
||||||
SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE,
|
CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM,
|
||||||
CHAR_TARGET_SECURITY_STATE)
|
CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -27,14 +27,13 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
|
|||||||
class SecuritySystem(HomeAccessory):
|
class SecuritySystem(HomeAccessory):
|
||||||
"""Generate an SecuritySystem accessory for an alarm control panel."""
|
"""Generate an SecuritySystem accessory for an alarm control panel."""
|
||||||
|
|
||||||
def __init__(self, hass, entity_id, display_name,
|
def __init__(self, hass, entity_id, display_name, alarm_code, **kwargs):
|
||||||
alarm_code, *args, **kwargs):
|
|
||||||
"""Initialize a SecuritySystem accessory object."""
|
"""Initialize a SecuritySystem accessory object."""
|
||||||
super().__init__(display_name, entity_id, 'ALARM_SYSTEM',
|
super().__init__(display_name, entity_id,
|
||||||
*args, **kwargs)
|
CATEGORY_ALARM_SYSTEM, **kwargs)
|
||||||
|
|
||||||
self._hass = hass
|
self.hass = hass
|
||||||
self._entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
self._alarm_code = alarm_code
|
self._alarm_code = alarm_code
|
||||||
|
|
||||||
self.flag_target_state = False
|
self.flag_target_state = False
|
||||||
@ -52,16 +51,16 @@ class SecuritySystem(HomeAccessory):
|
|||||||
def set_security_state(self, value):
|
def set_security_state(self, value):
|
||||||
"""Move security state to value if call came from HomeKit."""
|
"""Move security state to value if call came from HomeKit."""
|
||||||
_LOGGER.debug('%s: Set security state to %d',
|
_LOGGER.debug('%s: Set security state to %d',
|
||||||
self._entity_id, value)
|
self.entity_id, value)
|
||||||
self.flag_target_state = True
|
self.flag_target_state = True
|
||||||
self.char_target_state.set_value(value, should_callback=False)
|
self.char_target_state.set_value(value, should_callback=False)
|
||||||
hass_value = HOMEKIT_TO_HASS[value]
|
hass_value = HOMEKIT_TO_HASS[value]
|
||||||
service = STATE_TO_SERVICE[hass_value]
|
service = STATE_TO_SERVICE[hass_value]
|
||||||
|
|
||||||
params = {ATTR_ENTITY_ID: self._entity_id}
|
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||||
if self._alarm_code:
|
if self._alarm_code:
|
||||||
params[ATTR_CODE] = self._alarm_code
|
params[ATTR_CODE] = self._alarm_code
|
||||||
self._hass.services.call('alarm_control_panel', service, params)
|
self.hass.services.call('alarm_control_panel', service, params)
|
||||||
|
|
||||||
def update_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."""
|
"""Update security state after state changed."""
|
||||||
@ -76,7 +75,7 @@ class SecuritySystem(HomeAccessory):
|
|||||||
self.char_current_state.set_value(current_security_state,
|
self.char_current_state.set_value(current_security_state,
|
||||||
should_callback=False)
|
should_callback=False)
|
||||||
_LOGGER.debug('%s: Updated current state to %s (%d)',
|
_LOGGER.debug('%s: Updated current state to %s (%d)',
|
||||||
self._entity_id, hass_state, current_security_state)
|
self.entity_id, hass_state, current_security_state)
|
||||||
|
|
||||||
if not self.flag_target_state:
|
if not self.flag_target_state:
|
||||||
self.char_target_state.set_value(current_security_state,
|
self.char_target_state.set_value(current_security_state,
|
||||||
|
@ -5,8 +5,7 @@ from homeassistant.const import (
|
|||||||
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
|
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import (
|
from .accessories import HomeAccessory, add_preload_service
|
||||||
HomeAccessory, add_preload_service, override_properties)
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
|
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
|
||||||
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
|
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
|
||||||
@ -23,16 +22,16 @@ class TemperatureSensor(HomeAccessory):
|
|||||||
Sensor entity must return temperature in °C, °F.
|
Sensor entity must return temperature in °C, °F.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hass, entity_id, name, *args, **kwargs):
|
def __init__(self, hass, entity_id, name, **kwargs):
|
||||||
"""Initialize a TemperatureSensor accessory object."""
|
"""Initialize a TemperatureSensor accessory object."""
|
||||||
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
|
super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs)
|
||||||
|
|
||||||
self._hass = hass
|
self.hass = hass
|
||||||
self._entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
|
|
||||||
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
|
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
|
||||||
self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE)
|
self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE)
|
||||||
override_properties(self.char_temp, PROP_CELSIUS)
|
self.char_temp.override_properties(properties=PROP_CELSIUS)
|
||||||
self.char_temp.value = 0
|
self.char_temp.value = 0
|
||||||
self.unit = None
|
self.unit = None
|
||||||
|
|
||||||
@ -47,7 +46,7 @@ class TemperatureSensor(HomeAccessory):
|
|||||||
temperature = temperature_to_homekit(temperature, unit)
|
temperature = temperature_to_homekit(temperature, unit)
|
||||||
self.char_temp.set_value(temperature, should_callback=False)
|
self.char_temp.set_value(temperature, should_callback=False)
|
||||||
_LOGGER.debug('%s: Current temperature set to %d°C',
|
_LOGGER.debug('%s: Current temperature set to %d°C',
|
||||||
self._entity_id, temperature)
|
self.entity_id, temperature)
|
||||||
|
|
||||||
|
|
||||||
@TYPES.register('HumiditySensor')
|
@TYPES.register('HumiditySensor')
|
||||||
@ -58,8 +57,8 @@ class HumiditySensor(HomeAccessory):
|
|||||||
"""Initialize a HumiditySensor accessory object."""
|
"""Initialize a HumiditySensor accessory object."""
|
||||||
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
|
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
|
||||||
|
|
||||||
self._hass = hass
|
self.hass = hass
|
||||||
self._entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
|
|
||||||
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
|
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
|
||||||
self.char_humidity = serv_humidity \
|
self.char_humidity = serv_humidity \
|
||||||
@ -75,4 +74,4 @@ class HumiditySensor(HomeAccessory):
|
|||||||
if humidity:
|
if humidity:
|
||||||
self.char_humidity.set_value(humidity, should_callback=False)
|
self.char_humidity.set_value(humidity, should_callback=False)
|
||||||
_LOGGER.debug('%s: Percent set to %d%%',
|
_LOGGER.debug('%s: Percent set to %d%%',
|
||||||
self._entity_id, humidity)
|
self.entity_id, humidity)
|
||||||
|
@ -7,7 +7,7 @@ from homeassistant.core import split_entity_id
|
|||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import HomeAccessory, add_preload_service
|
from .accessories import HomeAccessory, add_preload_service
|
||||||
from .const import SERV_SWITCH, CHAR_ON
|
from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -16,12 +16,12 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class Switch(HomeAccessory):
|
class Switch(HomeAccessory):
|
||||||
"""Generate a Switch accessory."""
|
"""Generate a Switch accessory."""
|
||||||
|
|
||||||
def __init__(self, hass, entity_id, display_name, *args, **kwargs):
|
def __init__(self, hass, entity_id, display_name, **kwargs):
|
||||||
"""Initialize a Switch accessory object to represent a remote."""
|
"""Initialize a Switch accessory object to represent a remote."""
|
||||||
super().__init__(display_name, entity_id, 'SWITCH', *args, **kwargs)
|
super().__init__(display_name, entity_id, CATEGORY_SWITCH, **kwargs)
|
||||||
|
|
||||||
self._hass = hass
|
self.hass = hass
|
||||||
self._entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
self._domain = split_entity_id(entity_id)[0]
|
self._domain = split_entity_id(entity_id)[0]
|
||||||
|
|
||||||
self.flag_target_state = False
|
self.flag_target_state = False
|
||||||
@ -34,12 +34,12 @@ class Switch(HomeAccessory):
|
|||||||
def set_state(self, value):
|
def set_state(self, value):
|
||||||
"""Move switch state to value if call came from HomeKit."""
|
"""Move switch state to value if call came from HomeKit."""
|
||||||
_LOGGER.debug('%s: Set switch state to %s',
|
_LOGGER.debug('%s: Set switch state to %s',
|
||||||
self._entity_id, value)
|
self.entity_id, value)
|
||||||
self.flag_target_state = True
|
self.flag_target_state = True
|
||||||
self.char_on.set_value(value, should_callback=False)
|
self.char_on.set_value(value, should_callback=False)
|
||||||
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
|
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
|
||||||
self._hass.services.call(self._domain, service,
|
self.hass.services.call(self._domain, service,
|
||||||
{ATTR_ENTITY_ID: self._entity_id})
|
{ATTR_ENTITY_ID: self.entity_id})
|
||||||
|
|
||||||
def update_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 switch state after state changed."""
|
"""Update switch state after state changed."""
|
||||||
@ -49,7 +49,7 @@ class Switch(HomeAccessory):
|
|||||||
current_state = (new_state.state == STATE_ON)
|
current_state = (new_state.state == STATE_ON)
|
||||||
if not self.flag_target_state:
|
if not self.flag_target_state:
|
||||||
_LOGGER.debug('%s: Set current state to %s',
|
_LOGGER.debug('%s: Set current state to %s',
|
||||||
self._entity_id, current_state)
|
self.entity_id, current_state)
|
||||||
self.char_on.set_value(current_state, should_callback=False)
|
self.char_on.set_value(current_state, should_callback=False)
|
||||||
|
|
||||||
self.flag_target_state = False
|
self.flag_target_state = False
|
||||||
|
@ -7,12 +7,12 @@ from homeassistant.components.climate import (
|
|||||||
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
|
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
|
||||||
STATE_HEAT, STATE_COOL, STATE_AUTO)
|
STATE_HEAT, STATE_COOL, STATE_AUTO)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||||
|
|
||||||
from . import TYPES
|
from . import TYPES
|
||||||
from .accessories import HomeAccessory, add_preload_service
|
from .accessories import HomeAccessory, add_preload_service
|
||||||
from .const import (
|
from .const import (
|
||||||
SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
|
CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
|
||||||
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
|
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
|
||||||
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
|
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
|
||||||
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
||||||
@ -20,7 +20,6 @@ from .util import temperature_to_homekit, temperature_to_states
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
STATE_OFF = 'off'
|
|
||||||
UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1}
|
UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1}
|
||||||
UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()}
|
UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()}
|
||||||
HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1,
|
HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1,
|
||||||
@ -32,14 +31,13 @@ HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
|
|||||||
class Thermostat(HomeAccessory):
|
class Thermostat(HomeAccessory):
|
||||||
"""Generate a Thermostat accessory for a climate."""
|
"""Generate a Thermostat accessory for a climate."""
|
||||||
|
|
||||||
def __init__(self, hass, entity_id, display_name,
|
def __init__(self, hass, entity_id, display_name, support_auto, **kwargs):
|
||||||
support_auto, *args, **kwargs):
|
|
||||||
"""Initialize a Thermostat accessory object."""
|
"""Initialize a Thermostat accessory object."""
|
||||||
super().__init__(display_name, entity_id, 'THERMOSTAT',
|
super().__init__(display_name, entity_id,
|
||||||
*args, **kwargs)
|
CATEGORY_THERMOSTAT, **kwargs)
|
||||||
|
|
||||||
self._hass = hass
|
self.hass = hass
|
||||||
self._entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
self._call_timer = None
|
self._call_timer = None
|
||||||
self._unit = TEMP_CELSIUS
|
self._unit = TEMP_CELSIUS
|
||||||
|
|
||||||
@ -101,48 +99,48 @@ class Thermostat(HomeAccessory):
|
|||||||
"""Move operation mode to value if call came from HomeKit."""
|
"""Move operation mode to value if call came from HomeKit."""
|
||||||
self.char_target_heat_cool.set_value(value, should_callback=False)
|
self.char_target_heat_cool.set_value(value, should_callback=False)
|
||||||
if value in HC_HOMEKIT_TO_HASS:
|
if value in HC_HOMEKIT_TO_HASS:
|
||||||
_LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value)
|
_LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
|
||||||
self.heat_cool_flag_target_state = True
|
self.heat_cool_flag_target_state = True
|
||||||
hass_value = HC_HOMEKIT_TO_HASS[value]
|
hass_value = HC_HOMEKIT_TO_HASS[value]
|
||||||
self._hass.components.climate.set_operation_mode(
|
self.hass.components.climate.set_operation_mode(
|
||||||
operation_mode=hass_value, entity_id=self._entity_id)
|
operation_mode=hass_value, entity_id=self.entity_id)
|
||||||
|
|
||||||
def set_cooling_threshold(self, value):
|
def set_cooling_threshold(self, value):
|
||||||
"""Set cooling threshold temp to value if call came from HomeKit."""
|
"""Set cooling threshold temp to value if call came from HomeKit."""
|
||||||
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
|
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
|
||||||
self._entity_id, value)
|
self.entity_id, value)
|
||||||
self.coolingthresh_flag_target_state = True
|
self.coolingthresh_flag_target_state = True
|
||||||
self.char_cooling_thresh_temp.set_value(value, should_callback=False)
|
self.char_cooling_thresh_temp.set_value(value, should_callback=False)
|
||||||
low = self.char_heating_thresh_temp.value
|
low = self.char_heating_thresh_temp.value
|
||||||
low = temperature_to_states(low, self._unit)
|
low = temperature_to_states(low, self._unit)
|
||||||
value = temperature_to_states(value, self._unit)
|
value = temperature_to_states(value, self._unit)
|
||||||
self._hass.components.climate.set_temperature(
|
self.hass.components.climate.set_temperature(
|
||||||
entity_id=self._entity_id, target_temp_high=value,
|
entity_id=self.entity_id, target_temp_high=value,
|
||||||
target_temp_low=low)
|
target_temp_low=low)
|
||||||
|
|
||||||
def set_heating_threshold(self, value):
|
def set_heating_threshold(self, value):
|
||||||
"""Set heating threshold temp to value if call came from HomeKit."""
|
"""Set heating threshold temp to value if call came from HomeKit."""
|
||||||
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
|
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
|
||||||
self._entity_id, value)
|
self.entity_id, value)
|
||||||
self.heatingthresh_flag_target_state = True
|
self.heatingthresh_flag_target_state = True
|
||||||
self.char_heating_thresh_temp.set_value(value, should_callback=False)
|
self.char_heating_thresh_temp.set_value(value, should_callback=False)
|
||||||
# Home assistant always wants to set low and high at the same time
|
# Home assistant always wants to set low and high at the same time
|
||||||
high = self.char_cooling_thresh_temp.value
|
high = self.char_cooling_thresh_temp.value
|
||||||
high = temperature_to_states(high, self._unit)
|
high = temperature_to_states(high, self._unit)
|
||||||
value = temperature_to_states(value, self._unit)
|
value = temperature_to_states(value, self._unit)
|
||||||
self._hass.components.climate.set_temperature(
|
self.hass.components.climate.set_temperature(
|
||||||
entity_id=self._entity_id, target_temp_high=high,
|
entity_id=self.entity_id, target_temp_high=high,
|
||||||
target_temp_low=value)
|
target_temp_low=value)
|
||||||
|
|
||||||
def set_target_temperature(self, value):
|
def set_target_temperature(self, value):
|
||||||
"""Set target temperature to value if call came from HomeKit."""
|
"""Set target temperature to value if call came from HomeKit."""
|
||||||
_LOGGER.debug('%s: Set target temperature to %.2f°C',
|
_LOGGER.debug('%s: Set target temperature to %.2f°C',
|
||||||
self._entity_id, value)
|
self.entity_id, value)
|
||||||
self.temperature_flag_target_state = True
|
self.temperature_flag_target_state = True
|
||||||
self.char_target_temp.set_value(value, should_callback=False)
|
self.char_target_temp.set_value(value, should_callback=False)
|
||||||
value = temperature_to_states(value, self._unit)
|
value = temperature_to_states(value, self._unit)
|
||||||
self._hass.components.climate.set_temperature(
|
self.hass.components.climate.set_temperature(
|
||||||
temperature=value, entity_id=self._entity_id)
|
temperature=value, entity_id=self.entity_id)
|
||||||
|
|
||||||
def update_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."""
|
"""Update security state after state changed."""
|
||||||
|
@ -4,31 +4,23 @@ This component provides basic support for the Philips Hue system.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/hue/
|
https://home-assistant.io/components/hue/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
import async_timeout
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.components.discovery import SERVICE_HUE
|
|
||||||
from homeassistant.const import CONF_FILENAME, CONF_HOST
|
from homeassistant.const import CONF_FILENAME, CONF_HOST
|
||||||
import homeassistant.helpers.config_validation as cv
|
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||||
from homeassistant.helpers import discovery, aiohttp_client
|
|
||||||
from homeassistant import config_entries
|
from .const import DOMAIN, API_NUPNP
|
||||||
from homeassistant.util.json import save_json
|
from .bridge import HueBridge
|
||||||
|
# Loading the config flow file will register the flow
|
||||||
|
from .config_flow import configured_hosts
|
||||||
|
|
||||||
REQUIREMENTS = ['aiohue==1.3.0']
|
REQUIREMENTS = ['aiohue==1.3.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "hue"
|
|
||||||
SERVICE_HUE_SCENE = "hue_activate_scene"
|
|
||||||
API_NUPNP = 'https://www.meethue.com/api/nupnp'
|
|
||||||
|
|
||||||
CONF_BRIDGES = "bridges"
|
CONF_BRIDGES = "bridges"
|
||||||
|
|
||||||
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
|
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
|
||||||
@ -42,6 +34,7 @@ DEFAULT_ALLOW_HUE_GROUPS = True
|
|||||||
BRIDGE_CONFIG_SCHEMA = vol.Schema({
|
BRIDGE_CONFIG_SCHEMA = vol.Schema({
|
||||||
# Validate as IP address and then convert back to a string.
|
# Validate as IP address and then convert back to a string.
|
||||||
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
|
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
|
||||||
|
# This is for legacy reasons and is only used for importing auth.
|
||||||
vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string,
|
vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string,
|
||||||
vol.Optional(CONF_ALLOW_UNREACHABLE,
|
vol.Optional(CONF_ALLOW_UNREACHABLE,
|
||||||
default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean,
|
default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean,
|
||||||
@ -56,19 +49,6 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
}),
|
}),
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
ATTR_GROUP_NAME = "group_name"
|
|
||||||
ATTR_SCENE_NAME = "scene_name"
|
|
||||||
SCENE_SCHEMA = vol.Schema({
|
|
||||||
vol.Required(ATTR_GROUP_NAME): cv.string,
|
|
||||||
vol.Required(ATTR_SCENE_NAME): cv.string,
|
|
||||||
})
|
|
||||||
|
|
||||||
CONFIG_INSTRUCTIONS = """
|
|
||||||
Press the button on the bridge to register Philips Hue with Home Assistant.
|
|
||||||
|
|
||||||
![Location of button on bridge](/static/images/config_philips_hue.jpg)
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up the Hue platform."""
|
"""Set up the Hue platform."""
|
||||||
@ -76,20 +56,8 @@ async def async_setup(hass, config):
|
|||||||
if conf is None:
|
if conf is None:
|
||||||
conf = {}
|
conf = {}
|
||||||
|
|
||||||
if DOMAIN not in hass.data:
|
|
||||||
hass.data[DOMAIN] = {}
|
hass.data[DOMAIN] = {}
|
||||||
|
configured = configured_hosts(hass)
|
||||||
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
|
# User has configured bridges
|
||||||
if CONF_BRIDGES in conf:
|
if CONF_BRIDGES in conf:
|
||||||
@ -103,12 +71,19 @@ async def async_setup(hass, config):
|
|||||||
async with websession.get(API_NUPNP) as req:
|
async with websession.get(API_NUPNP) as req:
|
||||||
hosts = await req.json()
|
hosts = await req.json()
|
||||||
|
|
||||||
# Run through config schema to populate defaults
|
bridges = []
|
||||||
bridges = [BRIDGE_CONFIG_SCHEMA({
|
for entry in hosts:
|
||||||
CONF_HOST: entry['internalipaddress'],
|
# Filter out already configured hosts
|
||||||
CONF_FILENAME: '.hue_{}.conf'.format(entry['id']),
|
if entry['internalipaddress'] in configured:
|
||||||
}) for entry in hosts]
|
continue
|
||||||
|
|
||||||
|
# Run through config schema to populate defaults
|
||||||
|
bridges.append(BRIDGE_CONFIG_SCHEMA({
|
||||||
|
CONF_HOST: entry['internalipaddress'],
|
||||||
|
# Careful with using entry['id'] for other reasons. The
|
||||||
|
# value is in lowercase but is returned uppercase from hub.
|
||||||
|
CONF_FILENAME: '.hue_{}.conf'.format(entry['id']),
|
||||||
|
}))
|
||||||
else:
|
else:
|
||||||
# Component not specified in config, we're loaded via discovery
|
# Component not specified in config, we're loaded via discovery
|
||||||
bridges = []
|
bridges = []
|
||||||
@ -116,277 +91,43 @@ async def async_setup(hass, config):
|
|||||||
if not bridges:
|
if not bridges:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
await asyncio.wait([
|
for bridge_conf in bridges:
|
||||||
async_setup_bridge(
|
host = bridge_conf[CONF_HOST]
|
||||||
hass, bridge[CONF_HOST], bridge[CONF_FILENAME],
|
|
||||||
bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS]
|
# Store config in hass.data so the config entry can find it
|
||||||
) for bridge in bridges
|
hass.data[DOMAIN][host] = bridge_conf
|
||||||
])
|
|
||||||
|
# If configured, the bridge will be set up during config entry phase
|
||||||
|
if host in configured:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# No existing config entry found, try importing it or trigger link
|
||||||
|
# config flow if no existing auth. Because we're inside the setup of
|
||||||
|
# this component we'll have to use hass.async_add_job to avoid a
|
||||||
|
# deadlock: creating a config entry will set up the component but the
|
||||||
|
# setup would block till the entry is created!
|
||||||
|
hass.async_add_job(hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, source='import', data={
|
||||||
|
'host': bridge_conf[CONF_HOST],
|
||||||
|
'path': bridge_conf[CONF_FILENAME],
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
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 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_hue_groups)
|
|
||||||
await bridge.async_setup()
|
|
||||||
|
|
||||||
|
|
||||||
def _find_username_from_config(hass, filename):
|
|
||||||
"""Load username from config."""
|
|
||||||
path = hass.config.path(filename)
|
|
||||||
|
|
||||||
if not os.path.isfile(path):
|
|
||||||
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_groups=True):
|
|
||||||
"""Initialize the system."""
|
|
||||||
self.host = host
|
|
||||||
self.hass = hass
|
|
||||||
self.filename = filename
|
|
||||||
self.username = username
|
|
||||||
self.allow_unreachable = allow_unreachable
|
|
||||||
self.allow_groups = allow_groups
|
|
||||||
self.available = True
|
|
||||||
self.config_request_id = None
|
|
||||||
self.api = None
|
|
||||||
|
|
||||||
async def async_setup(self):
|
|
||||||
"""Set up a phue bridge based on host parameter."""
|
|
||||||
import aiohue
|
|
||||||
|
|
||||||
api = aiohue.Bridge(
|
|
||||||
self.host,
|
|
||||||
username=self.username,
|
|
||||||
websession=aiohttp_client.async_get_clientsession(self.hass)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
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 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
|
|
||||||
self.hass.components.configurator.async_request_done(request_id)
|
|
||||||
|
|
||||||
self.username = api.username
|
|
||||||
|
|
||||||
# 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,
|
|
||||||
{'host': self.host}))
|
|
||||||
|
|
||||||
self.hass.services.async_register(
|
|
||||||
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
|
|
||||||
schema=SCENE_SCHEMA)
|
|
||||||
|
|
||||||
@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.async_notify_errors(
|
|
||||||
self.config_request_id,
|
|
||||||
"Failed to register, please try again.")
|
|
||||||
return
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
group = next(
|
|
||||||
(group for group in self.api.groups.values()
|
|
||||||
if group.name == group_name), None)
|
|
||||||
|
|
||||||
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)
|
|
||||||
class HueFlowHandler(config_entries.ConfigFlowHandler):
|
|
||||||
"""Handle a Hue config flow."""
|
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize the Hue flow."""
|
|
||||||
self.host = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _websession(self):
|
|
||||||
"""Return a websession.
|
|
||||||
|
|
||||||
Cannot assign in init because hass variable is not set yet.
|
|
||||||
"""
|
|
||||||
return aiohttp_client.async_get_clientsession(self.hass)
|
|
||||||
|
|
||||||
async def async_step_init(self, user_input=None):
|
|
||||||
"""Handle a flow start."""
|
|
||||||
from aiohue.discovery import discover_nupnp
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
self.host = user_input['host']
|
|
||||||
return await self.async_step_link()
|
|
||||||
|
|
||||||
try:
|
|
||||||
with async_timeout.timeout(5):
|
|
||||||
bridges = await discover_nupnp(websession=self._websession)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
return self.async_abort(
|
|
||||||
reason='discover_timeout'
|
|
||||||
)
|
|
||||||
|
|
||||||
if not bridges:
|
|
||||||
return self.async_abort(
|
|
||||||
reason='no_bridges'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Find already configured hosts
|
|
||||||
configured_hosts = set(
|
|
||||||
entry.data['host'] for entry
|
|
||||||
in self.hass.config_entries.async_entries(DOMAIN))
|
|
||||||
|
|
||||||
hosts = [bridge.host for bridge in bridges
|
|
||||||
if bridge.host not in configured_hosts]
|
|
||||||
|
|
||||||
if not hosts:
|
|
||||||
return self.async_abort(
|
|
||||||
reason='all_configured'
|
|
||||||
)
|
|
||||||
|
|
||||||
elif len(hosts) == 1:
|
|
||||||
self.host = hosts[0]
|
|
||||||
return await self.async_step_link()
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id='init',
|
|
||||||
data_schema=vol.Schema({
|
|
||||||
vol.Required('host'): vol.In(hosts)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_link(self, user_input=None):
|
|
||||||
"""Attempt to link with the Hue bridge."""
|
|
||||||
import aiohue
|
|
||||||
errors = {}
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
bridge = aiohue.Bridge(self.host, websession=self._websession)
|
|
||||||
try:
|
|
||||||
with async_timeout.timeout(5):
|
|
||||||
# Create auth token
|
|
||||||
await bridge.create_user('home-assistant')
|
|
||||||
# Fetches name and id
|
|
||||||
await bridge.initialize()
|
|
||||||
except (asyncio.TimeoutError, aiohue.RequestError,
|
|
||||||
aiohue.LinkButtonNotPressed):
|
|
||||||
errors['base'] = 'register_failed'
|
|
||||||
except aiohue.AiohueException:
|
|
||||||
errors['base'] = 'linking'
|
|
||||||
_LOGGER.exception('Unknown Hue linking error occurred')
|
|
||||||
else:
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=bridge.config.name,
|
|
||||||
data={
|
|
||||||
'host': bridge.host,
|
|
||||||
'bridge_id': bridge.config.bridgeid,
|
|
||||||
'username': bridge.username,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id='link',
|
|
||||||
errors=errors,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, entry):
|
async def async_setup_entry(hass, entry):
|
||||||
"""Set up a bridge for a config entry."""
|
"""Set up a bridge from a config entry."""
|
||||||
await async_setup_bridge(hass, entry.data['host'],
|
host = entry.data['host']
|
||||||
username=entry.data['username'])
|
config = hass.data[DOMAIN].get(host)
|
||||||
return True
|
|
||||||
|
if config is None:
|
||||||
|
allow_unreachable = DEFAULT_ALLOW_UNREACHABLE
|
||||||
|
allow_groups = DEFAULT_ALLOW_HUE_GROUPS
|
||||||
|
else:
|
||||||
|
allow_unreachable = config[CONF_ALLOW_UNREACHABLE]
|
||||||
|
allow_groups = config[CONF_ALLOW_HUE_GROUPS]
|
||||||
|
|
||||||
|
bridge = HueBridge(hass, entry, allow_unreachable, allow_groups)
|
||||||
|
hass.data[DOMAIN][host] = bridge
|
||||||
|
return await bridge.async_setup()
|
||||||
|
148
homeassistant/components/hue/bridge.py
Normal file
148
homeassistant/components/hue/bridge.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
"""Code to handle a Hue bridge."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import async_timeout
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||||
|
|
||||||
|
from .const import DOMAIN, LOGGER
|
||||||
|
from .errors import AuthenticationRequired, CannotConnect
|
||||||
|
|
||||||
|
SERVICE_HUE_SCENE = "hue_activate_scene"
|
||||||
|
ATTR_GROUP_NAME = "group_name"
|
||||||
|
ATTR_SCENE_NAME = "scene_name"
|
||||||
|
SCENE_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(ATTR_GROUP_NAME): cv.string,
|
||||||
|
vol.Required(ATTR_SCENE_NAME): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class HueBridge(object):
|
||||||
|
"""Manages a single Hue bridge."""
|
||||||
|
|
||||||
|
def __init__(self, hass, config_entry, allow_unreachable, allow_groups):
|
||||||
|
"""Initialize the system."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
self.hass = hass
|
||||||
|
self.allow_unreachable = allow_unreachable
|
||||||
|
self.allow_groups = allow_groups
|
||||||
|
self.available = True
|
||||||
|
self.api = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host(self):
|
||||||
|
"""Return the host of this bridge."""
|
||||||
|
return self.config_entry.data['host']
|
||||||
|
|
||||||
|
async def async_setup(self, tries=0):
|
||||||
|
"""Set up a phue bridge based on host parameter."""
|
||||||
|
host = self.host
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.api = await get_bridge(
|
||||||
|
self.hass, host,
|
||||||
|
self.config_entry.data['username']
|
||||||
|
)
|
||||||
|
except AuthenticationRequired:
|
||||||
|
# usernames can become invalid if hub is reset or user removed.
|
||||||
|
# We are going to fail the config entry setup and initiate a new
|
||||||
|
# linking procedure. When linking succeeds, it will remove the
|
||||||
|
# old config entry.
|
||||||
|
self.hass.async_add_job(self.hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, source='import', data={
|
||||||
|
'host': host,
|
||||||
|
}
|
||||||
|
))
|
||||||
|
return False
|
||||||
|
|
||||||
|
except CannotConnect:
|
||||||
|
retry_delay = 2 ** (tries + 1)
|
||||||
|
LOGGER.error("Error connecting to the Hue bridge at %s. Retrying "
|
||||||
|
"in %d seconds", host, retry_delay)
|
||||||
|
|
||||||
|
async def retry_setup(_now):
|
||||||
|
"""Retry setup."""
|
||||||
|
if await self.async_setup(tries + 1):
|
||||||
|
# This feels hacky, we should find a better way to do this
|
||||||
|
self.config_entry.state = config_entries.ENTRY_STATE_LOADED
|
||||||
|
|
||||||
|
# Unhandled edge case: cancel this if we discover bridge on new IP
|
||||||
|
self.hass.helpers.event.async_call_later(retry_delay, retry_setup)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
LOGGER.exception('Unknown error connecting with Hue bridge at %s',
|
||||||
|
host)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.hass.async_add_job(
|
||||||
|
self.hass.helpers.discovery.async_load_platform(
|
||||||
|
'light', DOMAIN, {'host': host}))
|
||||||
|
|
||||||
|
self.hass.services.async_register(
|
||||||
|
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
|
||||||
|
schema=SCENE_SCHEMA)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
group = next(
|
||||||
|
(group for group in self.api.groups.values()
|
||||||
|
if group.name == group_name), None)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_bridge(hass, host, username=None):
|
||||||
|
"""Create a bridge object and verify authentication."""
|
||||||
|
import aiohue
|
||||||
|
|
||||||
|
bridge = aiohue.Bridge(
|
||||||
|
host, username=username,
|
||||||
|
websession=aiohttp_client.async_get_clientsession(hass)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(5):
|
||||||
|
# Create username if we don't have one
|
||||||
|
if not username:
|
||||||
|
await bridge.create_user('home-assistant')
|
||||||
|
# Initialize bridge (and validate our username)
|
||||||
|
await bridge.initialize()
|
||||||
|
|
||||||
|
return bridge
|
||||||
|
except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized):
|
||||||
|
LOGGER.warning("Connected to Hue at %s but not registered.", host)
|
||||||
|
raise AuthenticationRequired
|
||||||
|
except (asyncio.TimeoutError, aiohue.RequestError):
|
||||||
|
LOGGER.error("Error connecting to the Hue bridge at %s", host)
|
||||||
|
raise CannotConnect
|
||||||
|
except aiohue.AiohueException:
|
||||||
|
LOGGER.exception('Unknown Hue linking error occurred')
|
||||||
|
raise AuthenticationRequired
|
235
homeassistant/components/hue/config_flow.py
Normal file
235
homeassistant/components/hue/config_flow.py
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
"""Config flow to configure Philips Hue."""
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import async_timeout
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
|
from .bridge import get_bridge
|
||||||
|
from .const import DOMAIN, LOGGER
|
||||||
|
from .errors import AuthenticationRequired, CannotConnect
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def configured_hosts(hass):
|
||||||
|
"""Return a set of the configured hosts."""
|
||||||
|
return set(entry.data['host'] for entry
|
||||||
|
in hass.config_entries.async_entries(DOMAIN))
|
||||||
|
|
||||||
|
|
||||||
|
def _find_username_from_config(hass, filename):
|
||||||
|
"""Load username from config.
|
||||||
|
|
||||||
|
This was a legacy way of configuring Hue until Home Assistant 0.67.
|
||||||
|
"""
|
||||||
|
path = hass.config.path(filename)
|
||||||
|
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(path) as inp:
|
||||||
|
try:
|
||||||
|
return list(json.load(inp).values())[0]['username']
|
||||||
|
except ValueError:
|
||||||
|
# If we get invalid JSON
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@config_entries.HANDLERS.register(DOMAIN)
|
||||||
|
class HueFlowHandler(config_entries.ConfigFlowHandler):
|
||||||
|
"""Handle a Hue config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the Hue flow."""
|
||||||
|
self.host = None
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input=None):
|
||||||
|
"""Handle a flow start."""
|
||||||
|
from aiohue.discovery import discover_nupnp
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
self.host = user_input['host']
|
||||||
|
return await self.async_step_link()
|
||||||
|
|
||||||
|
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(5):
|
||||||
|
bridges = await discover_nupnp(websession=websession)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return self.async_abort(
|
||||||
|
reason='discover_timeout'
|
||||||
|
)
|
||||||
|
|
||||||
|
if not bridges:
|
||||||
|
return self.async_abort(
|
||||||
|
reason='no_bridges'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find already configured hosts
|
||||||
|
configured = configured_hosts(self.hass)
|
||||||
|
|
||||||
|
hosts = [bridge.host for bridge in bridges
|
||||||
|
if bridge.host not in configured]
|
||||||
|
|
||||||
|
if not hosts:
|
||||||
|
return self.async_abort(
|
||||||
|
reason='all_configured'
|
||||||
|
)
|
||||||
|
|
||||||
|
elif len(hosts) == 1:
|
||||||
|
self.host = hosts[0]
|
||||||
|
return await self.async_step_link()
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id='init',
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required('host'): vol.In(hosts)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_link(self, user_input=None):
|
||||||
|
"""Attempt to link with the Hue bridge.
|
||||||
|
|
||||||
|
Given a configured host, will ask the user to press the link button
|
||||||
|
to connect to the bridge.
|
||||||
|
"""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
# We will always try linking in case the user has already pressed
|
||||||
|
# the link button.
|
||||||
|
try:
|
||||||
|
bridge = await get_bridge(
|
||||||
|
self.hass, self.host, username=None
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self._entry_from_bridge(bridge)
|
||||||
|
except AuthenticationRequired:
|
||||||
|
errors['base'] = 'register_failed'
|
||||||
|
|
||||||
|
except CannotConnect:
|
||||||
|
LOGGER.error("Error connecting to the Hue bridge at %s", self.host)
|
||||||
|
errors['base'] = 'linking'
|
||||||
|
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
LOGGER.exception(
|
||||||
|
'Unknown error connecting with Hue bridge at %s',
|
||||||
|
self.host)
|
||||||
|
errors['base'] = 'linking'
|
||||||
|
|
||||||
|
# If there was no user input, do not show the errors.
|
||||||
|
if user_input is None:
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id='link',
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_discovery(self, discovery_info):
|
||||||
|
"""Handle a discovered Hue bridge.
|
||||||
|
|
||||||
|
This flow is triggered by the discovery component. It will check if the
|
||||||
|
host is already configured and delegate to the import step if not.
|
||||||
|
"""
|
||||||
|
# Filter out emulated Hue
|
||||||
|
if "HASS Bridge" in discovery_info.get('name', ''):
|
||||||
|
return self.async_abort(reason='already_configured')
|
||||||
|
|
||||||
|
host = discovery_info.get('host')
|
||||||
|
|
||||||
|
if host in configured_hosts(self.hass):
|
||||||
|
return self.async_abort(reason='already_configured')
|
||||||
|
|
||||||
|
# This value is based off host/description.xml and is, weirdly, missing
|
||||||
|
# 4 characters in the middle of the serial compared to results returned
|
||||||
|
# from the NUPNP API or when querying the bridge API for bridgeid.
|
||||||
|
# (on first gen Hue hub)
|
||||||
|
serial = discovery_info.get('serial')
|
||||||
|
|
||||||
|
return await self.async_step_import({
|
||||||
|
'host': host,
|
||||||
|
# This format is the legacy format that Hue used for discovery
|
||||||
|
'path': 'phue-{}.conf'.format(serial)
|
||||||
|
})
|
||||||
|
|
||||||
|
async def async_step_import(self, import_info):
|
||||||
|
"""Import a new bridge as a config entry.
|
||||||
|
|
||||||
|
Will read authentication from Phue config file if available.
|
||||||
|
|
||||||
|
This flow is triggered by `async_setup` for both configured and
|
||||||
|
discovered bridges. Triggered for any bridge that does not have a
|
||||||
|
config entry yet (based on host).
|
||||||
|
|
||||||
|
This flow is also triggered by `async_step_discovery`.
|
||||||
|
|
||||||
|
If an existing config file is found, we will validate the credentials
|
||||||
|
and create an entry. Otherwise we will delegate to `link` step which
|
||||||
|
will ask user to link the bridge.
|
||||||
|
"""
|
||||||
|
host = import_info['host']
|
||||||
|
path = import_info.get('path')
|
||||||
|
|
||||||
|
if path is not None:
|
||||||
|
username = await self.hass.async_add_job(
|
||||||
|
_find_username_from_config, self.hass,
|
||||||
|
self.hass.config.path(path))
|
||||||
|
else:
|
||||||
|
username = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
bridge = await get_bridge(
|
||||||
|
self.hass, host, username
|
||||||
|
)
|
||||||
|
|
||||||
|
LOGGER.info('Imported authentication for %s from %s', host, path)
|
||||||
|
|
||||||
|
return await self._entry_from_bridge(bridge)
|
||||||
|
except AuthenticationRequired:
|
||||||
|
self.host = host
|
||||||
|
|
||||||
|
LOGGER.info('Invalid authentication for %s, requesting link.',
|
||||||
|
host)
|
||||||
|
|
||||||
|
return await self.async_step_link()
|
||||||
|
|
||||||
|
except CannotConnect:
|
||||||
|
LOGGER.error("Error connecting to the Hue bridge at %s", host)
|
||||||
|
return self.async_abort(reason='cannot_connect')
|
||||||
|
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
LOGGER.exception('Unknown error connecting with Hue bridge at %s',
|
||||||
|
host)
|
||||||
|
return self.async_abort(reason='unknown')
|
||||||
|
|
||||||
|
async def _entry_from_bridge(self, bridge):
|
||||||
|
"""Return a config entry from an initialized bridge."""
|
||||||
|
# Remove all other entries of hubs with same ID or host
|
||||||
|
host = bridge.host
|
||||||
|
bridge_id = bridge.config.bridgeid
|
||||||
|
|
||||||
|
same_hub_entries = [entry.entry_id for entry
|
||||||
|
in self.hass.config_entries.async_entries(DOMAIN)
|
||||||
|
if entry.data['bridge_id'] == bridge_id or
|
||||||
|
entry.data['host'] == host]
|
||||||
|
|
||||||
|
if same_hub_entries:
|
||||||
|
await asyncio.wait([self.hass.config_entries.async_remove(entry_id)
|
||||||
|
for entry_id in same_hub_entries])
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=bridge.config.name,
|
||||||
|
data={
|
||||||
|
'host': host,
|
||||||
|
'bridge_id': bridge_id,
|
||||||
|
'username': bridge.username,
|
||||||
|
}
|
||||||
|
)
|
6
homeassistant/components/hue/const.py
Normal file
6
homeassistant/components/hue/const.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
"""Constants for the Hue component."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger('homeassistant.components.hue')
|
||||||
|
DOMAIN = "hue"
|
||||||
|
API_NUPNP = 'https://www.meethue.com/api/nupnp'
|
14
homeassistant/components/hue/errors.py
Normal file
14
homeassistant/components/hue/errors.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""Errors for the Hue component."""
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
|
|
||||||
|
class HueException(HomeAssistantError):
|
||||||
|
"""Base class for Hue exceptions."""
|
||||||
|
|
||||||
|
|
||||||
|
class CannotConnect(HueException):
|
||||||
|
"""Unable to connect to the bridge."""
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationRequired(HueException):
|
||||||
|
"""Unknown error occurred."""
|
@ -20,7 +20,10 @@
|
|||||||
"abort": {
|
"abort": {
|
||||||
"discover_timeout": "Unable to discover Hue bridges",
|
"discover_timeout": "Unable to discover Hue bridges",
|
||||||
"no_bridges": "No Philips Hue bridges discovered",
|
"no_bridges": "No Philips Hue bridges discovered",
|
||||||
"all_configured": "All Philips Hue bridges are already configured"
|
"all_configured": "All Philips Hue bridges are already configured",
|
||||||
|
"unknown": "Unknown error occurred",
|
||||||
|
"cannot_connect": "Unable to connect to the bridge",
|
||||||
|
"already_configured": "Bridge is already configured"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
REQUIREMENTS = ['insteonplm==0.8.3']
|
REQUIREMENTS = ['insteonplm==0.8.6']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -457,12 +457,14 @@ class Light(ToggleEntity):
|
|||||||
def min_mireds(self):
|
def min_mireds(self):
|
||||||
"""Return the coldest color_temp that this light supports."""
|
"""Return the coldest color_temp that this light supports."""
|
||||||
# Default to the Philips Hue value that HA has always assumed
|
# Default to the Philips Hue value that HA has always assumed
|
||||||
return 154
|
# https://developers.meethue.com/documentation/core-concepts
|
||||||
|
return 153
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_mireds(self):
|
def max_mireds(self):
|
||||||
"""Return the warmest color_temp that this light supports."""
|
"""Return the warmest color_temp that this light supports."""
|
||||||
# Default to the Philips Hue value that HA has always assumed
|
# Default to the Philips Hue value that HA has always assumed
|
||||||
|
# https://developers.meethue.com/documentation/core-concepts
|
||||||
return 500
|
return 500
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -300,8 +300,14 @@ class HueLight(Light):
|
|||||||
command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10)
|
command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10)
|
||||||
|
|
||||||
if ATTR_HS_COLOR in kwargs:
|
if ATTR_HS_COLOR in kwargs:
|
||||||
|
if self.is_osram:
|
||||||
command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
|
command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
|
||||||
command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
|
command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
|
||||||
|
else:
|
||||||
|
# Philips hue bulb models respond differently to hue/sat
|
||||||
|
# requests, so we convert to XY first to ensure a consistent
|
||||||
|
# color.
|
||||||
|
command['xy'] = color.color_hs_to_xy(*kwargs[ATTR_HS_COLOR])
|
||||||
elif ATTR_COLOR_TEMP in kwargs:
|
elif ATTR_COLOR_TEMP in kwargs:
|
||||||
temp = kwargs[ATTR_COLOR_TEMP]
|
temp = kwargs[ATTR_COLOR_TEMP]
|
||||||
command['ct'] = max(self.min_mireds, min(temp, self.max_mireds))
|
command['ct'] = max(self.min_mireds, min(temp, self.max_mireds))
|
||||||
|
@ -79,7 +79,7 @@ class IGloLamp(Light):
|
|||||||
@property
|
@property
|
||||||
def hs_color(self):
|
def hs_color(self):
|
||||||
"""Return the hs value."""
|
"""Return the hs value."""
|
||||||
return color_util.color_RGB_to_hsv(*self._lamp.state()['rgb'])
|
return color_util.color_RGB_to_hs(*self._lamp.state()['rgb'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def effect(self):
|
def effect(self):
|
||||||
|
@ -15,8 +15,9 @@ import homeassistant.util.color as color_util
|
|||||||
SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE
|
SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
async def async_setup_platform(
|
||||||
"""Set up the MySensors platform for lights."""
|
hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Set up the mysensors platform for lights."""
|
||||||
device_class_map = {
|
device_class_map = {
|
||||||
'S_DIMMER': MySensorsLightDimmer,
|
'S_DIMMER': MySensorsLightDimmer,
|
||||||
'S_RGB_LIGHT': MySensorsLightRGB,
|
'S_RGB_LIGHT': MySensorsLightRGB,
|
||||||
@ -24,7 +25,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
}
|
}
|
||||||
mysensors.setup_mysensors_platform(
|
mysensors.setup_mysensors_platform(
|
||||||
hass, DOMAIN, discovery_info, device_class_map,
|
hass, DOMAIN, discovery_info, device_class_map,
|
||||||
add_devices=add_devices)
|
async_add_devices=async_add_devices)
|
||||||
|
|
||||||
|
|
||||||
class MySensorsLight(mysensors.MySensorsEntity, Light):
|
class MySensorsLight(mysensors.MySensorsEntity, Light):
|
||||||
@ -140,12 +141,12 @@ class MySensorsLight(mysensors.MySensorsEntity, Light):
|
|||||||
self._values[value_type] = STATE_OFF
|
self._values[value_type] = STATE_OFF
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def _update_light(self):
|
def _async_update_light(self):
|
||||||
"""Update the controller with values from light child."""
|
"""Update the controller with values from light child."""
|
||||||
value_type = self.gateway.const.SetReq.V_LIGHT
|
value_type = self.gateway.const.SetReq.V_LIGHT
|
||||||
self._state = self._values[value_type] == STATE_ON
|
self._state = self._values[value_type] == STATE_ON
|
||||||
|
|
||||||
def _update_dimmer(self):
|
def _async_update_dimmer(self):
|
||||||
"""Update the controller with values from dimmer child."""
|
"""Update the controller with values from dimmer child."""
|
||||||
value_type = self.gateway.const.SetReq.V_DIMMER
|
value_type = self.gateway.const.SetReq.V_DIMMER
|
||||||
if value_type in self._values:
|
if value_type in self._values:
|
||||||
@ -153,7 +154,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light):
|
|||||||
if self._brightness == 0:
|
if self._brightness == 0:
|
||||||
self._state = False
|
self._state = False
|
||||||
|
|
||||||
def _update_rgb_or_w(self):
|
def _async_update_rgb_or_w(self):
|
||||||
"""Update the controller with values from RGB or RGBW child."""
|
"""Update the controller with values from RGB or RGBW child."""
|
||||||
value = self._values[self.value_type]
|
value = self._values[self.value_type]
|
||||||
color_list = rgb_hex_to_rgb_list(value)
|
color_list = rgb_hex_to_rgb_list(value)
|
||||||
@ -177,11 +178,11 @@ class MySensorsLightDimmer(MySensorsLight):
|
|||||||
if self.gateway.optimistic:
|
if self.gateway.optimistic:
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def update(self):
|
async def async_update(self):
|
||||||
"""Update the controller with the latest value from a sensor."""
|
"""Update the controller with the latest value from a sensor."""
|
||||||
super().update()
|
await super().async_update()
|
||||||
self._update_light()
|
self._async_update_light()
|
||||||
self._update_dimmer()
|
self._async_update_dimmer()
|
||||||
|
|
||||||
|
|
||||||
class MySensorsLightRGB(MySensorsLight):
|
class MySensorsLightRGB(MySensorsLight):
|
||||||
@ -203,12 +204,12 @@ class MySensorsLightRGB(MySensorsLight):
|
|||||||
if self.gateway.optimistic:
|
if self.gateway.optimistic:
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def update(self):
|
async def async_update(self):
|
||||||
"""Update the controller with the latest value from a sensor."""
|
"""Update the controller with the latest value from a sensor."""
|
||||||
super().update()
|
await super().async_update()
|
||||||
self._update_light()
|
self._async_update_light()
|
||||||
self._update_dimmer()
|
self._async_update_dimmer()
|
||||||
self._update_rgb_or_w()
|
self._async_update_rgb_or_w()
|
||||||
|
|
||||||
|
|
||||||
class MySensorsLightRGBW(MySensorsLightRGB):
|
class MySensorsLightRGBW(MySensorsLightRGB):
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.components.light import (
|
|||||||
ATTR_HS_COLOR)
|
ATTR_HS_COLOR)
|
||||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN
|
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN
|
||||||
|
|
||||||
REQUIREMENTS = ['python-mystrom==0.3.8']
|
REQUIREMENTS = ['python-mystrom==0.4.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the myStrom Light platform."""
|
"""Set up the myStrom Light platform."""
|
||||||
from pymystrom import MyStromBulb
|
from pymystrom.bulb import MyStromBulb
|
||||||
from pymystrom.exceptions import MyStromConnectionError
|
from pymystrom.exceptions import MyStromConnectionError
|
||||||
|
|
||||||
host = config.get(CONF_HOST)
|
host = config.get(CONF_HOST)
|
||||||
|
153
homeassistant/components/light/nanoleaf_aurora.py
Normal file
153
homeassistant/components/light/nanoleaf_aurora.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
"""
|
||||||
|
Support for Nanoleaf Aurora platform.
|
||||||
|
|
||||||
|
Based in large parts upon Software-2's ha-aurora and fully
|
||||||
|
reliant on Software-2's nanoleaf-aurora Python Library, see
|
||||||
|
https://github.com/software-2/ha-aurora as well as
|
||||||
|
https://github.com/software-2/nanoleaf
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/light.nanoleaf_aurora/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.light import (
|
||||||
|
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR,
|
||||||
|
SUPPORT_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
|
||||||
|
SUPPORT_COLOR, PLATFORM_SCHEMA, Light)
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_NAME
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.util import color as color_util
|
||||||
|
from homeassistant.util.color import \
|
||||||
|
color_temperature_mired_to_kelvin as mired_to_kelvin
|
||||||
|
|
||||||
|
REQUIREMENTS = ['nanoleaf==0.4.1']
|
||||||
|
|
||||||
|
SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
|
||||||
|
SUPPORT_COLOR)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Required(CONF_TOKEN): cv.string,
|
||||||
|
vol.Optional(CONF_NAME, default='Aurora'): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup Nanoleaf Aurora device."""
|
||||||
|
import nanoleaf
|
||||||
|
host = config.get(CONF_HOST)
|
||||||
|
name = config.get(CONF_NAME)
|
||||||
|
token = config.get(CONF_TOKEN)
|
||||||
|
aurora_light = nanoleaf.Aurora(host, token)
|
||||||
|
aurora_light.hass_name = name
|
||||||
|
|
||||||
|
if aurora_light.on is None:
|
||||||
|
_LOGGER.error("Could not connect to \
|
||||||
|
Nanoleaf Aurora: %s on %s", name, host)
|
||||||
|
add_devices([AuroraLight(aurora_light)], True)
|
||||||
|
|
||||||
|
|
||||||
|
class AuroraLight(Light):
|
||||||
|
"""Representation of a Nanoleaf Aurora."""
|
||||||
|
|
||||||
|
def __init__(self, light):
|
||||||
|
"""Initialize an Aurora."""
|
||||||
|
self._brightness = None
|
||||||
|
self._color_temp = None
|
||||||
|
self._effect = None
|
||||||
|
self._effects_list = None
|
||||||
|
self._light = light
|
||||||
|
self._name = light.hass_name
|
||||||
|
self._hs_color = None
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Return the brightness of the light."""
|
||||||
|
if self._brightness is not None:
|
||||||
|
return int(self._brightness * 2.55)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_temp(self):
|
||||||
|
"""Return the current color temperature."""
|
||||||
|
if self._color_temp is not None:
|
||||||
|
return color_util.color_temperature_kelvin_to_mired(
|
||||||
|
self._color_temp)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect(self):
|
||||||
|
"""Return the current effect."""
|
||||||
|
return self._effect
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect_list(self):
|
||||||
|
"""Return the list of supported effects."""
|
||||||
|
return self._effects_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the display name of this light."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon to use in the frontend, if any."""
|
||||||
|
return "mdi:triangle-outline"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if light is on."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hs_color(self):
|
||||||
|
"""Return the color in HS."""
|
||||||
|
return self._hs_color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag supported features."""
|
||||||
|
return SUPPORT_AURORA
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
"""Instruct the light to turn on."""
|
||||||
|
self._light.on = True
|
||||||
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||||
|
hs_color = kwargs.get(ATTR_HS_COLOR)
|
||||||
|
color_temp_mired = kwargs.get(ATTR_COLOR_TEMP)
|
||||||
|
effect = kwargs.get(ATTR_EFFECT)
|
||||||
|
|
||||||
|
if hs_color:
|
||||||
|
hue, saturation = hs_color
|
||||||
|
self._light.hue = int(hue)
|
||||||
|
self._light.saturation = int(saturation)
|
||||||
|
|
||||||
|
if color_temp_mired:
|
||||||
|
self._light.color_temperature = mired_to_kelvin(color_temp_mired)
|
||||||
|
if brightness:
|
||||||
|
self._light.brightness = int(brightness / 2.55)
|
||||||
|
if effect:
|
||||||
|
self._light.effect = effect
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Instruct the light to turn off."""
|
||||||
|
self._light.on = False
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Fetch new state data for this light.
|
||||||
|
|
||||||
|
This is the only method that should fetch new data for Home Assistant.
|
||||||
|
"""
|
||||||
|
self._brightness = self._light.brightness
|
||||||
|
self._color_temp = self._light.color_temperature
|
||||||
|
self._effect = self._light.effect
|
||||||
|
self._effects_list = self._light.effects_list
|
||||||
|
self._hs_color = self._light.hue, self._light.saturation
|
||||||
|
self._state = self._light.on
|
@ -4,21 +4,32 @@ Support for Qwikswitch Relays and Dimmers.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/light.qwikswitch/
|
https://home-assistant.io/components/light.qwikswitch/
|
||||||
"""
|
"""
|
||||||
import logging
|
from homeassistant.components.qwikswitch import (
|
||||||
|
QSToggleEntity, DOMAIN as QWIKSWITCH)
|
||||||
|
from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light
|
||||||
|
|
||||||
import homeassistant.components.qwikswitch as qwikswitch
|
DEPENDENCIES = [QWIKSWITCH]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
DEPENDENCIES = ['qwikswitch']
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
async def async_setup_platform(hass, _, add_devices, discovery_info=None):
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
"""Add lights from the main Qwikswitch component."""
|
||||||
"""Set up the lights from the main Qwikswitch component."""
|
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
_LOGGER.error("Configure Qwikswitch component failed")
|
return
|
||||||
return False
|
|
||||||
|
|
||||||
add_devices(qwikswitch.QSUSB['light'])
|
qsusb = hass.data[QWIKSWITCH]
|
||||||
return True
|
devs = [QSLight(qsid, qsusb) for qsid in discovery_info[QWIKSWITCH]]
|
||||||
|
add_devices(devs)
|
||||||
|
|
||||||
|
|
||||||
|
class QSLight(QSToggleEntity, Light):
|
||||||
|
"""Light based on a Qwikswitch relay/dimmer module."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Return the brightness of this light (0-255)."""
|
||||||
|
return self._qsusb[self.qsid, 1] if self._dim else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag supported features."""
|
||||||
|
return SUPPORT_BRIGHTNESS if self._dim else 0
|
||||||
|
@ -15,6 +15,9 @@ turn_on:
|
|||||||
color_name:
|
color_name:
|
||||||
description: A human readable color name.
|
description: A human readable color name.
|
||||||
example: 'red'
|
example: 'red'
|
||||||
|
hs_color:
|
||||||
|
description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.
|
||||||
|
example: '[300, 70]'
|
||||||
xy_color:
|
xy_color:
|
||||||
description: Color for the light in XY-format.
|
description: Color for the light in XY-format.
|
||||||
example: '[0.52, 0.43]'
|
example: '[0.52, 0.43]'
|
||||||
@ -179,3 +182,13 @@ xiaomi_miio_set_delayed_turn_off:
|
|||||||
time_period:
|
time_period:
|
||||||
description: Time period for the delayed turn off.
|
description: Time period for the delayed turn off.
|
||||||
example: "5, '0:05', {'minutes': 5}"
|
example: "5, '0:05', {'minutes': 5}"
|
||||||
|
|
||||||
|
yeelight_set_mode:
|
||||||
|
description: Set a operation mode.
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the light entity.
|
||||||
|
example: 'light.yeelight'
|
||||||
|
mode:
|
||||||
|
description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'.
|
||||||
|
example: 'moonlight'
|
||||||
|
@ -4,11 +4,9 @@ Support for the IKEA Tradfri platform.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/light.tradfri/
|
https://home-assistant.io/components/light.tradfri/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION,
|
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION,
|
||||||
SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP,
|
SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP,
|
||||||
@ -17,20 +15,20 @@ from homeassistant.components.light import \
|
|||||||
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA
|
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA
|
||||||
from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \
|
from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \
|
||||||
KEY_API
|
KEY_API
|
||||||
from homeassistant.util import color as color_util
|
import homeassistant.util.color as color_util
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ATTR_TRANSITION_TIME = 'transition_time'
|
||||||
DEPENDENCIES = ['tradfri']
|
DEPENDENCIES = ['tradfri']
|
||||||
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA
|
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA
|
||||||
IKEA = 'IKEA of Sweden'
|
IKEA = 'IKEA of Sweden'
|
||||||
TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager'
|
TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager'
|
||||||
SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION)
|
SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION)
|
||||||
ALLOWED_TEMPERATURES = {IKEA}
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_setup_platform(hass, config,
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
async_add_devices, discovery_info=None):
|
||||||
"""Set up the IKEA Tradfri Light platform."""
|
"""Set up the IKEA Tradfri Light platform."""
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
@ -40,41 +38,43 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
gateway = hass.data[KEY_GATEWAY][gateway_id]
|
gateway = hass.data[KEY_GATEWAY][gateway_id]
|
||||||
|
|
||||||
devices_command = gateway.get_devices()
|
devices_command = gateway.get_devices()
|
||||||
devices_commands = yield from api(devices_command)
|
devices_commands = await api(devices_command)
|
||||||
devices = yield from api(devices_commands)
|
devices = await api(devices_commands)
|
||||||
lights = [dev for dev in devices if dev.has_light_control]
|
lights = [dev for dev in devices if dev.has_light_control]
|
||||||
if lights:
|
if lights:
|
||||||
async_add_devices(TradfriLight(light, api) for light in lights)
|
async_add_devices(
|
||||||
|
TradfriLight(light, api, gateway_id) for light in lights)
|
||||||
|
|
||||||
allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id]
|
allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id]
|
||||||
if allow_tradfri_groups:
|
if allow_tradfri_groups:
|
||||||
groups_command = gateway.get_groups()
|
groups_command = gateway.get_groups()
|
||||||
groups_commands = yield from api(groups_command)
|
groups_commands = await api(groups_command)
|
||||||
groups = yield from api(groups_commands)
|
groups = await api(groups_commands)
|
||||||
if groups:
|
if groups:
|
||||||
async_add_devices(TradfriGroup(group, api) for group in groups)
|
async_add_devices(
|
||||||
|
TradfriGroup(group, api, gateway_id) for group in groups)
|
||||||
|
|
||||||
|
|
||||||
class TradfriGroup(Light):
|
class TradfriGroup(Light):
|
||||||
"""The platform class required by hass."""
|
"""The platform class required by hass."""
|
||||||
|
|
||||||
def __init__(self, light, api):
|
def __init__(self, group, api, gateway_id):
|
||||||
"""Initialize a Group."""
|
"""Initialize a Group."""
|
||||||
self._api = api
|
self._api = api
|
||||||
self._group = light
|
self._unique_id = "group-{}-{}".format(gateway_id, group.id)
|
||||||
self._name = light.name
|
self._group = group
|
||||||
|
self._name = group.name
|
||||||
|
|
||||||
self._refresh(light)
|
self._refresh(group)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_added_to_hass(self):
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Start thread when added to hass."""
|
"""Start thread when added to hass."""
|
||||||
self._async_start_observe()
|
self._async_start_observe()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def unique_id(self):
|
||||||
"""No polling needed for tradfri group."""
|
"""Return unique ID for this group."""
|
||||||
return False
|
return self._unique_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
@ -96,13 +96,11 @@ class TradfriGroup(Light):
|
|||||||
"""Return the brightness of the group lights."""
|
"""Return the brightness of the group lights."""
|
||||||
return self._group.dimmer
|
return self._group.dimmer
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_turn_off(self, **kwargs):
|
||||||
def async_turn_off(self, **kwargs):
|
|
||||||
"""Instruct the group lights to turn off."""
|
"""Instruct the group lights to turn off."""
|
||||||
yield from self._api(self._group.set_state(0))
|
await self._api(self._group.set_state(0))
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_turn_on(self, **kwargs):
|
||||||
def async_turn_on(self, **kwargs):
|
|
||||||
"""Instruct the group lights to turn on, or dim."""
|
"""Instruct the group lights to turn on, or dim."""
|
||||||
keys = {}
|
keys = {}
|
||||||
if ATTR_TRANSITION in kwargs:
|
if ATTR_TRANSITION in kwargs:
|
||||||
@ -112,16 +110,16 @@ class TradfriGroup(Light):
|
|||||||
if kwargs[ATTR_BRIGHTNESS] == 255:
|
if kwargs[ATTR_BRIGHTNESS] == 255:
|
||||||
kwargs[ATTR_BRIGHTNESS] = 254
|
kwargs[ATTR_BRIGHTNESS] = 254
|
||||||
|
|
||||||
yield from self._api(
|
await self._api(
|
||||||
self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys))
|
self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys))
|
||||||
else:
|
else:
|
||||||
yield from self._api(self._group.set_state(1))
|
await self._api(self._group.set_state(1))
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_start_observe(self, exc=None):
|
def _async_start_observe(self, exc=None):
|
||||||
"""Start observation of light."""
|
"""Start observation of light."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
from pytradfri.error import PyTradFriError
|
from pytradfri.error import PytradfriError
|
||||||
if exc:
|
if exc:
|
||||||
_LOGGER.warning("Observation failed for %s", self._name,
|
_LOGGER.warning("Observation failed for %s", self._name,
|
||||||
exc_info=exc)
|
exc_info=exc)
|
||||||
@ -131,7 +129,7 @@ class TradfriGroup(Light):
|
|||||||
err_callback=self._async_start_observe,
|
err_callback=self._async_start_observe,
|
||||||
duration=0)
|
duration=0)
|
||||||
self.hass.async_add_job(self._api(cmd))
|
self.hass.async_add_job(self._api(cmd))
|
||||||
except PyTradFriError as err:
|
except PytradfriError as err:
|
||||||
_LOGGER.warning("Observation failed, trying again", exc_info=err)
|
_LOGGER.warning("Observation failed, trying again", exc_info=err)
|
||||||
self._async_start_observe()
|
self._async_start_observe()
|
||||||
|
|
||||||
@ -146,54 +144,44 @@ class TradfriGroup(Light):
|
|||||||
self._refresh(tradfri_device)
|
self._refresh(tradfri_device)
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Fetch new state data for the group."""
|
||||||
|
await self._api(self._group.update())
|
||||||
|
|
||||||
|
|
||||||
class TradfriLight(Light):
|
class TradfriLight(Light):
|
||||||
"""The platform class required by Home Assistant."""
|
"""The platform class required by Home Assistant."""
|
||||||
|
|
||||||
def __init__(self, light, api):
|
def __init__(self, light, api, gateway_id):
|
||||||
"""Initialize a Light."""
|
"""Initialize a Light."""
|
||||||
self._api = api
|
self._api = api
|
||||||
|
self._unique_id = "light-{}-{}".format(gateway_id, light.id)
|
||||||
self._light = None
|
self._light = None
|
||||||
self._light_control = None
|
self._light_control = None
|
||||||
self._light_data = None
|
self._light_data = None
|
||||||
self._name = None
|
self._name = None
|
||||||
self._hs_color = None
|
self._hs_color = None
|
||||||
self._features = SUPPORTED_FEATURES
|
self._features = SUPPORTED_FEATURES
|
||||||
self._temp_supported = False
|
|
||||||
self._available = True
|
self._available = True
|
||||||
|
|
||||||
self._refresh(light)
|
self._refresh(light)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return unique ID for light."""
|
||||||
|
return self._unique_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def min_mireds(self):
|
def min_mireds(self):
|
||||||
"""Return the coldest color_temp that this light supports."""
|
"""Return the coldest color_temp that this light supports."""
|
||||||
if self._light_control.max_kelvin is not None:
|
return self._light_control.min_mireds
|
||||||
return color_util.color_temperature_kelvin_to_mired(
|
|
||||||
self._light_control.max_kelvin
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_mireds(self):
|
def max_mireds(self):
|
||||||
"""Return the warmest color_temp that this light supports."""
|
"""Return the warmest color_temp that this light supports."""
|
||||||
if self._light_control.min_kelvin is not None:
|
return self._light_control.max_mireds
|
||||||
return color_util.color_temperature_kelvin_to_mired(
|
|
||||||
self._light_control.min_kelvin
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
async def async_added_to_hass(self):
|
||||||
def device_state_attributes(self):
|
|
||||||
"""Return the devices' state attributes."""
|
|
||||||
info = self._light.device_info
|
|
||||||
|
|
||||||
attrs = {}
|
|
||||||
|
|
||||||
if info.battery_level is not None:
|
|
||||||
attrs[ATTR_BATTERY_LEVEL] = info.battery_level
|
|
||||||
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def async_added_to_hass(self):
|
|
||||||
"""Start thread when added to hass."""
|
"""Start thread when added to hass."""
|
||||||
self._async_start_observe()
|
self._async_start_observe()
|
||||||
|
|
||||||
@ -229,64 +217,87 @@ class TradfriLight(Light):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def color_temp(self):
|
def color_temp(self):
|
||||||
"""Return the CT color value in mireds."""
|
"""Return the color temp value in mireds."""
|
||||||
kelvin_color = self._light_data.kelvin_color_inferred
|
return self._light_data.color_temp
|
||||||
if kelvin_color is not None:
|
|
||||||
return color_util.color_temperature_kelvin_to_mired(
|
|
||||||
kelvin_color
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hs_color(self):
|
def hs_color(self):
|
||||||
"""HS color of the light."""
|
"""HS color of the light."""
|
||||||
return self._hs_color
|
if self._light_control.can_set_color:
|
||||||
|
hsbxy = self._light_data.hsb_xy_color
|
||||||
|
hue = hsbxy[0] / (65535 / 360)
|
||||||
|
sat = hsbxy[1] / (65279 / 100)
|
||||||
|
if hue is not None and sat is not None:
|
||||||
|
return hue, sat
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_turn_off(self, **kwargs):
|
||||||
def async_turn_off(self, **kwargs):
|
|
||||||
"""Instruct the light to turn off."""
|
"""Instruct the light to turn off."""
|
||||||
yield from self._api(self._light_control.set_state(False))
|
await self._api(self._light_control.set_state(False))
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_turn_on(self, **kwargs):
|
||||||
def async_turn_on(self, **kwargs):
|
"""Instruct the light to turn on."""
|
||||||
"""
|
params = {}
|
||||||
Instruct the light to turn on.
|
transition_time = None
|
||||||
|
|
||||||
After adding "self._light_data.hexcolor is not None"
|
|
||||||
for ATTR_HS_COLOR, this also supports Philips Hue bulbs.
|
|
||||||
"""
|
|
||||||
if ATTR_HS_COLOR in kwargs and self._light_data.hex_color is not None:
|
|
||||||
rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
|
|
||||||
yield from self._api(
|
|
||||||
self._light.light_control.set_rgb_color(*rgb))
|
|
||||||
|
|
||||||
elif ATTR_COLOR_TEMP in kwargs and \
|
|
||||||
self._light_data.hex_color is not None and \
|
|
||||||
self._temp_supported:
|
|
||||||
kelvin = color_util.color_temperature_mired_to_kelvin(
|
|
||||||
kwargs[ATTR_COLOR_TEMP])
|
|
||||||
yield from self._api(
|
|
||||||
self._light_control.set_kelvin_color(kelvin))
|
|
||||||
|
|
||||||
keys = {}
|
|
||||||
if ATTR_TRANSITION in kwargs:
|
if ATTR_TRANSITION in kwargs:
|
||||||
keys['transition_time'] = int(kwargs[ATTR_TRANSITION]) * 10
|
transition_time = int(kwargs[ATTR_TRANSITION]) * 10
|
||||||
|
|
||||||
if ATTR_BRIGHTNESS in kwargs:
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||||
if kwargs[ATTR_BRIGHTNESS] == 255:
|
|
||||||
kwargs[ATTR_BRIGHTNESS] = 254
|
|
||||||
|
|
||||||
yield from self._api(
|
if brightness is not None:
|
||||||
self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS],
|
if brightness > 254:
|
||||||
**keys))
|
brightness = 254
|
||||||
|
elif brightness < 0:
|
||||||
|
brightness = 0
|
||||||
|
|
||||||
|
if ATTR_HS_COLOR in kwargs and self._light_control.can_set_color:
|
||||||
|
params[ATTR_BRIGHTNESS] = brightness
|
||||||
|
hue = int(kwargs[ATTR_HS_COLOR][0] * (65535 / 360))
|
||||||
|
sat = int(kwargs[ATTR_HS_COLOR][1] * (65279 / 100))
|
||||||
|
await self._api(
|
||||||
|
self._light_control.set_hsb(hue, sat, **params))
|
||||||
|
return
|
||||||
|
|
||||||
|
if ATTR_COLOR_TEMP in kwargs and (self._light_control.can_set_temp or
|
||||||
|
self._light_control.can_set_color):
|
||||||
|
temp = kwargs[ATTR_COLOR_TEMP]
|
||||||
|
if temp > self.max_mireds:
|
||||||
|
temp = self.max_mireds
|
||||||
|
elif temp < self.min_mireds:
|
||||||
|
temp = self.min_mireds
|
||||||
|
|
||||||
|
if brightness is None:
|
||||||
|
params[ATTR_TRANSITION_TIME] = transition_time
|
||||||
|
# White Spectrum bulb
|
||||||
|
if (self._light_control.can_set_temp and
|
||||||
|
not self._light_control.can_set_color):
|
||||||
|
await self._api(
|
||||||
|
self._light_control.set_color_temp(temp, **params))
|
||||||
|
# Color bulb (CWS)
|
||||||
|
# color_temp needs to be set with hue/saturation
|
||||||
|
if self._light_control.can_set_color:
|
||||||
|
params[ATTR_BRIGHTNESS] = brightness
|
||||||
|
temp_k = color_util.color_temperature_mired_to_kelvin(temp)
|
||||||
|
hs_color = color_util.color_temperature_to_hs(temp_k)
|
||||||
|
hue = int(hs_color[0] * (65535 / 360))
|
||||||
|
sat = int(hs_color[1] * (65279 / 100))
|
||||||
|
await self._api(
|
||||||
|
self._light_control.set_hsb(hue, sat,
|
||||||
|
**params))
|
||||||
|
|
||||||
|
if brightness is not None:
|
||||||
|
params[ATTR_TRANSITION_TIME] = transition_time
|
||||||
|
await self._api(
|
||||||
|
self._light_control.set_dimmer(brightness,
|
||||||
|
**params))
|
||||||
else:
|
else:
|
||||||
yield from self._api(
|
await self._api(
|
||||||
self._light_control.set_state(True))
|
self._light_control.set_state(True))
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_start_observe(self, exc=None):
|
def _async_start_observe(self, exc=None):
|
||||||
"""Start observation of light."""
|
"""Start observation of light."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
from pytradfri.error import PyTradFriError
|
from pytradfri.error import PytradfriError
|
||||||
if exc:
|
if exc:
|
||||||
_LOGGER.warning("Observation failed for %s", self._name,
|
_LOGGER.warning("Observation failed for %s", self._name,
|
||||||
exc_info=exc)
|
exc_info=exc)
|
||||||
@ -296,7 +307,7 @@ class TradfriLight(Light):
|
|||||||
err_callback=self._async_start_observe,
|
err_callback=self._async_start_observe,
|
||||||
duration=0)
|
duration=0)
|
||||||
self.hass.async_add_job(self._api(cmd))
|
self.hass.async_add_job(self._api(cmd))
|
||||||
except PyTradFriError as err:
|
except PytradfriError as err:
|
||||||
_LOGGER.warning("Observation failed, trying again", exc_info=err)
|
_LOGGER.warning("Observation failed, trying again", exc_info=err)
|
||||||
self._async_start_observe()
|
self._async_start_observe()
|
||||||
|
|
||||||
@ -309,27 +320,15 @@ class TradfriLight(Light):
|
|||||||
self._light_control = light.light_control
|
self._light_control = light.light_control
|
||||||
self._light_data = light.light_control.lights[0]
|
self._light_data = light.light_control.lights[0]
|
||||||
self._name = light.name
|
self._name = light.name
|
||||||
self._hs_color = None
|
|
||||||
self._features = SUPPORTED_FEATURES
|
self._features = SUPPORTED_FEATURES
|
||||||
|
|
||||||
if self._light.device_info.manufacturer == IKEA:
|
if light.light_control.can_set_color:
|
||||||
if self._light_control.can_set_kelvin:
|
self._features |= SUPPORT_COLOR
|
||||||
|
if light.light_control.can_set_temp:
|
||||||
self._features |= SUPPORT_COLOR_TEMP
|
self._features |= SUPPORT_COLOR_TEMP
|
||||||
if self._light_control.can_set_color:
|
|
||||||
self._features |= SUPPORT_COLOR
|
|
||||||
else:
|
|
||||||
if self._light_data.hex_color is not None:
|
|
||||||
self._features |= SUPPORT_COLOR
|
|
||||||
|
|
||||||
self._temp_supported = self._light.device_info.manufacturer \
|
|
||||||
in ALLOWED_TEMPERATURES
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _observe_update(self, tradfri_device):
|
def _observe_update(self, tradfri_device):
|
||||||
"""Receive new state data for this light."""
|
"""Receive new state data for this light."""
|
||||||
self._refresh(tradfri_device)
|
self._refresh(tradfri_device)
|
||||||
rgb = color_util.rgb_hex_to_rgb_list(
|
|
||||||
self._light_data.hex_color_inferred
|
|
||||||
)
|
|
||||||
self._hs_color = color_util.color_RGB_to_hs(*rgb)
|
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
@ -38,6 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
'philips.light.ceiling',
|
'philips.light.ceiling',
|
||||||
'philips.light.zyceiling',
|
'philips.light.zyceiling',
|
||||||
'philips.light.bulb',
|
'philips.light.bulb',
|
||||||
|
'philips.light.candle',
|
||||||
'philips.light.candle2']),
|
'philips.light.candle2']),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -149,7 +150,9 @@ async def async_setup_platform(hass, config, async_add_devices,
|
|||||||
device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id)
|
device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id)
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
hass.data[DATA_KEY][host] = device
|
hass.data[DATA_KEY][host] = device
|
||||||
elif model in ['philips.light.bulb', 'philips.light.candle2']:
|
elif model in ['philips.light.bulb',
|
||||||
|
'philips.light.candle',
|
||||||
|
'philips.light.candle2']:
|
||||||
from miio import PhilipsBulb
|
from miio import PhilipsBulb
|
||||||
light = PhilipsBulb(host, token)
|
light = PhilipsBulb(host, token)
|
||||||
device = XiaomiPhilipsBulb(name, light, model, unique_id)
|
device = XiaomiPhilipsBulb(name, light, model, unique_id)
|
||||||
|
@ -16,7 +16,7 @@ from homeassistant.components.light import (
|
|||||||
ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP,
|
ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP,
|
||||||
ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS,
|
ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS,
|
||||||
SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH,
|
SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH,
|
||||||
SUPPORT_EFFECT, Light, PLATFORM_SCHEMA)
|
SUPPORT_EFFECT, Light, PLATFORM_SCHEMA, ATTR_ENTITY_ID, DOMAIN)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.util.color as color_util
|
import homeassistant.util.color as color_util
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ DEFAULT_TRANSITION = 350
|
|||||||
CONF_SAVE_ON_CHANGE = 'save_on_change'
|
CONF_SAVE_ON_CHANGE = 'save_on_change'
|
||||||
CONF_MODE_MUSIC = 'use_music_mode'
|
CONF_MODE_MUSIC = 'use_music_mode'
|
||||||
|
|
||||||
DOMAIN = 'yeelight'
|
DATA_KEY = 'light.yeelight'
|
||||||
|
|
||||||
DEVICE_SCHEMA = vol.Schema({
|
DEVICE_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
@ -90,6 +90,13 @@ YEELIGHT_EFFECT_LIST = [
|
|||||||
EFFECT_TWITTER,
|
EFFECT_TWITTER,
|
||||||
EFFECT_STOP]
|
EFFECT_STOP]
|
||||||
|
|
||||||
|
SERVICE_SET_MODE = 'yeelight_set_mode'
|
||||||
|
ATTR_MODE = 'mode'
|
||||||
|
|
||||||
|
YEELIGHT_SERVICE_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def _cmd(func):
|
def _cmd(func):
|
||||||
"""Define a wrapper to catch exceptions from the bulb."""
|
"""Define a wrapper to catch exceptions from the bulb."""
|
||||||
@ -106,6 +113,11 @@ def _cmd(func):
|
|||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the Yeelight bulbs."""
|
"""Set up the Yeelight bulbs."""
|
||||||
|
from yeelight.enums import PowerMode
|
||||||
|
|
||||||
|
if DATA_KEY not in hass.data:
|
||||||
|
hass.data[DATA_KEY] = {}
|
||||||
|
|
||||||
lights = []
|
lights = []
|
||||||
if discovery_info is not None:
|
if discovery_info is not None:
|
||||||
_LOGGER.debug("Adding autodetected %s", discovery_info['hostname'])
|
_LOGGER.debug("Adding autodetected %s", discovery_info['hostname'])
|
||||||
@ -115,16 +127,44 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
discovery_info['properties']['mac'])
|
discovery_info['properties']['mac'])
|
||||||
device = {'name': name, 'ipaddr': discovery_info['host']}
|
device = {'name': name, 'ipaddr': discovery_info['host']}
|
||||||
|
|
||||||
lights.append(YeelightLight(device, DEVICE_SCHEMA({})))
|
light = YeelightLight(device, DEVICE_SCHEMA({}))
|
||||||
|
lights.append(light)
|
||||||
|
hass.data[DATA_KEY][name] = light
|
||||||
else:
|
else:
|
||||||
for ipaddr, device_config in config[CONF_DEVICES].items():
|
for ipaddr, device_config in config[CONF_DEVICES].items():
|
||||||
_LOGGER.debug("Adding configured %s", device_config[CONF_NAME])
|
name = device_config[CONF_NAME]
|
||||||
|
_LOGGER.debug("Adding configured %s", name)
|
||||||
|
|
||||||
device = {'name': device_config[CONF_NAME], 'ipaddr': ipaddr}
|
device = {'name': name, 'ipaddr': ipaddr}
|
||||||
lights.append(YeelightLight(device, device_config))
|
light = YeelightLight(device, device_config)
|
||||||
|
lights.append(light)
|
||||||
|
hass.data[DATA_KEY][name] = light
|
||||||
|
|
||||||
add_devices(lights, True)
|
add_devices(lights, True)
|
||||||
|
|
||||||
|
def service_handler(service):
|
||||||
|
"""Dispatch service calls to target entities."""
|
||||||
|
params = {key: value for key, value in service.data.items()
|
||||||
|
if key != ATTR_ENTITY_ID}
|
||||||
|
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||||
|
if entity_ids:
|
||||||
|
target_devices = [dev for dev in hass.data[DATA_KEY].values()
|
||||||
|
if dev.entity_id in entity_ids]
|
||||||
|
else:
|
||||||
|
target_devices = hass.data[DATA_KEY].values()
|
||||||
|
|
||||||
|
for target_device in target_devices:
|
||||||
|
if service.service == SERVICE_SET_MODE:
|
||||||
|
target_device.set_mode(**params)
|
||||||
|
|
||||||
|
service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend({
|
||||||
|
vol.Required(ATTR_MODE):
|
||||||
|
vol.In([mode.name.lower() for mode in PowerMode])
|
||||||
|
})
|
||||||
|
hass.services.register(
|
||||||
|
DOMAIN, SERVICE_SET_MODE, service_handler,
|
||||||
|
schema=service_schema_set_mode)
|
||||||
|
|
||||||
|
|
||||||
class YeelightLight(Light):
|
class YeelightLight(Light):
|
||||||
"""Representation of a Yeelight light."""
|
"""Representation of a Yeelight light."""
|
||||||
@ -444,3 +484,11 @@ class YeelightLight(Light):
|
|||||||
self._bulb.turn_off(duration=duration)
|
self._bulb.turn_off(duration=duration)
|
||||||
except yeelight.BulbException as ex:
|
except yeelight.BulbException as ex:
|
||||||
_LOGGER.error("Unable to turn the bulb off: %s", ex)
|
_LOGGER.error("Unable to turn the bulb off: %s", ex)
|
||||||
|
|
||||||
|
def set_mode(self, mode: str):
|
||||||
|
"""Set a power mode."""
|
||||||
|
import yeelight
|
||||||
|
try:
|
||||||
|
self._bulb.set_power_mode(yeelight.enums.PowerMode[mode.upper()])
|
||||||
|
except yeelight.BulbException as ex:
|
||||||
|
_LOGGER.error("Unable to set the power mode: %s", ex)
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.components.light import (
|
|||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
import homeassistant.util.color as color_util
|
import homeassistant.util.color as color_util
|
||||||
|
|
||||||
REQUIREMENTS = ['yeelightsunflower==0.0.8']
|
REQUIREMENTS = ['yeelightsunflower==0.0.10']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ class BMWLock(LockDevice):
|
|||||||
self._account = account
|
self._account = account
|
||||||
self._vehicle = vehicle
|
self._vehicle = vehicle
|
||||||
self._attribute = attribute
|
self._attribute = attribute
|
||||||
self._name = '{} {}'.format(self._vehicle.modelName, self._attribute)
|
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
|
||||||
self._sensor_name = sensor_name
|
self._sensor_name = sensor_name
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ class BMWLock(LockDevice):
|
|||||||
"""Return the state attributes of the lock."""
|
"""Return the state attributes of the lock."""
|
||||||
vehicle_state = self._vehicle.state
|
vehicle_state = self._vehicle.state
|
||||||
return {
|
return {
|
||||||
'car': self._vehicle.modelName,
|
'car': self._vehicle.name,
|
||||||
'door_lock_state': vehicle_state.door_lock_state.value
|
'door_lock_state': vehicle_state.door_lock_state.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ class BMWLock(LockDevice):
|
|||||||
|
|
||||||
def lock(self, **kwargs):
|
def lock(self, **kwargs):
|
||||||
"""Lock the car."""
|
"""Lock the car."""
|
||||||
_LOGGER.debug("%s: locking doors", self._vehicle.modelName)
|
_LOGGER.debug("%s: locking doors", self._vehicle.name)
|
||||||
# Optimistic state set here because it takes some time before the
|
# Optimistic state set here because it takes some time before the
|
||||||
# update callback response
|
# update callback response
|
||||||
self._state = STATE_LOCKED
|
self._state = STATE_LOCKED
|
||||||
@ -79,7 +79,7 @@ class BMWLock(LockDevice):
|
|||||||
|
|
||||||
def unlock(self, **kwargs):
|
def unlock(self, **kwargs):
|
||||||
"""Unlock the car."""
|
"""Unlock the car."""
|
||||||
_LOGGER.debug("%s: unlocking doors", self._vehicle.modelName)
|
_LOGGER.debug("%s: unlocking doors", self._vehicle.name)
|
||||||
# Optimistic state set here because it takes some time before the
|
# Optimistic state set here because it takes some time before the
|
||||||
# update callback response
|
# update callback response
|
||||||
self._state = STATE_UNLOCKED
|
self._state = STATE_UNLOCKED
|
||||||
@ -88,13 +88,17 @@ class BMWLock(LockDevice):
|
|||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update state of the lock."""
|
"""Update state of the lock."""
|
||||||
_LOGGER.debug("%s: updating data for %s", self._vehicle.modelName,
|
from bimmer_connected.state import LockState
|
||||||
|
|
||||||
|
_LOGGER.debug("%s: updating data for %s", self._vehicle.name,
|
||||||
self._attribute)
|
self._attribute)
|
||||||
vehicle_state = self._vehicle.state
|
vehicle_state = self._vehicle.state
|
||||||
|
|
||||||
# Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED
|
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
||||||
self._state = (STATE_LOCKED if vehicle_state.door_lock_state.value
|
self._state = STATE_LOCKED \
|
||||||
in ('LOCKED', 'SECURED') else STATE_UNLOCKED)
|
if vehicle_state.door_lock_state \
|
||||||
|
in [LockState.LOCKED, LockState.SECURED] \
|
||||||
|
else STATE_UNLOCKED
|
||||||
|
|
||||||
def update_callback(self):
|
def update_callback(self):
|
||||||
"""Schedule a state update."""
|
"""Schedule a state update."""
|
||||||
|
@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
|
|||||||
SERVICE_PLAY_MEDIA)
|
SERVICE_PLAY_MEDIA)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['youtube_dl==2018.03.10']
|
REQUIREMENTS = ['youtube_dl==2018.04.03']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -83,7 +83,8 @@ ATTR_MEDIA_SHUFFLE = 'shuffle'
|
|||||||
|
|
||||||
MEDIA_TYPE_MUSIC = 'music'
|
MEDIA_TYPE_MUSIC = 'music'
|
||||||
MEDIA_TYPE_TVSHOW = 'tvshow'
|
MEDIA_TYPE_TVSHOW = 'tvshow'
|
||||||
MEDIA_TYPE_VIDEO = 'movie'
|
MEDIA_TYPE_MOVIE = 'movie'
|
||||||
|
MEDIA_TYPE_VIDEO = 'video'
|
||||||
MEDIA_TYPE_EPISODE = 'episode'
|
MEDIA_TYPE_EPISODE = 'episode'
|
||||||
MEDIA_TYPE_CHANNEL = 'channel'
|
MEDIA_TYPE_CHANNEL = 'channel'
|
||||||
MEDIA_TYPE_PLAYLIST = 'playlist'
|
MEDIA_TYPE_PLAYLIST = 'playlist'
|
||||||
|
@ -18,7 +18,7 @@ from homeassistant.core import callback
|
|||||||
from homeassistant.helpers.dispatcher import (dispatcher_send,
|
from homeassistant.helpers.dispatcher import (dispatcher_send,
|
||||||
async_dispatcher_connect)
|
async_dispatcher_connect)
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
|
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK,
|
||||||
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
|
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
|
||||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||||
SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA)
|
SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||||
@ -517,7 +517,7 @@ class CastDevice(MediaPlayerDevice):
|
|||||||
elif self.media_status.media_is_tvshow:
|
elif self.media_status.media_is_tvshow:
|
||||||
return MEDIA_TYPE_TVSHOW
|
return MEDIA_TYPE_TVSHOW
|
||||||
elif self.media_status.media_is_movie:
|
elif self.media_status.media_is_movie:
|
||||||
return MEDIA_TYPE_VIDEO
|
return MEDIA_TYPE_MOVIE
|
||||||
elif self.media_status.media_is_musictrack:
|
elif self.media_status.media_is_musictrack:
|
||||||
return MEDIA_TYPE_MUSIC
|
return MEDIA_TYPE_MUSIC
|
||||||
return None
|
return None
|
||||||
|
@ -10,7 +10,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MEDIA_TYPE_CHANNEL, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_EPISODE,
|
MEDIA_TYPE_CHANNEL, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_EPISODE,
|
||||||
MEDIA_TYPE_VIDEO, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP,
|
MEDIA_TYPE_MOVIE, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP,
|
||||||
SUPPORT_VOLUME_MUTE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
|
SUPPORT_VOLUME_MUTE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
|
||||||
SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, DOMAIN, PLATFORM_SCHEMA,
|
SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, DOMAIN, PLATFORM_SCHEMA,
|
||||||
MediaPlayerDevice)
|
MediaPlayerDevice)
|
||||||
@ -281,7 +281,7 @@ class ChannelsPlayer(MediaPlayerDevice):
|
|||||||
if media_type == MEDIA_TYPE_CHANNEL:
|
if media_type == MEDIA_TYPE_CHANNEL:
|
||||||
response = self.client.play_channel(media_id)
|
response = self.client.play_channel(media_id)
|
||||||
self.update_state(response)
|
self.update_state(response)
|
||||||
elif media_type in [MEDIA_TYPE_VIDEO, MEDIA_TYPE_EPISODE,
|
elif media_type in [MEDIA_TYPE_MOVIE, MEDIA_TYPE_EPISODE,
|
||||||
MEDIA_TYPE_TVSHOW]:
|
MEDIA_TYPE_TVSHOW]:
|
||||||
response = self.client.play_recording(media_id)
|
response = self.client.play_recording(media_id)
|
||||||
self.update_state(response)
|
self.update_state(response)
|
||||||
|
@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation
|
|||||||
https://home-assistant.io/components/demo/
|
https://home-assistant.io/components/demo/
|
||||||
"""
|
"""
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
|
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK,
|
||||||
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
|
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
|
||||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||||
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_PLAY,
|
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_PLAY,
|
||||||
@ -147,7 +147,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
|
|||||||
@property
|
@property
|
||||||
def media_content_type(self):
|
def media_content_type(self):
|
||||||
"""Return the content type of current playing media."""
|
"""Return the content type of current playing media."""
|
||||||
return MEDIA_TYPE_VIDEO
|
return MEDIA_TYPE_MOVIE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_duration(self):
|
def media_duration(self):
|
||||||
|
@ -8,7 +8,7 @@ import voluptuous as vol
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
|
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
|
||||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA,
|
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA,
|
||||||
SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY,
|
SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY,
|
||||||
MediaPlayerDevice)
|
MediaPlayerDevice)
|
||||||
@ -154,7 +154,7 @@ class DirecTvDevice(MediaPlayerDevice):
|
|||||||
"""Return the content type of current playing media."""
|
"""Return the content type of current playing media."""
|
||||||
if 'episodeTitle' in self._current:
|
if 'episodeTitle' in self._current:
|
||||||
return MEDIA_TYPE_TVSHOW
|
return MEDIA_TYPE_TVSHOW
|
||||||
return MEDIA_TYPE_VIDEO
|
return MEDIA_TYPE_MOVIE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_channel(self):
|
def media_channel(self):
|
||||||
|
@ -10,7 +10,7 @@ import logging
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
||||||
SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
|
SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
|
||||||
MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA)
|
MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -231,7 +231,7 @@ class EmbyDevice(MediaPlayerDevice):
|
|||||||
if media_type == 'Episode':
|
if media_type == 'Episode':
|
||||||
return MEDIA_TYPE_TVSHOW
|
return MEDIA_TYPE_TVSHOW
|
||||||
elif media_type == 'Movie':
|
elif media_type == 'Movie':
|
||||||
return MEDIA_TYPE_VIDEO
|
return MEDIA_TYPE_MOVIE
|
||||||
elif media_type == 'Trailer':
|
elif media_type == 'Trailer':
|
||||||
return MEDIA_TYPE_TRAILER
|
return MEDIA_TYPE_TRAILER
|
||||||
elif media_type == 'Music':
|
elif media_type == 'Music':
|
||||||
|
@ -19,8 +19,8 @@ from homeassistant.components.media_player import (
|
|||||||
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
|
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
|
||||||
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET,
|
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET,
|
||||||
MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
|
MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
|
||||||
MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_PLAYLIST,
|
MEDIA_TYPE_MOVIE, MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL,
|
||||||
MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON)
|
MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME,
|
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME,
|
||||||
CONF_PORT, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD,
|
CONF_PORT, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD,
|
||||||
@ -67,7 +67,7 @@ MEDIA_TYPES = {
|
|||||||
'video': MEDIA_TYPE_VIDEO,
|
'video': MEDIA_TYPE_VIDEO,
|
||||||
'set': MEDIA_TYPE_PLAYLIST,
|
'set': MEDIA_TYPE_PLAYLIST,
|
||||||
'musicvideo': MEDIA_TYPE_VIDEO,
|
'musicvideo': MEDIA_TYPE_VIDEO,
|
||||||
'movie': MEDIA_TYPE_VIDEO,
|
'movie': MEDIA_TYPE_MOVIE,
|
||||||
'tvshow': MEDIA_TYPE_TVSHOW,
|
'tvshow': MEDIA_TYPE_TVSHOW,
|
||||||
'season': MEDIA_TYPE_TVSHOW,
|
'season': MEDIA_TYPE_TVSHOW,
|
||||||
'episode': MEDIA_TYPE_TVSHOW,
|
'episode': MEDIA_TYPE_TVSHOW,
|
||||||
|
@ -22,7 +22,7 @@ from homeassistant.const import (
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
REQUIREMENTS = ['liveboxplaytv==2.0.2', 'pyteleloisirs==3.3']
|
REQUIREMENTS = ['liveboxplaytv==2.0.2', 'pyteleloisirs==3.4']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.helpers.script import Script
|
from homeassistant.helpers.script import Script
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
REQUIREMENTS = ['ha-philipsjs==0.0.2']
|
REQUIREMENTS = ['ha-philipsjs==0.0.3']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant import util
|
from homeassistant import util
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, PLATFORM_SCHEMA,
|
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, PLATFORM_SCHEMA,
|
||||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK,
|
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK,
|
||||||
SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||||
MediaPlayerDevice)
|
MediaPlayerDevice)
|
||||||
@ -480,7 +480,7 @@ class PlexClient(MediaPlayerDevice):
|
|||||||
self._media_episode = str(self._session.index).zfill(2)
|
self._media_episode = str(self._session.index).zfill(2)
|
||||||
|
|
||||||
elif self._session_type == 'movie':
|
elif self._session_type == 'movie':
|
||||||
self._media_content_type = MEDIA_TYPE_VIDEO
|
self._media_content_type = MEDIA_TYPE_MOVIE
|
||||||
if self._session.year is not None and \
|
if self._session.year is not None and \
|
||||||
self._media_title is not None:
|
self._media_title is not None:
|
||||||
self._media_title += ' (' + str(self._session.year) + ')'
|
self._media_title += ' (' + str(self._session.year) + ')'
|
||||||
@ -576,7 +576,7 @@ class PlexClient(MediaPlayerDevice):
|
|||||||
elif self._session_type == 'episode':
|
elif self._session_type == 'episode':
|
||||||
return MEDIA_TYPE_TVSHOW
|
return MEDIA_TYPE_TVSHOW
|
||||||
elif self._session_type == 'movie':
|
elif self._session_type == 'movie':
|
||||||
return MEDIA_TYPE_VIDEO
|
return MEDIA_TYPE_MOVIE
|
||||||
elif self._session_type == 'track':
|
elif self._session_type == 'track':
|
||||||
return MEDIA_TYPE_MUSIC
|
return MEDIA_TYPE_MUSIC
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import logging
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA,
|
MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA,
|
||||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||||
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA)
|
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -155,7 +155,7 @@ class RokuDevice(MediaPlayerDevice):
|
|||||||
return None
|
return None
|
||||||
elif self.current_app.name == "Roku":
|
elif self.current_app.name == "Roku":
|
||||||
return None
|
return None
|
||||||
return MEDIA_TYPE_VIDEO
|
return MEDIA_TYPE_MOVIE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_image_url(self):
|
def media_image_url(self):
|
||||||
|
@ -17,7 +17,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.exceptions import PlatformNotReady
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['python-songpal==0.0.6']
|
REQUIREMENTS = ['python-songpal==0.0.7']
|
||||||
|
|
||||||
SUPPORT_SONGPAL = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | \
|
SUPPORT_SONGPAL = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | \
|
||||||
SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | \
|
SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | \
|
||||||
@ -101,7 +101,7 @@ class SongpalDevice(MediaPlayerDevice):
|
|||||||
import songpal
|
import songpal
|
||||||
self._name = name
|
self._name = name
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
self.dev = songpal.Protocol(self.endpoint)
|
self.dev = songpal.Device(self.endpoint)
|
||||||
self._sysinfo = None
|
self._sysinfo = None
|
||||||
|
|
||||||
self._state = False
|
self._state = False
|
||||||
|
@ -35,6 +35,7 @@ CONF_SOURCES = 'sources'
|
|||||||
CONF_ON_ACTION = 'turn_on_action'
|
CONF_ON_ACTION = 'turn_on_action'
|
||||||
|
|
||||||
DEFAULT_NAME = 'LG webOS Smart TV'
|
DEFAULT_NAME = 'LG webOS Smart TV'
|
||||||
|
LIVETV_APP_ID = 'com.webos.app.livetv'
|
||||||
|
|
||||||
WEBOSTV_CONFIG_FILE = 'webostv.conf'
|
WEBOSTV_CONFIG_FILE = 'webostv.conf'
|
||||||
|
|
||||||
@ -357,8 +358,16 @@ class LgWebOSDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
def media_next_track(self):
|
def media_next_track(self):
|
||||||
"""Send next track command."""
|
"""Send next track command."""
|
||||||
|
current_input = self._client.get_input()
|
||||||
|
if current_input == LIVETV_APP_ID:
|
||||||
|
self._client.channel_up()
|
||||||
|
else:
|
||||||
self._client.fast_forward()
|
self._client.fast_forward()
|
||||||
|
|
||||||
def media_previous_track(self):
|
def media_previous_track(self):
|
||||||
"""Send the previous track command."""
|
"""Send the previous track command."""
|
||||||
|
current_input = self._client.get_input()
|
||||||
|
if current_input == LIVETV_APP_ID:
|
||||||
|
self._client.channel_down()
|
||||||
|
else:
|
||||||
self._client.rewind()
|
self._client.rewind()
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user