mirror of
https://github.com/home-assistant/core.git
synced 2026-01-09 00:58:32 +01:00
Compare commits
205 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
224c258876 | ||
|
|
e4d76d5c44 | ||
|
|
c702e1e3c6 | ||
|
|
47660f9312 | ||
|
|
1a5028f56f | ||
|
|
ca729b178b | ||
|
|
557b745053 | ||
|
|
0400e29f7a | ||
|
|
8db2152230 | ||
|
|
47848e26f9 | ||
|
|
c7ff8d4996 | ||
|
|
69ec7980ad | ||
|
|
5a0c707a37 | ||
|
|
473bf93973 | ||
|
|
ed75549123 | ||
|
|
3e98aad8a2 | ||
|
|
542160fc56 | ||
|
|
e2cc1564a0 | ||
|
|
2836ff86fe | ||
|
|
cb07ea0d60 | ||
|
|
91aa874c0c | ||
|
|
ca0ee509e7 | ||
|
|
8062f48973 | ||
|
|
ed299a9137 | ||
|
|
349de19316 | ||
|
|
7c9597f824 | ||
|
|
a011048a4e | ||
|
|
0ef9882e2e | ||
|
|
ec4495bd0c | ||
|
|
c5c64e738e | ||
|
|
89fc3b2a1b | ||
|
|
48f0e8311b | ||
|
|
e0e5b860e4 | ||
|
|
988bcf9399 | ||
|
|
e95c50c742 | ||
|
|
6ff4ea1126 | ||
|
|
a7c74151bc | ||
|
|
6859d5216e | ||
|
|
73a0c664b8 | ||
|
|
b0ff51b0ef | ||
|
|
e22802a4d4 | ||
|
|
aa29eeba04 | ||
|
|
cc74035c3b | ||
|
|
fe47253f68 | ||
|
|
3ee3acd550 | ||
|
|
234c759b45 | ||
|
|
ba4d4bcd29 | ||
|
|
d7bbdb033d | ||
|
|
63fd5f2d31 | ||
|
|
f353d51ab1 | ||
|
|
3f484228cb | ||
|
|
717a0c2b2d | ||
|
|
34090bd021 | ||
|
|
bb1583c453 | ||
|
|
d7ba2aad1d | ||
|
|
bb4ca1f525 | ||
|
|
bd335e1ac1 | ||
|
|
2c7060896b | ||
|
|
abeb875c61 | ||
|
|
41c1997b88 | ||
|
|
995758b8ac | ||
|
|
f33e432cab | ||
|
|
29984efd8c | ||
|
|
d179686edf | ||
|
|
0c87fb421e | ||
|
|
f575d1d3a6 | ||
|
|
3d4f2926e9 | ||
|
|
7412d0f97c | ||
|
|
f91ff76b95 | ||
|
|
648adcc708 | ||
|
|
8804f55fcc | ||
|
|
1f93984fd5 | ||
|
|
ae84a91ea8 | ||
|
|
2aab646be2 | ||
|
|
10e3698fd7 | ||
|
|
7368c623d4 | ||
|
|
d82e5ecbb0 | ||
|
|
239c60c09f | ||
|
|
38c33bd01e | ||
|
|
86f5f0226c | ||
|
|
a368db9ad4 | ||
|
|
87316c4e83 | ||
|
|
38b1ce3fe0 | ||
|
|
9920699bb8 | ||
|
|
b9bf6963fd | ||
|
|
60dc337f3f | ||
|
|
85c72fbca6 | ||
|
|
85ccd71d39 | ||
|
|
09cbcb74bc | ||
|
|
0e453fe492 | ||
|
|
1d16bb2cd4 | ||
|
|
05d41bc0ee | ||
|
|
f3285f96bb | ||
|
|
1d5ffe9ad5 | ||
|
|
ed6e349515 | ||
|
|
a85e018bc4 | ||
|
|
a0b93c2add | ||
|
|
e593383b4d | ||
|
|
b3c3721a79 | ||
|
|
310c073c64 | ||
|
|
d39784906b | ||
|
|
6d2e7db123 | ||
|
|
d8e43978b7 | ||
|
|
76c0295403 | ||
|
|
2bc7444427 | ||
|
|
4518e6bdf7 | ||
|
|
d6c12e47f4 | ||
|
|
cea2bf94bd | ||
|
|
1fcaaf93ad | ||
|
|
d4c7515681 | ||
|
|
c94834d8f6 | ||
|
|
ec5da05804 | ||
|
|
d84cd01cbf | ||
|
|
a1da6a677a | ||
|
|
55943cfac0 | ||
|
|
400aaf8a3a | ||
|
|
046683ee3f | ||
|
|
c7f5beb794 | ||
|
|
c508ba166c | ||
|
|
68bd5f5df8 | ||
|
|
70c5807976 | ||
|
|
3b1534c126 | ||
|
|
2559bc4226 | ||
|
|
0be922dc9c | ||
|
|
7038dd484a | ||
|
|
1bd31e3459 | ||
|
|
074fcd96ed | ||
|
|
5580bec1d3 | ||
|
|
af3afb673a | ||
|
|
697c331903 | ||
|
|
971d933140 | ||
|
|
0300ef2040 | ||
|
|
a396ee2cb5 | ||
|
|
7ca7951526 | ||
|
|
5bf3b2dd9f | ||
|
|
c6cee1ccd3 | ||
|
|
16a4180fab | ||
|
|
e8f0e534f9 | ||
|
|
07f1e2ce75 | ||
|
|
eaa9c4d437 | ||
|
|
db277ad023 | ||
|
|
3484e506e8 | ||
|
|
e964750ac1 | ||
|
|
c87c5797db | ||
|
|
5b25188474 | ||
|
|
91ef78adc5 | ||
|
|
e2a4fdeadf | ||
|
|
9b7780edf0 | ||
|
|
f84c0ee473 | ||
|
|
89ba374d51 | ||
|
|
a8ef7a2774 | ||
|
|
5a30b0507d | ||
|
|
d419471372 | ||
|
|
3e056a24dd | ||
|
|
6511e11ec9 | ||
|
|
4b3cdb9f4e | ||
|
|
bb21cb6c89 | ||
|
|
c36c708068 | ||
|
|
6ca0da5c52 | ||
|
|
e4f42d1282 | ||
|
|
ec9575a86f | ||
|
|
5c208da82e | ||
|
|
9482a6303d | ||
|
|
5999df1953 | ||
|
|
935e5c67a3 | ||
|
|
3fcbcd5a38 | ||
|
|
dbba3eb0d4 | ||
|
|
89e9d827a2 | ||
|
|
ab4e4787e3 | ||
|
|
b6e1675c46 | ||
|
|
e69ca810e4 | ||
|
|
62844e237c | ||
|
|
1218127d83 | ||
|
|
08a57959b9 | ||
|
|
f771667c14 | ||
|
|
d5dcb8f140 | ||
|
|
362ac725bf | ||
|
|
58bb6f2e99 | ||
|
|
5b8cb10ad7 | ||
|
|
2eb5ce9dfe | ||
|
|
fd2cff6b1c | ||
|
|
0e5fa010a7 | ||
|
|
7c25389f0d | ||
|
|
a8d3a904e7 | ||
|
|
6bf42ad43d | ||
|
|
0987219b28 | ||
|
|
fb52f66da0 | ||
|
|
8000b97180 | ||
|
|
5b8f64093b | ||
|
|
440d479be8 | ||
|
|
e80702a45c | ||
|
|
63b19094c1 | ||
|
|
84b1fcbc36 | ||
|
|
81a5208762 | ||
|
|
afa019ae47 | ||
|
|
6800871c13 | ||
|
|
f094a7369d | ||
|
|
234f348ba1 | ||
|
|
d1c6eb4f3e | ||
|
|
5232df34cb | ||
|
|
0fe5d567a2 | ||
|
|
136364f5db | ||
|
|
2de6a94506 | ||
|
|
e1b63d9706 | ||
|
|
27a8171a8b |
40
.coveragerc
40
.coveragerc
@@ -19,6 +19,9 @@ omit =
|
||||
homeassistant/components/alarmdecoder.py
|
||||
homeassistant/components/*/alarmdecoder.py
|
||||
|
||||
homeassistant/components/ambient_station/__init__.py
|
||||
homeassistant/components/ambient_station/sensor.py
|
||||
|
||||
homeassistant/components/amcrest.py
|
||||
homeassistant/components/*/amcrest.py
|
||||
|
||||
@@ -80,17 +83,24 @@ omit =
|
||||
homeassistant/components/digital_ocean.py
|
||||
homeassistant/components/*/digital_ocean.py
|
||||
|
||||
homeassistant/components/danfoss_air/*
|
||||
|
||||
homeassistant/components/dominos.py
|
||||
|
||||
homeassistant/components/doorbird.py
|
||||
homeassistant/components/*/doorbird.py
|
||||
|
||||
homeassistant/components/dovado/*
|
||||
|
||||
homeassistant/components/dweet.py
|
||||
homeassistant/components/*/dweet.py
|
||||
|
||||
homeassistant/components/eight_sleep.py
|
||||
homeassistant/components/*/eight_sleep.py
|
||||
|
||||
homeassistant/components/ecoal_boiler.py
|
||||
homeassistant/components/*/ecoal_boiler.py
|
||||
|
||||
homeassistant/components/ecobee.py
|
||||
homeassistant/components/*/ecobee.py
|
||||
|
||||
@@ -163,13 +173,13 @@ omit =
|
||||
homeassistant/components/hlk_sw16.py
|
||||
homeassistant/components/*/hlk_sw16.py
|
||||
|
||||
homeassistant/components/homekit_controller/__init__.py
|
||||
homeassistant/components/*/homekit_controller.py
|
||||
homeassistant/components/homekit_controller/*
|
||||
|
||||
homeassistant/components/homematic/__init__.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/homematicip_cloud.py
|
||||
homeassistant/components/homematicip_cloud/hap.py
|
||||
homeassistant/components/homematicip_cloud/device.py
|
||||
homeassistant/components/*/homematicip_cloud.py
|
||||
|
||||
homeassistant/components/homeworks.py
|
||||
@@ -384,6 +394,9 @@ omit =
|
||||
|
||||
homeassistant/components/tradfri.py
|
||||
homeassistant/components/*/tradfri.py
|
||||
|
||||
homeassistant/components/transmission.py
|
||||
homeassistant/components/*/transmission.py
|
||||
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
@@ -443,15 +456,19 @@ omit =
|
||||
homeassistant/components/zha/sensor.py
|
||||
homeassistant/components/zha/switch.py
|
||||
homeassistant/components/zha/api.py
|
||||
homeassistant/components/zha/entities/*
|
||||
homeassistant/components/zha/helpers.py
|
||||
homeassistant/components/zha/entity.py
|
||||
homeassistant/components/zha/device_entity.py
|
||||
homeassistant/components/zha/core/helpers.py
|
||||
homeassistant/components/zha/core/const.py
|
||||
homeassistant/components/zha/core/device.py
|
||||
homeassistant/components/zha/core/listeners.py
|
||||
homeassistant/components/zha/core/gateway.py
|
||||
homeassistant/components/*/zha.py
|
||||
|
||||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/zoneminder/*
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/tuya.py
|
||||
homeassistant/components/*/tuya.py
|
||||
@@ -460,6 +477,7 @@ omit =
|
||||
homeassistant/components/*/spider.py
|
||||
|
||||
homeassistant/components/air_quality/opensensemap.py
|
||||
homeassistant/components/air_quality/nilu.py
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
@@ -552,6 +570,7 @@ omit =
|
||||
homeassistant/components/device_tracker/sky_hub.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/device_tracker/swisscom.py
|
||||
homeassistant/components/device_tracker/synology_srm.py
|
||||
homeassistant/components/device_tracker/tado.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tile.py
|
||||
@@ -574,6 +593,7 @@ omit =
|
||||
homeassistant/components/image_processing/dlib_face_identify.py
|
||||
homeassistant/components/image_processing/seven_segments.py
|
||||
homeassistant/components/image_processing/tensorflow.py
|
||||
homeassistant/components/image_processing/qrcode.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/avion.py
|
||||
@@ -581,6 +601,7 @@ omit =
|
||||
homeassistant/components/light/blinkt.py
|
||||
homeassistant/components/light/decora_wifi.py
|
||||
homeassistant/components/light/decora.py
|
||||
homeassistant/components/light/everlights.py
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/futurenow.py
|
||||
homeassistant/components/light/greenwave.py
|
||||
@@ -719,7 +740,6 @@ omit =
|
||||
homeassistant/components/sensor/aftership.py
|
||||
homeassistant/components/sensor/airvisual.py
|
||||
homeassistant/components/sensor/alpha_vantage.py
|
||||
homeassistant/components/sensor/ambient_station.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
@@ -744,7 +764,6 @@ omit =
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/discogs.py
|
||||
homeassistant/components/sensor/dnsip.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/domain_expiry.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
@@ -781,6 +800,7 @@ omit =
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/htu21d.py
|
||||
homeassistant/components/sensor/upnp.py
|
||||
homeassistant/components/sensor/iliad_italy.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
@@ -835,7 +855,9 @@ omit =
|
||||
homeassistant/components/sensor/qnap.py
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/rainbird.py
|
||||
homeassistant/components/sensor/recollect_waste.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/rova.py
|
||||
homeassistant/components/sensor/rtorrent.py
|
||||
homeassistant/components/sensor/ruter.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
@@ -874,7 +896,6 @@ omit =
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/torque.py
|
||||
homeassistant/components/sensor/trafikverket_weatherstation.py
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/travisci.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
@@ -919,7 +940,6 @@ omit =
|
||||
homeassistant/components/switch/switchmate.py
|
||||
homeassistant/components/switch/telnet.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/vesync.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
|
||||
@@ -95,6 +95,7 @@ homeassistant/components/notify/syslog.py @fabaff
|
||||
homeassistant/components/notify/xmpp.py @fabaff
|
||||
homeassistant/components/notify/yessssms.py @flowolf
|
||||
homeassistant/components/plant.py @ChristianKuehnel
|
||||
homeassistant/components/remote/harmony.py @ehendrix23
|
||||
homeassistant/components/scene/lifx_cloud.py @amelchio
|
||||
homeassistant/components/sensor/airvisual.py @bachya
|
||||
homeassistant/components/sensor/alpha_vantage.py @fabaff
|
||||
@@ -152,6 +153,7 @@ homeassistant/components/weather/openweathermap.py @fabaff
|
||||
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
|
||||
# A
|
||||
homeassistant/components/ambient_station/* @bachya
|
||||
homeassistant/components/arduino.py @fabaff
|
||||
homeassistant/components/*/arduino.py @fabaff
|
||||
homeassistant/components/*/arest.py @fabaff
|
||||
@@ -234,6 +236,7 @@ homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
|
||||
# S
|
||||
homeassistant/components/simplisafe/* @bachya
|
||||
homeassistant/components/smartthings/* @andrewsayre
|
||||
|
||||
# T
|
||||
homeassistant/components/tahoma.py @philklei
|
||||
@@ -268,8 +271,7 @@ homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
|
||||
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
|
||||
|
||||
# Z
|
||||
homeassistant/components/zoneminder/ @rohankapoorcom
|
||||
homeassistant/components/*/zoneminder.py @rohankapoorcom
|
||||
homeassistant/components/zoneminder/* @rohankapoorcom
|
||||
|
||||
# Other code
|
||||
homeassistant/scripts/check_config.py @kellerza
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, cast
|
||||
|
||||
from typing import Any, Dict, List, Optional, Set, cast # noqa: F401
|
||||
|
||||
import bcrypt
|
||||
import voluptuous as vol
|
||||
@@ -52,6 +53,9 @@ class Data:
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
|
||||
private=True)
|
||||
self._data = None # type: Optional[Dict[str, Any]]
|
||||
# Legacy mode will allow usernames to start/end with whitespace
|
||||
# and will compare usernames case-insensitive.
|
||||
# Remove in 2020 or when we launch 1.0.
|
||||
self.is_legacy = False
|
||||
|
||||
@callback
|
||||
@@ -60,7 +64,7 @@ class Data:
|
||||
if self.is_legacy:
|
||||
return username
|
||||
|
||||
return username.strip()
|
||||
return username.strip().casefold()
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load stored data."""
|
||||
@@ -71,9 +75,26 @@ class Data:
|
||||
'users': []
|
||||
}
|
||||
|
||||
seen = set() # type: Set[str]
|
||||
|
||||
for user in data['users']:
|
||||
username = user['username']
|
||||
|
||||
# check if we have duplicates
|
||||
folded = username.casefold()
|
||||
|
||||
if folded in seen:
|
||||
self.is_legacy = True
|
||||
|
||||
logging.getLogger(__name__).warning(
|
||||
"Home Assistant auth provider is running in legacy mode "
|
||||
"because we detected usernames that are case-insensitive"
|
||||
"equivalent. Please change the username: '%s'.", username)
|
||||
|
||||
break
|
||||
|
||||
seen.add(folded)
|
||||
|
||||
# check if we have unstripped usernames
|
||||
if username != username.strip():
|
||||
self.is_legacy = True
|
||||
@@ -81,7 +102,7 @@ class Data:
|
||||
logging.getLogger(__name__).warning(
|
||||
"Home Assistant auth provider is running in legacy mode "
|
||||
"because we detected usernames that start or end in a "
|
||||
"space. Please change the username.")
|
||||
"space. Please change the username: '%s'.", username)
|
||||
|
||||
break
|
||||
|
||||
@@ -103,7 +124,7 @@ class Data:
|
||||
|
||||
# Compare all users to avoid timing attacks.
|
||||
for user in self.users:
|
||||
if username == user['username']:
|
||||
if self.normalize_username(user['username']) == username:
|
||||
found = user
|
||||
|
||||
if found is None:
|
||||
|
||||
@@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_AQI = 'air_quality_index'
|
||||
ATTR_ATTRIBUTION = 'attribution'
|
||||
ATTR_C02 = 'carbon_dioxide'
|
||||
ATTR_CO2 = 'carbon_dioxide'
|
||||
ATTR_CO = 'carbon_monoxide'
|
||||
ATTR_N2O = 'nitrogen_oxide'
|
||||
ATTR_NO = 'nitrogen_monoxide'
|
||||
@@ -35,7 +35,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
|
||||
PROP_TO_ATTR = {
|
||||
'air_quality_index': ATTR_AQI,
|
||||
'attribution': ATTR_ATTRIBUTION,
|
||||
'carbon_dioxide': ATTR_C02,
|
||||
'carbon_dioxide': ATTR_CO2,
|
||||
'carbon_monoxide': ATTR_CO,
|
||||
'nitrogen_oxide': ATTR_N2O,
|
||||
'nitrogen_monoxide': ATTR_NO,
|
||||
|
||||
252
homeassistant/components/air_quality/nilu.py
Normal file
252
homeassistant/components/air_quality/nilu.py
Normal file
@@ -0,0 +1,252 @@
|
||||
"""
|
||||
Sensor for checking the air quality around Norway.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/air_quality.nilu/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.air_quality import (
|
||||
PLATFORM_SCHEMA, AirQualityEntity)
|
||||
from homeassistant.const import (
|
||||
CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_SHOW_ON_MAP)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['niluclient==0.1.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_AREA = 'area'
|
||||
ATTR_POLLUTION_INDEX = 'nilu_pollution_index'
|
||||
ATTRIBUTION = "Data provided by luftkvalitet.info and nilu.no"
|
||||
|
||||
CONF_AREA = 'area'
|
||||
CONF_STATION = 'stations'
|
||||
|
||||
DEFAULT_NAME = 'NILU'
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=30)
|
||||
|
||||
CONF_ALLOWED_AREAS = [
|
||||
'Bergen',
|
||||
'Birkenes',
|
||||
'Bodø',
|
||||
'Brumunddal',
|
||||
'Bærum',
|
||||
'Drammen',
|
||||
'Elverum',
|
||||
'Fredrikstad',
|
||||
'Gjøvik',
|
||||
'Grenland',
|
||||
'Halden',
|
||||
'Hamar',
|
||||
'Harstad',
|
||||
'Hurdal',
|
||||
'Karasjok',
|
||||
'Kristiansand',
|
||||
'Kårvatn',
|
||||
'Lillehammer',
|
||||
'Lillesand',
|
||||
'Lillestrøm',
|
||||
'Lørenskog',
|
||||
'Mo i Rana',
|
||||
'Moss',
|
||||
'Narvik',
|
||||
'Oslo',
|
||||
'Prestebakke',
|
||||
'Sandve',
|
||||
'Sarpsborg',
|
||||
'Stavanger',
|
||||
'Sør-Varanger',
|
||||
'Tromsø',
|
||||
'Trondheim',
|
||||
'Tustervatn',
|
||||
'Zeppelinfjellet',
|
||||
'Ålesund',
|
||||
]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Inclusive(CONF_LATITUDE, 'coordinates',
|
||||
'Latitude and longitude must exist together'): cv.latitude,
|
||||
vol.Inclusive(CONF_LONGITUDE, 'coordinates',
|
||||
'Latitude and longitude must exist together'): cv.longitude,
|
||||
vol.Exclusive(CONF_AREA, 'station_collection',
|
||||
'Can only configure one specific station or '
|
||||
'stations in a specific area pr sensor. '
|
||||
'Please only configure station or area.'
|
||||
): vol.All(cv.string, vol.In(CONF_ALLOWED_AREAS)),
|
||||
vol.Exclusive(CONF_STATION, 'station_collection',
|
||||
'Can only configure one specific station or '
|
||||
'stations in a specific area pr sensor. '
|
||||
'Please only configure station or area.'
|
||||
): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the NILU air quality sensor."""
|
||||
import niluclient as nilu
|
||||
name = config.get(CONF_NAME)
|
||||
area = config.get(CONF_AREA)
|
||||
stations = config.get(CONF_STATION)
|
||||
show_on_map = config.get(CONF_SHOW_ON_MAP)
|
||||
|
||||
sensors = []
|
||||
|
||||
if area:
|
||||
stations = nilu.lookup_stations_in_area(area)
|
||||
elif not area and not stations:
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
location_client = nilu.create_location_client(latitude, longitude)
|
||||
stations = location_client.station_names
|
||||
|
||||
for station in stations:
|
||||
client = NiluData(nilu.create_station_client(station))
|
||||
client.update()
|
||||
if client.data.sensors:
|
||||
sensors.append(NiluSensor(client, name, show_on_map))
|
||||
else:
|
||||
_LOGGER.warning("%s didn't give any sensors results", station)
|
||||
|
||||
add_entities(sensors, True)
|
||||
|
||||
|
||||
class NiluData:
|
||||
"""Class for handling the data retrieval."""
|
||||
|
||||
def __init__(self, api):
|
||||
"""Initialize the data object."""
|
||||
self.api = api
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
"""Get data cached in client."""
|
||||
return self.api.data
|
||||
|
||||
@Throttle(SCAN_INTERVAL)
|
||||
def update(self):
|
||||
"""Get the latest data from nilu API."""
|
||||
self.api.update()
|
||||
|
||||
|
||||
class NiluSensor(AirQualityEntity):
|
||||
"""Single nilu station air sensor."""
|
||||
|
||||
def __init__(self, api_data: NiluData, name: str, show_on_map: bool):
|
||||
"""Initialize the sensor."""
|
||||
self._api = api_data
|
||||
self._name = "{} {}".format(name, api_data.data.name)
|
||||
self._max_aqi = None
|
||||
self._attrs = {}
|
||||
|
||||
if show_on_map:
|
||||
self._attrs[CONF_LATITUDE] = api_data.data.latitude
|
||||
self._attrs[CONF_LONGITUDE] = api_data.data.longitude
|
||||
|
||||
@property
|
||||
def attribution(self) -> str:
|
||||
"""Return the attribution."""
|
||||
return ATTRIBUTION
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> dict:
|
||||
"""Return other details about the sensor state."""
|
||||
return self._attrs
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def air_quality_index(self) -> str:
|
||||
"""Return the Air Quality Index (AQI)."""
|
||||
return self._max_aqi
|
||||
|
||||
@property
|
||||
def carbon_monoxide(self) -> str:
|
||||
"""Return the CO (carbon monoxide) level."""
|
||||
from niluclient import CO
|
||||
return self.get_component_state(CO)
|
||||
|
||||
@property
|
||||
def carbon_dioxide(self) -> str:
|
||||
"""Return the CO2 (carbon dioxide) level."""
|
||||
from niluclient import CO2
|
||||
return self.get_component_state(CO2)
|
||||
|
||||
@property
|
||||
def nitrogen_oxide(self) -> str:
|
||||
"""Return the N2O (nitrogen oxide) level."""
|
||||
from niluclient import NOX
|
||||
return self.get_component_state(NOX)
|
||||
|
||||
@property
|
||||
def nitrogen_monoxide(self) -> str:
|
||||
"""Return the NO (nitrogen monoxide) level."""
|
||||
from niluclient import NO
|
||||
return self.get_component_state(NO)
|
||||
|
||||
@property
|
||||
def nitrogen_dioxide(self) -> str:
|
||||
"""Return the NO2 (nitrogen dioxide) level."""
|
||||
from niluclient import NO2
|
||||
return self.get_component_state(NO2)
|
||||
|
||||
@property
|
||||
def ozone(self) -> str:
|
||||
"""Return the O3 (ozone) level."""
|
||||
from niluclient import OZONE
|
||||
return self.get_component_state(OZONE)
|
||||
|
||||
@property
|
||||
def particulate_matter_2_5(self) -> str:
|
||||
"""Return the particulate matter 2.5 level."""
|
||||
from niluclient import PM25
|
||||
return self.get_component_state(PM25)
|
||||
|
||||
@property
|
||||
def particulate_matter_10(self) -> str:
|
||||
"""Return the particulate matter 10 level."""
|
||||
from niluclient import PM10
|
||||
return self.get_component_state(PM10)
|
||||
|
||||
@property
|
||||
def particulate_matter_0_1(self) -> str:
|
||||
"""Return the particulate matter 0.1 level."""
|
||||
from niluclient import PM1
|
||||
return self.get_component_state(PM1)
|
||||
|
||||
@property
|
||||
def sulphur_dioxide(self) -> str:
|
||||
"""Return the SO2 (sulphur dioxide) level."""
|
||||
from niluclient import SO2
|
||||
return self.get_component_state(SO2)
|
||||
|
||||
def get_component_state(self, component_name: str) -> str:
|
||||
"""Return formatted value of specified component."""
|
||||
if component_name in self._api.data.sensors:
|
||||
sensor = self._api.data.sensors[component_name]
|
||||
return sensor.value
|
||||
return None
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update the sensor."""
|
||||
import niluclient as nilu
|
||||
self._api.update()
|
||||
|
||||
sensors = self._api.data.sensors.values()
|
||||
if sensors:
|
||||
max_index = max([s.pollution_index for s in sensors])
|
||||
self._max_aqi = max_index
|
||||
self._attrs[ATTR_POLLUTION_INDEX] = \
|
||||
nilu.POLLUTION_INDEX[self._max_aqi]
|
||||
|
||||
self._attrs[ATTR_AREA] = self._api.data.area
|
||||
@@ -13,7 +13,8 @@ from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
|
||||
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.helpers.config_validation import ( # noqa
|
||||
PLATFORM_SCHEMA_BASE, PLATFORM_SCHEMA_2 as PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
@@ -6,9 +6,9 @@ https://home-assistant.io/components/alarm_control_panel.abode/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.abode import CONF_ATTRIBUTION, AbodeDevice
|
||||
from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
@@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
add_entities(alarm_devices)
|
||||
|
||||
|
||||
class AbodeAlarm(AbodeDevice, AlarmControlPanel):
|
||||
class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel):
|
||||
"""An alarm_control_panel implementation for Abode."""
|
||||
|
||||
def __init__(self, data, device, name):
|
||||
@@ -57,6 +57,11 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanel):
|
||||
state = None
|
||||
return state
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
return alarm.FORMAT_NUMBER
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._device.set_standby()
|
||||
|
||||
@@ -13,7 +13,7 @@ import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@@ -57,7 +57,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._websession = async_get_clientsession(self._hass)
|
||||
self._state = STATE_UNKNOWN
|
||||
self._state = None
|
||||
self._alarm = Alarmdotcom(
|
||||
username, password, self._websession, hass.loop)
|
||||
|
||||
@@ -93,7 +93,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
if self._alarm.state.lower() == 'armed away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return STATE_UNKNOWN
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
||||
@@ -5,18 +5,17 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.concord232/
|
||||
"""
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
|
||||
REQUIREMENTS = ['concord232==0.15']
|
||||
|
||||
@@ -26,7 +25,7 @@ DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'CONCORD232'
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=10)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
@@ -44,33 +43,24 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
url = 'http://{}:{}'.format(host, port)
|
||||
|
||||
try:
|
||||
add_entities([Concord232Alarm(hass, url, name)])
|
||||
add_entities([Concord232Alarm(url, name)], True)
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
return
|
||||
|
||||
|
||||
class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
"""Representation of the Concord232-based alarm panel."""
|
||||
|
||||
def __init__(self, hass, url, name):
|
||||
def __init__(self, url, name):
|
||||
"""Initialize the Concord232 alarm panel."""
|
||||
from concord232 import client as concord232_client
|
||||
|
||||
self._state = STATE_UNKNOWN
|
||||
self._hass = hass
|
||||
self._state = None
|
||||
self._name = name
|
||||
self._url = url
|
||||
|
||||
try:
|
||||
client = concord232_client.Client(self._url)
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
|
||||
self._alarm = client
|
||||
self._alarm = concord232_client.Client(self._url)
|
||||
self._alarm.partitions = self._alarm.list_partitions()
|
||||
self._alarm.last_partition_update = datetime.datetime.now()
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -94,22 +84,17 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
|
||||
dict(host=self._url, reason=ex))
|
||||
newstate = STATE_UNKNOWN
|
||||
return
|
||||
except IndexError:
|
||||
_LOGGER.error("Concord232 reports no partitions")
|
||||
newstate = STATE_UNKNOWN
|
||||
return
|
||||
|
||||
if part['arming_level'] == 'Off':
|
||||
newstate = STATE_ALARM_DISARMED
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif 'Home' in part['arming_level']:
|
||||
newstate = STATE_ALARM_ARMED_HOME
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
newstate = STATE_ALARM_ARMED_AWAY
|
||||
|
||||
if not newstate == self._state:
|
||||
_LOGGER.info("State change from %s to %s", self._state, newstate)
|
||||
self._state = newstate
|
||||
return self._state
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -76,7 +76,7 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
|
||||
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
await self._home.set_security_zones_activation(True, False)
|
||||
await self._home.set_security_zones_activation(False, True)
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME,
|
||||
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, CONF_NAME,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class TotalConnect(alarm.AlarmControlPanel):
|
||||
self._name = name
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._state = STATE_UNKNOWN
|
||||
self._state = None
|
||||
self._client = TotalConnectClient.TotalConnectClient(
|
||||
username, password)
|
||||
|
||||
@@ -85,7 +85,7 @@ class TotalConnect(alarm.AlarmControlPanel):
|
||||
elif status == self._client.DISARMING:
|
||||
state = STATE_ALARM_DISARMING
|
||||
else:
|
||||
state = STATE_UNKNOWN
|
||||
state = None
|
||||
|
||||
self._state = state
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.verisure import CONF_ALARM, CONF_CODE_DIGITS
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN)
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,7 +43,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Verisure alarm panel."""
|
||||
self._state = STATE_UNKNOWN
|
||||
self._state = None
|
||||
self._digits = hub.config.get(CONF_CODE_DIGITS)
|
||||
self._changed_by = None
|
||||
|
||||
|
||||
@@ -9,8 +9,7 @@ import logging
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.wink import DOMAIN, WinkDevice
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN)
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -52,7 +51,7 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
||||
elif wink_state == "night":
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
state = STATE_UNKNOWN
|
||||
state = None
|
||||
return state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
|
||||
@@ -5,19 +5,19 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alert/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_MESSAGE, DOMAIN as DOMAIN_NOTIFY)
|
||||
ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA, DOMAIN as DOMAIN_NOTIFY)
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers import service, event
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,6 +30,8 @@ CONF_REPEAT = 'repeat'
|
||||
CONF_SKIP_FIRST = 'skip_first'
|
||||
CONF_ALERT_MESSAGE = 'message'
|
||||
CONF_DONE_MESSAGE = 'done_message'
|
||||
CONF_TITLE = 'title'
|
||||
CONF_DATA = 'data'
|
||||
|
||||
DEFAULT_CAN_ACK = True
|
||||
DEFAULT_SKIP_FIRST = False
|
||||
@@ -43,13 +45,14 @@ ALERT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
|
||||
vol.Optional(CONF_ALERT_MESSAGE): cv.template,
|
||||
vol.Optional(CONF_DONE_MESSAGE): cv.template,
|
||||
vol.Optional(CONF_TITLE): cv.template,
|
||||
vol.Optional(CONF_DATA): dict,
|
||||
vol.Required(CONF_NOTIFIERS): cv.ensure_list})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
ALERT_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
@@ -77,12 +80,14 @@ async def async_setup(hass, config):
|
||||
done_message_template = cfg.get(CONF_DONE_MESSAGE)
|
||||
notifiers = cfg.get(CONF_NOTIFIERS)
|
||||
can_ack = cfg.get(CONF_CAN_ACK)
|
||||
title_template = cfg.get(CONF_TITLE)
|
||||
data = cfg.get(CONF_DATA)
|
||||
|
||||
entities.append(Alert(hass, object_id, name,
|
||||
watched_entity_id, alert_state, repeat,
|
||||
skip_first, message_template,
|
||||
done_message_template, notifiers,
|
||||
can_ack))
|
||||
can_ack, title_template, data))
|
||||
|
||||
if not entities:
|
||||
return False
|
||||
@@ -127,12 +132,14 @@ class Alert(ToggleEntity):
|
||||
|
||||
def __init__(self, hass, entity_id, name, watched_entity_id,
|
||||
state, repeat, skip_first, message_template,
|
||||
done_message_template, notifiers, can_ack):
|
||||
done_message_template, notifiers, can_ack, title_template,
|
||||
data):
|
||||
"""Initialize the alert."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._alert_state = state
|
||||
self._skip_first = skip_first
|
||||
self._data = data
|
||||
|
||||
self._message_template = message_template
|
||||
if self._message_template is not None:
|
||||
@@ -142,6 +149,10 @@ class Alert(ToggleEntity):
|
||||
if self._done_message_template is not None:
|
||||
self._done_message_template.hass = hass
|
||||
|
||||
self._title_template = title_template
|
||||
if self._title_template is not None:
|
||||
self._title_template.hass = hass
|
||||
|
||||
self._notifiers = notifiers
|
||||
self._can_ack = can_ack
|
||||
|
||||
@@ -251,9 +262,20 @@ class Alert(ToggleEntity):
|
||||
await self._send_notification_message(message)
|
||||
|
||||
async def _send_notification_message(self, message):
|
||||
|
||||
msg_payload = {ATTR_MESSAGE: message}
|
||||
|
||||
if self._title_template is not None:
|
||||
title = self._title_template.async_render()
|
||||
msg_payload.update({ATTR_TITLE: title})
|
||||
if self._data:
|
||||
msg_payload.update({ATTR_DATA: self._data})
|
||||
|
||||
_LOGGER.debug(msg_payload)
|
||||
|
||||
for target in self._notifiers:
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN_NOTIFY, target, {ATTR_MESSAGE: message})
|
||||
DOMAIN_NOTIFY, target, msg_payload)
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Async Unacknowledge alert."""
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"identifier_exists": "Application Key and/or API Key already registered",
|
||||
"invalid_key": "Invalid API Key and/or Application Key",
|
||||
"no_devices": "No devices found in account"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"app_key": "Application Key"
|
||||
},
|
||||
"title": "Fill in your information"
|
||||
}
|
||||
},
|
||||
"title": "Ambient PWS"
|
||||
}
|
||||
}
|
||||
212
homeassistant/components/ambient_station/__init__.py
Normal file
212
homeassistant/components/ambient_station/__init__.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
Support for Ambient Weather Station Service.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/ambient_station/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import (
|
||||
ATTR_NAME, ATTR_LOCATION, CONF_API_KEY, CONF_MONITORED_CONDITIONS,
|
||||
CONF_UNIT_SYSTEM, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .config_flow import configured_instances
|
||||
from .const import (
|
||||
ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, UNITS_SI,
|
||||
UNITS_US)
|
||||
|
||||
REQUIREMENTS = ['aioambient==0.1.0']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_SOCKET_MIN_RETRY = 15
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'24hourrainin': ['24 Hr Rain', 'in'],
|
||||
'baromabsin': ['Abs Pressure', 'inHg'],
|
||||
'baromrelin': ['Rel Pressure', 'inHg'],
|
||||
'battout': ['Battery', ''],
|
||||
'co2': ['co2', 'ppm'],
|
||||
'dailyrainin': ['Daily Rain', 'in'],
|
||||
'dewPoint': ['Dew Point', ['°F', '°C']],
|
||||
'eventrainin': ['Event Rain', 'in'],
|
||||
'feelsLike': ['Feels Like', ['°F', '°C']],
|
||||
'hourlyrainin': ['Hourly Rain Rate', 'in/hr'],
|
||||
'humidity': ['Humidity', '%'],
|
||||
'humidityin': ['Humidity In', '%'],
|
||||
'lastRain': ['Last Rain', ''],
|
||||
'maxdailygust': ['Max Gust', 'mph'],
|
||||
'monthlyrainin': ['Monthly Rain', 'in'],
|
||||
'solarradiation': ['Solar Rad', 'W/m^2'],
|
||||
'tempf': ['Temp', ['°F', '°C']],
|
||||
'tempinf': ['Inside Temp', ['°F', '°C']],
|
||||
'totalrainin': ['Lifetime Rain', 'in'],
|
||||
'uv': ['uv', 'Index'],
|
||||
'weeklyrainin': ['Weekly Rain', 'in'],
|
||||
'winddir': ['Wind Dir', '°'],
|
||||
'winddir_avg10m': ['Wind Dir Avg 10m', '°'],
|
||||
'winddir_avg2m': ['Wind Dir Avg 2m', 'mph'],
|
||||
'windgustdir': ['Gust Dir', '°'],
|
||||
'windgustmph': ['Wind Gust', 'mph'],
|
||||
'windspdmph_avg10m': ['Wind Avg 10m', 'mph'],
|
||||
'windspdmph_avg2m': ['Wind Avg 2m', 'mph'],
|
||||
'windspeedmph': ['Wind Speed', 'mph'],
|
||||
'yearlyrainin': ['Yearly Rain', 'in'],
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN:
|
||||
vol.Schema({
|
||||
vol.Required(CONF_APP_KEY):
|
||||
cv.string,
|
||||
vol.Required(CONF_API_KEY):
|
||||
cv.string,
|
||||
vol.Optional(
|
||||
CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
vol.Optional(CONF_UNIT_SYSTEM):
|
||||
vol.In([UNITS_SI, UNITS_US]),
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the Ambient PWS component."""
|
||||
hass.data[DOMAIN] = {}
|
||||
hass.data[DOMAIN][DATA_CLIENT] = {}
|
||||
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
conf = config[DOMAIN]
|
||||
|
||||
if conf[CONF_APP_KEY] in configured_instances(hass):
|
||||
return True
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={'source': SOURCE_IMPORT}, data=conf))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
"""Set up the Ambient PWS as config entry."""
|
||||
from aioambient import Client
|
||||
from aioambient.errors import WebsocketConnectionError
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
try:
|
||||
ambient = AmbientStation(
|
||||
hass,
|
||||
config_entry,
|
||||
Client(
|
||||
config_entry.data[CONF_API_KEY],
|
||||
config_entry.data[CONF_APP_KEY], session),
|
||||
config_entry.data.get(
|
||||
CONF_MONITORED_CONDITIONS, list(SENSOR_TYPES)),
|
||||
config_entry.data.get(CONF_UNIT_SYSTEM))
|
||||
hass.loop.create_task(ambient.ws_connect())
|
||||
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient
|
||||
except WebsocketConnectionError as err:
|
||||
_LOGGER.error('Config entry failed: %s', err)
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, ambient.client.websocket.disconnect())
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, config_entry):
|
||||
"""Unload an Ambient PWS config entry."""
|
||||
ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id)
|
||||
hass.async_create_task(ambient.ws_disconnect())
|
||||
|
||||
await hass.config_entries.async_forward_entry_unload(
|
||||
config_entry, 'sensor')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AmbientStation:
|
||||
"""Define a class to handle the Ambient websocket."""
|
||||
|
||||
def __init__(
|
||||
self, hass, config_entry, client, monitored_conditions,
|
||||
unit_system):
|
||||
"""Initialize."""
|
||||
self._config_entry = config_entry
|
||||
self._hass = hass
|
||||
self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
|
||||
self.client = client
|
||||
self.monitored_conditions = monitored_conditions
|
||||
self.stations = {}
|
||||
self.unit_system = unit_system
|
||||
|
||||
async def ws_connect(self):
|
||||
"""Register handlers and connect to the websocket."""
|
||||
from aioambient.errors import WebsocketError
|
||||
|
||||
def on_connect():
|
||||
"""Define a handler to fire when the websocket is connected."""
|
||||
_LOGGER.info('Connected to websocket')
|
||||
|
||||
def on_data(data):
|
||||
"""Define a handler to fire when the data is received."""
|
||||
mac_address = data['macAddress']
|
||||
if data != self.stations[mac_address][ATTR_LAST_DATA]:
|
||||
_LOGGER.debug('New data received: %s', data)
|
||||
self.stations[mac_address][ATTR_LAST_DATA] = data
|
||||
async_dispatcher_send(self._hass, TOPIC_UPDATE)
|
||||
|
||||
def on_disconnect():
|
||||
"""Define a handler to fire when the websocket is disconnected."""
|
||||
_LOGGER.info('Disconnected from websocket')
|
||||
|
||||
def on_subscribed(data):
|
||||
"""Define a handler to fire when the subscription is set."""
|
||||
for station in data['devices']:
|
||||
if station['macAddress'] in self.stations:
|
||||
continue
|
||||
|
||||
_LOGGER.debug('New station subscription: %s', data)
|
||||
|
||||
self.stations[station['macAddress']] = {
|
||||
ATTR_LAST_DATA: station['lastData'],
|
||||
ATTR_LOCATION: station['info']['location'],
|
||||
ATTR_NAME: station['info']['name'],
|
||||
}
|
||||
|
||||
self._hass.async_create_task(
|
||||
self._hass.config_entries.async_forward_entry_setup(
|
||||
self._config_entry, 'sensor'))
|
||||
|
||||
self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
|
||||
|
||||
self.client.websocket.on_connect(on_connect)
|
||||
self.client.websocket.on_data(on_data)
|
||||
self.client.websocket.on_disconnect(on_disconnect)
|
||||
self.client.websocket.on_subscribed(on_subscribed)
|
||||
|
||||
try:
|
||||
await self.client.websocket.connect()
|
||||
except WebsocketError as err:
|
||||
_LOGGER.error("Error with the websocket connection: %s", err)
|
||||
|
||||
self._ws_reconnect_delay = min(
|
||||
2 * self._ws_reconnect_delay, 480)
|
||||
|
||||
async_call_later(
|
||||
self._hass, self._ws_reconnect_delay, self.ws_connect)
|
||||
|
||||
async def ws_disconnect(self):
|
||||
"""Disconnect from the websocket."""
|
||||
await self.client.websocket.disconnect()
|
||||
72
homeassistant/components/ambient_station/config_flow.py
Normal file
72
homeassistant/components/ambient_station/config_flow.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Config flow to configure the Ambient PWS component."""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import CONF_APP_KEY, DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def configured_instances(hass):
|
||||
"""Return a set of configured Ambient PWS instances."""
|
||||
return set(
|
||||
entry.data[CONF_APP_KEY]
|
||||
for entry in hass.config_entries.async_entries(DOMAIN))
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class AmbientStationFlowHandler(config_entries.ConfigFlow):
|
||||
"""Handle an Ambient PWS config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
|
||||
|
||||
async def _show_form(self, errors=None):
|
||||
"""Show the form to the user."""
|
||||
data_schema = vol.Schema({
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Required(CONF_APP_KEY): str,
|
||||
})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='user',
|
||||
data_schema=data_schema,
|
||||
errors=errors if errors else {},
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_config):
|
||||
"""Import a config entry from configuration.yaml."""
|
||||
return await self.async_step_user(import_config)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the start of the config flow."""
|
||||
from aioambient import Client
|
||||
from aioambient.errors import AmbientError
|
||||
|
||||
if not user_input:
|
||||
return await self._show_form()
|
||||
|
||||
if user_input[CONF_APP_KEY] in configured_instances(self.hass):
|
||||
return await self._show_form({CONF_APP_KEY: 'identifier_exists'})
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
client = Client(
|
||||
user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session)
|
||||
|
||||
try:
|
||||
devices = await client.api.get_devices()
|
||||
except AmbientError:
|
||||
return await self._show_form({'base': 'invalid_key'})
|
||||
|
||||
if not devices:
|
||||
return await self._show_form({'base': 'no_devices'})
|
||||
|
||||
# The Application Key (which identifies each config entry) is too long
|
||||
# to show nicely in the UI, so we take the first 12 characters (similar
|
||||
# to how GitHub does it):
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_APP_KEY][:12], data=user_input)
|
||||
13
homeassistant/components/ambient_station/const.py
Normal file
13
homeassistant/components/ambient_station/const.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Define constants for the Ambient PWS component."""
|
||||
DOMAIN = 'ambient_station'
|
||||
|
||||
ATTR_LAST_DATA = 'last_data'
|
||||
|
||||
CONF_APP_KEY = 'app_key'
|
||||
|
||||
DATA_CLIENT = 'data_client'
|
||||
|
||||
TOPIC_UPDATE = 'update'
|
||||
|
||||
UNITS_SI = 'si'
|
||||
UNITS_US = 'us'
|
||||
115
homeassistant/components/ambient_station/sensor.py
Normal file
115
homeassistant/components/ambient_station/sensor.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
Support for Ambient Weather Station Service.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.ambient_station/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.ambient_station import SENSOR_TYPES
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .const import (
|
||||
ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, UNITS_SI, UNITS_US)
|
||||
|
||||
DEPENDENCIES = ['ambient_station']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UNIT_SYSTEM = {UNITS_US: 0, UNITS_SI: 1}
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up an Ambient PWS sensor based on existing config."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up an Ambient PWS sensor based on a config entry."""
|
||||
ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||
|
||||
if ambient.unit_system:
|
||||
sys_units = ambient.unit_system
|
||||
elif hass.config.units.is_metric:
|
||||
sys_units = UNITS_SI
|
||||
else:
|
||||
sys_units = UNITS_US
|
||||
|
||||
sensor_list = []
|
||||
for mac_address, station in ambient.stations.items():
|
||||
for condition in ambient.monitored_conditions:
|
||||
name, unit = SENSOR_TYPES[condition]
|
||||
if isinstance(unit, list):
|
||||
unit = unit[UNIT_SYSTEM[sys_units]]
|
||||
|
||||
sensor_list.append(
|
||||
AmbientWeatherSensor(
|
||||
ambient, mac_address, station[ATTR_NAME], condition, name,
|
||||
unit))
|
||||
|
||||
async_add_entities(sensor_list, True)
|
||||
|
||||
|
||||
class AmbientWeatherSensor(Entity):
|
||||
"""Define an Ambient sensor."""
|
||||
|
||||
def __init__(
|
||||
self, ambient, mac_address, station_name, sensor_type, sensor_name,
|
||||
units):
|
||||
"""Initialize the sensor."""
|
||||
self._ambient = ambient
|
||||
self._async_unsub_dispatcher_connect = None
|
||||
self._mac_address = mac_address
|
||||
self._sensor_name = sensor_name
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
self._station_name = station_name
|
||||
self._units = units
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return '{0}_{1}'.format(self._station_name, self._sensor_name)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._units
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique, unchanging string that represents this sensor."""
|
||||
return '{0}_{1}'.format(self._mac_address, self._sensor_name)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@callback
|
||||
def update():
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
|
||||
self.hass, TOPIC_UPDATE, update)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect dispatcher listener when removed."""
|
||||
if self._async_unsub_dispatcher_connect:
|
||||
self._async_unsub_dispatcher_connect()
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch new state data for the sensor."""
|
||||
self._state = self._ambient.stations[
|
||||
self._mac_address][ATTR_LAST_DATA].get(self._sensor_type)
|
||||
19
homeassistant/components/ambient_station/strings.json
Normal file
19
homeassistant/components/ambient_station/strings.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Ambient PWS",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Fill in your information",
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"app_key": "Application Key"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"identifier_exists": "Application Key and/or API Key already registered",
|
||||
"invalid_key": "Invalid API Key and/or Application Key",
|
||||
"no_devices": "No devices found in account"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,8 +123,9 @@ ICON_MAP = {
|
||||
'whitebalance_lock': 'mdi:white-balance-auto'
|
||||
}
|
||||
|
||||
SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision',
|
||||
'overlay', 'torch', 'whitebalance_lock', 'video_recording']
|
||||
SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active',
|
||||
'motion_detect', 'night_vision', 'overlay',
|
||||
'torch', 'whitebalance_lock', 'video_recording']
|
||||
|
||||
SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
|
||||
'battery_voltage', 'light', 'motion', 'pressure', 'proximity',
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
|
||||
REQUIREMENTS = ['aioasuswrt==1.1.17']
|
||||
REQUIREMENTS = ['aioasuswrt==1.1.20']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -15,19 +15,23 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
CONF_ENCODING = 'encoding'
|
||||
CONF_TOPIC = 'topic'
|
||||
DEFAULT_ENCODING = 'utf-8'
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): mqtt.DOMAIN,
|
||||
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD): cv.string,
|
||||
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
|
||||
})
|
||||
|
||||
|
||||
async def async_trigger(hass, config, action, automation_info):
|
||||
"""Listen for state changes based on configuration."""
|
||||
topic = config.get(CONF_TOPIC)
|
||||
topic = config[CONF_TOPIC]
|
||||
payload = config.get(CONF_PAYLOAD)
|
||||
encoding = config[CONF_ENCODING] or None
|
||||
|
||||
@callback
|
||||
def mqtt_automation_listener(msg_topic, msg_payload, qos):
|
||||
@@ -50,5 +54,5 @@ async def async_trigger(hass, config, action, automation_info):
|
||||
})
|
||||
|
||||
remove = await mqtt.async_subscribe(
|
||||
hass, topic, mqtt_automation_listener)
|
||||
hass, topic, mqtt_automation_listener, encoding=encoding)
|
||||
return remove
|
||||
|
||||
@@ -15,8 +15,6 @@ DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_SMOKE_OFF = 'IDLE_OFF'
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
@@ -65,7 +63,7 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
|
||||
return True
|
||||
if self._device.windowState is None:
|
||||
return None
|
||||
return self._device.windowState == WindowState.OPEN
|
||||
return self._device.windowState != WindowState.CLOSED
|
||||
|
||||
|
||||
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
@@ -95,7 +93,9 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if smoke is detected."""
|
||||
return self._device.smokeDetectorAlarmType != STATE_SMOKE_OFF
|
||||
from homematicip.base.enums import SmokeDetectorAlarmType
|
||||
return (self._device.smokeDetectorAlarmType
|
||||
!= SmokeDetectorAlarmType.IDLE_OFF)
|
||||
|
||||
|
||||
class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
|
||||
@@ -8,7 +8,6 @@ import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.maxcube import DATA_KEY
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -40,7 +39,7 @@ class MaxCubeShutter(BinarySensorDevice):
|
||||
self._sensor_type = 'window'
|
||||
self._rf_address = rf_address
|
||||
self._cubehandle = handler
|
||||
self._state = STATE_UNKNOWN
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
||||
@@ -22,7 +22,7 @@ from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
REQUIREMENTS = ['numpy==1.15.4']
|
||||
REQUIREMENTS = ['numpy==1.16.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
|
||||
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
|
||||
|
||||
REQUIREMENTS = ['blinkpy==0.11.2']
|
||||
REQUIREMENTS = ['blinkpy==0.12.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['libpyfoscam==1.0']
|
||||
REQUIREMENTS = ['pyfoscam==1.2']
|
||||
|
||||
CONF_IP = 'ip'
|
||||
|
||||
@@ -43,7 +43,7 @@ class FoscamCam(Camera):
|
||||
|
||||
def __init__(self, device_info):
|
||||
"""Initialize a Foscam camera."""
|
||||
from libpyfoscam import FoscamCamera
|
||||
from foscam import FoscamCamera
|
||||
|
||||
super(FoscamCam, self).__init__()
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE,
|
||||
STATE_ON, STATE_OFF, TEMP_CELSIUS, PRECISION_WHOLE,
|
||||
PRECISION_TENTHS)
|
||||
|
||||
DEFAULT_MIN_TEMP = 7
|
||||
@@ -208,7 +208,7 @@ class ClimateDevice(Entity):
|
||||
return self.current_operation
|
||||
if self.is_on:
|
||||
return STATE_ON
|
||||
return STATE_UNKNOWN
|
||||
return None
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
REQUIREMENTS = ['millheater==0.3.3']
|
||||
REQUIREMENTS = ['millheater==0.3.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.components.climate import (
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
||||
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
@@ -163,7 +163,7 @@ class NestThermostat(ClimateDevice):
|
||||
return self._mode
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
return STATE_AUTO
|
||||
return STATE_UNKNOWN
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
|
||||
@@ -219,6 +219,11 @@ class RadioThermostat(ClimateDevice):
|
||||
"""Return true if away mode is on."""
|
||||
return self._away
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
return self._tstate != STATE_IDLE
|
||||
|
||||
def update(self):
|
||||
"""Update and validate the data from the thermostat."""
|
||||
# Radio thermostats are very slow, and sometimes don't respond
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.util.yaml import load_yaml, dump
|
||||
DOMAIN = 'config'
|
||||
DEPENDENCIES = ['http']
|
||||
SECTIONS = (
|
||||
'area_registry',
|
||||
'auth',
|
||||
'auth_provider_homeassistant',
|
||||
'automation',
|
||||
|
||||
126
homeassistant/components/config/area_registry.py
Normal file
126
homeassistant/components/config/area_registry.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""HTTP views to interact with the area registry."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.websocket_api.decorators import (
|
||||
async_response, require_admin)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.area_registry import async_get_registry
|
||||
|
||||
|
||||
DEPENDENCIES = ['websocket_api']
|
||||
|
||||
WS_TYPE_LIST = 'config/area_registry/list'
|
||||
SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_LIST,
|
||||
})
|
||||
|
||||
WS_TYPE_CREATE = 'config/area_registry/create'
|
||||
SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_CREATE,
|
||||
vol.Required('name'): str,
|
||||
})
|
||||
|
||||
WS_TYPE_DELETE = 'config/area_registry/delete'
|
||||
SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_DELETE,
|
||||
vol.Required('area_id'): str,
|
||||
})
|
||||
|
||||
WS_TYPE_UPDATE = 'config/area_registry/update'
|
||||
SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_UPDATE,
|
||||
vol.Required('area_id'): str,
|
||||
vol.Required('name'): str,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Enable the Area Registry views."""
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_LIST, websocket_list_areas, SCHEMA_WS_LIST
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_CREATE, websocket_create_area, SCHEMA_WS_CREATE
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_DELETE, websocket_delete_area, SCHEMA_WS_DELETE
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_UPDATE, websocket_update_area, SCHEMA_WS_UPDATE
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@async_response
|
||||
async def websocket_list_areas(hass, connection, msg):
|
||||
"""Handle list areas command."""
|
||||
registry = await async_get_registry(hass)
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], [{
|
||||
'name': entry.name,
|
||||
'area_id': entry.id,
|
||||
} for entry in registry.async_list_areas()]
|
||||
))
|
||||
|
||||
|
||||
@require_admin
|
||||
@async_response
|
||||
async def websocket_create_area(hass, connection, msg):
|
||||
"""Create area command."""
|
||||
registry = await async_get_registry(hass)
|
||||
try:
|
||||
entry = registry.async_create(msg['name'])
|
||||
except ValueError as err:
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], 'invalid_info', str(err)
|
||||
))
|
||||
else:
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], _entry_dict(entry)
|
||||
))
|
||||
|
||||
|
||||
@require_admin
|
||||
@async_response
|
||||
async def websocket_delete_area(hass, connection, msg):
|
||||
"""Delete area command."""
|
||||
registry = await async_get_registry(hass)
|
||||
|
||||
try:
|
||||
await registry.async_delete(msg['area_id'])
|
||||
except KeyError:
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], 'invalid_info', "Area ID doesn't exist"
|
||||
))
|
||||
else:
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], 'success'
|
||||
))
|
||||
|
||||
|
||||
@require_admin
|
||||
@async_response
|
||||
async def websocket_update_area(hass, connection, msg):
|
||||
"""Handle update area websocket command."""
|
||||
registry = await async_get_registry(hass)
|
||||
|
||||
try:
|
||||
entry = registry.async_update(msg['area_id'], msg['name'])
|
||||
except ValueError as err:
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], 'invalid_info', str(err)
|
||||
))
|
||||
else:
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], _entry_dict(entry)
|
||||
))
|
||||
|
||||
|
||||
@callback
|
||||
def _entry_dict(entry):
|
||||
"""Convert entry to API format."""
|
||||
return {
|
||||
'area_id': entry.id,
|
||||
'name': entry.name
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
"""HTTP views to interact with the device registry."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.device_registry import async_get_registry
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.websocket_api.decorators import (
|
||||
async_response, require_admin)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import async_get_registry
|
||||
|
||||
DEPENDENCIES = ['websocket_api']
|
||||
|
||||
@@ -11,29 +14,60 @@ SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_LIST,
|
||||
})
|
||||
|
||||
WS_TYPE_UPDATE = 'config/device_registry/update'
|
||||
SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_UPDATE,
|
||||
vol.Required('device_id'): str,
|
||||
vol.Optional('area_id'): vol.Any(str, None),
|
||||
})
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Enable the Entity Registry views."""
|
||||
"""Enable the Device Registry views."""
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_LIST, websocket_list_devices,
|
||||
SCHEMA_WS_LIST
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_UPDATE, websocket_update_device, SCHEMA_WS_UPDATE
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
@async_response
|
||||
async def websocket_list_devices(hass, connection, msg):
|
||||
"""Handle list devices command."""
|
||||
registry = await async_get_registry(hass)
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], [{
|
||||
'config_entries': list(entry.config_entries),
|
||||
'connections': list(entry.connections),
|
||||
'manufacturer': entry.manufacturer,
|
||||
'model': entry.model,
|
||||
'name': entry.name,
|
||||
'sw_version': entry.sw_version,
|
||||
'id': entry.id,
|
||||
'hub_device_id': entry.hub_device_id,
|
||||
} for entry in registry.devices.values()]
|
||||
msg['id'], [_entry_dict(entry) for entry in registry.devices.values()]
|
||||
))
|
||||
|
||||
|
||||
@require_admin
|
||||
@async_response
|
||||
async def websocket_update_device(hass, connection, msg):
|
||||
"""Handle update area websocket command."""
|
||||
registry = await async_get_registry(hass)
|
||||
|
||||
entry = registry.async_update_device(
|
||||
msg['device_id'], area_id=msg['area_id'])
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], _entry_dict(entry)
|
||||
))
|
||||
|
||||
|
||||
@callback
|
||||
def _entry_dict(entry):
|
||||
"""Convert entry to API format."""
|
||||
return {
|
||||
'config_entries': list(entry.config_entries),
|
||||
'connections': list(entry.connections),
|
||||
'manufacturer': entry.manufacturer,
|
||||
'model': entry.model,
|
||||
'name': entry.name,
|
||||
'sw_version': entry.sw_version,
|
||||
'id': entry.id,
|
||||
'hub_device_id': entry.hub_device_id,
|
||||
'area_id': entry.area_id,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity_registry import async_get_registry
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.websocket_api.const import ERR_NOT_FOUND
|
||||
from homeassistant.components.websocket_api.decorators import async_response
|
||||
from homeassistant.components.websocket_api.decorators import (
|
||||
async_response, require_admin)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['websocket_api']
|
||||
@@ -30,6 +31,12 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Optional('new_entity_id'): str,
|
||||
})
|
||||
|
||||
WS_TYPE_REMOVE = 'config/entity_registry/remove'
|
||||
SCHEMA_WS_REMOVE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_REMOVE,
|
||||
vol.Required('entity_id'): cv.entity_id
|
||||
})
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Enable the Entity Registry views."""
|
||||
@@ -45,6 +52,10 @@ async def async_setup(hass):
|
||||
WS_TYPE_UPDATE, websocket_update_entity,
|
||||
SCHEMA_WS_UPDATE
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_REMOVE, websocket_remove_entity,
|
||||
SCHEMA_WS_REMOVE
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -56,14 +67,7 @@ async def websocket_list_entities(hass, connection, msg):
|
||||
"""
|
||||
registry = await async_get_registry(hass)
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], [{
|
||||
'config_entry_id': entry.config_entry_id,
|
||||
'device_id': entry.device_id,
|
||||
'disabled_by': entry.disabled_by,
|
||||
'entity_id': entry.entity_id,
|
||||
'name': entry.name,
|
||||
'platform': entry.platform,
|
||||
} for entry in registry.entities.values()]
|
||||
msg['id'], [_entry_dict(entry) for entry in registry.entities.values()]
|
||||
))
|
||||
|
||||
|
||||
@@ -86,6 +90,7 @@ async def websocket_get_entity(hass, connection, msg):
|
||||
))
|
||||
|
||||
|
||||
@require_admin
|
||||
@async_response
|
||||
async def websocket_update_entity(hass, connection, msg):
|
||||
"""Handle update entity websocket command.
|
||||
@@ -125,10 +130,32 @@ async def websocket_update_entity(hass, connection, msg):
|
||||
))
|
||||
|
||||
|
||||
@require_admin
|
||||
@async_response
|
||||
async def websocket_remove_entity(hass, connection, msg):
|
||||
"""Handle remove entity websocket command.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
registry = await async_get_registry(hass)
|
||||
|
||||
if msg['entity_id'] not in registry.entities:
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], ERR_NOT_FOUND, 'Entity not found'))
|
||||
return
|
||||
|
||||
registry.async_remove(msg['entity_id'])
|
||||
connection.send_message(websocket_api.result_message(msg['id']))
|
||||
|
||||
|
||||
@callback
|
||||
def _entry_dict(entry):
|
||||
"""Convert entry to API format."""
|
||||
return {
|
||||
'config_entry_id': entry.config_entry_id,
|
||||
'device_id': entry.device_id,
|
||||
'disabled_by': entry.disabled_by,
|
||||
'entity_id': entry.entity_id,
|
||||
'name': entry.name
|
||||
'name': entry.name,
|
||||
'platform': entry.platform,
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.const import (
|
||||
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
|
||||
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
|
||||
SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION, STATE_OPEN,
|
||||
STATE_CLOSED, STATE_UNKNOWN, STATE_OPENING, STATE_CLOSING, ATTR_ENTITY_ID)
|
||||
STATE_CLOSED, STATE_OPENING, STATE_CLOSING, ATTR_ENTITY_ID)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -178,7 +178,7 @@ class CoverDevice(Entity):
|
||||
closed = self.is_closed
|
||||
|
||||
if closed is None:
|
||||
return STATE_UNKNOWN
|
||||
return None
|
||||
|
||||
return STATE_CLOSED if closed else STATE_OPEN
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE, CONF_USERNAME, CONF_PASSWORD, CONF_ACCESS_TOKEN, CONF_NAME,
|
||||
STATE_UNKNOWN, STATE_CLOSED, STATE_OPEN, CONF_COVERS)
|
||||
STATE_CLOSED, STATE_OPEN, CONF_COVERS)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -83,7 +83,7 @@ class GaradgetCover(CoverDevice):
|
||||
self.obtained_token = False
|
||||
self._username = args['username']
|
||||
self._password = args['password']
|
||||
self._state = STATE_UNKNOWN
|
||||
self._state = None
|
||||
self.time_in_state = None
|
||||
self.signal = None
|
||||
self.sensor = None
|
||||
@@ -156,7 +156,7 @@ class GaradgetCover(CoverDevice):
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
if self._state is None:
|
||||
return None
|
||||
return self._state == STATE_CLOSED
|
||||
|
||||
@@ -226,7 +226,7 @@ class GaradgetCover(CoverDevice):
|
||||
try:
|
||||
status = self._get_variable('doorStatus')
|
||||
_LOGGER.debug("Current Status: %s", status['status'])
|
||||
self._state = STATES_MAP.get(status['status'], STATE_UNKNOWN)
|
||||
self._state = STATES_MAP.get(status['status'], None)
|
||||
self.time_in_state = status['time']
|
||||
self.signal = status['signal']
|
||||
self.sensor = status['sensor']
|
||||
|
||||
70
homeassistant/components/cover/homematicip_cloud.py
Normal file
70
homeassistant/components/cover/homematicip_cloud.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Support for HomematicIP Cloud cover devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.homematicip_cloud/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION, CoverDevice)
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HMIPC_HAPID, HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN)
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the HomematicIP Cloud cover devices."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the HomematicIP cover from a config entry."""
|
||||
from homematicip.aio.device import AsyncFullFlushShutter
|
||||
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = []
|
||||
for device in home.devices:
|
||||
if isinstance(device, AsyncFullFlushShutter):
|
||||
devices.append(HomematicipCoverShutter(home, device))
|
||||
|
||||
if devices:
|
||||
async_add_entities(devices)
|
||||
|
||||
|
||||
class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice):
|
||||
"""Representation of a HomematicIP Cloud cover device."""
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover."""
|
||||
return int(self._device.shutterLevel * 100)
|
||||
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
position = kwargs[ATTR_POSITION]
|
||||
level = position / 100.0
|
||||
await self._device.set_shutter_level(level)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self._device.shutterLevel is not None:
|
||||
return self._device.shutterLevel == 0
|
||||
return None
|
||||
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
await self._device.set_shutter_level(1)
|
||||
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
await self._device.set_shutter_level(0)
|
||||
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Stop the device if in motion."""
|
||||
await self._device.set_shutter_stop()
|
||||
@@ -4,8 +4,7 @@ Support for Wink Covers.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.wink/
|
||||
"""
|
||||
from homeassistant.components.cover import CoverDevice, STATE_UNKNOWN, \
|
||||
ATTR_POSITION
|
||||
from homeassistant.components.cover import CoverDevice, ATTR_POSITION
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
@@ -54,7 +53,7 @@ class WinkCoverDevice(WinkDevice, CoverDevice):
|
||||
"""Return the current position of cover shutter."""
|
||||
if self.wink.state() is not None:
|
||||
return int(self.wink.state()*100)
|
||||
return STATE_UNKNOWN
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
|
||||
80
homeassistant/components/danfoss_air/__init__.py
Normal file
80
homeassistant/components/danfoss_air/__init__.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Support for Danfoss Air HRV.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/danfoss_air/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['pydanfossair==0.0.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DANFOSS_AIR_PLATFORMS = ['sensor', 'binary_sensor']
|
||||
DOMAIN = 'danfoss_air'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Danfoss Air component."""
|
||||
conf = config[DOMAIN]
|
||||
|
||||
hass.data[DOMAIN] = DanfossAir(conf[CONF_HOST])
|
||||
|
||||
for platform in DANFOSS_AIR_PLATFORMS:
|
||||
discovery.load_platform(hass, platform, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DanfossAir:
|
||||
"""Handle all communication with Danfoss Air CCM unit."""
|
||||
|
||||
def __init__(self, host):
|
||||
"""Initialize the Danfoss Air CCM connection."""
|
||||
self._data = {}
|
||||
|
||||
from pydanfossair.danfossclient import DanfossClient
|
||||
|
||||
self._client = DanfossClient(host)
|
||||
|
||||
def get_value(self, item):
|
||||
"""Get value for sensor."""
|
||||
return self._data.get(item)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Use the data from Danfoss Air API."""
|
||||
_LOGGER.debug("Fetching data from Danfoss Air CCM module")
|
||||
from pydanfossair.commands import ReadCommand
|
||||
self._data[ReadCommand.exhaustTemperature] \
|
||||
= self._client.command(ReadCommand.exhaustTemperature)
|
||||
self._data[ReadCommand.outdoorTemperature] \
|
||||
= self._client.command(ReadCommand.outdoorTemperature)
|
||||
self._data[ReadCommand.supplyTemperature] \
|
||||
= self._client.command(ReadCommand.supplyTemperature)
|
||||
self._data[ReadCommand.extractTemperature] \
|
||||
= self._client.command(ReadCommand.extractTemperature)
|
||||
self._data[ReadCommand.humidity] \
|
||||
= round(self._client.command(ReadCommand.humidity), 2)
|
||||
self._data[ReadCommand.filterPercent] \
|
||||
= round(self._client.command(ReadCommand.filterPercent), 2)
|
||||
self._data[ReadCommand.bypass] \
|
||||
= self._client.command(ReadCommand.bypass)
|
||||
|
||||
_LOGGER.debug("Done fetching data from Danfoss Air CCM module")
|
||||
56
homeassistant/components/danfoss_air/binary_sensor.py
Normal file
56
homeassistant/components/danfoss_air/binary_sensor.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Support for the for Danfoss Air HRV binary sensor platform.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.danfoss_air/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.danfoss_air import DOMAIN \
|
||||
as DANFOSS_AIR_DOMAIN
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the available Danfoss Air sensors etc."""
|
||||
from pydanfossair.commands import ReadCommand
|
||||
data = hass.data[DANFOSS_AIR_DOMAIN]
|
||||
|
||||
sensors = [["Danfoss Air Bypass Active", ReadCommand.bypass]]
|
||||
|
||||
dev = []
|
||||
|
||||
for sensor in sensors:
|
||||
dev.append(DanfossAirBinarySensor(data, sensor[0], sensor[1]))
|
||||
|
||||
add_entities(dev, True)
|
||||
|
||||
|
||||
class DanfossAirBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Danfoss Air binary sensor."""
|
||||
|
||||
def __init__(self, data, name, sensor_type):
|
||||
"""Initialize the Danfoss Air binary sensor."""
|
||||
self._data = data
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._type = sensor_type
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Type of device class."""
|
||||
return "opening"
|
||||
|
||||
def update(self):
|
||||
"""Fetch new state data for the sensor."""
|
||||
self._data.update()
|
||||
|
||||
self._state = self._data.get_value(self._type)
|
||||
76
homeassistant/components/danfoss_air/sensor.py
Normal file
76
homeassistant/components/danfoss_air/sensor.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Support for the for Danfoss Air HRV sensor platform.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.danfoss_air/
|
||||
"""
|
||||
from homeassistant.components.danfoss_air import DOMAIN \
|
||||
as DANFOSS_AIR_DOMAIN
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the available Danfoss Air sensors etc."""
|
||||
from pydanfossair.commands import ReadCommand
|
||||
|
||||
data = hass.data[DANFOSS_AIR_DOMAIN]
|
||||
|
||||
sensors = [
|
||||
["Danfoss Air Exhaust Temperature", TEMP_CELSIUS,
|
||||
ReadCommand.exhaustTemperature],
|
||||
["Danfoss Air Outdoor Temperature", TEMP_CELSIUS,
|
||||
ReadCommand.outdoorTemperature],
|
||||
["Danfoss Air Supply Temperature", TEMP_CELSIUS,
|
||||
ReadCommand.supplyTemperature],
|
||||
["Danfoss Air Extract Temperature", TEMP_CELSIUS,
|
||||
ReadCommand.extractTemperature],
|
||||
["Danfoss Air Remaining Filter", '%',
|
||||
ReadCommand.filterPercent],
|
||||
["Danfoss Air Humidity", '%',
|
||||
ReadCommand.humidity]
|
||||
]
|
||||
|
||||
dev = []
|
||||
|
||||
for sensor in sensors:
|
||||
dev.append(DanfossAir(data, sensor[0], sensor[1], sensor[2]))
|
||||
|
||||
add_entities(dev, True)
|
||||
|
||||
|
||||
class DanfossAir(Entity):
|
||||
"""Representation of a Sensor."""
|
||||
|
||||
def __init__(self, data, name, sensor_unit, sensor_type):
|
||||
"""Initialize the sensor."""
|
||||
self._data = data
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._type = sensor_type
|
||||
self._unit = sensor_unit
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
def update(self):
|
||||
"""Update the new state of the sensor.
|
||||
|
||||
This is done through the DanfossAir object that does the actual
|
||||
communication with the Air CCM.
|
||||
"""
|
||||
self._data.update()
|
||||
|
||||
self._state = self._data.get_value(self._type)
|
||||
107
homeassistant/components/device_tracker/ee_brightbox.py
Normal file
107
homeassistant/components/device_tracker/ee_brightbox.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Support for EE Brightbox router.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.ee_brightbox/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['eebrightbox==0.0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_VERSION = 'version'
|
||||
|
||||
CONF_DEFAULT_IP = '192.168.1.1'
|
||||
CONF_DEFAULT_USERNAME = 'admin'
|
||||
CONF_DEFAULT_VERSION = 2
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_VERSION, default=CONF_DEFAULT_VERSION): cv.positive_int,
|
||||
vol.Required(CONF_HOST, default=CONF_DEFAULT_IP): cv.string,
|
||||
vol.Required(CONF_USERNAME, default=CONF_DEFAULT_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Return a router scanner instance."""
|
||||
scanner = EEBrightBoxScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.check_config() else None
|
||||
|
||||
|
||||
class EEBrightBoxScanner(DeviceScanner):
|
||||
"""Scan EE Brightbox router."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialise the scanner."""
|
||||
self.config = config
|
||||
self.devices = {}
|
||||
|
||||
def check_config(self):
|
||||
"""Check if provided configuration and credentials are correct."""
|
||||
from eebrightbox import EEBrightBox, EEBrightBoxException
|
||||
|
||||
try:
|
||||
with EEBrightBox(self.config) as ee_brightbox:
|
||||
return bool(ee_brightbox.get_devices())
|
||||
except EEBrightBoxException:
|
||||
_LOGGER.exception("Failed to connect to the router")
|
||||
return False
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for devices."""
|
||||
from eebrightbox import EEBrightBox
|
||||
|
||||
with EEBrightBox(self.config) as ee_brightbox:
|
||||
self.devices = {d['mac']: d for d in ee_brightbox.get_devices()}
|
||||
|
||||
macs = [d['mac'] for d in self.devices.values() if d['activity_ip']]
|
||||
|
||||
_LOGGER.debug('Scan devices %s', macs)
|
||||
|
||||
return macs
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Get the name of a device from hostname."""
|
||||
if device in self.devices:
|
||||
return self.devices[device]['hostname'] or None
|
||||
|
||||
return None
|
||||
|
||||
def get_extra_attributes(self, device):
|
||||
"""
|
||||
Get the extra attributes of a device.
|
||||
|
||||
Extra attributes include:
|
||||
- ip
|
||||
- mac
|
||||
- port - ethX or wifiX
|
||||
- last_active
|
||||
"""
|
||||
port_map = {
|
||||
'wl1': 'wifi5Ghz',
|
||||
'wl0': 'wifi2.4Ghz',
|
||||
'eth0': 'eth0',
|
||||
'eth1': 'eth1',
|
||||
'eth2': 'eth2',
|
||||
'eth3': 'eth3',
|
||||
}
|
||||
|
||||
if device in self.devices:
|
||||
return {
|
||||
'ip': self.devices[device]['ip'],
|
||||
'mac': self.devices[device]['mac'],
|
||||
'port': port_map[self.devices[device]['port']],
|
||||
'last_active': self.devices[device]['time_last_active'],
|
||||
}
|
||||
|
||||
return {}
|
||||
@@ -1,32 +0,0 @@
|
||||
"""
|
||||
Support for the GPSLogger platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.gpslogger/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.gpslogger import TRACKER_UPDATE
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['gpslogger']
|
||||
|
||||
|
||||
async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType,
|
||||
async_see, discovery_info=None):
|
||||
"""Set up an endpoint for the GPSLogger device tracker."""
|
||||
async def _set_location(device, gps_location, battery, accuracy, attrs):
|
||||
"""Fire HA event to set location."""
|
||||
await async_see(
|
||||
dev_id=device,
|
||||
gps=gps_location,
|
||||
battery=battery,
|
||||
gps_accuracy=accuracy,
|
||||
attributes=attrs
|
||||
)
|
||||
|
||||
async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location)
|
||||
return True
|
||||
100
homeassistant/components/device_tracker/synology_srm.py
Normal file
100
homeassistant/components/device_tracker/synology_srm.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Device tracker for Synology SRM routers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.synology_srm/
|
||||
"""
|
||||
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_USERNAME, CONF_PASSWORD,
|
||||
CONF_PORT, CONF_SSL, CONF_VERIFY_SSL)
|
||||
|
||||
REQUIREMENTS = ['synology-srm==0.0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_USERNAME = 'admin'
|
||||
DEFAULT_PORT = 8001
|
||||
DEFAULT_SSL = True
|
||||
DEFAULT_VERIFY_SSL = False
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return Synology SRM scanner."""
|
||||
scanner = SynologySrmDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class SynologySrmDeviceScanner(DeviceScanner):
|
||||
"""This class scans for devices connected to a Synology SRM router."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
import synology_srm
|
||||
|
||||
self.client = synology_srm.Client(
|
||||
host=config[CONF_HOST],
|
||||
port=config[CONF_PORT],
|
||||
username=config[CONF_USERNAME],
|
||||
password=config[CONF_PASSWORD],
|
||||
https=config[CONF_SSL]
|
||||
)
|
||||
|
||||
if not config[CONF_VERIFY_SSL]:
|
||||
self.client.http.disable_https_verify()
|
||||
|
||||
self.last_results = []
|
||||
self.success_init = self._update_info()
|
||||
|
||||
_LOGGER.info("Synology SRM scanner initialized")
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return [device['mac'] for device in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
filter_named = [result['hostname'] for result in self.last_results if
|
||||
result['mac'] == device]
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
|
||||
return None
|
||||
|
||||
def _update_info(self):
|
||||
"""Check the router for connected devices."""
|
||||
_LOGGER.debug("Scanning for connected devices")
|
||||
|
||||
devices = self.client.mesh.network_wifidevice()
|
||||
last_results = []
|
||||
|
||||
for device in devices:
|
||||
last_results.append({
|
||||
'mac': device['mac'],
|
||||
'hostname': device['hostname']
|
||||
})
|
||||
|
||||
_LOGGER.debug(
|
||||
"Found %d device(s) connected to the router",
|
||||
len(devices)
|
||||
)
|
||||
|
||||
self.last_results = last_results
|
||||
return True
|
||||
79
homeassistant/components/dovado/__init__.py
Normal file
79
homeassistant/components/dovado/__init__.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Support for Dovado router.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/dovado/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT,
|
||||
DEVICE_DEFAULT_NAME)
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['dovado==0.4.1']
|
||||
|
||||
DOMAIN = 'dovado'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT): cv.port,
|
||||
})
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Dovado component."""
|
||||
import dovado
|
||||
|
||||
hass.data[DOMAIN] = DovadoData(
|
||||
dovado.Dovado(
|
||||
config[CONF_USERNAME],
|
||||
config[CONF_PASSWORD],
|
||||
config.get(CONF_HOST),
|
||||
config.get(CONF_PORT)
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class DovadoData:
|
||||
"""Maintains a connection to the router."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Set up a new Dovado connection."""
|
||||
self._client = client
|
||||
self.state = {}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Name of the router."""
|
||||
return self.state.get("product name", DEVICE_DEFAULT_NAME)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update device state."""
|
||||
try:
|
||||
self.state = self._client.state or {}
|
||||
if not self.state:
|
||||
return False
|
||||
self.state.update(
|
||||
connected=self.state.get("modem status") == "CONNECTED")
|
||||
_LOGGER.debug("Received: %s", self.state)
|
||||
return True
|
||||
except OSError as error:
|
||||
_LOGGER.warning("Could not contact the router: %s", error)
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
"""Dovado client instance."""
|
||||
return self._client
|
||||
38
homeassistant/components/dovado/notify.py
Normal file
38
homeassistant/components/dovado/notify.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Support for SMS notifications from the Dovado router.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.dovado/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.dovado import DOMAIN as DOVADO_DOMAIN
|
||||
from homeassistant.components.notify import BaseNotificationService, \
|
||||
ATTR_TARGET
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['dovado']
|
||||
|
||||
|
||||
def get_service(hass, config, discovery_info=None):
|
||||
"""Get the Dovado Router SMS notification service."""
|
||||
return DovadoSMSNotificationService(hass.data[DOVADO_DOMAIN].client)
|
||||
|
||||
|
||||
class DovadoSMSNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for the Dovado SMS component."""
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initialize the service."""
|
||||
self._client = client
|
||||
|
||||
def send_message(self, message, **kwargs):
|
||||
"""Send SMS to the specified target phone number."""
|
||||
target = kwargs.get(ATTR_TARGET)
|
||||
|
||||
if not target:
|
||||
_LOGGER.error("One target is required")
|
||||
return
|
||||
|
||||
self._client.send_sms(target, message)
|
||||
116
homeassistant/components/dovado/sensor.py
Normal file
116
homeassistant/components/dovado/sensor.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Support for sensors from the Dovado router.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.dovado/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.dovado import DOMAIN as DOVADO_DOMAIN
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_SENSORS
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['dovado']
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
|
||||
SENSOR_UPLOAD = 'upload'
|
||||
SENSOR_DOWNLOAD = 'download'
|
||||
SENSOR_SIGNAL = 'signal'
|
||||
SENSOR_NETWORK = 'network'
|
||||
SENSOR_SMS_UNREAD = 'sms'
|
||||
|
||||
SENSORS = {
|
||||
SENSOR_NETWORK: ('signal strength', 'Network', None,
|
||||
'mdi:access-point-network'),
|
||||
SENSOR_SIGNAL: ('signal strength', 'Signal Strength', '%',
|
||||
'mdi:signal'),
|
||||
SENSOR_SMS_UNREAD: ('sms unread', 'SMS unread', '',
|
||||
'mdi:message-text-outline'),
|
||||
SENSOR_UPLOAD: ('traffic modem tx', 'Sent', 'GB',
|
||||
'mdi:cloud-upload'),
|
||||
SENSOR_DOWNLOAD: ('traffic modem rx', 'Received', 'GB',
|
||||
'mdi:cloud-download'),
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SENSORS): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSORS)]
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Dovado sensor platform."""
|
||||
dovado = hass.data[DOVADO_DOMAIN]
|
||||
|
||||
entities = []
|
||||
for sensor in config[CONF_SENSORS]:
|
||||
entities.append(DovadoSensor(dovado, sensor))
|
||||
|
||||
add_entities(entities)
|
||||
|
||||
|
||||
class DovadoSensor(Entity):
|
||||
"""Representation of a Dovado sensor."""
|
||||
|
||||
def __init__(self, data, sensor):
|
||||
"""Initialize the sensor."""
|
||||
self._data = data
|
||||
self._sensor = sensor
|
||||
self._state = self._compute_state()
|
||||
|
||||
def _compute_state(self):
|
||||
state = self._data.state.get(SENSORS[self._sensor][0])
|
||||
if self._sensor == SENSOR_NETWORK:
|
||||
match = re.search(r"\((.+)\)", state)
|
||||
return match.group(1) if match else None
|
||||
if self._sensor == SENSOR_SIGNAL:
|
||||
try:
|
||||
return int(state.split()[0])
|
||||
except ValueError:
|
||||
return None
|
||||
if self._sensor == SENSOR_SMS_UNREAD:
|
||||
return int(state)
|
||||
if self._sensor in [SENSOR_UPLOAD, SENSOR_DOWNLOAD]:
|
||||
return round(float(state) / 1e6, 1)
|
||||
return state
|
||||
|
||||
def update(self):
|
||||
"""Update sensor values."""
|
||||
self._data.update()
|
||||
self._state = self._compute_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return "{} {}".format(self._data.name, SENSORS[self._sensor][1])
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the sensor state."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon for the sensor."""
|
||||
return SENSORS[self._sensor][3]
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return SENSORS[self._sensor][2]
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {k: v for k, v in self._data.state.items()
|
||||
if k not in ['date', 'time']}
|
||||
98
homeassistant/components/ecoal_boiler.py
Normal file
98
homeassistant/components/ecoal_boiler.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Component to control ecoal/esterownik.pl coal/wood boiler controller.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/ecoal_boiler/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
|
||||
CONF_MONITORED_CONDITIONS, CONF_SENSORS,
|
||||
CONF_SWITCHES)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
|
||||
REQUIREMENTS = ['ecoaliface==0.4.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "ecoal_boiler"
|
||||
DATA_ECOAL_BOILER = 'data_' + DOMAIN
|
||||
|
||||
DEFAULT_USERNAME = "admin"
|
||||
DEFAULT_PASSWORD = "admin"
|
||||
|
||||
|
||||
# Available pump ids with assigned HA names
|
||||
# Available as switches
|
||||
AVAILABLE_PUMPS = {
|
||||
"central_heating_pump": "Central heating pump",
|
||||
"central_heating_pump2": "Central heating pump2",
|
||||
"domestic_hot_water_pump": "Domestic hot water pump",
|
||||
}
|
||||
|
||||
# Available temp sensor ids with assigned HA names
|
||||
# Available as sensors
|
||||
AVAILABLE_SENSORS = {
|
||||
"outdoor_temp": 'Outdoor temperature',
|
||||
"indoor_temp": 'Indoor temperature',
|
||||
"indoor2_temp": 'Indoor temperature 2',
|
||||
"domestic_hot_water_temp": 'Domestic hot water temperature',
|
||||
"target_domestic_hot_water_temp": 'Target hot water temperature',
|
||||
"feedwater_in_temp": 'Feedwater input temperature',
|
||||
"feedwater_out_temp": 'Feedwater output temperature',
|
||||
"target_feedwater_temp": 'Target feedwater temperature',
|
||||
"fuel_feeder_temp": 'Fuel feeder temperature',
|
||||
"exhaust_temp": 'Exhaust temperature',
|
||||
}
|
||||
|
||||
SWITCH_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_PUMPS)):
|
||||
vol.All(cv.ensure_list, [vol.In(AVAILABLE_PUMPS)])
|
||||
})
|
||||
|
||||
SENSOR_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_SENSORS)):
|
||||
vol.All(cv.ensure_list, [vol.In(AVAILABLE_SENSORS)])
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_USERNAME,
|
||||
default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD,
|
||||
default=DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA,
|
||||
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, hass_config):
|
||||
"""Set up global ECoalController instance same for sensors and switches."""
|
||||
from ecoaliface.simple import ECoalController
|
||||
|
||||
conf = hass_config[DOMAIN]
|
||||
host = conf[CONF_HOST]
|
||||
username = conf[CONF_USERNAME]
|
||||
passwd = conf[CONF_PASSWORD]
|
||||
# Creating ECoalController instance makes HTTP request to controller.
|
||||
ecoal_contr = ECoalController(host, username, passwd)
|
||||
if ecoal_contr.version is None:
|
||||
# Wrong credentials nor network config
|
||||
_LOGGER.error("Unable to read controller status from %s@%s"
|
||||
" (wrong host/credentials)", username, host, )
|
||||
return False
|
||||
_LOGGER.debug("Detected controller version: %r @%s",
|
||||
ecoal_contr.version, host, )
|
||||
hass.data[DATA_ECOAL_BOILER] = ecoal_contr
|
||||
# Setup switches
|
||||
switches = conf[CONF_SWITCHES][CONF_MONITORED_CONDITIONS]
|
||||
load_platform(hass, 'switch', DOMAIN, switches, hass_config)
|
||||
# Setup temp sensors
|
||||
sensors = conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS]
|
||||
load_platform(hass, 'sensor', DOMAIN, sensors, hass_config)
|
||||
return True
|
||||
@@ -22,7 +22,7 @@ from homeassistant.components.http import real_ip
|
||||
|
||||
from .hue_api import (
|
||||
HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
|
||||
HueOneLightChangeView, HueGroupView)
|
||||
HueOneLightChangeView, HueGroupView, HueAllGroupsStateView)
|
||||
from .upnp import DescriptionXmlView, UPNPResponderThread
|
||||
|
||||
DOMAIN = 'emulated_hue'
|
||||
@@ -105,6 +105,7 @@ async def async_setup(hass, yaml_config):
|
||||
HueAllLightsStateView(config).register(app, app.router)
|
||||
HueOneLightStateView(config).register(app, app.router)
|
||||
HueOneLightChangeView(config).register(app, app.router)
|
||||
HueAllGroupsStateView(config).register(app, app.router)
|
||||
HueGroupView(config).register(app, app.router)
|
||||
|
||||
upnp_listener = UPNPResponderThread(
|
||||
|
||||
@@ -56,6 +56,28 @@ class HueUsernameView(HomeAssistantView):
|
||||
return self.json([{'success': {'username': '12345678901234567890'}}])
|
||||
|
||||
|
||||
class HueAllGroupsStateView(HomeAssistantView):
|
||||
"""Group handler."""
|
||||
|
||||
url = '/api/{username}/groups'
|
||||
name = 'emulated_hue:all_groups:state'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the instance of the view."""
|
||||
self.config = config
|
||||
|
||||
@core.callback
|
||||
def get(self, request, username):
|
||||
"""Process a request to make the Brilliant Lightpad work."""
|
||||
if not is_local(request[KEY_REAL_IP]):
|
||||
return self.json_message('only local IPs allowed',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
return self.json({
|
||||
})
|
||||
|
||||
|
||||
class HueGroupView(HomeAssistantView):
|
||||
"""Group handler to get Logitech Pop working."""
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from .const import (
|
||||
CONF_ADVERTISE_IP, CONF_ADVERTISE_PORT, CONF_HOST_IP, CONF_LISTEN_PORT,
|
||||
CONF_SERVERS, CONF_UPNP_BIND_MULTICAST, DOMAIN)
|
||||
|
||||
REQUIREMENTS = ['emulated_roku==0.1.7']
|
||||
REQUIREMENTS = ['emulated_roku==0.1.8']
|
||||
|
||||
SERVER_CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
|
||||
@@ -146,19 +146,19 @@ async def async_setup(hass, config):
|
||||
@callback
|
||||
def zones_updated_callback(data):
|
||||
"""Handle zone timer updates."""
|
||||
_LOGGER.info("Envisalink sent a zone update event. Updating zones...")
|
||||
_LOGGER.debug("Envisalink sent a zone update event. Updating zones...")
|
||||
async_dispatcher_send(hass, SIGNAL_ZONE_UPDATE, data)
|
||||
|
||||
@callback
|
||||
def alarm_data_updated_callback(data):
|
||||
"""Handle non-alarm based info updates."""
|
||||
_LOGGER.info("Envisalink sent new alarm info. Updating alarms...")
|
||||
_LOGGER.debug("Envisalink sent new alarm info. Updating alarms...")
|
||||
async_dispatcher_send(hass, SIGNAL_KEYPAD_UPDATE, data)
|
||||
|
||||
@callback
|
||||
def partition_updated_callback(data):
|
||||
"""Handle partition changes thrown by evl (including alarms)."""
|
||||
_LOGGER.info("The envisalink sent a partition update event")
|
||||
_LOGGER.debug("The envisalink sent a partition update event")
|
||||
async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data)
|
||||
|
||||
@callback
|
||||
|
||||
@@ -12,8 +12,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import group
|
||||
from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE,
|
||||
SERVICE_TURN_OFF, ATTR_ENTITY_ID,
|
||||
STATE_UNKNOWN)
|
||||
SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@@ -94,7 +93,7 @@ def is_on(hass, entity_id: str = None) -> bool:
|
||||
"""Return if the fans are on based on the statemachine."""
|
||||
entity_id = entity_id or ENTITY_ID_ALL_FANS
|
||||
state = hass.states.get(entity_id)
|
||||
return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN]
|
||||
return state.attributes[ATTR_SPEED] not in [SPEED_OFF, None]
|
||||
|
||||
|
||||
async def async_setup(hass, config: dict):
|
||||
@@ -199,7 +198,7 @@ class FanEntity(ToggleEntity):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the entity is on."""
|
||||
return self.speed not in [SPEED_OFF, STATE_UNKNOWN]
|
||||
return self.speed not in [SPEED_OFF, None]
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.comfoconnect import (
|
||||
from homeassistant.components.fan import (
|
||||
FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
SUPPORT_SET_SPEED)
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.helpers.dispatcher import (dispatcher_connect)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -79,7 +78,7 @@ class ComfoConnectFan(FanEntity):
|
||||
speed = self._ccb.data[SENSOR_FAN_SPEED_MODE]
|
||||
return SPEED_MAPPING[speed]
|
||||
except KeyError:
|
||||
return STATE_UNKNOWN
|
||||
return None
|
||||
|
||||
@property
|
||||
def speed_list(self):
|
||||
|
||||
@@ -7,7 +7,7 @@ https://home-assistant.io/components/fan.wink/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, STATE_UNKNOWN, SUPPORT_DIRECTION,
|
||||
SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SUPPORT_DIRECTION,
|
||||
SUPPORT_SET_SPEED, FanEntity)
|
||||
from homeassistant.components.wink import DOMAIN, WinkDevice
|
||||
|
||||
@@ -71,7 +71,7 @@ class WinkFanDevice(WinkDevice, FanEntity):
|
||||
return SPEED_MEDIUM
|
||||
if SPEED_HIGH == current_wink_speed:
|
||||
return SPEED_HIGH
|
||||
return STATE_UNKNOWN
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_direction(self):
|
||||
|
||||
@@ -38,7 +38,7 @@ MODEL_AIRPURIFIER_MA1 = 'zhimi.airpurifier.ma1'
|
||||
MODEL_AIRPURIFIER_MA2 = 'zhimi.airpurifier.ma2'
|
||||
MODEL_AIRPURIFIER_SA1 = 'zhimi.airpurifier.sa1'
|
||||
MODEL_AIRPURIFIER_SA2 = 'zhimi.airpurifier.sa2'
|
||||
MODEL_AIRPURIFIER_MC1 = 'zhimi.airpurifier.mc1'
|
||||
MODEL_AIRPURIFIER_2S = 'zhimi.airpurifier.mc1'
|
||||
|
||||
MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1'
|
||||
MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1'
|
||||
@@ -62,7 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
MODEL_AIRPURIFIER_MA2,
|
||||
MODEL_AIRPURIFIER_SA1,
|
||||
MODEL_AIRPURIFIER_SA2,
|
||||
MODEL_AIRPURIFIER_MC1,
|
||||
MODEL_AIRPURIFIER_2S,
|
||||
MODEL_AIRHUMIDIFIER_V1,
|
||||
MODEL_AIRHUMIDIFIER_CA,
|
||||
MODEL_AIRFRESH_VA2,
|
||||
@@ -175,6 +175,15 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 = {
|
||||
ATTR_VOLUME: 'volume',
|
||||
}
|
||||
|
||||
AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S = {
|
||||
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
|
||||
ATTR_BUZZER: 'buzzer',
|
||||
ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
|
||||
ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
|
||||
ATTR_FILTER_TYPE: 'filter_type',
|
||||
ATTR_ILLUMINANCE: 'illuminance',
|
||||
}
|
||||
|
||||
AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = {
|
||||
# Common set isn't used here. It's a very basic version of the device.
|
||||
ATTR_AIR_QUALITY_INDEX: 'aqi',
|
||||
@@ -249,6 +258,7 @@ AVAILABLE_ATTRIBUTES_AIRFRESH = {
|
||||
OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle']
|
||||
OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite']
|
||||
OPERATION_MODES_AIRPURIFIER_PRO_V7 = OPERATION_MODES_AIRPURIFIER_PRO
|
||||
OPERATION_MODES_AIRPURIFIER_2S = ['Auto', 'Silent', 'Favorite']
|
||||
OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle',
|
||||
'Medium', 'High', 'Strong']
|
||||
OPERATION_MODES_AIRFRESH = ['Auto', 'Silent', 'Interval', 'Low',
|
||||
@@ -289,6 +299,11 @@ FEATURE_FLAGS_AIRPURIFIER_PRO_V7 = (FEATURE_SET_CHILD_LOCK |
|
||||
FEATURE_SET_FAVORITE_LEVEL |
|
||||
FEATURE_SET_VOLUME)
|
||||
|
||||
FEATURE_FLAGS_AIRPURIFIER_2S = (FEATURE_SET_BUZZER |
|
||||
FEATURE_SET_CHILD_LOCK |
|
||||
FEATURE_SET_LED |
|
||||
FEATURE_SET_FAVORITE_LEVEL)
|
||||
|
||||
FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_SET_BUZZER |
|
||||
FEATURE_SET_CHILD_LOCK |
|
||||
FEATURE_SET_LED)
|
||||
@@ -619,6 +634,10 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
|
||||
self._available_attributes = \
|
||||
AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7
|
||||
self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO_V7
|
||||
elif self._model == MODEL_AIRPURIFIER_2S:
|
||||
self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S
|
||||
self._speed_list = OPERATION_MODES_AIRPURIFIER_2S
|
||||
elif self._model == MODEL_AIRPURIFIER_V3:
|
||||
self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3
|
||||
|
||||
@@ -12,7 +12,8 @@ import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN)
|
||||
from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN,
|
||||
CONF_UPDATE_INTERVAL)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -24,8 +25,6 @@ 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,
|
||||
|
||||
@@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pyfritzhome==0.4.0']
|
||||
|
||||
SUPPORTED_DOMAINS = ['binary_sensor', 'climate', 'switch']
|
||||
SUPPORTED_DOMAINS = ['binary_sensor', 'climate', 'switch', 'sensor']
|
||||
|
||||
DOMAIN = 'fritzbox'
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20190121.1']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20190201.0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',
|
||||
|
||||
@@ -22,15 +22,12 @@ DOMAIN = 'geo_location'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
GROUP_NAME_ALL_EVENTS = 'All Geolocation Events'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the Geolocation component."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_EVENTS)
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
await component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ import voluptuous as vol
|
||||
from aiohttp import web
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME, \
|
||||
ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID, HTTP_OK, ATTR_NAME
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.util import slugify
|
||||
|
||||
@@ -68,13 +68,9 @@ WEBHOOK_SCHEMA = vol.Schema({
|
||||
|
||||
async def async_setup(hass, hass_config):
|
||||
"""Set up the Geofency component."""
|
||||
config = hass_config[DOMAIN]
|
||||
mobile_beacons = config[CONF_MOBILE_BEACONS]
|
||||
config = hass_config.get(DOMAIN, {})
|
||||
mobile_beacons = config.get(CONF_MOBILE_BEACONS, [])
|
||||
hass.data[DOMAIN] = [slugify(beacon) for beacon in mobile_beacons]
|
||||
|
||||
hass.async_create_task(
|
||||
async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -136,12 +132,18 @@ async def async_setup_entry(hass, entry):
|
||||
"""Configure based on config entry."""
|
||||
hass.components.webhook.async_register(
|
||||
DOMAIN, 'Geofency', entry.data[CONF_WEBHOOK_ID], handle_webhook)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
|
||||
|
||||
await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER)
|
||||
return True
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
|
||||
@@ -6,16 +6,21 @@ https://home-assistant.io/components/device_tracker.geofency/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.geofency import TRACKER_UPDATE
|
||||
from homeassistant.components.device_tracker import DOMAIN as \
|
||||
DEVICE_TRACKER_DOMAIN
|
||||
from homeassistant.components.geofency import TRACKER_UPDATE, \
|
||||
DOMAIN as GEOFENCY_DOMAIN
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['geofency']
|
||||
|
||||
DATA_KEY = '{}.{}'.format(GEOFENCY_DOMAIN, DEVICE_TRACKER_DOMAIN)
|
||||
|
||||
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Set up the Geofency device tracker."""
|
||||
|
||||
async def async_setup_entry(hass, entry, async_see):
|
||||
"""Configure a dispatcher connection based on a config entry."""
|
||||
async def _set_location(device, gps, location_name, attributes):
|
||||
"""Fire HA event to set location."""
|
||||
await async_see(
|
||||
@@ -25,5 +30,13 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
attributes=attributes
|
||||
)
|
||||
|
||||
async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location)
|
||||
hass.data[DATA_KEY] = async_dispatcher_connect(
|
||||
hass, TRACKER_UPDATE, _set_location
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload the config entry and remove the dispatcher connection."""
|
||||
hass.data[DATA_KEY]()
|
||||
return True
|
||||
|
||||
@@ -15,8 +15,8 @@ from homeassistant.components.device_tracker.tile import ATTR_ALTITUDE
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, \
|
||||
HTTP_OK, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,9 +57,6 @@ WEBHOOK_SCHEMA = vol.Schema({
|
||||
|
||||
async def async_setup(hass, hass_config):
|
||||
"""Set up the GPSLogger component."""
|
||||
hass.async_create_task(
|
||||
async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -103,12 +100,18 @@ async def async_setup_entry(hass, entry):
|
||||
"""Configure based on config entry."""
|
||||
hass.components.webhook.async_register(
|
||||
DOMAIN, 'GPSLogger', entry.data[CONF_WEBHOOK_ID], handle_webhook)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
|
||||
|
||||
await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER)
|
||||
return True
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
|
||||
44
homeassistant/components/gpslogger/device_tracker.py
Normal file
44
homeassistant/components/gpslogger/device_tracker.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Support for the GPSLogger platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.gpslogger/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN as \
|
||||
DEVICE_TRACKER_DOMAIN
|
||||
from homeassistant.components.gpslogger import DOMAIN as GPSLOGGER_DOMAIN, \
|
||||
TRACKER_UPDATE
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['gpslogger']
|
||||
|
||||
DATA_KEY = '{}.{}'.format(GPSLOGGER_DOMAIN, DEVICE_TRACKER_DOMAIN)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistantType, entry, async_see):
|
||||
"""Configure a dispatcher connection based on a config entry."""
|
||||
async def _set_location(device, gps_location, battery, accuracy, attrs):
|
||||
"""Fire HA event to set location."""
|
||||
await async_see(
|
||||
dev_id=device,
|
||||
gps=gps_location,
|
||||
battery=battery,
|
||||
gps_accuracy=accuracy,
|
||||
attributes=attrs
|
||||
)
|
||||
|
||||
hass.data[DATA_KEY] = async_dispatcher_connect(
|
||||
hass, TRACKER_UPDATE, _set_location
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistantType, entry):
|
||||
"""Unload the config entry and remove the dispatcher connection."""
|
||||
hass.data[DATA_KEY]()
|
||||
return True
|
||||
@@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START, STATE_UNKNOWN,
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP, STATE_ON,
|
||||
STATE_OFF, CONF_DEVICES, CONF_PLATFORM,
|
||||
STATE_PLAYING, STATE_IDLE,
|
||||
@@ -324,7 +324,7 @@ class CecDevice(Entity):
|
||||
"""Initialize the device."""
|
||||
self._device = device
|
||||
self._icon = None
|
||||
self._state = STATE_UNKNOWN
|
||||
self._state = None
|
||||
self._logical_address = logical
|
||||
self.entity_id = "%s.%d" % (DOMAIN, self._logical_address)
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import logging
|
||||
from pyhap.const import CATEGORY_DOOR_LOCK
|
||||
|
||||
from homeassistant.components.lock import (
|
||||
ATTR_ENTITY_ID, DOMAIN, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN)
|
||||
from homeassistant.const import ATTR_CODE
|
||||
ATTR_ENTITY_ID, DOMAIN, STATE_LOCKED, STATE_UNLOCKED)
|
||||
from homeassistant.const import ATTR_CODE, STATE_UNKNOWN
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import call_later
|
||||
|
||||
REQUIREMENTS = ['homekit==0.12.0']
|
||||
REQUIREMENTS = ['homekit==0.12.2']
|
||||
|
||||
DOMAIN = 'homekit_controller'
|
||||
HOMEKIT_DIR = '.homekit'
|
||||
@@ -28,7 +28,8 @@ HOMEKIT_ACCESSORY_DISPATCH = {
|
||||
'garage-door-opener': 'cover',
|
||||
'window': 'cover',
|
||||
'window-covering': 'cover',
|
||||
'lock-mechanism': 'lock'
|
||||
'lock-mechanism': 'lock',
|
||||
'motion': 'binary_sensor',
|
||||
}
|
||||
|
||||
HOMEKIT_IGNORE = [
|
||||
@@ -49,10 +50,6 @@ RETRY_INTERVAL = 60 # seconds
|
||||
PAIRING_FILE = "pairing.json"
|
||||
|
||||
|
||||
class HomeKitConnectionError(ConnectionError):
|
||||
"""Raised when unable to connect to target device."""
|
||||
|
||||
|
||||
def get_serial(accessory):
|
||||
"""Obtain the serial number of a HomeKit device."""
|
||||
# pylint: disable=import-error
|
||||
@@ -72,6 +69,11 @@ def get_serial(accessory):
|
||||
return None
|
||||
|
||||
|
||||
def escape_characteristic_name(char_name):
|
||||
"""Escape any dash or dots in a characteristics name."""
|
||||
return char_name.replace('-', '_').replace('.', '_')
|
||||
|
||||
|
||||
class HKDevice():
|
||||
"""HomeKit device."""
|
||||
|
||||
@@ -101,13 +103,14 @@ class HKDevice():
|
||||
"""Handle setup of a HomeKit accessory."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.model.services import ServicesTypes
|
||||
from homekit.exceptions import AccessoryDisconnectedError
|
||||
|
||||
self.pairing.pairing_data['AccessoryIP'] = self.host
|
||||
self.pairing.pairing_data['AccessoryPort'] = self.port
|
||||
|
||||
try:
|
||||
data = self.pairing.list_accessories_and_characteristics()
|
||||
except HomeKitConnectionError:
|
||||
except AccessoryDisconnectedError:
|
||||
call_later(
|
||||
self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup())
|
||||
return
|
||||
@@ -196,22 +199,80 @@ class HomeKitEntity(Entity):
|
||||
self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid)
|
||||
self._features = 0
|
||||
self._chars = {}
|
||||
self.setup()
|
||||
|
||||
def update(self):
|
||||
"""Obtain a HomeKit device's state."""
|
||||
try:
|
||||
pairing = self._accessory.pairing
|
||||
data = pairing.list_accessories_and_characteristics()
|
||||
except HomeKitConnectionError:
|
||||
return
|
||||
for accessory in data:
|
||||
def setup(self):
|
||||
"""Configure an entity baed on its HomeKit characterstics metadata."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.model.characteristics import CharacteristicsTypes
|
||||
|
||||
pairing_data = self._accessory.pairing.pairing_data
|
||||
|
||||
get_uuid = CharacteristicsTypes.get_uuid
|
||||
characteristic_types = [
|
||||
get_uuid(c) for c in self.get_characteristic_types()
|
||||
]
|
||||
|
||||
self._chars_to_poll = []
|
||||
self._chars = {}
|
||||
self._char_names = {}
|
||||
|
||||
for accessory in pairing_data.get('accessories', []):
|
||||
if accessory['aid'] != self._aid:
|
||||
continue
|
||||
for service in accessory['services']:
|
||||
if service['iid'] != self._iid:
|
||||
continue
|
||||
self.update_characteristics(service['characteristics'])
|
||||
break
|
||||
for char in service['characteristics']:
|
||||
uuid = CharacteristicsTypes.get_uuid(char['type'])
|
||||
if uuid not in characteristic_types:
|
||||
continue
|
||||
self._setup_characteristic(char)
|
||||
|
||||
def _setup_characteristic(self, char):
|
||||
"""Configure an entity based on a HomeKit characteristics metadata."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.model.characteristics import CharacteristicsTypes
|
||||
|
||||
# Build up a list of (aid, iid) tuples to poll on update()
|
||||
self._chars_to_poll.append((self._aid, char['iid']))
|
||||
|
||||
# Build a map of ctype -> iid
|
||||
short_name = CharacteristicsTypes.get_short(char['type'])
|
||||
self._chars[short_name] = char['iid']
|
||||
self._char_names[char['iid']] = short_name
|
||||
|
||||
# Callback to allow entity to configure itself based on this
|
||||
# characteristics metadata (valid values, value ranges, features, etc)
|
||||
setup_fn_name = escape_characteristic_name(short_name)
|
||||
setup_fn = getattr(self, '_setup_{}'.format(setup_fn_name), None)
|
||||
if not setup_fn:
|
||||
return
|
||||
# pylint: disable=not-callable
|
||||
setup_fn(char)
|
||||
|
||||
def update(self):
|
||||
"""Obtain a HomeKit device's state."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.exceptions import AccessoryDisconnectedError
|
||||
|
||||
pairing = self._accessory.pairing
|
||||
|
||||
try:
|
||||
new_values_dict = pairing.get_characteristics(self._chars_to_poll)
|
||||
except AccessoryDisconnectedError:
|
||||
return
|
||||
|
||||
for (_, iid), result in new_values_dict.items():
|
||||
if 'value' not in result:
|
||||
continue
|
||||
# Callback to update the entity with this characteristic value
|
||||
char_name = escape_characteristic_name(self._char_names[iid])
|
||||
update_fn = getattr(self, '_update_{}'.format(char_name), None)
|
||||
if not update_fn:
|
||||
continue
|
||||
# pylint: disable=not-callable
|
||||
update_fn(result['value'])
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -228,9 +289,13 @@ class HomeKitEntity(Entity):
|
||||
"""Return True if entity is available."""
|
||||
return self._accessory.pairing is not None
|
||||
|
||||
def get_characteristic_types(self):
|
||||
"""Define the homekit characteristics the entity cares about."""
|
||||
raise NotImplementedError
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise a HomeKit device state with Home Assistant."""
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
def put_characteristics(self, characteristics):
|
||||
"""Control a HomeKit device state from Home Assistant."""
|
||||
|
||||
@@ -54,24 +54,21 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel):
|
||||
self._state = None
|
||||
self._battery_level = None
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise the Alarm Control Panel state with Home Assistant."""
|
||||
def get_characteristic_types(self):
|
||||
"""Define the homekit characteristics the entity cares about."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.model.characteristics import CharacteristicsTypes
|
||||
return [
|
||||
CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT,
|
||||
CharacteristicsTypes.SECURITY_SYSTEM_STATE_TARGET,
|
||||
CharacteristicsTypes.BATTERY_LEVEL,
|
||||
]
|
||||
|
||||
for characteristic in characteristics:
|
||||
ctype = characteristic['type']
|
||||
ctype = CharacteristicsTypes.get_short(ctype)
|
||||
if ctype == "security-system-state.current":
|
||||
self._chars['security-system-state.current'] = \
|
||||
characteristic['iid']
|
||||
self._state = CURRENT_STATE_MAP[characteristic['value']]
|
||||
elif ctype == "security-system-state.target":
|
||||
self._chars['security-system-state.target'] = \
|
||||
characteristic['iid']
|
||||
elif ctype == "battery-level":
|
||||
self._chars['battery-level'] = characteristic['iid']
|
||||
self._battery_level = characteristic['value']
|
||||
def _update_security_system_state_current(self, value):
|
||||
self._state = CURRENT_STATE_MAP[value]
|
||||
|
||||
def _update_battery_level(self, value):
|
||||
self._battery_level = value
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
53
homeassistant/components/homekit_controller/binary_sensor.py
Normal file
53
homeassistant/components/homekit_controller/binary_sensor.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
Support for Homekit motion sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.homekit_controller/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.homekit_controller import (HomeKitEntity,
|
||||
KNOWN_ACCESSORIES)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
DEPENDENCIES = ['homekit_controller']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up Homekit motion sensor support."""
|
||||
if discovery_info is not None:
|
||||
accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']]
|
||||
add_entities([HomeKitMotionSensor(accessory, discovery_info)], True)
|
||||
|
||||
|
||||
class HomeKitMotionSensor(HomeKitEntity, BinarySensorDevice):
|
||||
"""Representation of a Homekit sensor."""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""Initialise the entity."""
|
||||
super().__init__(*args)
|
||||
self._on = False
|
||||
|
||||
def get_characteristic_types(self):
|
||||
"""Define the homekit characteristics the entity is tracking."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.model.characteristics import CharacteristicsTypes
|
||||
|
||||
return [
|
||||
CharacteristicsTypes.MOTION_DETECTED,
|
||||
]
|
||||
|
||||
def _update_motion_detected(self, value):
|
||||
self._on = value
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Define this binary_sensor as a motion sensor."""
|
||||
return 'motion'
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Has motion been detected."""
|
||||
return self._on
|
||||
@@ -27,6 +27,8 @@ MODE_HOMEKIT_TO_HASS = {
|
||||
# Map of hass operation modes to homekit modes
|
||||
MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()}
|
||||
|
||||
DEFAULT_VALID_MODES = list(MODE_HOMEKIT_TO_HASS)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up Homekit climate."""
|
||||
@@ -47,43 +49,54 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice):
|
||||
self._current_temp = None
|
||||
self._target_temp = None
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise device state with Home Assistant."""
|
||||
def get_characteristic_types(self):
|
||||
"""Define the homekit characteristics the entity cares about."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.models.characteristics import CharacteristicsTypes
|
||||
from homekit.model.characteristics import CharacteristicsTypes
|
||||
return [
|
||||
CharacteristicsTypes.HEATING_COOLING_CURRENT,
|
||||
CharacteristicsTypes.HEATING_COOLING_TARGET,
|
||||
CharacteristicsTypes.TEMPERATURE_CURRENT,
|
||||
CharacteristicsTypes.TEMPERATURE_TARGET,
|
||||
]
|
||||
|
||||
for characteristic in characteristics:
|
||||
ctype = characteristic['type']
|
||||
if ctype == CharacteristicsTypes.HEATING_COOLING_CURRENT:
|
||||
self._state = MODE_HOMEKIT_TO_HASS.get(
|
||||
characteristic['value'])
|
||||
if ctype == CharacteristicsTypes.HEATING_COOLING_TARGET:
|
||||
self._chars['target_mode'] = characteristic['iid']
|
||||
self._features |= SUPPORT_OPERATION_MODE
|
||||
self._current_mode = MODE_HOMEKIT_TO_HASS.get(
|
||||
characteristic['value'])
|
||||
self._valid_modes = [MODE_HOMEKIT_TO_HASS.get(
|
||||
mode) for mode in characteristic['valid-values']]
|
||||
elif ctype == CharacteristicsTypes.TEMPERATURE_CURRENT:
|
||||
self._current_temp = characteristic['value']
|
||||
elif ctype == CharacteristicsTypes.TEMPERATURE_TARGET:
|
||||
self._chars['target_temp'] = characteristic['iid']
|
||||
self._features |= SUPPORT_TARGET_TEMPERATURE
|
||||
self._target_temp = characteristic['value']
|
||||
def _setup_heating_cooling_target(self, characteristic):
|
||||
self._features |= SUPPORT_OPERATION_MODE
|
||||
|
||||
valid_values = characteristic.get(
|
||||
'valid-values', DEFAULT_VALID_MODES)
|
||||
self._valid_modes = [
|
||||
MODE_HOMEKIT_TO_HASS.get(mode) for mode in valid_values
|
||||
]
|
||||
|
||||
def _setup_temperature_target(self, characteristic):
|
||||
self._features |= SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
def _update_heating_cooling_current(self, value):
|
||||
self._state = MODE_HOMEKIT_TO_HASS.get(value)
|
||||
|
||||
def _update_heating_cooling_target(self, value):
|
||||
self._current_mode = MODE_HOMEKIT_TO_HASS.get(value)
|
||||
|
||||
def _update_temperature_current(self, value):
|
||||
self._current_temp = value
|
||||
|
||||
def _update_temperature_target(self, value):
|
||||
self._target_temp = value
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
characteristics = [{'aid': self._aid,
|
||||
'iid': self._chars['target_temp'],
|
||||
'iid': self._chars['temperature.target'],
|
||||
'value': temp}]
|
||||
self.put_characteristics(characteristics)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
characteristics = [{'aid': self._aid,
|
||||
'iid': self._chars['target_mode'],
|
||||
'iid': self._chars['heating-cooling.target'],
|
||||
'value': MODE_HASS_TO_HOMEKIT[operation_mode]}]
|
||||
self.put_characteristics(characteristics)
|
||||
|
||||
@@ -62,7 +62,6 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice):
|
||||
def __init__(self, accessory, discovery_info):
|
||||
"""Initialise the Cover."""
|
||||
super().__init__(accessory, discovery_info)
|
||||
self._name = None
|
||||
self._state = None
|
||||
self._obstruction_detected = None
|
||||
self.lock_state = None
|
||||
@@ -72,32 +71,28 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice):
|
||||
"""Define this cover as a garage door."""
|
||||
return 'garage'
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise the Cover state with Home Assistant."""
|
||||
def get_characteristic_types(self):
|
||||
"""Define the homekit characteristics the entity cares about."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.model.characteristics import CharacteristicsTypes
|
||||
return [
|
||||
CharacteristicsTypes.DOOR_STATE_CURRENT,
|
||||
CharacteristicsTypes.DOOR_STATE_TARGET,
|
||||
CharacteristicsTypes.OBSTRUCTION_DETECTED,
|
||||
CharacteristicsTypes.NAME,
|
||||
]
|
||||
|
||||
for characteristic in characteristics:
|
||||
ctype = characteristic['type']
|
||||
ctype = CharacteristicsTypes.get_short(ctype)
|
||||
if ctype == "door-state.current":
|
||||
self._chars['door-state.current'] = \
|
||||
characteristic['iid']
|
||||
self._state = CURRENT_GARAGE_STATE_MAP[characteristic['value']]
|
||||
elif ctype == "door-state.target":
|
||||
self._chars['door-state.target'] = \
|
||||
characteristic['iid']
|
||||
elif ctype == "obstruction-detected":
|
||||
self._chars['obstruction-detected'] = characteristic['iid']
|
||||
self._obstruction_detected = characteristic['value']
|
||||
elif ctype == "name":
|
||||
self._chars['name'] = characteristic['iid']
|
||||
self._name = characteristic['value']
|
||||
def _setup_name(self, char):
|
||||
self._name = char['value']
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
def _update_door_state_current(self, value):
|
||||
self._state = CURRENT_GARAGE_STATE_MAP[value]
|
||||
|
||||
def _update_obstruction_detected(self, value):
|
||||
self._obstruction_detected = value
|
||||
|
||||
def _update_name(self, value):
|
||||
self._name = value
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
@@ -156,7 +151,6 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice):
|
||||
def __init__(self, accessory, discovery_info):
|
||||
"""Initialise the Cover."""
|
||||
super().__init__(accessory, discovery_info)
|
||||
self._name = None
|
||||
self._state = None
|
||||
self._position = None
|
||||
self._tilt_position = None
|
||||
@@ -169,57 +163,46 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice):
|
||||
"""Return True if entity is available."""
|
||||
return self._state is not None
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise the Cover state with Home Assistant."""
|
||||
def get_characteristic_types(self):
|
||||
"""Define the homekit characteristics the entity cares about."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.model.characteristics import CharacteristicsTypes
|
||||
return [
|
||||
CharacteristicsTypes.POSITION_STATE,
|
||||
CharacteristicsTypes.POSITION_CURRENT,
|
||||
CharacteristicsTypes.POSITION_TARGET,
|
||||
CharacteristicsTypes.POSITION_HOLD,
|
||||
CharacteristicsTypes.VERTICAL_TILT_CURRENT,
|
||||
CharacteristicsTypes.VERTICAL_TILT_TARGET,
|
||||
CharacteristicsTypes.HORIZONTAL_TILT_CURRENT,
|
||||
CharacteristicsTypes.HORIZONTAL_TILT_TARGET,
|
||||
CharacteristicsTypes.OBSTRUCTION_DETECTED,
|
||||
CharacteristicsTypes.NAME,
|
||||
]
|
||||
|
||||
for characteristic in characteristics:
|
||||
ctype = characteristic['type']
|
||||
ctype = CharacteristicsTypes.get_short(ctype)
|
||||
if ctype == "position.state":
|
||||
self._chars['position.state'] = \
|
||||
characteristic['iid']
|
||||
if 'value' in characteristic:
|
||||
self._state = \
|
||||
CURRENT_WINDOW_STATE_MAP[characteristic['value']]
|
||||
elif ctype == "position.current":
|
||||
self._chars['position.current'] = \
|
||||
characteristic['iid']
|
||||
self._position = characteristic['value']
|
||||
elif ctype == "position.target":
|
||||
self._chars['position.target'] = \
|
||||
characteristic['iid']
|
||||
elif ctype == "position.hold":
|
||||
self._chars['position.hold'] = characteristic['iid']
|
||||
if 'value' in characteristic:
|
||||
self._hold = characteristic['value']
|
||||
elif ctype == "vertical-tilt.current":
|
||||
self._chars['vertical-tilt.current'] = characteristic['iid']
|
||||
if characteristic['value'] is not None:
|
||||
self._tilt_position = characteristic['value']
|
||||
elif ctype == "horizontal-tilt.current":
|
||||
self._chars['horizontal-tilt.current'] = characteristic['iid']
|
||||
if characteristic['value'] is not None:
|
||||
self._tilt_position = characteristic['value']
|
||||
elif ctype == "vertical-tilt.target":
|
||||
self._chars['vertical-tilt.target'] = \
|
||||
characteristic['iid']
|
||||
elif ctype == "horizontal-tilt.target":
|
||||
self._chars['vertical-tilt.target'] = \
|
||||
characteristic['iid']
|
||||
elif ctype == "obstruction-detected":
|
||||
self._chars['obstruction-detected'] = characteristic['iid']
|
||||
self._obstruction_detected = characteristic['value']
|
||||
elif ctype == "name":
|
||||
self._chars['name'] = characteristic['iid']
|
||||
if 'value' in characteristic:
|
||||
self._name = characteristic['value']
|
||||
def _setup_name(self, char):
|
||||
self._name = char['value']
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
def _update_position_state(self, value):
|
||||
self._state = CURRENT_WINDOW_STATE_MAP[value]
|
||||
|
||||
def _update_position_current(self, value):
|
||||
self._position = value
|
||||
|
||||
def _update_position_hold(self, value):
|
||||
self._hold = value
|
||||
|
||||
def _update_vertical_tilt_current(self, value):
|
||||
self._tilt_position = value
|
||||
|
||||
def _update_horizontal_tilt_current(self, value):
|
||||
self._tilt_position = value
|
||||
|
||||
def _update_obstruction_detected(self, value):
|
||||
self._obstruction_detected = value
|
||||
|
||||
def _update_name(self, value):
|
||||
self._hold = value
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@@ -36,33 +36,44 @@ class HomeKitLight(HomeKitEntity, Light):
|
||||
self._hue = None
|
||||
self._saturation = None
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise light state with Home Assistant."""
|
||||
def get_characteristic_types(self):
|
||||
"""Define the homekit characteristics the entity cares about."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.model.characteristics import CharacteristicsTypes
|
||||
return [
|
||||
CharacteristicsTypes.ON,
|
||||
CharacteristicsTypes.BRIGHTNESS,
|
||||
CharacteristicsTypes.COLOR_TEMPERATURE,
|
||||
CharacteristicsTypes.HUE,
|
||||
CharacteristicsTypes.SATURATION,
|
||||
]
|
||||
|
||||
for characteristic in characteristics:
|
||||
ctype = characteristic['type']
|
||||
ctype = CharacteristicsTypes.get_short(ctype)
|
||||
if ctype == "on":
|
||||
self._chars['on'] = characteristic['iid']
|
||||
self._on = characteristic['value']
|
||||
elif ctype == 'brightness':
|
||||
self._chars['brightness'] = characteristic['iid']
|
||||
self._features |= SUPPORT_BRIGHTNESS
|
||||
self._brightness = characteristic['value']
|
||||
elif ctype == 'color-temperature':
|
||||
self._chars['color_temperature'] = characteristic['iid']
|
||||
self._features |= SUPPORT_COLOR_TEMP
|
||||
self._color_temperature = characteristic['value']
|
||||
elif ctype == "hue":
|
||||
self._chars['hue'] = characteristic['iid']
|
||||
self._features |= SUPPORT_COLOR
|
||||
self._hue = characteristic['value']
|
||||
elif ctype == "saturation":
|
||||
self._chars['saturation'] = characteristic['iid']
|
||||
self._features |= SUPPORT_COLOR
|
||||
self._saturation = characteristic['value']
|
||||
def _setup_brightness(self, char):
|
||||
self._features |= SUPPORT_BRIGHTNESS
|
||||
|
||||
def _setup_color_temperature(self, char):
|
||||
self._features |= SUPPORT_COLOR_TEMP
|
||||
|
||||
def _setup_hue(self, char):
|
||||
self._features |= SUPPORT_COLOR
|
||||
|
||||
def _setup_saturation(self, char):
|
||||
self._features |= SUPPORT_COLOR
|
||||
|
||||
def _update_on(self, value):
|
||||
self._on = value
|
||||
|
||||
def _update_brightness(self, value):
|
||||
self._brightness = value
|
||||
|
||||
def _update_color_temperature(self, value):
|
||||
self._color_temperature = value
|
||||
|
||||
def _update_hue(self, value):
|
||||
self._hue = value
|
||||
|
||||
def _update_saturation(self, value):
|
||||
self._saturation = value
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -51,24 +51,21 @@ class HomeKitLock(HomeKitEntity, LockDevice):
|
||||
self._name = discovery_info['model']
|
||||
self._battery_level = None
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise the Lock state with Home Assistant."""
|
||||
def get_characteristic_types(self):
|
||||
"""Define the homekit characteristics the entity cares about."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.model.characteristics import CharacteristicsTypes
|
||||
return [
|
||||
CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE,
|
||||
CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE,
|
||||
CharacteristicsTypes.BATTERY_LEVEL,
|
||||
]
|
||||
|
||||
for characteristic in characteristics:
|
||||
ctype = characteristic['type']
|
||||
ctype = CharacteristicsTypes.get_short(ctype)
|
||||
if ctype == "lock-mechanism.current-state":
|
||||
self._chars['lock-mechanism.current-state'] = \
|
||||
characteristic['iid']
|
||||
self._state = CURRENT_STATE_MAP[characteristic['value']]
|
||||
elif ctype == "lock-mechanism.target-state":
|
||||
self._chars['lock-mechanism.target-state'] = \
|
||||
characteristic['iid']
|
||||
elif ctype == "battery-level":
|
||||
self._chars['battery-level'] = characteristic['iid']
|
||||
self._battery_level = characteristic['value']
|
||||
def _update_lock_mechanism_current_state(self, value):
|
||||
self._state = CURRENT_STATE_MAP[value]
|
||||
|
||||
def _update_battery_level(self, value):
|
||||
self._battery_level = value
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -33,20 +33,20 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice):
|
||||
self._on = None
|
||||
self._outlet_in_use = None
|
||||
|
||||
def update_characteristics(self, characteristics):
|
||||
"""Synchronise the switch state with Home Assistant."""
|
||||
def get_characteristic_types(self):
|
||||
"""Define the homekit characteristics the entity cares about."""
|
||||
# pylint: disable=import-error
|
||||
from homekit.model.characteristics import CharacteristicsTypes
|
||||
return [
|
||||
CharacteristicsTypes.ON,
|
||||
CharacteristicsTypes.OUTLET_IN_USE,
|
||||
]
|
||||
|
||||
for characteristic in characteristics:
|
||||
ctype = characteristic['type']
|
||||
ctype = CharacteristicsTypes.get_short(ctype)
|
||||
if ctype == "on":
|
||||
self._chars['on'] = characteristic['iid']
|
||||
self._on = characteristic['value']
|
||||
elif ctype == "outlet-in-use":
|
||||
self._chars['outlet-in-use'] = characteristic['iid']
|
||||
self._outlet_in_use = characteristic['value']
|
||||
def _update_on(self, value):
|
||||
self._on = value
|
||||
|
||||
def _update_outlet_in_use(self, value):
|
||||
self._outlet_in_use = value
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -19,7 +19,7 @@ from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.54']
|
||||
REQUIREMENTS = ['pyhomematic==0.1.55']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -65,7 +65,7 @@ HM_DEVICE_TYPES = {
|
||||
DISCOVER_SWITCHES: [
|
||||
'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren',
|
||||
'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic',
|
||||
'IPKeySwitchPowermeter', 'IPGarage'],
|
||||
'IPKeySwitchPowermeter', 'IPGarage', 'IPKeySwitch', 'IPMultiIO'],
|
||||
DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer', 'IPDimmer',
|
||||
'ColorEffectLight'],
|
||||
DISCOVER_SENSORS: [
|
||||
@@ -79,7 +79,7 @@ HM_DEVICE_TYPES = {
|
||||
'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor',
|
||||
'IPKeySwitchPowermeter', 'IPThermostatWall230V', 'IPWeatherSensorPlus',
|
||||
'IPWeatherSensorBasic', 'IPBrightnessSensor', 'IPGarage',
|
||||
'UniversalSensor', 'MotionIPV2'],
|
||||
'UniversalSensor', 'MotionIPV2', 'IPMultiIO'],
|
||||
DISCOVER_CLIMATE: [
|
||||
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
|
||||
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
|
||||
@@ -89,7 +89,8 @@ HM_DEVICE_TYPES = {
|
||||
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
|
||||
'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
|
||||
'WiredSensor', 'PresenceIP', 'IPWeatherSensor', 'IPPassageSensor',
|
||||
'SmartwareMotion', 'IPWeatherSensorPlus', 'MotionIPV2'],
|
||||
'SmartwareMotion', 'IPWeatherSensorPlus', 'MotionIPV2', 'WaterIP',
|
||||
'IPMultiIO'],
|
||||
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'],
|
||||
DISCOVER_LOCKS: ['KeyMatic']
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ from .const import (
|
||||
from .device import HomematicipGenericDevice # noqa: F401
|
||||
from .hap import HomematicipAuth, HomematicipHAP # noqa: F401
|
||||
|
||||
REQUIREMENTS = ['homematicip==0.10.3']
|
||||
REQUIREMENTS = ['homematicip==0.10.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ COMPONENTS = [
|
||||
'alarm_control_panel',
|
||||
'binary_sensor',
|
||||
'climate',
|
||||
'cover',
|
||||
'light',
|
||||
'sensor',
|
||||
'switch',
|
||||
|
||||
@@ -99,6 +99,7 @@ class ApiConfig:
|
||||
self.port = port
|
||||
self.api_password = api_password
|
||||
|
||||
host = host.rstrip('/')
|
||||
if host.startswith(("http://", "https://")):
|
||||
self.base_url = host
|
||||
elif use_ssl:
|
||||
|
||||
@@ -104,7 +104,7 @@ async def process_wrong_login(request):
|
||||
|
||||
request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1
|
||||
|
||||
if (request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] >
|
||||
if (request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] >=
|
||||
request.app[KEY_LOGIN_THRESHOLD]):
|
||||
new_ban = IpBan(remote_addr)
|
||||
request.app[KEY_BANNED_IPS].append(new_ban)
|
||||
|
||||
@@ -19,7 +19,7 @@ from .bridge import HueBridge
|
||||
# Loading the config flow file will register the flow
|
||||
from .config_flow import configured_hosts
|
||||
|
||||
REQUIREMENTS = ['aiohue==1.8.0']
|
||||
REQUIREMENTS = ['aiohue==1.9.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ SUPPORT_HUE = {
|
||||
}
|
||||
|
||||
ATTR_IS_HUE_GROUP = 'is_hue_group'
|
||||
GAMUT_TYPE_UNAVAILABLE = 'None'
|
||||
# Minimum Hue Bridge API version to support groups
|
||||
# 1.4.0 introduced extended group info
|
||||
# 1.12 introduced the state object for groups
|
||||
@@ -221,7 +222,7 @@ class HueLight(Light):
|
||||
if is_group:
|
||||
self.is_osram = False
|
||||
self.is_philips = False
|
||||
self.gamut_typ = 'None'
|
||||
self.gamut_typ = GAMUT_TYPE_UNAVAILABLE
|
||||
self.gamut = None
|
||||
else:
|
||||
self.is_osram = light.manufacturername == 'OSRAM'
|
||||
@@ -229,6 +230,21 @@ class HueLight(Light):
|
||||
self.gamut_typ = self.light.colorgamuttype
|
||||
self.gamut = self.light.colorgamut
|
||||
_LOGGER.debug("Color gamut of %s: %s", self.name, str(self.gamut))
|
||||
if self.light.swupdatestate == "readytoinstall":
|
||||
err = (
|
||||
"Please check for software updates of the bridge "
|
||||
"and/or the bulb: %s, in the Philips Hue App."
|
||||
)
|
||||
_LOGGER.warning(err, self.name)
|
||||
if self.gamut:
|
||||
if not color.check_valid_gamut(self.gamut):
|
||||
err = (
|
||||
"Color gamut of %s: %s, not valid, "
|
||||
"setting gamut to None."
|
||||
)
|
||||
_LOGGER.warning(err, self.name, str(self.gamut))
|
||||
self.gamut_typ = GAMUT_TYPE_UNAVAILABLE
|
||||
self.gamut = None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
||||
@@ -13,7 +13,7 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import split_entity_id, callback
|
||||
from homeassistant.const import STATE_UNKNOWN, CONF_REGION
|
||||
from homeassistant.const import CONF_REGION
|
||||
from homeassistant.components.image_processing import (
|
||||
PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE,
|
||||
CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE)
|
||||
@@ -82,7 +82,7 @@ class ImageProcessingAlprEntity(ImageProcessingEntity):
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
confidence = 0
|
||||
plate = STATE_UNKNOWN
|
||||
plate = None
|
||||
|
||||
# search high plate
|
||||
for i_pl, i_co in self.plates.items():
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.components.image_processing import (
|
||||
from homeassistant.core import split_entity_id
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['numpy==1.15.4']
|
||||
REQUIREMENTS = ['numpy==1.16.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
69
homeassistant/components/image_processing/qrcode.py
Normal file
69
homeassistant/components/image_processing/qrcode.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Support for the QR image processing.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/image_processing.qr/
|
||||
"""
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.components.image_processing import (
|
||||
ImageProcessingEntity, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME)
|
||||
|
||||
REQUIREMENTS = ['pyzbar==0.1.7', 'pillow==5.4.1']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the demo image processing platform."""
|
||||
# pylint: disable=unused-argument
|
||||
entities = []
|
||||
for camera in config[CONF_SOURCE]:
|
||||
entities.append(QrEntity(
|
||||
camera[CONF_ENTITY_ID], camera.get(CONF_NAME)
|
||||
))
|
||||
|
||||
add_entities(entities)
|
||||
|
||||
|
||||
class QrEntity(ImageProcessingEntity):
|
||||
"""QR image processing entity."""
|
||||
|
||||
def __init__(self, camera_entity, name):
|
||||
"""Initialize QR image processing entity."""
|
||||
super().__init__()
|
||||
|
||||
self._camera = camera_entity
|
||||
if name:
|
||||
self._name = name
|
||||
else:
|
||||
self._name = "QR {0}".format(
|
||||
split_entity_id(camera_entity)[1])
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def camera_entity(self):
|
||||
"""Return camera entity id from process pictures."""
|
||||
return self._camera
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
def process_image(self, image):
|
||||
"""Process image."""
|
||||
import io
|
||||
from pyzbar import pyzbar
|
||||
from PIL import Image
|
||||
|
||||
stream = io.BytesIO(image)
|
||||
img = Image.open(stream)
|
||||
|
||||
barcodes = pyzbar.decode(img)
|
||||
if barcodes:
|
||||
self._state = barcodes[0].data.decode("utf-8")
|
||||
else:
|
||||
self._state = None
|
||||
@@ -20,7 +20,7 @@ from homeassistant.core import split_entity_id
|
||||
from homeassistant.helpers import template
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['numpy==1.15.4', 'pillow==5.4.1', 'protobuf==3.6.1']
|
||||
REQUIREMENTS = ['numpy==1.16.0', 'pillow==5.4.1', 'protobuf==3.6.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers import discovery, config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType, Dict
|
||||
|
||||
REQUIREMENTS = ['PyISY==1.1.0']
|
||||
REQUIREMENTS = ['PyISY==1.1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_ADDRESS, CONF_HOST, CONF_LIGHTS, CONF_NAME, CONF_PASSWORD, CONF_PORT,
|
||||
CONF_USERNAME)
|
||||
CONF_SWITCHES, CONF_USERNAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -32,8 +32,10 @@ CONF_TRANSITION = 'transition'
|
||||
CONF_DIMMABLE = 'dimmable'
|
||||
CONF_CONNECTIONS = 'connections'
|
||||
|
||||
DIM_MODES = ['steps50', 'steps200']
|
||||
OUTPUT_PORTS = ['output1', 'output2', 'output3', 'output4']
|
||||
DIM_MODES = ['STEPS50', 'STEPS200']
|
||||
OUTPUT_PORTS = ['OUTPUT1', 'OUTPUT2', 'OUTPUT3', 'OUTPUT4']
|
||||
RELAY_PORTS = ['RELAY1', 'RELAY2', 'RELAY3', 'RELAY4',
|
||||
'RELAY5', 'RELAY6', 'RELAY7', 'RELAY8']
|
||||
|
||||
# Regex for address validation
|
||||
PATTERN_ADDRESS = re.compile('^((?P<conn_id>\\w+)\\.)?s?(?P<seg_id>\\d+)'
|
||||
@@ -85,21 +87,29 @@ def is_address(value):
|
||||
LIGHTS_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_ADDRESS): is_address,
|
||||
vol.Required(CONF_OUTPUT): vol.All(vol.In(OUTPUT_PORTS), vol.Upper),
|
||||
vol.Required(CONF_OUTPUT): vol.All(vol.Upper,
|
||||
vol.In(OUTPUT_PORTS + RELAY_PORTS)),
|
||||
vol.Optional(CONF_DIMMABLE, default=False): vol.Coerce(bool),
|
||||
vol.Optional(CONF_TRANSITION, default=0):
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0., max=486.),
|
||||
lambda value: value * 1000),
|
||||
})
|
||||
|
||||
SWITCHES_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_ADDRESS): is_address,
|
||||
vol.Required(CONF_OUTPUT): vol.All(vol.Upper,
|
||||
vol.In(OUTPUT_PORTS + RELAY_PORTS))
|
||||
})
|
||||
|
||||
CONNECTION_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_SK_NUM_TRIES, default=3): cv.positive_int,
|
||||
vol.Optional(CONF_DIM_MODE, default='steps50'): vol.All(vol.In(DIM_MODES),
|
||||
vol.Upper),
|
||||
vol.Optional(CONF_DIM_MODE, default='steps50'): vol.All(vol.Upper,
|
||||
vol.In(DIM_MODES)),
|
||||
vol.Optional(CONF_NAME): cv.string
|
||||
})
|
||||
|
||||
@@ -107,7 +117,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_CONNECTIONS): vol.All(
|
||||
cv.ensure_list, has_unique_connection_names, [CONNECTION_SCHEMA]),
|
||||
vol.Required(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA])
|
||||
vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA]),
|
||||
vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCHES_SCHEMA])
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -165,6 +176,10 @@ async def async_setup(hass, config):
|
||||
async_load_platform(hass, 'light', DOMAIN,
|
||||
config[DOMAIN][CONF_LIGHTS], config))
|
||||
|
||||
hass.async_create_task(
|
||||
async_load_platform(hass, 'switch', DOMAIN,
|
||||
config[DOMAIN][CONF_SWITCHES], config))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -180,11 +195,11 @@ class LcnDevice(Entity):
|
||||
self._name = config[CONF_NAME]
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
def should_poll(self):
|
||||
"""Lcn device entity pushes its state to HA."""
|
||||
return False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added to hass."""
|
||||
self.address_connection.register_for_inputs(
|
||||
self.input_received)
|
||||
|
||||
@@ -3,6 +3,7 @@ import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PORT
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
|
||||
@@ -15,6 +16,7 @@ CONF_BROADCAST = 'broadcast'
|
||||
|
||||
INTERFACE_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_SERVER): cv.string,
|
||||
vol.Optional(CONF_PORT): cv.port,
|
||||
vol.Optional(CONF_BROADCAST): cv.string,
|
||||
})
|
||||
|
||||
|
||||
177
homeassistant/components/light/everlights.py
Normal file
177
homeassistant/components/light/everlights.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
Support for EverLights lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.everlights/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Tuple
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_HOSTS
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_COLOR,
|
||||
Light, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
|
||||
REQUIREMENTS = ['pyeverlights==0.1.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_EVERLIGHTS = (SUPPORT_EFFECT | SUPPORT_BRIGHTNESS | SUPPORT_COLOR)
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
NAME_FORMAT = "EverLights {} Zone {}"
|
||||
|
||||
|
||||
def color_rgb_to_int(red: int, green: int, blue: int) -> int:
|
||||
"""Return a RGB color as an integer."""
|
||||
return red*256*256+green*256+blue
|
||||
|
||||
|
||||
def color_int_to_rgb(value: int) -> Tuple[int, int, int]:
|
||||
"""Return an RGB tuple from an integer."""
|
||||
return (value >> 16, (value >> 8) & 0xff, value & 0xff)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the EverLights lights from configuration.yaml."""
|
||||
import pyeverlights
|
||||
lights = []
|
||||
|
||||
for ipaddr in config[CONF_HOSTS]:
|
||||
api = pyeverlights.EverLights(ipaddr,
|
||||
async_get_clientsession(hass))
|
||||
|
||||
try:
|
||||
status = await api.get_status()
|
||||
|
||||
effects = await api.get_all_patterns()
|
||||
|
||||
except pyeverlights.ConnectionError:
|
||||
raise PlatformNotReady
|
||||
|
||||
else:
|
||||
lights.append(EverLightsLight(api, pyeverlights.ZONE_1,
|
||||
status, effects))
|
||||
lights.append(EverLightsLight(api, pyeverlights.ZONE_2,
|
||||
status, effects))
|
||||
|
||||
async_add_entities(lights)
|
||||
|
||||
|
||||
class EverLightsLight(Light):
|
||||
"""Representation of a Flux light."""
|
||||
|
||||
def __init__(self, api, channel, status, effects):
|
||||
"""Initialize the light."""
|
||||
self._api = api
|
||||
self._channel = channel
|
||||
self._status = status
|
||||
self._effects = effects
|
||||
self._mac = status['mac']
|
||||
self._error_reported = False
|
||||
self._hs_color = [255, 255]
|
||||
self._brightness = 255
|
||||
self._effect = None
|
||||
self._available = True
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
return '{}-{}'.format(self._mac, self._channel)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return NAME_FORMAT.format(self._mac, self._channel)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._status['ch{}Active'.format(self._channel)] == 1
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
"""Return the color property."""
|
||||
return self._hs_color
|
||||
|
||||
@property
|
||||
def effect(self):
|
||||
"""Return the effect property."""
|
||||
return self._effect
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_EVERLIGHTS
|
||||
|
||||
@property
|
||||
def effect_list(self):
|
||||
"""Return the list of supported effects."""
|
||||
return self._effects
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
hs_color = kwargs.get(ATTR_HS_COLOR, self._hs_color)
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness)
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
|
||||
if effect is not None:
|
||||
colors = await self._api.set_pattern_by_id(self._channel, effect)
|
||||
|
||||
rgb = color_int_to_rgb(colors[0])
|
||||
hsv = color_util.color_RGB_to_hsv(*rgb)
|
||||
hs_color = hsv[:2]
|
||||
brightness = hsv[2] / 100 * 255
|
||||
|
||||
else:
|
||||
rgb = color_util.color_hsv_to_RGB(*hs_color, brightness/255*100)
|
||||
colors = [color_rgb_to_int(*rgb)]
|
||||
|
||||
await self._api.set_pattern(self._channel, colors)
|
||||
|
||||
self._hs_color = hs_color
|
||||
self._brightness = brightness
|
||||
self._effect = effect
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
await self._api.clear_pattern(self._channel)
|
||||
|
||||
async def async_update(self):
|
||||
"""Synchronize state with control box."""
|
||||
import pyeverlights
|
||||
|
||||
try:
|
||||
self._status = await self._api.get_status()
|
||||
except pyeverlights.ConnectionError:
|
||||
if self._available:
|
||||
_LOGGER.warning("EverLights control box connection lost.")
|
||||
self._available = False
|
||||
else:
|
||||
if not self._available:
|
||||
_LOGGER.warning("EverLights control box connection restored.")
|
||||
self._available = True
|
||||
@@ -177,11 +177,6 @@ class Hyperion(Light):
|
||||
def turn_off(self, **kwargs):
|
||||
"""Disconnect all remotes."""
|
||||
self.json_request({'command': 'clearall'})
|
||||
self.json_request({
|
||||
'command': 'color',
|
||||
'priority': self._priority,
|
||||
'color': [0, 0, 0]
|
||||
})
|
||||
|
||||
def update(self):
|
||||
"""Get the lights status."""
|
||||
|
||||
@@ -7,7 +7,7 @@ https://home-assistant.io/components/light.lcn/
|
||||
|
||||
from homeassistant.components.lcn import (
|
||||
CONF_CONNECTIONS, CONF_DIMMABLE, CONF_OUTPUT, CONF_TRANSITION, DATA_LCN,
|
||||
LcnDevice, get_connection)
|
||||
OUTPUT_PORTS, LcnDevice, get_connection)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION,
|
||||
Light)
|
||||
@@ -19,6 +19,9 @@ DEPENDENCIES = ['lcn']
|
||||
async def async_setup_platform(hass, hass_config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the LCN light platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
import pypck
|
||||
|
||||
devices = []
|
||||
@@ -29,7 +32,13 @@ async def async_setup_platform(hass, hass_config, async_add_entities,
|
||||
connection = get_connection(connections, connection_id)
|
||||
address_connection = connection.get_address_conn(addr)
|
||||
|
||||
devices.append(LcnOutputLight(config, address_connection))
|
||||
if config[CONF_OUTPUT] in OUTPUT_PORTS:
|
||||
device = LcnOutputLight(config, address_connection)
|
||||
else: # in RELAY_PORTS
|
||||
device = LcnRelayLight(config, address_connection)
|
||||
|
||||
devices.append(device)
|
||||
|
||||
async_add_entities(devices)
|
||||
|
||||
|
||||
@@ -50,7 +59,7 @@ class LcnOutputLight(LcnDevice, Light):
|
||||
self._is_on = None
|
||||
self._is_dimming_to_zero = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.hass.async_create_task(
|
||||
@@ -119,3 +128,55 @@ class LcnOutputLight(LcnDevice, Light):
|
||||
if not self._is_dimming_to_zero:
|
||||
self._is_on = self.brightness > 0
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
|
||||
class LcnRelayLight(LcnDevice, Light):
|
||||
"""Representation of a LCN light for relay ports."""
|
||||
|
||||
def __init__(self, config, address_connection):
|
||||
"""Initialize the LCN light."""
|
||||
super().__init__(config, address_connection)
|
||||
|
||||
self.output = self.pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]]
|
||||
|
||||
self._is_on = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.hass.async_create_task(
|
||||
self.address_connection.activate_status_request_handler(
|
||||
self.output))
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if entity is on."""
|
||||
return self._is_on
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the entity on."""
|
||||
self._is_on = True
|
||||
|
||||
states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8
|
||||
states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.ON
|
||||
self.address_connection.control_relays(states)
|
||||
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the entity off."""
|
||||
self._is_on = False
|
||||
|
||||
states = [self.pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8
|
||||
states[self.output.value] = self.pypck.lcn_defs.RelayStateModifier.OFF
|
||||
self.address_connection.control_relays(states)
|
||||
|
||||
await self.async_update_ha_state()
|
||||
|
||||
def input_received(self, input_obj):
|
||||
"""Set light state when LCN input object (command) is received."""
|
||||
if not isinstance(input_obj, self.pypck.inputs.ModStatusRelays):
|
||||
return
|
||||
|
||||
self._is_on = input_obj.get_state(self.output.value)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@@ -22,7 +22,8 @@ from homeassistant.components.light import (
|
||||
SUPPORT_TRANSITION, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, Light,
|
||||
preprocess_turn_on_alternatives)
|
||||
from homeassistant.components.lifx import (
|
||||
DOMAIN as LIFX_DOMAIN, DATA_LIFX_MANAGER, CONF_SERVER, CONF_BROADCAST)
|
||||
DOMAIN as LIFX_DOMAIN, DATA_LIFX_MANAGER, CONF_SERVER, CONF_PORT,
|
||||
CONF_BROADCAST)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -230,6 +231,9 @@ class LIFXManager:
|
||||
listen_ip = interface.get(CONF_SERVER)
|
||||
if listen_ip:
|
||||
kwargs['listen_ip'] = listen_ip
|
||||
listen_port = interface.get(CONF_PORT)
|
||||
if listen_port:
|
||||
kwargs['listen_port'] = listen_port
|
||||
lifx_discovery.start(**kwargs)
|
||||
|
||||
self.discoveries.append(lifx_discovery)
|
||||
@@ -710,3 +714,7 @@ class LIFXStrip(LIFXColor):
|
||||
if resp:
|
||||
zone += 8
|
||||
top = resp.count
|
||||
|
||||
# We only await multizone responses so don't ask for just one
|
||||
if zone == top-1:
|
||||
zone -= 1
|
||||
|
||||
@@ -192,3 +192,16 @@ yeelight_set_mode:
|
||||
mode:
|
||||
description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'.
|
||||
example: 'moonlight'
|
||||
|
||||
yeelight_start_flow:
|
||||
description: Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the light entity.
|
||||
example: 'light.yeelight'
|
||||
count:
|
||||
description: The number of times to run this flow (0 to run forever).
|
||||
example: 0
|
||||
transitions:
|
||||
description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html
|
||||
example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]'
|
||||
|
||||
@@ -39,15 +39,48 @@ CONF_MODEL = 'model'
|
||||
CONF_TRANSITION = 'transition'
|
||||
CONF_SAVE_ON_CHANGE = 'save_on_change'
|
||||
CONF_MODE_MUSIC = 'use_music_mode'
|
||||
CONF_CUSTOM_EFFECTS = 'custom_effects'
|
||||
CONF_FLOW_PARAMS = 'flow_params'
|
||||
|
||||
DATA_KEY = 'light.yeelight'
|
||||
|
||||
ATTR_MODE = 'mode'
|
||||
ATTR_COUNT = 'count'
|
||||
ATTR_TRANSITIONS = 'transitions'
|
||||
|
||||
YEELIGHT_RGB_TRANSITION = 'RGBTransition'
|
||||
YEELIGHT_HSV_TRANSACTION = 'HSVTransition'
|
||||
YEELIGHT_TEMPERATURE_TRANSACTION = 'TemperatureTransition'
|
||||
YEELIGHT_SLEEP_TRANSACTION = 'SleepTransition'
|
||||
|
||||
YEELIGHT_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
YEELIGHT_FLOW_TRANSITION_SCHEMA = {
|
||||
vol.Optional(ATTR_COUNT, default=0): cv.positive_int,
|
||||
vol.Required(ATTR_TRANSITIONS): [{
|
||||
vol.Exclusive(YEELIGHT_RGB_TRANSITION, CONF_TRANSITION):
|
||||
vol.All(cv.ensure_list, [cv.positive_int]),
|
||||
vol.Exclusive(YEELIGHT_HSV_TRANSACTION, CONF_TRANSITION):
|
||||
vol.All(cv.ensure_list, [cv.positive_int]),
|
||||
vol.Exclusive(YEELIGHT_TEMPERATURE_TRANSACTION, CONF_TRANSITION):
|
||||
vol.All(cv.ensure_list, [cv.positive_int]),
|
||||
vol.Exclusive(YEELIGHT_SLEEP_TRANSACTION, CONF_TRANSITION):
|
||||
vol.All(cv.ensure_list, [cv.positive_int]),
|
||||
}]
|
||||
}
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int,
|
||||
vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_MODEL): cv.string,
|
||||
vol.Optional(CONF_CUSTOM_EFFECTS): [{
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_FLOW_PARAMS): YEELIGHT_FLOW_TRANSITION_SCHEMA
|
||||
}]
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
@@ -103,11 +136,7 @@ YEELIGHT_EFFECT_LIST = [
|
||||
EFFECT_STOP]
|
||||
|
||||
SERVICE_SET_MODE = 'yeelight_set_mode'
|
||||
ATTR_MODE = 'mode'
|
||||
|
||||
YEELIGHT_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
SERVICE_START_FLOW = 'yeelight_start_flow'
|
||||
|
||||
|
||||
def _cmd(func):
|
||||
@@ -123,6 +152,19 @@ def _cmd(func):
|
||||
return _wrap
|
||||
|
||||
|
||||
def _parse_custom_effects(effects_config):
|
||||
effects = {}
|
||||
for config in effects_config:
|
||||
params = config[CONF_FLOW_PARAMS]
|
||||
transitions = YeelightLight.transitions_config_parser(
|
||||
params[ATTR_TRANSITIONS])
|
||||
|
||||
effects[config[CONF_NAME]] = \
|
||||
{ATTR_COUNT: params[ATTR_COUNT], ATTR_TRANSITIONS: transitions}
|
||||
|
||||
return effects
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Yeelight bulbs."""
|
||||
from yeelight.enums import PowerMode
|
||||
@@ -152,7 +194,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
_LOGGER.debug("Adding configured %s", name)
|
||||
|
||||
device = {'name': name, 'ipaddr': ipaddr}
|
||||
light = YeelightLight(device, device_config)
|
||||
|
||||
if CONF_CUSTOM_EFFECTS in config:
|
||||
custom_effects = \
|
||||
_parse_custom_effects(config[CONF_CUSTOM_EFFECTS])
|
||||
else:
|
||||
custom_effects = None
|
||||
|
||||
light = YeelightLight(device, device_config,
|
||||
custom_effects=custom_effects)
|
||||
lights.append(light)
|
||||
hass.data[DATA_KEY][name] = light
|
||||
|
||||
@@ -163,15 +213,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
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()
|
||||
target_devices = [dev for dev in hass.data[DATA_KEY].values()
|
||||
if dev.entity_id in entity_ids]
|
||||
|
||||
for target_device in target_devices:
|
||||
if service.service == SERVICE_SET_MODE:
|
||||
target_device.set_mode(**params)
|
||||
elif service.service == SERVICE_START_FLOW:
|
||||
target_device.start_flow(**params)
|
||||
|
||||
service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend({
|
||||
vol.Required(ATTR_MODE):
|
||||
@@ -181,11 +230,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
DOMAIN, SERVICE_SET_MODE, service_handler,
|
||||
schema=service_schema_set_mode)
|
||||
|
||||
service_schema_start_flow = YEELIGHT_SERVICE_SCHEMA.extend(
|
||||
YEELIGHT_FLOW_TRANSITION_SCHEMA
|
||||
)
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_START_FLOW, service_handler,
|
||||
schema=service_schema_start_flow)
|
||||
|
||||
|
||||
class YeelightLight(Light):
|
||||
"""Representation of a Yeelight light."""
|
||||
|
||||
def __init__(self, device, config):
|
||||
def __init__(self, device, config, custom_effects=None):
|
||||
"""Initialize the Yeelight light."""
|
||||
self.config = config
|
||||
self._name = device['name']
|
||||
@@ -204,6 +260,11 @@ class YeelightLight(Light):
|
||||
self._min_mireds = None
|
||||
self._max_mireds = None
|
||||
|
||||
if custom_effects:
|
||||
self._custom_effects = custom_effects
|
||||
else:
|
||||
self._custom_effects = {}
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if bulb is available."""
|
||||
@@ -217,7 +278,7 @@ class YeelightLight(Light):
|
||||
@property
|
||||
def effect_list(self):
|
||||
"""Return the list of supported effects."""
|
||||
return YEELIGHT_EFFECT_LIST
|
||||
return YEELIGHT_EFFECT_LIST + self.custom_effects_names
|
||||
|
||||
@property
|
||||
def color_temp(self) -> int:
|
||||
@@ -249,6 +310,16 @@ class YeelightLight(Light):
|
||||
"""Return maximum supported color temperature."""
|
||||
return self._max_mireds
|
||||
|
||||
@property
|
||||
def custom_effects(self):
|
||||
"""Return dict with custom effects."""
|
||||
return self._custom_effects
|
||||
|
||||
@property
|
||||
def custom_effects_names(self):
|
||||
"""Return list with custom effects names."""
|
||||
return list(self.custom_effects.keys())
|
||||
|
||||
def _get_hs_from_properties(self):
|
||||
rgb = self._properties.get('rgb', None)
|
||||
color_mode = self._properties.get('color_mode', None)
|
||||
@@ -435,15 +506,17 @@ class YeelightLight(Light):
|
||||
EFFECT_SLOWDOWN: slowdown,
|
||||
}
|
||||
|
||||
if effect in effects_map:
|
||||
if effect in self.custom_effects_names:
|
||||
flow = Flow(**self.custom_effects[effect])
|
||||
elif effect in effects_map:
|
||||
flow = Flow(count=0, transitions=effects_map[effect]())
|
||||
if effect == EFFECT_FAST_RANDOM_LOOP:
|
||||
elif effect == EFFECT_FAST_RANDOM_LOOP:
|
||||
flow = Flow(count=0, transitions=randomloop(duration=250))
|
||||
if effect == EFFECT_WHATSAPP:
|
||||
elif effect == EFFECT_WHATSAPP:
|
||||
flow = Flow(count=2, transitions=pulse(37, 211, 102))
|
||||
if effect == EFFECT_FACEBOOK:
|
||||
elif effect == EFFECT_FACEBOOK:
|
||||
flow = Flow(count=2, transitions=pulse(59, 89, 152))
|
||||
if effect == EFFECT_TWITTER:
|
||||
elif effect == EFFECT_TWITTER:
|
||||
flow = Flow(count=2, transitions=pulse(0, 172, 237))
|
||||
|
||||
try:
|
||||
@@ -518,3 +591,28 @@ class YeelightLight(Light):
|
||||
self.async_schedule_update_ha_state(True)
|
||||
except yeelight.BulbException as ex:
|
||||
_LOGGER.error("Unable to set the power mode: %s", ex)
|
||||
|
||||
@staticmethod
|
||||
def transitions_config_parser(transitions):
|
||||
"""Parse transitions config into initialized objects."""
|
||||
import yeelight
|
||||
|
||||
transition_objects = []
|
||||
for transition_config in transitions:
|
||||
transition, params = list(transition_config.items())[0]
|
||||
transition_objects.append(getattr(yeelight, transition)(*params))
|
||||
|
||||
return transition_objects
|
||||
|
||||
def start_flow(self, transitions, count=0):
|
||||
"""Start flow."""
|
||||
import yeelight
|
||||
|
||||
try:
|
||||
flow = yeelight.Flow(
|
||||
count=count,
|
||||
transitions=self.transitions_config_parser(transitions))
|
||||
|
||||
self._bulb.start_flow(flow)
|
||||
except yeelight.BulbException as ex:
|
||||
_LOGGER.error("Unable to set effect: %s", ex)
|
||||
|
||||
@@ -12,11 +12,10 @@ from aiohttp import web
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import \
|
||||
DOMAIN as DEVICE_TRACKER_DOMAIN
|
||||
DOMAIN as DEVICE_TRACKER
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, ATTR_LATITUDE, \
|
||||
ATTR_LONGITUDE, STATE_NOT_HOME, CONF_WEBHOOK_ID, ATTR_ID, HTTP_OK
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -57,9 +56,6 @@ WEBHOOK_SCHEMA = vol.All(
|
||||
|
||||
async def async_setup(hass, hass_config):
|
||||
"""Set up the Locative component."""
|
||||
hass.async_create_task(
|
||||
async_load_platform(hass, 'device_tracker', DOMAIN, {}, hass_config)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -93,7 +89,7 @@ async def handle_webhook(hass, webhook_id, request):
|
||||
|
||||
if direction == 'exit':
|
||||
current_state = hass.states.get(
|
||||
'{}.{}'.format(DEVICE_TRACKER_DOMAIN, device))
|
||||
'{}.{}'.format(DEVICE_TRACKER, device))
|
||||
|
||||
if current_state is None or current_state.state == location_name:
|
||||
location_name = STATE_NOT_HOME
|
||||
@@ -140,12 +136,18 @@ async def async_setup_entry(hass, entry):
|
||||
"""Configure based on config entry."""
|
||||
hass.components.webhook.async_register(
|
||||
DOMAIN, 'Locative', entry.data[CONF_WEBHOOK_ID], handle_webhook)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
|
||||
|
||||
await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER)
|
||||
return True
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
|
||||
@@ -6,6 +6,9 @@ https://home-assistant.io/components/device_tracker.locative/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.device_tracker import \
|
||||
DOMAIN as DEVICE_TRACKER_DOMAIN
|
||||
from homeassistant.components.locative import DOMAIN as LOCATIVE_DOMAIN
|
||||
from homeassistant.components.locative import TRACKER_UPDATE
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
@@ -13,9 +16,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['locative']
|
||||
|
||||
DATA_KEY = '{}.{}'.format(LOCATIVE_DOMAIN, DEVICE_TRACKER_DOMAIN)
|
||||
|
||||
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Set up an endpoint for the Locative device tracker."""
|
||||
|
||||
async def async_setup_entry(hass, entry, async_see):
|
||||
"""Configure a dispatcher connection based on a config entry."""
|
||||
async def _set_location(device, gps_location, location_name):
|
||||
"""Fire HA event to set location."""
|
||||
await async_see(
|
||||
@@ -24,5 +29,13 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
location_name=location_name
|
||||
)
|
||||
|
||||
async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location)
|
||||
hass.data[DATA_KEY] = async_dispatcher_connect(
|
||||
hass, TRACKER_UPDATE, _set_location
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload the config entry and remove the dispatcher connection."""
|
||||
hass.data[DATA_KEY]()
|
||||
return True
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED,
|
||||
STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN)
|
||||
SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN)
|
||||
from homeassistant.components import group
|
||||
|
||||
ATTR_CHANGED_BY = 'changed_by'
|
||||
@@ -150,5 +150,5 @@ class LockDevice(Entity):
|
||||
"""Return the state."""
|
||||
locked = self.is_locked
|
||||
if locked is None:
|
||||
return STATE_UNKNOWN
|
||||
return None
|
||||
return STATE_LOCKED if locked else STATE_UNLOCKED
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.components.verisure import (
|
||||
CONF_LOCKS, CONF_DEFAULT_LOCK_CODE, CONF_CODE_DIGITS)
|
||||
from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED)
|
||||
ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,7 +36,7 @@ class VerisureDoorlock(LockDevice):
|
||||
def __init__(self, device_label):
|
||||
"""Initialize the Verisure lock."""
|
||||
self._device_label = device_label
|
||||
self._state = STATE_UNKNOWN
|
||||
self._state = None
|
||||
self._digits = hub.config.get(CONF_CODE_DIGITS)
|
||||
self._changed_by = None
|
||||
self._change_timestamp = 0
|
||||
@@ -80,7 +80,7 @@ class VerisureDoorlock(LockDevice):
|
||||
"$.doorLockStatusList[?(@.deviceLabel=='%s')].lockedState",
|
||||
self._device_label)
|
||||
if status == 'UNLOCKED':
|
||||
self._state = STATE_UNLOCKED
|
||||
self._state = None
|
||||
elif status == 'LOCKED':
|
||||
self._state = STATE_LOCKED
|
||||
elif status != 'PENDING':
|
||||
@@ -96,7 +96,7 @@ class VerisureDoorlock(LockDevice):
|
||||
|
||||
def unlock(self, **kwargs):
|
||||
"""Send unlock command."""
|
||||
if self._state == STATE_UNLOCKED:
|
||||
if self._state is None:
|
||||
return
|
||||
|
||||
code = kwargs.get(ATTR_CODE, self._default_lock_code)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user