mirror of
https://github.com/home-assistant/core.git
synced 2026-01-02 12:12:00 +01:00
Compare commits
306 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94b719e150 | ||
|
|
69d3a3dd32 | ||
|
|
4904653b70 | ||
|
|
dddf4d1460 | ||
|
|
9a6c9cff30 | ||
|
|
d3b62e1fe1 | ||
|
|
f63a79ee8f | ||
|
|
898ba56d9f | ||
|
|
64a5bff5b2 | ||
|
|
af7de8d5ae | ||
|
|
754d98bcd5 | ||
|
|
4874030b70 | ||
|
|
84c89686a9 | ||
|
|
48fd8f1f63 | ||
|
|
83a108b20a | ||
|
|
b0a800cc6d | ||
|
|
1f5f4e7a89 | ||
|
|
b1fbada02d | ||
|
|
08909ed420 | ||
|
|
ec8969351d | ||
|
|
801a69be3a | ||
|
|
51e20c92f9 | ||
|
|
443553ff16 | ||
|
|
2e6a48ff5f | ||
|
|
49cfe38cca | ||
|
|
8a042586f1 | ||
|
|
08f8e540e3 | ||
|
|
f09b888a8a | ||
|
|
279f82acc4 | ||
|
|
de6c5a503b | ||
|
|
898f89ffc7 | ||
|
|
5c807c6bd9 | ||
|
|
dd84b4e237 | ||
|
|
6dfae7a259 | ||
|
|
c6c8cd4f51 | ||
|
|
bde7176b3c | ||
|
|
4c03d670c1 | ||
|
|
406afbb369 | ||
|
|
9c6609cb79 | ||
|
|
e5504b39ec | ||
|
|
b1ef5042f9 | ||
|
|
b35fa4f1c1 | ||
|
|
71da9d2f50 | ||
|
|
86388f5af2 | ||
|
|
17f0fb69bd | ||
|
|
2d02baf3d0 | ||
|
|
66473120ab | ||
|
|
6ddbb4d568 | ||
|
|
154c69a454 | ||
|
|
ad4ec49f9c | ||
|
|
e8367f245a | ||
|
|
4bc37bd661 | ||
|
|
b4841a17a6 | ||
|
|
3b9d5cdf73 | ||
|
|
77d568dc47 | ||
|
|
9db1ff8cd4 | ||
|
|
248a90b71d | ||
|
|
d8c4af9c81 | ||
|
|
1e6c660f59 | ||
|
|
44a508e86c | ||
|
|
92c6cee2a1 | ||
|
|
d4bc8e23af | ||
|
|
f0db698f75 | ||
|
|
cf57db919e | ||
|
|
84b12ab007 | ||
|
|
601193b1d2 | ||
|
|
038b1c1fc6 | ||
|
|
0d734303a4 | ||
|
|
ff4cb23f2a | ||
|
|
e94b4ec006 | ||
|
|
be91207830 | ||
|
|
ecf285105c | ||
|
|
767f3d58ff | ||
|
|
34097cda24 | ||
|
|
0ce3703e30 | ||
|
|
464e843186 | ||
|
|
5d2b7a6e0b | ||
|
|
914a868fbd | ||
|
|
03e0c7c71c | ||
|
|
32ffd006fa | ||
|
|
58b85b2e0e | ||
|
|
61653a517d | ||
|
|
2a7bc0e55c | ||
|
|
95b439fbd5 | ||
|
|
1872481f47 | ||
|
|
44b6d23e0f | ||
|
|
58eb0ec52a | ||
|
|
febe16d700 | ||
|
|
8c56091af7 | ||
|
|
eacdce9ed9 | ||
|
|
42c99b0ccb | ||
|
|
2a6c0cfc17 | ||
|
|
84040892df | ||
|
|
345008c673 | ||
|
|
14d1494cd2 | ||
|
|
f1d11e77ed | ||
|
|
b1b8715f7d | ||
|
|
b6d559da1f | ||
|
|
475c412ae4 | ||
|
|
c04a002c55 | ||
|
|
5013a82655 | ||
|
|
05181bf232 | ||
|
|
c22a73e1d0 | ||
|
|
c9b353f7a7 | ||
|
|
64cfc4ff02 | ||
|
|
bb46009efa | ||
|
|
3f9250415f | ||
|
|
c294a534d0 | ||
|
|
85d6970df8 | ||
|
|
260a619a40 | ||
|
|
0c6ef3b7f9 | ||
|
|
0c47434aad | ||
|
|
1d8a1df2c4 | ||
|
|
65b85ec6c0 | ||
|
|
b6b9da7e6e | ||
|
|
d18f2684fb | ||
|
|
e93b079ef4 | ||
|
|
356ad6e468 | ||
|
|
8f35212dd6 | ||
|
|
0827a26642 | ||
|
|
b4756e6dda | ||
|
|
4cc192e445 | ||
|
|
962e5315ab | ||
|
|
2c7e895105 | ||
|
|
00019b9ff0 | ||
|
|
8e776b4dc0 | ||
|
|
ce13b0989d | ||
|
|
c81735cc84 | ||
|
|
5d18759146 | ||
|
|
9cdcfae8f3 | ||
|
|
547d93f631 | ||
|
|
d841ddc50b | ||
|
|
40b5824230 | ||
|
|
86f3e2455d | ||
|
|
9a065cc536 | ||
|
|
8e4dbcaf21 | ||
|
|
6863d2e0af | ||
|
|
c23809488b | ||
|
|
248f5c0209 | ||
|
|
e5aa40fa5d | ||
|
|
1f573b46a4 | ||
|
|
0647bb7f6b | ||
|
|
a73fbbaf7a | ||
|
|
755f5b61b7 | ||
|
|
6869c7401e | ||
|
|
835577b2bc | ||
|
|
859d0d5ad6 | ||
|
|
aed797f438 | ||
|
|
eb8093934f | ||
|
|
608b482906 | ||
|
|
7207c2cca1 | ||
|
|
ed1d0b4197 | ||
|
|
63461e9007 | ||
|
|
40a2145558 | ||
|
|
d883b18751 | ||
|
|
b8e462cf5b | ||
|
|
11df7becd3 | ||
|
|
99f5db8c02 | ||
|
|
19b08a975a | ||
|
|
123f4acfc1 | ||
|
|
0f90426023 | ||
|
|
8b6a94b0f5 | ||
|
|
0a333230c1 | ||
|
|
455e1df7cb | ||
|
|
f71396c293 | ||
|
|
d930c399fe | ||
|
|
f3748ce535 | ||
|
|
8beefcfc69 | ||
|
|
93747f2766 | ||
|
|
7af438fa2f | ||
|
|
2b5fcd737b | ||
|
|
2b320f23fc | ||
|
|
679d500e61 | ||
|
|
613615433a | ||
|
|
f70ff66d11 | ||
|
|
d2bbc6ef70 | ||
|
|
37e28428c1 | ||
|
|
c56f99baaf | ||
|
|
265232af98 | ||
|
|
e6c4113c5b | ||
|
|
c86e1b31b3 | ||
|
|
5912316496 | ||
|
|
58f0655298 | ||
|
|
43a93fb345 | ||
|
|
36b338051b | ||
|
|
fc566309c1 | ||
|
|
23ce9949b1 | ||
|
|
275c80183c | ||
|
|
cd1655f43b | ||
|
|
1a117d0bea | ||
|
|
944bb8474f | ||
|
|
779f520c56 | ||
|
|
82ed7b6b08 | ||
|
|
af77341494 | ||
|
|
23fb8c4cdd | ||
|
|
726bc5b670 | ||
|
|
b615b3349f | ||
|
|
0f59bb208c | ||
|
|
38d201a54a | ||
|
|
c8bc1e3c5d | ||
|
|
a862bc4edc | ||
|
|
b0e3d5a576 | ||
|
|
f006b00dc1 | ||
|
|
1fff6ce438 | ||
|
|
c06c82905a | ||
|
|
2b86d89bb4 | ||
|
|
7bdb79bd54 | ||
|
|
41aaeb715a | ||
|
|
5d8a465c18 | ||
|
|
c6f5a5443f | ||
|
|
d6cb102f63 | ||
|
|
edde76e544 | ||
|
|
d5fff2f94a | ||
|
|
0e0ba28249 | ||
|
|
6745e83a6c | ||
|
|
44bc057fdb | ||
|
|
96b8d8fcfa | ||
|
|
fc2df34206 | ||
|
|
09c29737de | ||
|
|
4c01b47945 | ||
|
|
7aaf3a46db | ||
|
|
d774ba46c7 | ||
|
|
4c37ee8884 | ||
|
|
7f5f458074 | ||
|
|
479457d6ec | ||
|
|
7e73d27dd1 | ||
|
|
e7ffec87ac | ||
|
|
2d47b187c5 | ||
|
|
fe2103dedb | ||
|
|
7bf5d1c662 | ||
|
|
cb24282040 | ||
|
|
bd9429d3af | ||
|
|
d7a005ad0f | ||
|
|
2e2a996a8e | ||
|
|
0364498dee | ||
|
|
c5fdd4392a | ||
|
|
895454b6c3 | ||
|
|
2109b7a1b9 | ||
|
|
71a305ea45 | ||
|
|
e73634e6c7 | ||
|
|
3d47ad5018 | ||
|
|
c823ea9f2a | ||
|
|
75bcb1ff0f | ||
|
|
1663cc9084 | ||
|
|
17cfcc981d | ||
|
|
60fabaec24 | ||
|
|
5e44934e7e | ||
|
|
01a6c1c1c8 | ||
|
|
cd1b0ac67d | ||
|
|
2bfded7153 | ||
|
|
20af5cb5b4 | ||
|
|
749fc583ea | ||
|
|
b07d887d77 | ||
|
|
9bb94a4512 | ||
|
|
e76d553513 | ||
|
|
844799a1f7 | ||
|
|
e005ebe989 | ||
|
|
e9d19c1dcc | ||
|
|
7d2ab4fce6 | ||
|
|
ba2ea35089 | ||
|
|
ade62faa38 | ||
|
|
ee322dbbdc | ||
|
|
0d4141bf13 | ||
|
|
d404ac8978 | ||
|
|
71da21dcc8 | ||
|
|
04dbc992ec | ||
|
|
6d0e08cf7d | ||
|
|
1e0025acae | ||
|
|
8fc853ba11 | ||
|
|
8cbb8f6527 | ||
|
|
4f86c9ecda | ||
|
|
9561fed650 | ||
|
|
67b599475e | ||
|
|
114ece1848 | ||
|
|
c05815cced | ||
|
|
2e0c185740 | ||
|
|
231ef40f53 | ||
|
|
b4159c7dc9 | ||
|
|
8cc5fc1369 | ||
|
|
67336a111b | ||
|
|
7d28d9d6b4 | ||
|
|
618a86a37c | ||
|
|
880ef8af48 | ||
|
|
95124c7ddb | ||
|
|
0aba227300 | ||
|
|
734bd75fd3 | ||
|
|
0c5e077091 | ||
|
|
1ed2f8ae91 | ||
|
|
a343c20404 | ||
|
|
d4e8b831a0 | ||
|
|
98f41d6b84 | ||
|
|
7774a03a55 | ||
|
|
c35e5c9997 | ||
|
|
5d862e426e | ||
|
|
bab8d574fe | ||
|
|
c980d26aae | ||
|
|
1ad14b8227 | ||
|
|
382ac5c3b5 | ||
|
|
ad8645baf4 | ||
|
|
62785c2431 | ||
|
|
22c3d014aa | ||
|
|
3f3127a290 | ||
|
|
88fc64c8a0 | ||
|
|
1463fc4fe0 | ||
|
|
ece58ce78f | ||
|
|
b67f1fed52 |
25
.coveragerc
25
.coveragerc
@@ -28,6 +28,9 @@ omit =
|
||||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/google.py
|
||||
homeassistant/components/*/google.py
|
||||
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
|
||||
@@ -37,9 +40,6 @@ omit =
|
||||
homeassistant/components/isy994.py
|
||||
homeassistant/components/*/isy994.py
|
||||
|
||||
homeassistant/components/litejet.py
|
||||
homeassistant/components/*/litejet.py
|
||||
|
||||
homeassistant/components/modbus.py
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
@@ -98,6 +98,9 @@ omit =
|
||||
homeassistant/components/netatmo.py
|
||||
homeassistant/components/*/netatmo.py
|
||||
|
||||
homeassistant/components/neato.py
|
||||
homeassistant/components/*/neato.py
|
||||
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
@@ -121,6 +124,7 @@ omit =
|
||||
homeassistant/components/binary_sensor/concord232.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/amcrest.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
@@ -132,7 +136,7 @@ omit =
|
||||
homeassistant/components/climate/knx.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/rpi_gpio.py
|
||||
homeassistant/components/cover/scsgate.py
|
||||
@@ -144,12 +148,15 @@ omit =
|
||||
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||
homeassistant/components/device_tracker/cisco_ios.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
homeassistant/components/device_tracker/nmap_tracker.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/device_tracker/swisscom.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
@@ -180,7 +187,9 @@ omit =
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/denonavr.py
|
||||
homeassistant/components/media_player/directv.py
|
||||
homeassistant/components/media_player/dunehd.py
|
||||
homeassistant/components/media_player/emby.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
@@ -232,6 +241,7 @@ omit =
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/openalpr.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
@@ -271,6 +281,7 @@ omit =
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nut.py
|
||||
homeassistant/components/sensor/nzbget.py
|
||||
homeassistant/components/sensor/ohmconnect.py
|
||||
homeassistant/components/sensor/onewire.py
|
||||
@@ -278,10 +289,12 @@ omit =
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/sonarr.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/steam_online.py
|
||||
homeassistant/components/sensor/supervisord.py
|
||||
@@ -297,16 +310,18 @@ omit =
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/anel_pwrctrl.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/hook.py
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/neato.py
|
||||
homeassistant/components/switch/netio.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/pilight.py
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -62,6 +62,7 @@ pip-log.txt
|
||||
.coverage
|
||||
.tox
|
||||
nosetests.xml
|
||||
htmlcov/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
2
.hound.yml
Normal file
2
.hound.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
python:
|
||||
enabled: true
|
||||
@@ -4,7 +4,7 @@ import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from collections import OrderedDict
|
||||
|
||||
from types import ModuleType
|
||||
from typing import Any, Optional, Dict
|
||||
@@ -57,7 +57,7 @@ def async_setup_component(hass: core.HomeAssistant, domain: str,
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
if config is None:
|
||||
config = defaultdict(dict)
|
||||
config = {}
|
||||
|
||||
components = loader.load_order_component(domain)
|
||||
|
||||
@@ -142,6 +142,7 @@ def _async_setup_component(hass: core.HomeAssistant,
|
||||
async_comp = hasattr(component, 'async_setup')
|
||||
|
||||
try:
|
||||
_LOGGER.info("Setting up %s", domain)
|
||||
if async_comp:
|
||||
result = yield from component.async_setup(hass, config)
|
||||
else:
|
||||
@@ -165,15 +166,6 @@ def _async_setup_component(hass: core.HomeAssistant,
|
||||
|
||||
hass.config.components.append(component.DOMAIN)
|
||||
|
||||
# Assumption: if a component does not depend on groups
|
||||
# it communicates with devices
|
||||
if (not async_comp and
|
||||
'group' not in getattr(component, 'DEPENDENCIES', [])):
|
||||
if hass.pool is None:
|
||||
hass.async_init_pool()
|
||||
if hass.pool.worker_count <= 10:
|
||||
hass.pool.add_worker()
|
||||
|
||||
hass.bus.async_fire(
|
||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
|
||||
)
|
||||
@@ -353,7 +345,7 @@ def from_config_dict(config: Dict[str, Any],
|
||||
|
||||
# run task
|
||||
future = asyncio.Future(loop=hass.loop)
|
||||
hass.loop.create_task(_async_init_from_config_dict(future))
|
||||
hass.async_add_job(_async_init_from_config_dict(future))
|
||||
hass.loop.run_until_complete(future)
|
||||
|
||||
return future.result()
|
||||
@@ -373,6 +365,13 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
Dynamically loads required components and its dependencies.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
hass.async_track_tasks()
|
||||
setup_lock = hass.data.get('setup_lock')
|
||||
if setup_lock is None:
|
||||
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
yield from setup_lock.acquire()
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
@@ -396,10 +395,12 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||
|
||||
# Make a copy because we are mutating it.
|
||||
# Convert it to defaultdict so components can always have config dict
|
||||
# Use OrderedDict in case original one was one.
|
||||
# Convert values to dictionaries if they are None
|
||||
config = defaultdict(
|
||||
dict, {key: value or {} for key, value in config.items()})
|
||||
new_config = OrderedDict()
|
||||
for key, value in config.items():
|
||||
new_config[key] = value or {}
|
||||
config = new_config
|
||||
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
components = set(key.split(' ')[0] for key in config.keys()
|
||||
@@ -425,6 +426,10 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
for domain in loader.load_order_components(components):
|
||||
yield from _async_setup_component(hass, domain, config)
|
||||
|
||||
setup_lock.release()
|
||||
|
||||
yield from hass.async_stop_track_tasks()
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ def async_setup(hass, config):
|
||||
tasks.append(hass.services.async_call(
|
||||
domain, service.service, data, blocking))
|
||||
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
||||
|
||||
@@ -4,20 +4,45 @@ Support for Envisalink-based alarm control panels (Honeywell/DSC).
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
from os import path
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.components.envisalink import (
|
||||
EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
||||
CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
|
||||
DEVICES = []
|
||||
|
||||
SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress'
|
||||
ATTR_KEYPRESS = 'keypress'
|
||||
ALARM_KEYPRESS_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_KEYPRESS): cv.string
|
||||
})
|
||||
|
||||
|
||||
def alarm_keypress_handler(service):
|
||||
"""Map services to methods on Alarm."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
keypress = service.data.get(ATTR_KEYPRESS)
|
||||
|
||||
_target_devices = [device for device in DEVICES
|
||||
if device.entity_id in entity_ids]
|
||||
|
||||
for device in _target_devices:
|
||||
EnvisalinkAlarm.alarm_keypress(device, keypress)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -35,8 +60,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_panic_type,
|
||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices([_device])
|
||||
DEVICES.append(_device)
|
||||
|
||||
add_devices(DEVICES)
|
||||
|
||||
# Register Envisalink specific services
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(alarm.DOMAIN, SERVICE_ALARM_KEYPRESS,
|
||||
alarm_keypress_handler,
|
||||
descriptions.get(SERVICE_ALARM_KEYPRESS),
|
||||
schema=ALARM_KEYPRESS_SCHEMA)
|
||||
return True
|
||||
|
||||
|
||||
@@ -66,42 +101,64 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""The characters if code is defined."""
|
||||
return self._code
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
else:
|
||||
return '^\\d{4,6}$'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
state = STATE_UNKNOWN
|
||||
|
||||
if self._info['status']['alarm']:
|
||||
return STATE_ALARM_TRIGGERED
|
||||
state = STATE_ALARM_TRIGGERED
|
||||
elif self._info['status']['armed_away']:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif self._info['status']['armed_stay']:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif self._info['status']['exit_delay']:
|
||||
state = STATE_ALARM_PENDING
|
||||
elif self._info['status']['entry_delay']:
|
||||
state = STATE_ALARM_PENDING
|
||||
elif self._info['status']['alpha']:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
state = STATE_ALARM_DISARMED
|
||||
return state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.disarm_partition(
|
||||
str(code), self._partition_number)
|
||||
if code:
|
||||
EVL_CONTROLLER.disarm_partition(str(code),
|
||||
self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.disarm_partition(str(self._code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_stay_partition(
|
||||
str(code), self._partition_number)
|
||||
if code:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(code),
|
||||
self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(self._code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_away_partition(
|
||||
str(code), self._partition_number)
|
||||
if code:
|
||||
EVL_CONTROLLER.arm_away_partition(str(code),
|
||||
self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.arm_away_partition(str(self._code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.panic_alarm(self._panic_type)
|
||||
EVL_CONTROLLER.panic_alarm(self._panic_type)
|
||||
|
||||
def alarm_keypress(self, keypress=None):
|
||||
"""Send custom keypress."""
|
||||
if keypress:
|
||||
EVL_CONTROLLER.keypresses_to_partition(self._partition_number,
|
||||
keypress)
|
||||
|
||||
@@ -129,7 +129,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
@@ -143,7 +143,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
@@ -155,11 +155,11 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
if self._trigger_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time + self._trigger_time)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
|
||||
@@ -117,11 +117,11 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._alarm.arm('home')
|
||||
self._alarm.arm('stay')
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('auto')
|
||||
self._alarm.arm('exit')
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command."""
|
||||
|
||||
@@ -41,3 +41,14 @@ alarm_trigger:
|
||||
code:
|
||||
description: An optional code to trigger the alarm control panel with
|
||||
example: 1234
|
||||
|
||||
envisalink_alarm_keypress:
|
||||
description: Send custom keypresses to the alarm
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel to trigger
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
keypress:
|
||||
description: 'String to send to the alarm panel (1-6 characters)'
|
||||
example: '*71'
|
||||
|
||||
@@ -118,7 +118,7 @@ class AlexaIntentsView(HomeAssistantView):
|
||||
|
||||
def __init__(self, hass, intents):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__(hass)
|
||||
super().__init__()
|
||||
|
||||
intents = copy.deepcopy(intents)
|
||||
template.attach(hass, intents)
|
||||
@@ -150,7 +150,7 @@ class AlexaIntentsView(HomeAssistantView):
|
||||
return None
|
||||
|
||||
intent = req.get('intent')
|
||||
response = AlexaResponse(self.hass, intent)
|
||||
response = AlexaResponse(request.app['hass'], intent)
|
||||
|
||||
if req_type == 'LaunchRequest':
|
||||
response.add_speech(
|
||||
@@ -282,7 +282,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
||||
|
||||
def __init__(self, hass, flash_briefings):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__(hass)
|
||||
super().__init__()
|
||||
self.flash_briefings = copy.deepcopy(flash_briefings)
|
||||
template.attach(hass, self.flash_briefings)
|
||||
|
||||
|
||||
@@ -77,8 +77,10 @@ class APIEventStream(HomeAssistantView):
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
# pylint: disable=no-self-use
|
||||
hass = request.app['hass']
|
||||
stop_obj = object()
|
||||
to_write = asyncio.Queue(loop=self.hass.loop)
|
||||
to_write = asyncio.Queue(loop=hass.loop)
|
||||
|
||||
restrict = request.GET.get('restrict')
|
||||
if restrict:
|
||||
@@ -106,7 +108,7 @@ class APIEventStream(HomeAssistantView):
|
||||
response.content_type = 'text/event-stream'
|
||||
yield from response.prepare(request)
|
||||
|
||||
unsub_stream = self.hass.bus.async_listen(MATCH_ALL, forward_events)
|
||||
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
|
||||
|
||||
try:
|
||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||
@@ -117,7 +119,7 @@ class APIEventStream(HomeAssistantView):
|
||||
while True:
|
||||
try:
|
||||
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
||||
loop=self.hass.loop):
|
||||
loop=hass.loop):
|
||||
payload = yield from to_write.get()
|
||||
|
||||
if payload is stop_obj:
|
||||
@@ -145,7 +147,7 @@ class APIConfigView(HomeAssistantView):
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current configuration."""
|
||||
return self.json(self.hass.config.as_dict())
|
||||
return self.json(request.app['hass'].config.as_dict())
|
||||
|
||||
|
||||
class APIDiscoveryView(HomeAssistantView):
|
||||
@@ -158,10 +160,11 @@ class APIDiscoveryView(HomeAssistantView):
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get discovery info."""
|
||||
needs_auth = self.hass.config.api.api_password is not None
|
||||
hass = request.app['hass']
|
||||
needs_auth = hass.config.api.api_password is not None
|
||||
return self.json({
|
||||
'base_url': self.hass.config.api.base_url,
|
||||
'location_name': self.hass.config.location_name,
|
||||
'base_url': hass.config.api.base_url,
|
||||
'location_name': hass.config.location_name,
|
||||
'requires_api_password': needs_auth,
|
||||
'version': __version__
|
||||
})
|
||||
@@ -176,7 +179,7 @@ class APIStatesView(HomeAssistantView):
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current states."""
|
||||
return self.json(self.hass.states.async_all())
|
||||
return self.json(request.app['hass'].states.async_all())
|
||||
|
||||
|
||||
class APIEntityStateView(HomeAssistantView):
|
||||
@@ -188,7 +191,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
@ha.callback
|
||||
def get(self, request, entity_id):
|
||||
"""Retrieve state of entity."""
|
||||
state = self.hass.states.get(entity_id)
|
||||
state = request.app['hass'].states.get(entity_id)
|
||||
if state:
|
||||
return self.json(state)
|
||||
else:
|
||||
@@ -197,6 +200,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
@asyncio.coroutine
|
||||
def post(self, request, entity_id):
|
||||
"""Update state of entity."""
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
@@ -211,15 +215,14 @@ class APIEntityStateView(HomeAssistantView):
|
||||
attributes = data.get('attributes')
|
||||
force_update = data.get('force_update', False)
|
||||
|
||||
is_new_state = self.hass.states.get(entity_id) is None
|
||||
is_new_state = hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
self.hass.states.async_set(entity_id, new_state, attributes,
|
||||
force_update)
|
||||
hass.states.async_set(entity_id, new_state, attributes, force_update)
|
||||
|
||||
# Read the state back for our response
|
||||
status_code = HTTP_CREATED if is_new_state else 200
|
||||
resp = self.json(self.hass.states.get(entity_id), status_code)
|
||||
resp = self.json(hass.states.get(entity_id), status_code)
|
||||
|
||||
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
|
||||
|
||||
@@ -228,7 +231,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
@ha.callback
|
||||
def delete(self, request, entity_id):
|
||||
"""Remove entity."""
|
||||
if self.hass.states.async_remove(entity_id):
|
||||
if request.app['hass'].states.async_remove(entity_id):
|
||||
return self.json_message('Entity removed')
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
@@ -243,7 +246,7 @@ class APIEventListenersView(HomeAssistantView):
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get event listeners."""
|
||||
return self.json(async_events_json(self.hass))
|
||||
return self.json(async_events_json(request.app['hass']))
|
||||
|
||||
|
||||
class APIEventView(HomeAssistantView):
|
||||
@@ -271,7 +274,8 @@ class APIEventView(HomeAssistantView):
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
self.hass.bus.async_fire(event_type, event_data, ha.EventOrigin.remote)
|
||||
request.app['hass'].bus.async_fire(event_type, event_data,
|
||||
ha.EventOrigin.remote)
|
||||
|
||||
return self.json_message("Event {} fired.".format(event_type))
|
||||
|
||||
@@ -285,7 +289,7 @@ class APIServicesView(HomeAssistantView):
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get registered services."""
|
||||
return self.json(async_services_json(self.hass))
|
||||
return self.json(async_services_json(request.app['hass']))
|
||||
|
||||
|
||||
class APIDomainServicesView(HomeAssistantView):
|
||||
@@ -300,12 +304,12 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
|
||||
Returns a list of changed states.
|
||||
"""
|
||||
hass = request.app['hass']
|
||||
body = yield from request.text()
|
||||
data = json.loads(body) if body else None
|
||||
|
||||
with AsyncTrackStates(self.hass) as changed_states:
|
||||
yield from self.hass.services.async_call(domain, service, data,
|
||||
True)
|
||||
with AsyncTrackStates(hass) as changed_states:
|
||||
yield from hass.services.async_call(domain, service, data, True)
|
||||
|
||||
return self.json(changed_states)
|
||||
|
||||
@@ -320,6 +324,7 @@ class APIEventForwardingView(HomeAssistantView):
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Setup an event forwarder."""
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
@@ -340,14 +345,14 @@ class APIEventForwardingView(HomeAssistantView):
|
||||
|
||||
api = rem.API(host, api_password, port)
|
||||
|
||||
valid = yield from self.hass.loop.run_in_executor(
|
||||
valid = yield from hass.loop.run_in_executor(
|
||||
None, api.validate_api)
|
||||
if not valid:
|
||||
return self.json_message("Unable to validate API.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if self.event_forwarder is None:
|
||||
self.event_forwarder = rem.EventForwarder(self.hass)
|
||||
self.event_forwarder = rem.EventForwarder(hass)
|
||||
|
||||
self.event_forwarder.async_connect(api)
|
||||
|
||||
@@ -389,7 +394,7 @@ class APIComponentsView(HomeAssistantView):
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current loaded components."""
|
||||
return self.json(self.hass.config.components)
|
||||
return self.json(request.app['hass'].config.components)
|
||||
|
||||
|
||||
class APIErrorLogView(HomeAssistantView):
|
||||
@@ -402,7 +407,7 @@ class APIErrorLogView(HomeAssistantView):
|
||||
def get(self, request):
|
||||
"""Serve error log."""
|
||||
resp = yield from self.file(
|
||||
request, self.hass.config.path(ERROR_LOG_FILENAME))
|
||||
request, request.app['hass'].config.path(ERROR_LOG_FILENAME))
|
||||
return resp
|
||||
|
||||
|
||||
@@ -417,7 +422,7 @@ class APITemplateView(HomeAssistantView):
|
||||
"""Render a template."""
|
||||
try:
|
||||
data = yield from request.json()
|
||||
tpl = template.Template(data['template'], self.hass)
|
||||
tpl = template.Template(data['template'], request.app['hass'])
|
||||
return tpl.async_render(data.get('variables'))
|
||||
except (ValueError, TemplateError) as ex:
|
||||
return self.json_message('Error rendering template: {}'.format(ex),
|
||||
|
||||
@@ -66,6 +66,7 @@ def _platform_validator(config):
|
||||
|
||||
return getattr(platform, 'TRIGGER_SCHEMA')(config)
|
||||
|
||||
|
||||
_TRIGGER_SCHEMA = vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
@@ -165,7 +166,7 @@ def async_setup(hass, config):
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(entity.async_trigger(
|
||||
service_call.data.get(ATTR_VARIABLES), True))
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def turn_onoff_service_handler(service_call):
|
||||
@@ -174,7 +175,7 @@ def async_setup(hass, config):
|
||||
method = 'async_{}'.format(service_call.service)
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(getattr(entity, method)())
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def toggle_service_handler(service_call):
|
||||
@@ -185,7 +186,7 @@ def async_setup(hass, config):
|
||||
tasks.append(entity.async_turn_off())
|
||||
else:
|
||||
tasks.append(entity.async_turn_on())
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def reload_service_handler(service_call):
|
||||
@@ -348,8 +349,10 @@ def _async_process_config(hass, config, component):
|
||||
tasks.append(entity.async_enable())
|
||||
entities.append(entity)
|
||||
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from component.async_add_entities(entities)
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
if entities:
|
||||
yield from component.async_add_entities(entities)
|
||||
|
||||
return len(entities) > 0
|
||||
|
||||
|
||||
@@ -11,22 +11,34 @@ import voluptuous as vol
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
|
||||
DEPENDENCIES = ['litejet']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NUMBER = 'number'
|
||||
CONF_HELD_MORE_THAN = 'held_more_than'
|
||||
CONF_HELD_LESS_THAN = 'held_less_than'
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'litejet',
|
||||
vol.Required(CONF_NUMBER): cv.positive_int
|
||||
vol.Required(CONF_NUMBER): cv.positive_int,
|
||||
vol.Optional(CONF_HELD_MORE_THAN):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_HELD_LESS_THAN):
|
||||
vol.All(cv.time_period, cv.positive_timedelta)
|
||||
})
|
||||
|
||||
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
number = config.get(CONF_NUMBER)
|
||||
held_more_than = config.get(CONF_HELD_MORE_THAN)
|
||||
held_less_than = config.get(CONF_HELD_LESS_THAN)
|
||||
pressed_time = None
|
||||
cancel_pressed_more_than = None
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
@@ -34,8 +46,53 @@ def async_trigger(hass, config, action):
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
CONF_PLATFORM: 'litejet',
|
||||
CONF_NUMBER: number
|
||||
CONF_NUMBER: number,
|
||||
CONF_HELD_MORE_THAN: held_more_than,
|
||||
CONF_HELD_LESS_THAN: held_less_than
|
||||
},
|
||||
})
|
||||
|
||||
hass.data['litejet_system'].on_switch_released(number, call_action)
|
||||
# held_more_than and held_less_than: trigger on released (if in time range)
|
||||
# held_more_than: trigger after pressed with calculation
|
||||
# held_less_than: trigger on released with calculation
|
||||
# neither: trigger on pressed
|
||||
|
||||
@callback
|
||||
def pressed_more_than_satisfied(now):
|
||||
"""Handle the LiteJet's switch's button pressed >= held_more_than."""
|
||||
call_action()
|
||||
|
||||
def pressed():
|
||||
"""Handle the press of the LiteJet switch's button."""
|
||||
nonlocal cancel_pressed_more_than, pressed_time
|
||||
nonlocal held_less_than, held_more_than
|
||||
pressed_time = dt_util.utcnow()
|
||||
if held_more_than is None and held_less_than is None:
|
||||
call_action()
|
||||
if held_more_than is not None and held_less_than is None:
|
||||
cancel_pressed_more_than = track_point_in_utc_time(
|
||||
hass,
|
||||
pressed_more_than_satisfied,
|
||||
dt_util.utcnow() + held_more_than)
|
||||
|
||||
def released():
|
||||
"""Handle the release of the LiteJet switch's button."""
|
||||
nonlocal cancel_pressed_more_than, pressed_time
|
||||
nonlocal held_less_than, held_more_than
|
||||
# pylint: disable=not-callable
|
||||
if cancel_pressed_more_than is not None:
|
||||
cancel_pressed_more_than()
|
||||
cancel_pressed_more_than = None
|
||||
held_time = dt_util.utcnow() - pressed_time
|
||||
if held_less_than is not None and held_time < held_less_than:
|
||||
if held_more_than is None or held_time > held_more_than:
|
||||
call_action()
|
||||
|
||||
hass.data['litejet_system'].on_switch_pressed(number, pressed)
|
||||
hass.data['litejet_system'].on_switch_released(number, released)
|
||||
|
||||
def async_remove():
|
||||
"""Remove all subscriptions used for this trigger."""
|
||||
return
|
||||
|
||||
return async_remove
|
||||
|
||||
@@ -4,6 +4,7 @@ Component to interface with binary sensors.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -39,13 +40,13 @@ SENSOR_CLASSES = [
|
||||
SENSOR_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(SENSOR_CLASSES))
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for binary sensors."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.command_line/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -23,7 +22,7 @@ DEFAULT_NAME = 'Binary Command Sensor'
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
SCAN_INTERVAL = 60
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COMMAND): cv.string,
|
||||
|
||||
@@ -138,7 +138,7 @@ class FFmpegBinarySensor(BinarySensorDevice):
|
||||
def _callback(self, state):
|
||||
"""HA-FFmpeg callback for noise detection."""
|
||||
self._state = state
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _start_ffmpeg(self, config):
|
||||
"""Start a FFmpeg instance."""
|
||||
|
||||
@@ -7,7 +7,8 @@ https://home-assistant.io/components/binary_sensor.homematic/
|
||||
import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
import homeassistant.components.homematic as homematic
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,14 +33,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMBinarySensor,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
|
||||
|
||||
class HMBinarySensor(homematic.HMDevice, BinarySensorDevice):
|
||||
class HMBinarySensor(HMDevice, BinarySensorDevice):
|
||||
"""Representation of a binary Homematic device."""
|
||||
|
||||
@property
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, SENSOR_CLASSES)
|
||||
@@ -66,17 +67,18 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
self._payload_off = payload_off
|
||||
self._qos = qos
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = value_template.render_with_possible_json_value(
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
if payload == self._payload_on:
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload == self._payload_off:
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
|
||||
@@ -22,7 +22,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||
if not gateways:
|
||||
return
|
||||
|
||||
for gateway in gateways:
|
||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||
# states. Map them in a dict of lists.
|
||||
pres = gateway.const.Presentation
|
||||
|
||||
@@ -4,46 +4,97 @@ Support for Nest Thermostat Binary Sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.nest/
|
||||
"""
|
||||
from itertools import chain
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.sensor.nest import NestSensor
|
||||
from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS)
|
||||
from homeassistant.components.nest import DATA_NEST
|
||||
from homeassistant.components.nest import (
|
||||
DATA_NEST, is_thermostat, is_camera)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
BINARY_TYPES = ['fan',
|
||||
'hvac_ac_state',
|
||||
'hvac_aux_heater_state',
|
||||
'hvac_heater_state',
|
||||
'hvac_heat_x2_state',
|
||||
'hvac_heat_x3_state',
|
||||
'hvac_alt_heat_state',
|
||||
'hvac_alt_heat_x2_state',
|
||||
'hvac_emer_heat_state',
|
||||
'online']
|
||||
|
||||
BINARY_TYPES = ['online']
|
||||
|
||||
CLIMATE_BINARY_TYPES = ['fan',
|
||||
'is_using_emergency_heat',
|
||||
'is_locked',
|
||||
'has_leaf']
|
||||
|
||||
CAMERA_BINARY_TYPES = [
|
||||
'motion_detected',
|
||||
'sound_detected',
|
||||
'person_detected']
|
||||
|
||||
_BINARY_TYPES_DEPRECATED = [
|
||||
'hvac_ac_state',
|
||||
'hvac_aux_heater_state',
|
||||
'hvac_heater_state',
|
||||
'hvac_heat_x2_state',
|
||||
'hvac_heat_x3_state',
|
||||
'hvac_alt_heat_state',
|
||||
'hvac_alt_heat_x2_state',
|
||||
'hvac_emer_heat_state']
|
||||
|
||||
_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \
|
||||
+ CAMERA_BINARY_TYPES
|
||||
_VALID_BINARY_SENSOR_TYPES_WITH_DEPRECATED = _VALID_BINARY_SENSOR_TYPES \
|
||||
+ _BINARY_TYPES_DEPRECATED
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SCAN_INTERVAL):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Required(CONF_MONITORED_CONDITIONS):
|
||||
vol.All(cv.ensure_list, [vol.In(BINARY_TYPES)]),
|
||||
vol.All(cv.ensure_list,
|
||||
[vol.In(_VALID_BINARY_SENSOR_TYPES_WITH_DEPRECATED)])
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Nest binary sensors."""
|
||||
nest = hass.data[DATA_NEST]
|
||||
conf = config.get(CONF_MONITORED_CONDITIONS, _VALID_BINARY_SENSOR_TYPES)
|
||||
|
||||
all_sensors = []
|
||||
for structure, device in nest.devices():
|
||||
all_sensors.extend(
|
||||
[NestBinarySensor(structure, device, variable)
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]])
|
||||
for variable in conf:
|
||||
if variable in _BINARY_TYPES_DEPRECATED:
|
||||
wstr = (variable + " is no a longer supported "
|
||||
"monitored_conditions. See "
|
||||
"https://home-assistant.io/components/binary_sensor.nest/ "
|
||||
"for valid options, or remove monitored_conditions "
|
||||
"entirely to get a reasonable default")
|
||||
_LOGGER.error(wstr)
|
||||
|
||||
add_devices(all_sensors, True)
|
||||
sensors = []
|
||||
device_chain = chain(nest.devices(),
|
||||
nest.protect_devices(),
|
||||
nest.camera_devices())
|
||||
for structure, device in device_chain:
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conf
|
||||
if variable in BINARY_TYPES]
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conf
|
||||
if variable in CLIMATE_BINARY_TYPES
|
||||
and is_thermostat(device)]
|
||||
|
||||
if is_camera(device):
|
||||
sensors += [NestBinarySensor(structure, device, variable)
|
||||
for variable in conf
|
||||
if variable in CAMERA_BINARY_TYPES]
|
||||
for activity_zone in device.activity_zones:
|
||||
sensors += [NestActivityZoneSensor(structure,
|
||||
device,
|
||||
activity_zone)]
|
||||
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||
@@ -57,3 +108,21 @@ class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._state = bool(getattr(self.device, self.variable))
|
||||
|
||||
|
||||
class NestActivityZoneSensor(NestBinarySensor):
|
||||
"""Represents a Nest binary sensor for activity in a zone."""
|
||||
|
||||
def __init__(self, structure, device, zone):
|
||||
"""Initialize the sensor."""
|
||||
super(NestActivityZoneSensor, self).__init__(structure, device, None)
|
||||
self.zone = zone
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
return "{} {} activity".format(self._name, self.zone.name)
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id)
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.netatmo import WelcomeData
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_TIMEOUT
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
DEPENDENCIES = ["netatmo"]
|
||||
@@ -33,6 +33,7 @@ CONF_CAMERAS = 'cameras'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_CAMERAS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()):
|
||||
@@ -45,6 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup access to Netatmo binary sensor."""
|
||||
netatmo = get_component('netatmo')
|
||||
home = config.get(CONF_HOME, None)
|
||||
timeout = config.get(CONF_TIMEOUT, 15)
|
||||
|
||||
import lnetatmo
|
||||
try:
|
||||
@@ -62,18 +64,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
for variable in sensors:
|
||||
add_devices([WelcomeBinarySensor(data, camera_name, home,
|
||||
add_devices([WelcomeBinarySensor(data, camera_name, home, timeout,
|
||||
variable)])
|
||||
|
||||
|
||||
class WelcomeBinarySensor(BinarySensorDevice):
|
||||
"""Represent a single binary sensor in a Netatmo Welcome device."""
|
||||
|
||||
def __init__(self, data, camera_name, home, sensor):
|
||||
def __init__(self, data, camera_name, home, timeout, sensor):
|
||||
"""Setup for access to the Netatmo camera events."""
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
self._home = home
|
||||
self._timeout = timeout
|
||||
if home:
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
@@ -114,14 +117,17 @@ class WelcomeBinarySensor(BinarySensorDevice):
|
||||
if self._sensor_name == "Someone known":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneKnownSeen(self._home,
|
||||
self._camera_name)
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Someone unknown":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneUnknownSeen(self._home,
|
||||
self._camera_name)
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Motion":
|
||||
self._state =\
|
||||
self._data.welcomedata.motionDetected(self._home,
|
||||
self._camera_name)
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -123,7 +123,7 @@ class NX584Watcher(threading.Thread):
|
||||
if not zone_sensor:
|
||||
return
|
||||
zone_sensor._zone['state'] = event['zone_state']
|
||||
zone_sensor.update_ha_state()
|
||||
zone_sensor.schedule_update_ha_state()
|
||||
|
||||
def _process_events(self, events):
|
||||
for event in events:
|
||||
|
||||
@@ -72,7 +72,7 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
def read_gpio(port):
|
||||
"""Read state from GPIO."""
|
||||
self._state = rpi_gpio.read_input(self._port)
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime)
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
_LOGGER.error('No sensors added')
|
||||
return False
|
||||
|
||||
hass.loop.create_task(async_add_devices(sensors, True))
|
||||
yield from async_add_devices(sensors, True)
|
||||
return True
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
hass.loop.create_task(self.async_update_ha_state(True))
|
||||
hass.async_add_job(self.async_update_ha_state, True)
|
||||
|
||||
async_track_state_change(
|
||||
hass, entity_ids, template_bsensor_state_listener)
|
||||
|
||||
128
homeassistant/components/binary_sensor/threshold.py
Normal file
128
homeassistant/components/binary_sensor/threshold.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Support for monitoring if a sensor value is below/above a threshold.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.threshold/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN, CONF_SENSOR_CLASS,
|
||||
ATTR_ENTITY_ID)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_SENSOR_VALUE = 'sensor_value'
|
||||
ATTR_THRESHOLD = 'threshold'
|
||||
ATTR_TYPE = 'type'
|
||||
|
||||
CONF_LOWER = 'lower'
|
||||
CONF_THRESHOLD = 'threshold'
|
||||
CONF_UPPER = 'upper'
|
||||
|
||||
DEFAULT_NAME = 'Threshold'
|
||||
|
||||
SENSOR_TYPES = [CONF_LOWER, CONF_UPPER]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_THRESHOLD): vol.Coerce(float),
|
||||
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Threshold sensor."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
name = config.get(CONF_NAME)
|
||||
threshold = config.get(CONF_THRESHOLD)
|
||||
limit_type = config.get(CONF_TYPE)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
|
||||
yield from async_add_devices(
|
||||
[ThresholdSensor(hass, entity_id, name, threshold, limit_type,
|
||||
sensor_class)], True)
|
||||
return True
|
||||
|
||||
|
||||
class ThresholdSensor(BinarySensorDevice):
|
||||
"""Representation of a Threshold sensor."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, threshold, limit_type,
|
||||
sensor_class):
|
||||
"""Initialize the Threshold sensor."""
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
self.is_upper = limit_type == 'upper'
|
||||
self._name = name
|
||||
self._threshold = threshold
|
||||
self._sensor_class = sensor_class
|
||||
self._deviation = False
|
||||
self.sensor_value = 0
|
||||
|
||||
@callback
|
||||
# pylint: disable=invalid-name
|
||||
def async_threshold_sensor_state_listener(
|
||||
entity, old_state, new_state):
|
||||
"""Called when the sensor changes state."""
|
||||
if new_state.state == STATE_UNKNOWN:
|
||||
return
|
||||
|
||||
try:
|
||||
self.sensor_value = float(new_state.state)
|
||||
except ValueError:
|
||||
_LOGGER.error("State is not numerical")
|
||||
|
||||
hass.async_add_job(self.async_update_ha_state, True)
|
||||
|
||||
async_track_state_change(
|
||||
hass, entity_id, async_threshold_sensor_state_listener)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._deviation
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._sensor_class
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
return {
|
||||
ATTR_ENTITY_ID: self._entity_id,
|
||||
ATTR_SENSOR_VALUE: self.sensor_value,
|
||||
ATTR_THRESHOLD: self._threshold,
|
||||
ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and updates the states."""
|
||||
if self.is_upper:
|
||||
self._deviation = bool(self.sensor_value > self._threshold)
|
||||
else:
|
||||
self._deviation = bool(self.sensor_value < self._threshold)
|
||||
@@ -4,8 +4,11 @@ A sensor that monitors trands in other components.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.trend/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@@ -87,13 +90,12 @@ class SensorTrend(BinarySensorDevice):
|
||||
self.from_state = None
|
||||
self.to_state = None
|
||||
|
||||
self.update()
|
||||
|
||||
@callback
|
||||
def trend_sensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
self.from_state = old_state
|
||||
self.to_state = new_state
|
||||
self.update_ha_state(True)
|
||||
hass.async_add_job(self.async_update_ha_state(True))
|
||||
|
||||
track_state_change(hass, target_entity,
|
||||
trend_sensor_state_listener)
|
||||
@@ -118,7 +120,8 @@ class SensorTrend(BinarySensorDevice):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
if self.from_state is None or self.to_state is None:
|
||||
return
|
||||
|
||||
@@ -45,10 +45,10 @@ class WemoBinarySensor(BinarySensorDevice):
|
||||
_LOGGER.info(
|
||||
'Subscription update for %s',
|
||||
_device)
|
||||
self.update()
|
||||
if not hasattr(self, 'hass'):
|
||||
self.update()
|
||||
return
|
||||
self.update_ha_state(True)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for Wink binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
at https://home-assistant.io/components/binary_sensor.wink/
|
||||
"""
|
||||
import json
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.wink import WinkDevice
|
||||
@@ -33,33 +32,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
for sensor in pywink.get_sensors():
|
||||
if sensor.capability() in SENSOR_TYPES:
|
||||
add_devices([WinkBinarySensorDevice(sensor)])
|
||||
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
||||
|
||||
for key in pywink.get_keys():
|
||||
add_devices([WinkBinarySensorDevice(key)])
|
||||
add_devices([WinkBinarySensorDevice(key, hass)])
|
||||
|
||||
for sensor in pywink.get_smoke_and_co_detectors():
|
||||
add_devices([WinkBinarySensorDevice(sensor)])
|
||||
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
||||
|
||||
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink binary sensor."""
|
||||
|
||||
def __init__(self, wink):
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink)
|
||||
super().__init__(wink, hass)
|
||||
wink = get_component('wink')
|
||||
self._unit_of_measurement = self.wink.UNIT
|
||||
self.capability = self.wink.capability()
|
||||
|
||||
def _pubnub_update(self, message, channel):
|
||||
if 'data' in message:
|
||||
json_data = json.dumps(message.get('data'))
|
||||
else:
|
||||
json_data = message
|
||||
self.wink.pubnub_update(json.loads(json_data))
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
|
||||
@@ -96,7 +96,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
|
||||
@@ -112,19 +112,19 @@ class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
|
||||
# If it's active make sure that we set the timeout tracker
|
||||
if sensor_value.data:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
if value.data:
|
||||
# only allow this value to be true for re_arm secs
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
@property
|
||||
|
||||
183
homeassistant/components/calendar/__init__.py
Normal file
183
homeassistant/components/calendar/__init__.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
Support for Google Calendar event device sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/calendar/
|
||||
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
|
||||
from homeassistant.components.google import (CONF_OFFSET,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_NAME)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.helpers.config_validation import time_period_str
|
||||
from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||
from homeassistant.util import dt
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'calendar'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for calendars."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, 60, DOMAIN)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
DEFAULT_CONF_TRACK_NEW = True
|
||||
DEFAULT_CONF_OFFSET = '!!'
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class CalendarEventDevice(Entity):
|
||||
"""A calendar event device."""
|
||||
|
||||
# Classes overloading this must set data to an object
|
||||
# with an update() method
|
||||
data = None
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, data):
|
||||
"""Create the Calendar Event Device."""
|
||||
self._name = data.get(CONF_NAME)
|
||||
self.dev_id = data.get(CONF_DEVICE_ID)
|
||||
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT,
|
||||
self.dev_id,
|
||||
hass=hass)
|
||||
|
||||
self._cal_data = {
|
||||
'all_day': False,
|
||||
'offset_time': dt.dt.timedelta(),
|
||||
'message': '',
|
||||
'start': None,
|
||||
'end': None,
|
||||
'location': '',
|
||||
'description': '',
|
||||
}
|
||||
|
||||
self.update()
|
||||
|
||||
def offset_reached(self):
|
||||
"""Have we reached the offset time specified in the event title."""
|
||||
if self._cal_data['start'] is None or \
|
||||
self._cal_data['offset_time'] == dt.dt.timedelta():
|
||||
return False
|
||||
|
||||
return self._cal_data['start'] + self._cal_data['offset_time'] <= \
|
||||
dt.now(self._cal_data['start'].tzinfo)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""State Attributes for HA."""
|
||||
start = self._cal_data.get('start', None)
|
||||
end = self._cal_data.get('end', None)
|
||||
start = start.strftime(DATE_STR_FORMAT) if start is not None else None
|
||||
end = end.strftime(DATE_STR_FORMAT) if end is not None else None
|
||||
|
||||
return {
|
||||
'message': self._cal_data.get('message', ''),
|
||||
'all_day': self._cal_data.get('all_day', False),
|
||||
'offset_reached': self.offset_reached(),
|
||||
'start_time': start,
|
||||
'end_time': end,
|
||||
'location': self._cal_data.get('location', None),
|
||||
'description': self._cal_data.get('description', None),
|
||||
}
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the calendar event."""
|
||||
start = self._cal_data.get('start', None)
|
||||
end = self._cal_data.get('end', None)
|
||||
if start is None or end is None:
|
||||
return STATE_OFF
|
||||
|
||||
now = dt.now()
|
||||
|
||||
if start <= now and end > now:
|
||||
return STATE_ON
|
||||
|
||||
if now >= end:
|
||||
self.cleanup()
|
||||
|
||||
return STATE_OFF
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup any start/end listeners that were setup."""
|
||||
self._cal_data = {
|
||||
'all_day': False,
|
||||
'offset_time': 0,
|
||||
'message': '',
|
||||
'start': None,
|
||||
'end': None,
|
||||
'location': None,
|
||||
'description': None
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Search for the next event."""
|
||||
if not self.data or not self.data.update():
|
||||
# update cached, don't do anything
|
||||
return
|
||||
|
||||
if not self.data.event:
|
||||
# we have no event to work on, make sure we're clean
|
||||
self.cleanup()
|
||||
return
|
||||
|
||||
def _get_date(date):
|
||||
"""Get the dateTime from date or dateTime as a local."""
|
||||
if 'date' in date:
|
||||
return dt.as_utc(dt.dt.datetime.combine(
|
||||
dt.parse_date(date['date']), dt.dt.time()))
|
||||
else:
|
||||
return dt.parse_datetime(date['dateTime'])
|
||||
|
||||
start = _get_date(self.data.event['start'])
|
||||
end = _get_date(self.data.event['end'])
|
||||
|
||||
summary = self.data.event['summary']
|
||||
|
||||
# check if we have an offset tag in the message
|
||||
# time is HH:MM or MM
|
||||
reg = '{}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)'.format(self._offset)
|
||||
search = re.search(reg, summary)
|
||||
if search and search.group(1):
|
||||
time = search.group(1)
|
||||
if ':' not in time:
|
||||
if time[0] == '+' or time[0] == '-':
|
||||
time = '{}0:{}'.format(time[0], time[1:])
|
||||
else:
|
||||
time = '0:{}'.format(time)
|
||||
|
||||
offset_time = time_period_str(time)
|
||||
summary = (summary[:search.start()] + summary[search.end():]) \
|
||||
.strip()
|
||||
else:
|
||||
offset_time = dt.dt.timedelta() # default it
|
||||
|
||||
# cleanup the string so we don't have a bunch of double+ spaces
|
||||
self._cal_data['message'] = re.sub(' +', '', summary).strip()
|
||||
|
||||
self._cal_data['offset_time'] = offset_time
|
||||
self._cal_data['location'] = self.data.event.get('location', '')
|
||||
self._cal_data['description'] = self.data.event.get('description', '')
|
||||
self._cal_data['start'] = start
|
||||
self._cal_data['end'] = end
|
||||
self._cal_data['all_day'] = 'date' in self.data.event['start']
|
||||
82
homeassistant/components/calendar/demo.py
Executable file
82
homeassistant/components/calendar/demo.py
Executable file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Demo platform that has two fake binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Demo binary sensor platform."""
|
||||
calendar_data_future = DemoGoogleCalendarDataFuture()
|
||||
calendar_data_current = DemoGoogleCalendarDataCurrent()
|
||||
add_devices([
|
||||
DemoGoogleCalendar(hass, calendar_data_future, {
|
||||
CONF_NAME: 'Future Event',
|
||||
CONF_DEVICE_ID: 'future_event',
|
||||
}),
|
||||
|
||||
DemoGoogleCalendar(hass, calendar_data_current, {
|
||||
CONF_NAME: 'Current Event',
|
||||
CONF_DEVICE_ID: 'current_event',
|
||||
}),
|
||||
])
|
||||
|
||||
|
||||
class DemoGoogleCalendarData(object):
|
||||
"""Setup base class for data."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def update(self):
|
||||
"""Return true so entity knows we have new data."""
|
||||
return True
|
||||
|
||||
|
||||
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
|
||||
"""Setup future data event."""
|
||||
|
||||
def __init__(self):
|
||||
"""Set the event to a future event."""
|
||||
one_hour_from_now = dt_util.now() \
|
||||
+ dt_util.dt.timedelta(minutes=30)
|
||||
self.event = {
|
||||
'start': {
|
||||
'dateTime': one_hour_from_now.isoformat()
|
||||
},
|
||||
'end': {
|
||||
'dateTime': (one_hour_from_now + dt_util.dt.
|
||||
timedelta(minutes=60)).isoformat()
|
||||
},
|
||||
'summary': 'Future Event',
|
||||
}
|
||||
|
||||
|
||||
class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
|
||||
"""Create a current event we're in the middle of."""
|
||||
|
||||
def __init__(self):
|
||||
"""Set the event data."""
|
||||
middle_of_event = dt_util.now() \
|
||||
- dt_util.dt.timedelta(minutes=30)
|
||||
self.event = {
|
||||
'start': {
|
||||
'dateTime': middle_of_event.isoformat()
|
||||
},
|
||||
'end': {
|
||||
'dateTime': (middle_of_event + dt_util.dt.
|
||||
timedelta(minutes=60)).isoformat()
|
||||
},
|
||||
'summary': 'Current Event',
|
||||
}
|
||||
|
||||
|
||||
class DemoGoogleCalendar(CalendarEventDevice):
|
||||
"""A Demo binary sensor."""
|
||||
|
||||
def __init__(self, hass, calendar_data, data):
|
||||
"""The same as a google calendar but without the api calls."""
|
||||
self.data = calendar_data
|
||||
super().__init__(hass, data)
|
||||
79
homeassistant/components/calendar/google.py
Normal file
79
homeassistant/components/calendar/google.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Support for Google Calendar Search binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.google_calendar/
|
||||
"""
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.calendar import CalendarEventDevice
|
||||
from homeassistant.components.google import (CONF_CAL_ID, CONF_ENTITIES,
|
||||
CONF_TRACK, TOKEN_FILE,
|
||||
GoogleCalendarService)
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
DEFAULT_GOOGLE_SEARCH_PARAMS = {
|
||||
'orderBy': 'startTime',
|
||||
'maxResults': 1,
|
||||
'singleEvents': True,
|
||||
}
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, disc_info=None):
|
||||
"""Setup the calendar platform for event devices."""
|
||||
if disc_info is None:
|
||||
return
|
||||
|
||||
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
|
||||
return
|
||||
|
||||
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
||||
add_devices([GoogleCalendarEventDevice(hass, calendar_service,
|
||||
disc_info[CONF_CAL_ID], data)
|
||||
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||
"""A calendar event device."""
|
||||
|
||||
def __init__(self, hass, calendar_service, calendar, data):
|
||||
"""Create the Calendar event device."""
|
||||
self.data = GoogleCalendarData(calendar_service, calendar,
|
||||
data.get('search', None))
|
||||
super().__init__(hass, data)
|
||||
|
||||
|
||||
class GoogleCalendarData(object):
|
||||
"""Class to utilize calendar service object to get next event."""
|
||||
|
||||
def __init__(self, calendar_service, calendar_id, search=None):
|
||||
"""Setup how we are going to search the google calendar."""
|
||||
self.calendar_service = calendar_service
|
||||
self.calendar_id = calendar_id
|
||||
self.search = search
|
||||
self.event = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
service = self.calendar_service.get()
|
||||
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
||||
params['timeMin'] = dt.utcnow().isoformat('T')
|
||||
params['calendarId'] = self.calendar_id
|
||||
if self.search:
|
||||
params['q'] = self.search
|
||||
|
||||
events = service.events() # pylint: disable=no-member
|
||||
result = events.list(**params).execute()
|
||||
|
||||
items = result.get('items', [])
|
||||
self.event = items[0] if len(items) == 1 else None
|
||||
return True
|
||||
@@ -13,7 +13,7 @@ from aiohttp import web
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||
|
||||
DOMAIN = 'camera'
|
||||
DEPENDENCIES = ['http']
|
||||
@@ -33,8 +33,8 @@ def async_setup(hass, config):
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
hass.http.register_view(CameraImageView(hass, component.entities))
|
||||
hass.http.register_view(CameraMjpegStream(hass, component.entities))
|
||||
hass.http.register_view(CameraImageView(component.entities))
|
||||
hass.http.register_view(CameraMjpegStream(component.entities))
|
||||
|
||||
yield from component.async_setup(config)
|
||||
return True
|
||||
@@ -165,9 +165,8 @@ class CameraView(HomeAssistantView):
|
||||
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass, entities):
|
||||
def __init__(self, entities):
|
||||
"""Initialize a basic camera view."""
|
||||
super().__init__(hass)
|
||||
self.entities = entities
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -178,7 +177,7 @@ class CameraView(HomeAssistantView):
|
||||
if camera is None:
|
||||
return web.Response(status=404)
|
||||
|
||||
authenticated = (request.authenticated or
|
||||
authenticated = (request[KEY_AUTHENTICATED] or
|
||||
request.GET.get('token') == camera.access_token)
|
||||
|
||||
if not authenticated:
|
||||
|
||||
79
homeassistant/components/camera/amcrest.py
Normal file
79
homeassistant/components/camera/amcrest.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
This component provides basic support for Amcrest IP cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.amcrest/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.0.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up an Amcrest IP Camera."""
|
||||
from amcrest import AmcrestCamera
|
||||
data = AmcrestCamera(
|
||||
config.get(CONF_HOST), config.get(CONF_PORT),
|
||||
config.get(CONF_USERNAME), config.get(CONF_PASSWORD))
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
try:
|
||||
data.camera.current_time
|
||||
# pylint: disable=broad-except
|
||||
except Exception as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||
persistent_notification.create(
|
||||
hass, 'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
add_devices([AmcrestCam(config, data)])
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestCam(Camera):
|
||||
"""An implementation of an Amcrest IP camera."""
|
||||
|
||||
def __init__(self, device_info, data):
|
||||
"""Initialize an Amcrest camera."""
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._data = data
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
response = self._data.camera.snapshot()
|
||||
return response.data
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
@@ -35,7 +35,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a FFmpeg Camera."""
|
||||
if not async_run_test(hass, config.get(CONF_INPUT)):
|
||||
return
|
||||
hass.loop.create_task(async_add_devices([FFmpegCamera(hass, config)]))
|
||||
yield from async_add_devices([FFmpegCamera(hass, config)])
|
||||
|
||||
|
||||
class FFmpegCamera(Camera):
|
||||
@@ -85,7 +85,7 @@ class FFmpegCamera(Camera):
|
||||
break
|
||||
response.write(data)
|
||||
finally:
|
||||
self.hass.loop.create_task(stream.close())
|
||||
self.hass.async_add_job(stream.close())
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.const import (
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
@@ -43,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a generic IP Camera."""
|
||||
hass.loop.create_task(async_add_devices([GenericCamera(hass, config)]))
|
||||
yield from async_add_devices([GenericCamera(hass, config)])
|
||||
|
||||
|
||||
class GenericCamera(Camera):
|
||||
@@ -96,8 +97,7 @@ class GenericCamera(Camera):
|
||||
def fetch():
|
||||
"""Read image from a URL."""
|
||||
try:
|
||||
kwargs = {'timeout': 10, 'auth': self._auth}
|
||||
response = requests.get(url, **kwargs)
|
||||
response = requests.get(url, timeout=10, auth=self._auth)
|
||||
return response.content
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
@@ -107,12 +107,13 @@ class GenericCamera(Camera):
|
||||
None, fetch)
|
||||
# async
|
||||
else:
|
||||
response = None
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from self.hass.websession.get(
|
||||
response = yield from websession.get(
|
||||
url, auth=self._auth)
|
||||
self._last_image = yield from response.read()
|
||||
yield from response.release()
|
||||
self._last_image = yield from response.read()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error('Timeout getting camera image')
|
||||
return self._last_image
|
||||
@@ -120,6 +121,9 @@ class GenericCamera(Camera):
|
||||
aiohttp.errors.ClientDisconnectedError) as err:
|
||||
_LOGGER.error('Error getting new camera image: %s', err)
|
||||
return self._last_image
|
||||
finally:
|
||||
if response is not None:
|
||||
self.hass.async_add_job(response.release())
|
||||
|
||||
self._last_url = url
|
||||
return self._last_image
|
||||
|
||||
@@ -20,6 +20,7 @@ from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -43,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a MJPEG IP Camera."""
|
||||
hass.loop.create_task(async_add_devices([MjpegCamera(hass, config)]))
|
||||
yield from async_add_devices([MjpegCamera(hass, config)])
|
||||
|
||||
|
||||
def extract_image_from_mjpeg(stream):
|
||||
@@ -101,29 +102,33 @@ class MjpegCamera(Camera):
|
||||
return
|
||||
|
||||
# connect to stream
|
||||
websession = async_get_clientsession(self.hass)
|
||||
stream = None
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
stream = yield from self.hass.websession.get(
|
||||
self._mjpeg_url,
|
||||
auth=self._auth
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
raise HTTPGatewayTimeout()
|
||||
stream = yield from websession.get(self._mjpeg_url,
|
||||
auth=self._auth)
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||
response = web.StreamResponse()
|
||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||
|
||||
yield from response.prepare(request)
|
||||
yield from response.prepare(request)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = yield from stream.content.read(102400)
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
finally:
|
||||
self.hass.loop.create_task(stream.release())
|
||||
yield from response.write_eof()
|
||||
if stream is not None:
|
||||
self.hass.async_add_job(stream.release())
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
108
homeassistant/components/camera/nest.py
Normal file
108
homeassistant/components/camera/nest.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Support for Nest Cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.nest/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
import homeassistant.components.nest as nest
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
|
||||
NEST_BRAND = 'Nest'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a Nest Cam."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
camera_devices = hass.data[nest.DATA_NEST].camera_devices()
|
||||
cameras = [NestCamera(structure, device)
|
||||
for structure, device in camera_devices]
|
||||
add_devices(cameras, True)
|
||||
|
||||
|
||||
class NestCamera(Camera):
|
||||
"""Representation of a Nest Camera."""
|
||||
|
||||
def __init__(self, structure, device):
|
||||
"""Initialize a Nest Camera."""
|
||||
super(NestCamera, self).__init__()
|
||||
self.structure = structure
|
||||
self.device = device
|
||||
self._location = None
|
||||
self._name = None
|
||||
self._is_online = None
|
||||
self._is_streaming = None
|
||||
self._is_video_history_enabled = False
|
||||
# Default to non-NestAware subscribed, but will be fixed during update
|
||||
self._time_between_snapshots = timedelta(seconds=30)
|
||||
self._last_image = None
|
||||
self._next_snapshot_at = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Nest camera should poll periodically."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return true if the device is recording."""
|
||||
return self._is_streaming
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Return the brand of the camera."""
|
||||
return NEST_BRAND
|
||||
|
||||
# This doesn't seem to be getting called regularly, for some reason
|
||||
def update(self):
|
||||
"""Cache value from Python-nest."""
|
||||
self._location = self.device.where
|
||||
self._name = self.device.name
|
||||
self._is_online = self.device.is_online
|
||||
self._is_streaming = self.device.is_streaming
|
||||
self._is_video_history_enabled = self.device.is_video_history_enabled
|
||||
|
||||
if self._is_video_history_enabled:
|
||||
# NestAware allowed 10/min
|
||||
self._time_between_snapshots = timedelta(seconds=6)
|
||||
else:
|
||||
# Otherwise, 2/min
|
||||
self._time_between_snapshots = timedelta(seconds=30)
|
||||
|
||||
def _ready_for_snapshot(self, now):
|
||||
return (self._next_snapshot_at is None or
|
||||
now > self._next_snapshot_at)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
now = utcnow()
|
||||
if self._ready_for_snapshot(now):
|
||||
url = self.device.snapshot_url
|
||||
|
||||
try:
|
||||
response = requests.get(url)
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error("Error getting camera image: %s", error)
|
||||
return None
|
||||
|
||||
self._next_snapshot_at = now + self._time_between_snapshots
|
||||
self._last_image = response.content
|
||||
|
||||
return self._last_image
|
||||
@@ -16,9 +16,11 @@ import async_timeout
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP)
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_create_clientsession)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
@@ -58,15 +60,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a Synology IP Camera."""
|
||||
if not config.get(CONF_VERIFY_SSL):
|
||||
connector = aiohttp.TCPConnector(verify_ssl=False)
|
||||
else:
|
||||
connector = None
|
||||
|
||||
websession_init = aiohttp.ClientSession(
|
||||
loop=hass.loop,
|
||||
connector=connector
|
||||
)
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||
websession_init = async_get_clientsession(hass, verify_ssl)
|
||||
|
||||
# Determine API to use for authentication
|
||||
syno_api_url = SYNO_API_URL.format(
|
||||
@@ -78,24 +73,27 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
'version': '1',
|
||||
'query': 'SYNO.'
|
||||
}
|
||||
query_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
query_req = yield from websession_init.get(
|
||||
syno_api_url,
|
||||
params=query_payload
|
||||
)
|
||||
|
||||
query_resp = yield from query_req.json()
|
||||
auth_path = query_resp['data'][AUTH_API]['path']
|
||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_api_url)
|
||||
return False
|
||||
|
||||
query_resp = yield from query_req.json()
|
||||
auth_path = query_resp['data'][AUTH_API]['path']
|
||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||
|
||||
# cleanup
|
||||
yield from query_req.release()
|
||||
finally:
|
||||
if query_req is not None:
|
||||
yield from query_req.release()
|
||||
|
||||
# Authticate to NAS to get a session id
|
||||
syno_auth_url = SYNO_API_URL.format(
|
||||
@@ -109,19 +107,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
syno_auth_url
|
||||
)
|
||||
|
||||
websession_init.detach()
|
||||
|
||||
# init websession
|
||||
websession = aiohttp.ClientSession(
|
||||
loop=hass.loop, connector=connector, cookies={'id': session_id})
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_close_websession(event):
|
||||
"""Close webssesion on shutdown."""
|
||||
yield from websession.close()
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, _async_close_websession)
|
||||
websession = async_create_clientsession(
|
||||
hass, verify_ssl, cookies={'id': session_id})
|
||||
|
||||
# Use SessionID to get cameras in system
|
||||
syno_camera_url = SYNO_API_URL.format(
|
||||
@@ -181,20 +169,23 @@ def get_session_id(hass, websession, username, password, login_url):
|
||||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'
|
||||
}
|
||||
auth_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
auth_req = yield from websession.get(
|
||||
login_url,
|
||||
params=auth_payload
|
||||
)
|
||||
auth_resp = yield from auth_req.json()
|
||||
return auth_resp['data']['sid']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", login_url)
|
||||
return False
|
||||
|
||||
auth_resp = yield from auth_req.json()
|
||||
yield from auth_req.release()
|
||||
|
||||
return auth_resp['data']['sid']
|
||||
finally:
|
||||
if auth_req is not None:
|
||||
yield from auth_req.release()
|
||||
|
||||
|
||||
class SynologyCamera(Camera):
|
||||
@@ -262,30 +253,34 @@ class SynologyCamera(Camera):
|
||||
'cameraId': self._camera_id,
|
||||
'format': 'mjpeg'
|
||||
}
|
||||
stream = None
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||
stream = yield from self._websession.get(
|
||||
streaming_url,
|
||||
params=streaming_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", streaming_url)
|
||||
raise HTTPGatewayTimeout()
|
||||
response = web.StreamResponse()
|
||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||
yield from response.prepare(request)
|
||||
|
||||
yield from response.prepare(request)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = yield from stream.content.read(102400)
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", streaming_url)
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
finally:
|
||||
self.hass.async_add_job(stream.release())
|
||||
yield from response.write_eof()
|
||||
if stream is not None:
|
||||
self.hass.async_add_job(stream.release())
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -58,6 +58,11 @@ ATTR_OPERATION_LIST = "operation_list"
|
||||
ATTR_SWING_MODE = "swing_mode"
|
||||
ATTR_SWING_LIST = "swing_list"
|
||||
|
||||
# The degree of precision for each platform
|
||||
PRECISION_WHOLE = 1
|
||||
PRECISION_HALVES = 0.5
|
||||
PRECISION_TENTHS = 0.1
|
||||
|
||||
CONVERTIBLE_ATTRIBUTE = [
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
@@ -371,6 +376,14 @@ class ClimateDevice(Entity):
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return the precision of the system."""
|
||||
if self.unit_of_measurement == TEMP_CELSIUS:
|
||||
return PRECISION_TENTHS
|
||||
else:
|
||||
return PRECISION_WHOLE
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
@@ -562,16 +575,18 @@ class ClimateDevice(Entity):
|
||||
|
||||
def _convert_for_display(self, temp):
|
||||
"""Convert temperature into preferred units for display purposes."""
|
||||
if temp is None or not isinstance(temp, Number):
|
||||
if (temp is None or not isinstance(temp, Number) or
|
||||
self.temperature_unit == self.unit_of_measurement):
|
||||
return temp
|
||||
|
||||
value = convert_temperature(temp, self.temperature_unit,
|
||||
self.unit_of_measurement)
|
||||
|
||||
if self.unit_of_measurement is TEMP_CELSIUS:
|
||||
decimal_count = 1
|
||||
# Round in the units appropriate
|
||||
if self.precision == PRECISION_HALVES:
|
||||
return round(value * 2) / 2.0
|
||||
elif self.precision == PRECISION_TENTHS:
|
||||
return round(value, 1)
|
||||
else:
|
||||
# Users of fahrenheit generally expect integer units.
|
||||
decimal_count = 0
|
||||
|
||||
return round(value, decimal_count)
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(value)
|
||||
|
||||
@@ -21,10 +21,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['switch', 'sensor']
|
||||
|
||||
TOL_TEMP = 0.3
|
||||
DEFAULT_TOLERANCE = 0.3
|
||||
DEFAULT_NAME = 'Generic Thermostat'
|
||||
|
||||
CONF_NAME = 'name'
|
||||
DEFAULT_NAME = 'Generic Thermostat'
|
||||
CONF_HEATER = 'heater'
|
||||
CONF_SENSOR = 'target_sensor'
|
||||
CONF_MIN_TEMP = 'min_temp'
|
||||
@@ -32,6 +32,7 @@ CONF_MAX_TEMP = 'max_temp'
|
||||
CONF_TARGET_TEMP = 'target_temp'
|
||||
CONF_AC_MODE = 'ac_mode'
|
||||
CONF_MIN_DUR = 'min_cycle_duration'
|
||||
CONF_TOLERANCE = 'tolerance'
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@@ -42,6 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float),
|
||||
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
|
||||
})
|
||||
|
||||
@@ -56,23 +58,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
target_temp = config.get(CONF_TARGET_TEMP)
|
||||
ac_mode = config.get(CONF_AC_MODE)
|
||||
min_cycle_duration = config.get(CONF_MIN_DUR)
|
||||
tolerance = config.get(CONF_TOLERANCE)
|
||||
|
||||
add_devices([GenericThermostat(
|
||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||
target_temp, ac_mode, min_cycle_duration)])
|
||||
target_temp, ac_mode, min_cycle_duration, tolerance)])
|
||||
|
||||
|
||||
class GenericThermostat(ClimateDevice):
|
||||
"""Representation of a GenericThermostat device."""
|
||||
|
||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration):
|
||||
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
|
||||
tolerance):
|
||||
"""Initialize the thermostat."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self.heater_entity_id = heater_entity_id
|
||||
self.ac_mode = ac_mode
|
||||
self.min_cycle_duration = min_cycle_duration
|
||||
self._tolerance = tolerance
|
||||
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
@@ -145,7 +150,7 @@ class GenericThermostat(ClimateDevice):
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
# pylint: disable=no-member
|
||||
if self._min_temp:
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
@@ -158,7 +163,7 @@ class GenericThermostat(ClimateDevice):
|
||||
|
||||
self._update_temp(new_state)
|
||||
self._control_heating()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _update_temp(self, state):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
@@ -193,7 +198,7 @@ class GenericThermostat(ClimateDevice):
|
||||
return
|
||||
|
||||
if self.ac_mode:
|
||||
too_hot = self._cur_temp - self._target_temp > TOL_TEMP
|
||||
too_hot = self._cur_temp - self._target_temp > self._tolerance
|
||||
is_cooling = self._is_device_active
|
||||
if too_hot and not is_cooling:
|
||||
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
|
||||
@@ -202,7 +207,7 @@ class GenericThermostat(ClimateDevice):
|
||||
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
|
||||
switch.turn_off(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
too_cold = self._target_temp - self._cur_temp > TOL_TEMP
|
||||
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
||||
is_heating = self._is_device_active
|
||||
|
||||
if too_cold and not is_heating:
|
||||
|
||||
@@ -5,10 +5,11 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.homematic/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.homematic as homematic
|
||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.util.temperature import convert
|
||||
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
@@ -29,14 +30,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMThermostat,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
|
||||
|
||||
class HMThermostat(homematic.HMDevice, ClimateDevice):
|
||||
class HMThermostat(HMDevice, ClimateDevice):
|
||||
"""Representation of a Homematic thermostat."""
|
||||
|
||||
@property
|
||||
@@ -94,13 +97,9 @@ class HMThermostat(homematic.HMDevice, ClimateDevice):
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if not self.available:
|
||||
if not self.available or temperature is None:
|
||||
return None
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return self._hmdevice.actionNodeData('MANU_MODE', temperature)
|
||||
self._hmdevice.set_temperature(temperature)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
|
||||
@@ -24,7 +24,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors climate."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
|
||||
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||
if not gateways:
|
||||
return
|
||||
|
||||
for gateway in gateways:
|
||||
if float(gateway.protocol_version) < 1.5:
|
||||
continue
|
||||
pres = gateway.const.Presentation
|
||||
|
||||
@@ -14,7 +14,8 @@ from homeassistant.components.climate import (
|
||||
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -24,10 +25,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
})
|
||||
|
||||
STATE_ECO = 'eco'
|
||||
STATE_HEAT_COOL = 'heat-cool'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Nest thermostat."""
|
||||
_LOGGER.debug("Setting up nest thermostat")
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
|
||||
add_devices(
|
||||
[NestThermostat(structure, device, temp_unit)
|
||||
for structure, device in hass.data[DATA_NEST].devices()],
|
||||
@@ -58,9 +67,9 @@ class NestThermostat(ClimateDevice):
|
||||
if self.device.can_heat and self.device.can_cool:
|
||||
self._operation_list.append(STATE_AUTO)
|
||||
|
||||
self._operation_list.append(STATE_ECO)
|
||||
|
||||
# feature of device
|
||||
self._has_humidifier = self.device.has_humidifier
|
||||
self._has_dehumidifier = self.device.has_dehumidifier
|
||||
self._has_fan = self.device.has_fan
|
||||
|
||||
# data attributes
|
||||
@@ -68,41 +77,24 @@ class NestThermostat(ClimateDevice):
|
||||
self._location = None
|
||||
self._name = None
|
||||
self._humidity = None
|
||||
self._target_humidity = None
|
||||
self._target_temperature = None
|
||||
self._temperature = None
|
||||
self._temperature_scale = None
|
||||
self._mode = None
|
||||
self._fan = None
|
||||
self._away_temperature = None
|
||||
self._eco_temperature = None
|
||||
self._is_locked = None
|
||||
self._locked_temperature = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
if self._location is None:
|
||||
return self._name
|
||||
else:
|
||||
if self._name == '':
|
||||
return self._location.capitalize()
|
||||
else:
|
||||
return self._location.capitalize() + '(' + self._name + ')'
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
if self._has_humidifier or self._has_dehumidifier:
|
||||
# Move these to Thermostat Device and make them global
|
||||
return {
|
||||
"humidity": self._humidity,
|
||||
"target_humidity": self._target_humidity,
|
||||
}
|
||||
else:
|
||||
# No way to control humidity not show setting
|
||||
return {}
|
||||
return self._temperature_scale
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
@@ -112,21 +104,17 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self._mode == 'cool':
|
||||
return STATE_COOL
|
||||
elif self._mode == 'heat':
|
||||
return STATE_HEAT
|
||||
elif self._mode == 'range':
|
||||
if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]:
|
||||
return self._mode
|
||||
elif self._mode == STATE_HEAT_COOL:
|
||||
return STATE_AUTO
|
||||
elif self._mode == 'off':
|
||||
return STATE_OFF
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._mode != 'range' and not self.is_away_mode_on:
|
||||
if self._mode != STATE_HEAT_COOL and not self.is_away_mode_on:
|
||||
return self._target_temperature
|
||||
else:
|
||||
return None
|
||||
@@ -134,10 +122,11 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self.is_away_mode_on and self._away_temperature[0]:
|
||||
# away_temperature is always a low, high tuple
|
||||
return self._away_temperature[0]
|
||||
if self._mode == 'range':
|
||||
if (self.is_away_mode_on or self._mode == STATE_ECO) and \
|
||||
self._eco_temperature[0]:
|
||||
# eco_temperature is always a low, high tuple
|
||||
return self._eco_temperature[0]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[0]
|
||||
else:
|
||||
return None
|
||||
@@ -145,10 +134,11 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if self.is_away_mode_on and self._away_temperature[1]:
|
||||
# away_temperature is always a low, high tuple
|
||||
return self._away_temperature[1]
|
||||
if self._mode == 'range':
|
||||
if (self.is_away_mode_on or self._mode == STATE_ECO) and \
|
||||
self._eco_temperature[1]:
|
||||
# eco_temperature is always a low, high tuple
|
||||
return self._eco_temperature[1]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[1]
|
||||
else:
|
||||
return None
|
||||
@@ -163,8 +153,7 @@ class NestThermostat(ClimateDevice):
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if target_temp_low is not None and target_temp_high is not None:
|
||||
|
||||
if self._mode == 'range':
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
temp = (target_temp_low, target_temp_high)
|
||||
else:
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
@@ -173,14 +162,11 @@ class NestThermostat(ClimateDevice):
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
self.device.mode = 'heat'
|
||||
elif operation_mode == STATE_COOL:
|
||||
self.device.mode = 'cool'
|
||||
if operation_mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]:
|
||||
device_mode = operation_mode
|
||||
elif operation_mode == STATE_AUTO:
|
||||
self.device.mode = 'range'
|
||||
elif operation_mode == STATE_OFF:
|
||||
self.device.mode = 'off'
|
||||
device_mode = STATE_HEAT_COOL
|
||||
self.device.mode = device_mode
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
@@ -217,30 +203,33 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Identify min_temp in Nest API or defaults if not available."""
|
||||
temp = self._away_temperature[0]
|
||||
if temp is None:
|
||||
return super().min_temp
|
||||
if self._is_locked:
|
||||
return self._locked_temperature[0]
|
||||
else:
|
||||
return temp
|
||||
return None
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Identify max_temp in Nest API or defaults if not available."""
|
||||
temp = self._away_temperature[1]
|
||||
if temp is None:
|
||||
return super().max_temp
|
||||
if self._is_locked:
|
||||
return self._locked_temperature[1]
|
||||
else:
|
||||
return temp
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
"""Cache value from Python-nest."""
|
||||
self._location = self.device.where
|
||||
self._name = self.device.name
|
||||
self._humidity = self.device.humidity,
|
||||
self._target_humidity = self.device.target_humidity,
|
||||
self._temperature = self.device.temperature
|
||||
self._mode = self.device.mode
|
||||
self._target_temperature = self.device.target
|
||||
self._fan = self.device.fan
|
||||
self._away = self.structure.away
|
||||
self._away_temperature = self.device.away_temperature
|
||||
self._away = self.structure.away == 'away'
|
||||
self._eco_temperature = self.device.eco_temperature
|
||||
self._locked_temperature = self.device.locked_temperature
|
||||
self._is_locked = self.device.is_locked
|
||||
if self.device.temperature == 'C':
|
||||
self._temperature_scale = TEMP_CELSIUS
|
||||
else:
|
||||
self._temperature_scale = TEMP_FAHRENHEIT
|
||||
|
||||
@@ -7,12 +7,13 @@ https://home-assistant.io/components/climate.proliphix/
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
||||
PRECISION_TENTHS, STATE_COOL, STATE_HEAT, STATE_IDLE,
|
||||
ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['proliphix==0.4.0']
|
||||
REQUIREMENTS = ['proliphix==0.4.1']
|
||||
|
||||
ATTR_FAN = 'fan'
|
||||
|
||||
@@ -60,6 +61,15 @@ class ProliphixThermostat(ClimateDevice):
|
||||
"""Return the name of the thermostat."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return the precision of the system.
|
||||
|
||||
Proliphix temperature values are passed back and forth in the
|
||||
API as tenths of degrees F (i.e. 690 for 69 degrees).
|
||||
"""
|
||||
return PRECISION_TENTHS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
|
||||
331
homeassistant/components/climate/wink.py
Normal file
331
homeassistant/components/climate/wink.py
Normal file
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
Support for Wink thermostats.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.wink/
|
||||
"""
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_CURRENT_HUMIDITY)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, STATE_ON,
|
||||
STATE_OFF, STATE_UNKNOWN)
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
STATE_AUX = 'aux'
|
||||
STATE_ECO = 'eco'
|
||||
|
||||
ATTR_EXTERNAL_TEMPERATURE = "external_temperature"
|
||||
ATTR_SMART_TEMPERATURE = "smart_temperature"
|
||||
ATTR_ECO_TARGET = "eco_target"
|
||||
ATTR_OCCUPIED = "occupied"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Wink thermostat."""
|
||||
import pywink
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
add_devices(WinkThermostat(thermostat, hass, temp_unit)
|
||||
for thermostat in pywink.get_thermostats())
|
||||
|
||||
|
||||
# pylint: disable=abstract-method,too-many-public-methods, too-many-branches
|
||||
class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink thermostat."""
|
||||
|
||||
def __init__(self, wink, hass, temp_unit):
|
||||
"""Initialize the Wink device."""
|
||||
super().__init__(wink, hass)
|
||||
wink = get_component('wink')
|
||||
self._config_temp_unit = temp_unit
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
# The Wink API always returns temp in Celsius
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
data = {}
|
||||
target_temp_high = self.target_temperature_high
|
||||
target_temp_low = self.target_temperature_low
|
||||
if target_temp_high is not None:
|
||||
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
|
||||
self.target_temperature_high)
|
||||
if target_temp_low is not None:
|
||||
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
|
||||
self.target_temperature_low)
|
||||
|
||||
if self.external_temperature:
|
||||
data[ATTR_EXTERNAL_TEMPERATURE] = self._convert_for_display(
|
||||
self.external_temperature)
|
||||
|
||||
if self.smart_temperature:
|
||||
data[ATTR_SMART_TEMPERATURE] = self.smart_temperature
|
||||
|
||||
if self.occupied:
|
||||
data[ATTR_OCCUPIED] = self.occupied
|
||||
|
||||
if self.eco_target:
|
||||
data[ATTR_ECO_TARGET] = self.eco_target
|
||||
|
||||
current_humidity = self.current_humidity
|
||||
if current_humidity is not None:
|
||||
data[ATTR_CURRENT_HUMIDITY] = current_humidity
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.wink.current_temperature()
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
if self.wink.current_humidity() is not None:
|
||||
# The API states humidity will be a float 0-1
|
||||
# the only example API response with humidity listed show an int
|
||||
# This will address both possibilities
|
||||
if self.wink.current_humidity() < 1:
|
||||
return self.wink.current_humidity() * 100
|
||||
else:
|
||||
return self.wink.current_humidity()
|
||||
|
||||
@property
|
||||
def external_temperature(self):
|
||||
"""Return the current external temperature."""
|
||||
return self.wink.current_external_temperature()
|
||||
|
||||
@property
|
||||
def smart_temperature(self):
|
||||
"""Return the current average temp of all remote sensor."""
|
||||
return self.wink.current_smart_temperature()
|
||||
|
||||
@property
|
||||
def eco_target(self):
|
||||
"""Return status of eco target (Is the termostat in eco mode)."""
|
||||
return self.wink.eco_target()
|
||||
|
||||
@property
|
||||
def occupied(self):
|
||||
"""Return status of if the thermostat has detected occupancy."""
|
||||
return self.wink.occupied()
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self.wink.is_on():
|
||||
current_op = STATE_OFF
|
||||
elif self.wink.current_hvac_mode() == 'cool_only':
|
||||
current_op = STATE_COOL
|
||||
elif self.wink.current_hvac_mode() == 'heat_only':
|
||||
current_op = STATE_HEAT
|
||||
elif self.wink.current_hvac_mode() == 'aux':
|
||||
current_op = STATE_HEAT
|
||||
elif self.wink.current_hvac_mode() == 'auto':
|
||||
current_op = STATE_AUTO
|
||||
elif self.wink.current_hvac_mode() == 'eco':
|
||||
current_op = STATE_ECO
|
||||
else:
|
||||
current_op = STATE_UNKNOWN
|
||||
return current_op
|
||||
|
||||
@property
|
||||
def target_humidity(self):
|
||||
"""Return the humidity we try to reach."""
|
||||
target_hum = None
|
||||
if self.wink.current_humidifier_mode() == 'on':
|
||||
if self.wink.current_humidifier_set_point() is not None:
|
||||
target_hum = self.wink.current_humidifier_set_point() * 100
|
||||
elif self.wink.current_dehumidifier_mode() == 'on':
|
||||
if self.wink.current_dehumidifier_set_point() is not None:
|
||||
target_hum = self.wink.current_dehumidifier_set_point() * 100
|
||||
else:
|
||||
target_hum = None
|
||||
return target_hum
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.current_operation != STATE_AUTO and not self.is_away_mode_on:
|
||||
if self.current_operation == STATE_COOL:
|
||||
return self.wink.current_max_set_point()
|
||||
elif self.current_operation == STATE_HEAT:
|
||||
return self.wink.current_min_set_point()
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return self.wink.current_min_set_point()
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the higher bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return self.wink.current_max_set_point()
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return if away mode is on."""
|
||||
return self.wink.away()
|
||||
|
||||
@property
|
||||
def is_aux_heat_on(self):
|
||||
"""Return true if aux heater."""
|
||||
if self.wink.current_hvac_mode() == 'aux' and self.wink.is_on():
|
||||
return True
|
||||
elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on():
|
||||
return False
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if target_temp is not None:
|
||||
if self.current_operation == STATE_COOL:
|
||||
target_temp_high = target_temp
|
||||
if self.current_operation == STATE_HEAT:
|
||||
target_temp_low = target_temp
|
||||
if target_temp_low is not None:
|
||||
target_temp_low = target_temp_low
|
||||
if target_temp_high is not None:
|
||||
target_temp_high = target_temp_high
|
||||
self.wink.set_temperature(target_temp_low, target_temp_high)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
self.wink.set_operation_mode('heat_only')
|
||||
elif operation_mode == STATE_COOL:
|
||||
self.wink.set_operation_mode('cool_only')
|
||||
elif operation_mode == STATE_AUTO:
|
||||
self.wink.set_operation_mode('auto')
|
||||
elif operation_mode == STATE_OFF:
|
||||
self.wink.set_operation_mode('off')
|
||||
elif operation_mode == STATE_AUX:
|
||||
self.wink.set_operation_mode('aux')
|
||||
elif operation_mode == STATE_ECO:
|
||||
self.wink.set_operation_mode('eco')
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
op_list = ['off']
|
||||
modes = self.wink.hvac_modes()
|
||||
if 'cool_only' in modes:
|
||||
op_list.append(STATE_COOL)
|
||||
if 'heat_only' in modes or 'aux' in modes:
|
||||
op_list.append(STATE_HEAT)
|
||||
if 'auto' in modes:
|
||||
op_list.append(STATE_AUTO)
|
||||
if 'eco' in modes:
|
||||
op_list.append(STATE_ECO)
|
||||
return op_list
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
self.wink.set_away_mode()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self.wink.set_away_mode(False)
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return whether the fan is on."""
|
||||
if self.wink.current_fan_mode() == 'on':
|
||||
return STATE_ON
|
||||
elif self.wink.current_fan_mode() == 'auto':
|
||||
return STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
if self.wink.has_fan():
|
||||
return self.wink.fan_modes()
|
||||
return None
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Turn fan on/off."""
|
||||
self.wink.set_fan_mode(fan.lower())
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
self.set_operation_mode(STATE_AUX)
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
self.set_operation_mode(STATE_AUTO)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
minimum = 7 # Default minimum
|
||||
min_min = self.wink.min_min_set_point()
|
||||
min_max = self.wink.min_max_set_point()
|
||||
return_value = minimum
|
||||
if self.current_operation == STATE_HEAT:
|
||||
if min_min:
|
||||
return_value = min_min
|
||||
else:
|
||||
return_value = minimum
|
||||
elif self.current_operation == STATE_COOL:
|
||||
if min_max:
|
||||
return_value = min_max
|
||||
else:
|
||||
return_value = minimum
|
||||
elif self.current_operation == STATE_AUTO:
|
||||
if min_min and min_max:
|
||||
return_value = min(min_min, min_max)
|
||||
else:
|
||||
return_value = minimum
|
||||
else:
|
||||
return_value = minimum
|
||||
return return_value
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
maximum = 35 # Default maximum
|
||||
max_min = self.wink.max_min_set_point()
|
||||
max_max = self.wink.max_max_set_point()
|
||||
return_value = maximum
|
||||
if self.current_operation == STATE_HEAT:
|
||||
if max_min:
|
||||
return_value = max_min
|
||||
else:
|
||||
return_value = maximum
|
||||
elif self.current_operation == STATE_COOL:
|
||||
if max_max:
|
||||
return_value = max_max
|
||||
else:
|
||||
return_value = maximum
|
||||
elif self.current_operation == STATE_AUTO:
|
||||
if max_min and max_max:
|
||||
return_value = min(max_min, max_max)
|
||||
else:
|
||||
return_value = maximum
|
||||
else:
|
||||
return_value = maximum
|
||||
return return_value
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for ZWave climate devices.
|
||||
Support for Z-Wave climate devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.zwave/
|
||||
@@ -8,8 +8,7 @@ https://home-assistant.io/components/climate.zwave/
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.climate import DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, ATTR_OPERATION_MODE)
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.const import (
|
||||
@@ -18,44 +17,23 @@ from homeassistant.const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NAME = 'name'
|
||||
DEFAULT_NAME = 'ZWave Climate'
|
||||
DEFAULT_NAME = 'Z-Wave Climate'
|
||||
|
||||
REMOTEC = 0x5254
|
||||
REMOTEC_ZXT_120 = 0x8377
|
||||
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120)
|
||||
|
||||
HORSTMANN = 0x0059
|
||||
HORSTMANN_HRT4_ZW = 0x3
|
||||
HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW)
|
||||
ATTR_OPERATING_STATE = 'operating_state'
|
||||
ATTR_FAN_STATE = 'fan_state'
|
||||
|
||||
WORKAROUND_ZXT_120 = 'zxt_120'
|
||||
WORKAROUND_HRT4_ZW = 'hrt4_zw'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120,
|
||||
HORSTMANN_HRT4_ZW_THERMOSTAT: WORKAROUND_HRT4_ZW
|
||||
}
|
||||
|
||||
SET_TEMP_TO_INDEX = {
|
||||
'Heat': 1,
|
||||
'Cool': 2,
|
||||
'Auto': 3,
|
||||
'Aux Heat': 4,
|
||||
'Resume': 5,
|
||||
'Fan Only': 6,
|
||||
'Furnace': 7,
|
||||
'Dry Air': 8,
|
||||
'Moist Air': 9,
|
||||
'Auto Changeover': 10,
|
||||
'Heat Econ': 11,
|
||||
'Cool Econ': 12,
|
||||
'Away': 13,
|
||||
'Unknown': 14
|
||||
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the ZWave Climate devices."""
|
||||
"""Set up the Z-Wave Climate devices."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
@@ -70,13 +48,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Represents a ZWave Climate device."""
|
||||
"""Representation of a Z-Wave Climate device."""
|
||||
|
||||
def __init__(self, value, temp_unit):
|
||||
"""Initialize the zwave climate device."""
|
||||
"""Initialize the Z-Wave climate device."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._index = value.index
|
||||
self._node = value.node
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
@@ -85,13 +64,12 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._operating_state = None
|
||||
self._current_fan_mode = None
|
||||
self._fan_list = None
|
||||
self._fan_state = None
|
||||
self._current_swing_mode = None
|
||||
self._swing_list = None
|
||||
self._unit = temp_unit
|
||||
self._index_operation = None
|
||||
_LOGGER.debug("temp_unit is %s", self._unit)
|
||||
self._zxt_120 = None
|
||||
self._hrt4_zw = None
|
||||
self.update_properties()
|
||||
# register listener
|
||||
dispatcher.connect(
|
||||
@@ -106,17 +84,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
|
||||
" workaround")
|
||||
self._zxt_120 = 1
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_HRT4_ZW:
|
||||
_LOGGER.debug("Horstmann HRT4-ZW Zwave Thermostat"
|
||||
" workaround")
|
||||
self._hrt4_zw = 1
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
self.update_properties()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
def update_properties(self):
|
||||
@@ -125,23 +99,23 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
self._current_operation = value.data
|
||||
self._index_operation = SET_TEMP_TO_INDEX.get(
|
||||
self._current_operation)
|
||||
self._operation_list = list(value.data_items)
|
||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||
_LOGGER.debug("self._current_operation=%s",
|
||||
self._current_operation)
|
||||
# Current Temp
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||
.values()):
|
||||
if value.label == 'Temperature':
|
||||
self._current_temperature = int(value.data)
|
||||
self._current_temperature = round((float(value.data)), 1)
|
||||
self._unit = value.units
|
||||
# Fan Mode
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||
.values()):
|
||||
self._current_fan_mode = value.data
|
||||
self._fan_list = list(value.data_items)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
@@ -149,9 +123,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._current_fan_mode)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||
.values()):
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_CONFIGURATION and \
|
||||
value.index == 33:
|
||||
@@ -161,35 +136,39 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("self._current_swing_mode=%s",
|
||||
self._current_swing_mode)
|
||||
# Set point
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
if value.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = int(self._current_temperature)
|
||||
break
|
||||
if self.current_operation is not None and \
|
||||
self.current_operation != 'Off':
|
||||
if self._index_operation != value.index:
|
||||
continue
|
||||
if self._zxt_120:
|
||||
temps = []
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
temps.append((round(float(value.data)), 1))
|
||||
if value.index == self._index:
|
||||
if value.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
break
|
||||
self._target_temperature = int(value.data)
|
||||
break
|
||||
_LOGGER.debug("Device can't set setpoint based on operation mode."
|
||||
" Defaulting to index=1")
|
||||
self._target_temperature = int(value.data)
|
||||
else:
|
||||
self._target_temperature = round((float(value.data)), 1)
|
||||
# Operating state
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const
|
||||
.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE).values()):
|
||||
self._operating_state = value.data
|
||||
|
||||
# Fan operating state
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE)
|
||||
.values()):
|
||||
self._fan_state = value.data
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling on ZWave."""
|
||||
"""No polling on Z-Wave."""
|
||||
return False
|
||||
|
||||
@property
|
||||
@@ -248,53 +227,19 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
else:
|
||||
return
|
||||
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
|
||||
_LOGGER.debug("set_temperature operation_mode=%s", operation_mode)
|
||||
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
if operation_mode is not None:
|
||||
setpoint_mode = SET_TEMP_TO_INDEX.get(operation_mode)
|
||||
if value.index != setpoint_mode:
|
||||
continue
|
||||
_LOGGER.debug("setpoint_mode=%s", setpoint_mode)
|
||||
value.data = temperature
|
||||
break
|
||||
|
||||
if self.current_operation is not None:
|
||||
if self._hrt4_zw and self.current_operation == 'Off':
|
||||
# HRT4-ZW can change setpoint when off.
|
||||
value.data = int(temperature)
|
||||
if self._index_operation != value.index:
|
||||
continue
|
||||
_LOGGER.debug("self._index_operation=%s and"
|
||||
" self._current_operation=%s",
|
||||
self._index_operation,
|
||||
self._current_operation)
|
||||
if value.index == self._index:
|
||||
if self._zxt_120:
|
||||
_LOGGER.debug("zxt_120: Setting new setpoint for %s, "
|
||||
" operation=%s, temp=%s",
|
||||
self._index_operation,
|
||||
self._current_operation, temperature)
|
||||
# ZXT-120 does not support get setpoint
|
||||
self._target_temperature = temperature
|
||||
# ZXT-120 responds only to whole int
|
||||
value.data = round(temperature, 0)
|
||||
self._target_temperature = temperature
|
||||
self.update_ha_state()
|
||||
break
|
||||
else:
|
||||
_LOGGER.debug("Setting new setpoint for %s, "
|
||||
"operation=%s, temp=%s",
|
||||
self._index_operation,
|
||||
self._current_operation, temperature)
|
||||
value.data = temperature
|
||||
break
|
||||
else:
|
||||
_LOGGER.debug("Setting new setpoint for no known "
|
||||
"operation mode. Index=1 and "
|
||||
"temperature=%s", temperature)
|
||||
value.data = temperature
|
||||
self.update_ha_state()
|
||||
break
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
@@ -331,9 +276,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
data = super().device_state_attributes
|
||||
if self._operating_state:
|
||||
return {
|
||||
"operating_state": self._operating_state,
|
||||
}
|
||||
else:
|
||||
return {}
|
||||
data[ATTR_OPERATING_STATE] = self._operating_state,
|
||||
if self._fan_state:
|
||||
data[ATTR_FAN_STATE] = self._fan_state
|
||||
return data
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.12.0']
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.14.0']
|
||||
|
||||
ATTR_TEXT = 'text'
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.cover import CoverDevice,\
|
||||
ATTR_POSITION
|
||||
import homeassistant.components.homematic as homematic
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,14 +25,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMCover,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
|
||||
|
||||
class HMCover(homematic.HMDevice, CoverDevice):
|
||||
class HMCover(HMDevice, CoverDevice):
|
||||
"""Represents a Homematic Cover in Home Assistant."""
|
||||
|
||||
@property
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.const import (
|
||||
@@ -89,29 +90,30 @@ class MqttCover(CoverDevice):
|
||||
self._retain = retain
|
||||
self._optimistic = optimistic or state_topic is None
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = value_template.render_with_possible_json_value(
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
if payload == self._state_open:
|
||||
self._state = False
|
||||
_LOGGER.warning("state=%s", int(self._state))
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload == self._state_closed:
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload.isnumeric() and 0 <= int(payload) <= 100:
|
||||
if int(payload) > 0:
|
||||
self._state = False
|
||||
else:
|
||||
self._state = True
|
||||
self._position = int(payload)
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Payload is not True, False, or integer (0-100): %s",
|
||||
payload)
|
||||
|
||||
if self._state_topic is None:
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
|
||||
@@ -18,7 +18,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors platform for covers."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
for gateway in mysensors.GATEWAYS.values():
|
||||
|
||||
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||
if not gateways:
|
||||
return
|
||||
|
||||
for gateway in gateways:
|
||||
pres = gateway.const.Presentation
|
||||
set_req = gateway.const.SetReq
|
||||
map_sv_types = {
|
||||
|
||||
@@ -15,18 +15,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Wink cover platform."""
|
||||
import pywink
|
||||
|
||||
add_devices(WinkCoverDevice(shade) for shade in
|
||||
add_devices(WinkCoverDevice(shade, hass) for shade in
|
||||
pywink.get_shades())
|
||||
add_devices(WinkCoverDevice(door) for door in
|
||||
add_devices(WinkCoverDevice(door, hass) for door in
|
||||
pywink.get_garage_doors())
|
||||
|
||||
|
||||
class WinkCoverDevice(WinkDevice, CoverDevice):
|
||||
"""Representation of a Wink cover device."""
|
||||
|
||||
def __init__(self, wink):
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the cover."""
|
||||
WinkDevice.__init__(self, wink)
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the shade."""
|
||||
|
||||
@@ -36,15 +36,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
and value.index == 0):
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveRollershutter(value)])
|
||||
elif value.node.specific == zwave.const.GENERIC_TYPE_ENTRY_CONTROL:
|
||||
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
||||
value.command_class ==
|
||||
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
if (value.type != zwave.const.TYPE_BOOL and
|
||||
value.genre != zwave.const.GENRE_USER):
|
||||
return
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
||||
value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
if (value.type != zwave.const.TYPE_BOOL and
|
||||
value.genre != zwave.const.GENRE_USER):
|
||||
return
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ DOMAIN = 'demo'
|
||||
COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
'alarm_control_panel',
|
||||
'binary_sensor',
|
||||
'calendar',
|
||||
'camera',
|
||||
'climate',
|
||||
'cover',
|
||||
@@ -85,16 +86,11 @@ def setup(hass, config):
|
||||
group.Group.create_group(hass, 'people', [
|
||||
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
|
||||
'device_tracker.demo_paulus'])
|
||||
group.Group.create_group(hass, 'thermostats', [
|
||||
'thermostat.nest', 'thermostat.thermostat'])
|
||||
group.Group.create_group(hass, 'downstairs', [
|
||||
'group.living_room', 'group.kitchen',
|
||||
'scene.romantic_lights', 'rollershutter.kitchen_window',
|
||||
'rollershutter.living_room_window', 'group.doors',
|
||||
'thermostat.nest',
|
||||
], view=True)
|
||||
group.Group.create_group(hass, 'Upstairs', [
|
||||
'thermostat.thermostat', 'group.bedroom',
|
||||
'thermostat.ecobee',
|
||||
], view=True)
|
||||
|
||||
# Setup scripts
|
||||
|
||||
@@ -9,6 +9,7 @@ from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
@@ -79,21 +80,22 @@ def setup(hass, config):
|
||||
return None
|
||||
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
|
||||
|
||||
def turn_light_on_before_sunset(light_id):
|
||||
def async_turn_on_before_sunset(light_id):
|
||||
"""Helper function to turn on lights.
|
||||
|
||||
Speed is slow if there are devices home and the light is not on yet.
|
||||
"""
|
||||
if not device_tracker.is_on(hass) or light.is_on(hass, light_id):
|
||||
return
|
||||
light.turn_on(hass, light_id,
|
||||
transition=LIGHT_TRANSITION_TIME.seconds,
|
||||
profile=light_profile)
|
||||
light.async_turn_on(hass, light_id,
|
||||
transition=LIGHT_TRANSITION_TIME.seconds,
|
||||
profile=light_profile)
|
||||
|
||||
# Track every time sun rises so we can schedule a time-based
|
||||
# pre-sun set event
|
||||
@track_state_change(sun.ENTITY_ID, sun.STATE_BELOW_HORIZON,
|
||||
sun.STATE_ABOVE_HORIZON)
|
||||
@callback
|
||||
def schedule_lights_at_sun_set(hass, entity, old_state, new_state):
|
||||
"""The moment sun sets we want to have all the lights on.
|
||||
|
||||
@@ -104,16 +106,21 @@ def setup(hass, config):
|
||||
if not start_point:
|
||||
return
|
||||
|
||||
def turn_on(light_id):
|
||||
def async_turn_on_factory(light_id):
|
||||
"""Lambda can keep track of function parameters.
|
||||
|
||||
No local parameters. If we put the lambda directly in the below
|
||||
statement only the last light will be turned on.
|
||||
"""
|
||||
return lambda now: turn_light_on_before_sunset(light_id)
|
||||
@callback
|
||||
def async_turn_on_light(now):
|
||||
"""Turn on specific light."""
|
||||
async_turn_on_before_sunset(light_id)
|
||||
|
||||
return async_turn_on_light
|
||||
|
||||
for index, light_id in enumerate(light_ids):
|
||||
track_point_in_time(hass, turn_on(light_id),
|
||||
track_point_in_time(hass, async_turn_on_factory(light_id),
|
||||
start_point + index * LIGHT_TRANSITION_TIME)
|
||||
|
||||
# If the sun is already above horizon schedule the time-based pre-sun set
|
||||
@@ -122,6 +129,7 @@ def setup(hass, config):
|
||||
schedule_lights_at_sun_set(hass, None, None, None)
|
||||
|
||||
@track_state_change(device_entity_ids, STATE_NOT_HOME, STATE_HOME)
|
||||
@callback
|
||||
def check_light_on_dev_state_change(hass, entity, old_state, new_state):
|
||||
"""Handle tracked device state changes."""
|
||||
# pylint: disable=unused-variable
|
||||
@@ -136,7 +144,7 @@ def setup(hass, config):
|
||||
# Do we need lights?
|
||||
if light_needed:
|
||||
logger.info("Home coming event for %s. Turning lights on", entity)
|
||||
light.turn_on(hass, light_ids, profile=light_profile)
|
||||
light.async_turn_on(hass, light_ids, profile=light_profile)
|
||||
|
||||
# Are we in the time span were we would turn on the lights
|
||||
# if someone would be home?
|
||||
@@ -149,7 +157,7 @@ def setup(hass, config):
|
||||
# when the fading in started and turn it on if so
|
||||
for index, light_id in enumerate(light_ids):
|
||||
if now > start_point + index * LIGHT_TRANSITION_TIME:
|
||||
light.turn_on(hass, light_id)
|
||||
light.async_turn_on(hass, light_id)
|
||||
|
||||
else:
|
||||
# If this light didn't happen to be turned on yet so
|
||||
@@ -158,6 +166,7 @@ def setup(hass, config):
|
||||
|
||||
if not disable_turn_off:
|
||||
@track_state_change(device_group, STATE_HOME, STATE_NOT_HOME)
|
||||
@callback
|
||||
def turn_off_lights_when_all_leave(hass, entity, old_state, new_state):
|
||||
"""Handle device group state change."""
|
||||
# pylint: disable=unused-variable
|
||||
@@ -166,6 +175,6 @@ def setup(hass, config):
|
||||
|
||||
logger.info(
|
||||
"Everyone has left but there are lights on. Turning them off")
|
||||
light.turn_off(hass, light_ids)
|
||||
light.async_turn_off(hass, light_ids)
|
||||
|
||||
return True
|
||||
|
||||
@@ -8,17 +8,20 @@ import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from typing import Any, Sequence, Callable
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import (
|
||||
prepare_setup_platform, log_exception)
|
||||
async_prepare_setup_platform, async_log_exception)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import group, zone
|
||||
from homeassistant.components.discovery import SERVICE_NETGEAR
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import config_per_platform, discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||
@@ -28,10 +31,10 @@ from homeassistant.util.async import run_coroutine_threadsafe
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.yaml import dump
|
||||
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.helpers.event import async_track_utc_time_change
|
||||
from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID)
|
||||
|
||||
DOMAIN = 'device_tracker'
|
||||
DEPENDENCIES = ['zone']
|
||||
@@ -106,14 +109,15 @@ def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
|
||||
hass.services.call(DOMAIN, SERVICE_SEE, data)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistantType, config: ConfigType):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
"""Setup device tracker."""
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
|
||||
try:
|
||||
conf = config.get(DOMAIN, [])
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, DOMAIN, config, hass)
|
||||
async_log_exception(ex, DOMAIN, config, hass)
|
||||
return False
|
||||
else:
|
||||
conf = conf[0] if len(conf) > 0 else {}
|
||||
@@ -121,60 +125,77 @@ def setup(hass: HomeAssistantType, config: ConfigType):
|
||||
timedelta(seconds=DEFAULT_CONSIDER_HOME))
|
||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||
|
||||
devices = load_config(yaml_path, hass, consider_home)
|
||||
|
||||
devices = yield from async_load_config(yaml_path, hass, consider_home)
|
||||
tracker = DeviceTracker(hass, consider_home, track_new, devices)
|
||||
|
||||
def setup_platform(p_type, p_config, disc_info=None):
|
||||
# update tracked devices
|
||||
update_tasks = [device.async_update_ha_state() for device in devices
|
||||
if device.track]
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(p_type, p_config, disc_info=None):
|
||||
"""Setup a device tracker platform."""
|
||||
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
|
||||
platform = yield from async_prepare_setup_platform(
|
||||
hass, config, DOMAIN, p_type)
|
||||
if platform is None:
|
||||
return
|
||||
|
||||
try:
|
||||
if hasattr(platform, 'get_scanner'):
|
||||
scanner = platform.get_scanner(hass, {DOMAIN: p_config})
|
||||
scanner = yield from hass.loop.run_in_executor(
|
||||
None, platform.get_scanner, hass, {DOMAIN: p_config})
|
||||
|
||||
if scanner is None:
|
||||
_LOGGER.error('Error setting up platform %s', p_type)
|
||||
return
|
||||
|
||||
setup_scanner_platform(hass, p_config, scanner, tracker.see)
|
||||
yield from async_setup_scanner_platform(
|
||||
hass, p_config, scanner, tracker.async_see)
|
||||
return
|
||||
|
||||
if not platform.setup_scanner(hass, p_config, tracker.see):
|
||||
ret = yield from hass.loop.run_in_executor(
|
||||
None, platform.setup_scanner, hass, p_config, tracker.see)
|
||||
if not ret:
|
||||
_LOGGER.error('Error setting up platform %s', p_type)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error setting up platform %s', p_type)
|
||||
|
||||
for p_type, p_config in config_per_platform(config, DOMAIN):
|
||||
setup_platform(p_type, p_config)
|
||||
setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config
|
||||
in config_per_platform(config, DOMAIN)]
|
||||
if setup_tasks:
|
||||
yield from asyncio.wait(setup_tasks, loop=hass.loop)
|
||||
|
||||
def device_tracker_discovered(service, info):
|
||||
yield from tracker.async_setup_group()
|
||||
|
||||
@callback
|
||||
def async_device_tracker_discovered(service, info):
|
||||
"""Called when a device tracker platform is discovered."""
|
||||
setup_platform(DISCOVERY_PLATFORMS[service], {}, info)
|
||||
hass.async_add_job(
|
||||
async_setup_platform(DISCOVERY_PLATFORMS[service], {}, info))
|
||||
|
||||
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(),
|
||||
device_tracker_discovered)
|
||||
discovery.async_listen(
|
||||
hass, DISCOVERY_PLATFORMS.keys(), async_device_tracker_discovered)
|
||||
|
||||
def update_stale(now):
|
||||
"""Clean up stale devices."""
|
||||
tracker.update_stale(now)
|
||||
track_utc_time_change(hass, update_stale, second=range(0, 60, 5))
|
||||
# Clean up stale devices
|
||||
async_track_utc_time_change(
|
||||
hass, tracker.async_update_stale, second=range(0, 60, 5))
|
||||
|
||||
tracker.setup_group()
|
||||
|
||||
def see_service(call):
|
||||
@asyncio.coroutine
|
||||
def async_see_service(call):
|
||||
"""Service to see a device."""
|
||||
args = {key: value for key, value in call.data.items() if key in
|
||||
(ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME,
|
||||
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
|
||||
tracker.see(**args)
|
||||
yield from tracker.async_see(**args)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_SEE, see_service,
|
||||
descriptions.get(SERVICE_SEE))
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE))
|
||||
|
||||
return True
|
||||
|
||||
@@ -188,91 +209,119 @@ class DeviceTracker(object):
|
||||
self.hass = hass
|
||||
self.devices = {dev.dev_id: dev for dev in devices}
|
||||
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
||||
self.consider_home = consider_home
|
||||
self.track_new = track_new
|
||||
self.group = None # type: group.Group
|
||||
self._is_updating = asyncio.Lock(loop=hass.loop)
|
||||
|
||||
for dev in devices:
|
||||
if self.devices[dev.dev_id] is not dev:
|
||||
_LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id)
|
||||
if dev.mac and self.mac_to_dev[dev.mac] is not dev:
|
||||
_LOGGER.warning('Duplicate device MAC addresses detected %s',
|
||||
dev.mac)
|
||||
self.consider_home = consider_home
|
||||
self.track_new = track_new
|
||||
self.lock = threading.Lock()
|
||||
|
||||
for device in devices:
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
|
||||
self.group = None # type: group.Group
|
||||
|
||||
def see(self, mac: str=None, dev_id: str=None, host_name: str=None,
|
||||
location_name: str=None, gps: GPSType=None, gps_accuracy=None,
|
||||
battery: str=None, attributes: dict=None):
|
||||
"""Notify the device tracker that you see a device."""
|
||||
with self.lock:
|
||||
if mac is None and dev_id is None:
|
||||
raise HomeAssistantError('Neither mac or device id passed in')
|
||||
elif mac is not None:
|
||||
mac = str(mac).upper()
|
||||
device = self.mac_to_dev.get(mac)
|
||||
if not device:
|
||||
dev_id = util.slugify(host_name or '') or util.slugify(mac)
|
||||
else:
|
||||
dev_id = cv.slug(str(dev_id).lower())
|
||||
device = self.devices.get(dev_id)
|
||||
self.hass.add_job(
|
||||
self.async_see(mac, dev_id, host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
)
|
||||
|
||||
if device:
|
||||
device.seen(host_name, location_name, gps, gps_accuracy,
|
||||
battery, attributes)
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
return
|
||||
@asyncio.coroutine
|
||||
def async_see(self, mac: str=None, dev_id: str=None, host_name: str=None,
|
||||
location_name: str=None, gps: GPSType=None,
|
||||
gps_accuracy=None, battery: str=None, attributes: dict=None):
|
||||
"""Notify the device tracker that you see a device.
|
||||
|
||||
# If no device can be found, create it
|
||||
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
|
||||
device = Device(
|
||||
self.hass, self.consider_home, self.track_new,
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '))
|
||||
self.devices[dev_id] = device
|
||||
if mac is not None:
|
||||
self.mac_to_dev[mac] = device
|
||||
|
||||
device.seen(host_name, location_name, gps, gps_accuracy, battery,
|
||||
attributes)
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if mac is None and dev_id is None:
|
||||
raise HomeAssistantError('Neither mac or device id passed in')
|
||||
elif mac is not None:
|
||||
mac = str(mac).upper()
|
||||
device = self.mac_to_dev.get(mac)
|
||||
if not device:
|
||||
dev_id = util.slugify(host_name or '') or util.slugify(mac)
|
||||
else:
|
||||
dev_id = cv.slug(str(dev_id).lower())
|
||||
device = self.devices.get(dev_id)
|
||||
|
||||
if device:
|
||||
yield from device.async_seen(host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
yield from device.async_update_ha_state()
|
||||
return
|
||||
|
||||
self.hass.bus.async_fire(EVENT_NEW_DEVICE, device)
|
||||
# If no device can be found, create it
|
||||
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
|
||||
device = Device(
|
||||
self.hass, self.consider_home, self.track_new,
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '))
|
||||
self.devices[dev_id] = device
|
||||
if mac is not None:
|
||||
self.mac_to_dev[mac] = device
|
||||
|
||||
# During init, we ignore the group
|
||||
if self.group is not None:
|
||||
self.group.update_tracked_entity_ids(
|
||||
list(self.group.tracking) + [device.entity_id])
|
||||
update_config(self.hass.config.path(YAML_DEVICES), dev_id, device)
|
||||
yield from device.async_seen(host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
|
||||
def setup_group(self):
|
||||
"""Initialize group for all tracked devices."""
|
||||
run_coroutine_threadsafe(
|
||||
self.async_setup_group(), self.hass.loop).result()
|
||||
if device.track:
|
||||
yield from device.async_update_ha_state()
|
||||
|
||||
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
||||
ATTR_ENTITY_ID: device.entity_id,
|
||||
ATTR_HOST_NAME: device.host_name,
|
||||
})
|
||||
|
||||
# During init, we ignore the group
|
||||
if self.group is not None:
|
||||
yield from self.group.async_update_tracked_entity_ids(
|
||||
list(self.group.tracking) + [device.entity_id])
|
||||
|
||||
# lookup mac vendor string to be stored in config
|
||||
device.set_vendor_for_mac()
|
||||
|
||||
# update known_devices.yaml
|
||||
self.hass.async_add_job(
|
||||
self.async_update_config(self.hass.config.path(YAML_DEVICES),
|
||||
dev_id, device)
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_config(self, path, dev_id, device):
|
||||
"""Add device to YAML configuration file.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
with (yield from self._is_updating):
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
None, update_config, self.hass.config.path(YAML_DEVICES),
|
||||
dev_id, device)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_group(self):
|
||||
"""Initialize group for all tracked devices.
|
||||
|
||||
This method must be run in the event loop.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
entity_ids = (dev.entity_id for dev in self.devices.values()
|
||||
if dev.track)
|
||||
self.group = yield from group.Group.async_create_group(
|
||||
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
|
||||
|
||||
def update_stale(self, now: dt_util.dt.datetime):
|
||||
"""Update stale devices."""
|
||||
with self.lock:
|
||||
for device in self.devices.values():
|
||||
if (device.track and device.last_update_home and
|
||||
device.stale(now)):
|
||||
device.update_ha_state(True)
|
||||
@callback
|
||||
def async_update_stale(self, now: dt_util.dt.datetime):
|
||||
"""Update stale devices.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
for device in self.devices.values():
|
||||
if (device.track and device.last_update_home) and \
|
||||
device.stale(now):
|
||||
self.hass.async_add_job(device.async_update_ha_state(True))
|
||||
|
||||
|
||||
class Device(Entity):
|
||||
@@ -285,6 +334,7 @@ class Device(Entity):
|
||||
last_seen = None # type: dt_util.dt.datetime
|
||||
battery = None # type: str
|
||||
attributes = None # type: dict
|
||||
vendor = None # type: str
|
||||
|
||||
# Track if the last update of this device was HOME.
|
||||
last_update_home = False
|
||||
@@ -293,7 +343,7 @@ class Device(Entity):
|
||||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||
track: bool, dev_id: str, mac: str, name: str=None,
|
||||
picture: str=None, gravatar: str=None,
|
||||
hide_if_away: bool=False) -> None:
|
||||
hide_if_away: bool=False, vendor: str=None) -> None:
|
||||
"""Initialize a device."""
|
||||
self.hass = hass
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||
@@ -319,6 +369,7 @@ class Device(Entity):
|
||||
self.config_picture = picture
|
||||
|
||||
self.away_hide = hide_if_away
|
||||
self.vendor = vendor
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -359,9 +410,10 @@ class Device(Entity):
|
||||
"""If device should be hidden."""
|
||||
return self.away_hide and self.state != STATE_HOME
|
||||
|
||||
def seen(self, host_name: str=None, location_name: str=None,
|
||||
gps: GPSType=None, gps_accuracy=0, battery: str=None,
|
||||
attributes: dict=None):
|
||||
@asyncio.coroutine
|
||||
def async_seen(self, host_name: str=None, location_name: str=None,
|
||||
gps: GPSType=None, gps_accuracy=0, battery: str=None,
|
||||
attributes: dict=None):
|
||||
"""Mark the device as seen."""
|
||||
self.last_seen = dt_util.utcnow()
|
||||
self.host_name = host_name
|
||||
@@ -370,28 +422,38 @@ class Device(Entity):
|
||||
self.battery = battery
|
||||
self.attributes = attributes
|
||||
self.gps = None
|
||||
|
||||
if gps is not None:
|
||||
try:
|
||||
self.gps = float(gps[0]), float(gps[1])
|
||||
except (ValueError, TypeError, IndexError):
|
||||
_LOGGER.warning('Could not parse gps value for %s: %s',
|
||||
self.dev_id, gps)
|
||||
self.update()
|
||||
|
||||
# pylint: disable=not-an-iterable
|
||||
yield from self.async_update()
|
||||
|
||||
def stale(self, now: dt_util.dt.datetime=None):
|
||||
"""Return if device state is stale."""
|
||||
"""Return if device state is stale.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return self.last_seen and \
|
||||
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
|
||||
|
||||
def update(self):
|
||||
"""Update state of entity."""
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update state of entity.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not self.last_seen:
|
||||
return
|
||||
elif self.location_name:
|
||||
self._state = self.location_name
|
||||
elif self.gps is not None:
|
||||
zone_state = zone.active_zone(self.hass, self.gps[0], self.gps[1],
|
||||
self.gps_accuracy)
|
||||
zone_state = zone.async_active_zone(
|
||||
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
|
||||
if zone_state is None:
|
||||
self._state = STATE_NOT_HOME
|
||||
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
|
||||
@@ -406,9 +468,67 @@ class Device(Entity):
|
||||
self._state = STATE_HOME
|
||||
self.last_update_home = True
|
||||
|
||||
@asyncio.coroutine
|
||||
def set_vendor_for_mac(self):
|
||||
"""Set vendor string using api.macvendors.com."""
|
||||
self.vendor = yield from self.get_vendor_for_mac()
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_vendor_for_mac(self):
|
||||
"""Try to find the vendor string for a given MAC address."""
|
||||
# can't continue without a mac
|
||||
if not self.mac:
|
||||
return None
|
||||
|
||||
# prevent lookup of invalid macs
|
||||
if not len(self.mac.split(':')) == 6:
|
||||
return 'unknown'
|
||||
|
||||
# we only need the first 3 bytes of the mac for a lookup
|
||||
# this improves somewhat on privacy
|
||||
oui_bytes = self.mac.split(':')[0:3]
|
||||
# bytes like 00 get truncates to 0, API needs full bytes
|
||||
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
|
||||
url = 'http://api.macvendors.com/' + oui
|
||||
resp = None
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
|
||||
with async_timeout.timeout(5, loop=self.hass.loop):
|
||||
resp = yield from websession.get(url)
|
||||
# mac vendor found, response is the string
|
||||
if resp.status == 200:
|
||||
vendor_string = yield from resp.text()
|
||||
return vendor_string
|
||||
# if vendor is not known to the API (404) or there
|
||||
# was a failure during the lookup (500); set vendor
|
||||
# to something other then None to prevent retry
|
||||
# as the value is only relevant when it is to be stored
|
||||
# in the 'known_devices.yaml' file which only happens
|
||||
# the first time the device is seen.
|
||||
return 'unknown'
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError,
|
||||
aiohttp.errors.ClientDisconnectedError):
|
||||
# same as above
|
||||
return 'unknown'
|
||||
finally:
|
||||
if resp is not None:
|
||||
yield from resp.release()
|
||||
|
||||
|
||||
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
"""Load devices from YAML configuration file."""
|
||||
return run_coroutine_threadsafe(
|
||||
async_load_config(path, hass, consider_home), hass.loop).result()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_load_config(path: str, hass: HomeAssistantType,
|
||||
consider_home: timedelta):
|
||||
"""Load devices from YAML configuration file.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
dev_schema = vol.Schema({
|
||||
vol.Required('name'): cv.string,
|
||||
vol.Optional('track', default=False): cv.boolean,
|
||||
@@ -418,12 +538,14 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
vol.Optional('gravatar', default=None): vol.Any(None, cv.string),
|
||||
vol.Optional('picture', default=None): vol.Any(None, cv.string),
|
||||
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional('vendor', default=None): vol.Any(None, cv.string),
|
||||
})
|
||||
try:
|
||||
result = []
|
||||
try:
|
||||
devices = load_yaml_config_file(path)
|
||||
devices = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error('Unable to load %s: %s', path, str(err))
|
||||
return []
|
||||
@@ -433,7 +555,7 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
device = dev_schema(device)
|
||||
device['dev_id'] = cv.slugify(dev_id)
|
||||
except vol.Invalid as exp:
|
||||
log_exception(exp, dev_id, devices, hass)
|
||||
async_log_exception(exp, dev_id, devices, hass)
|
||||
else:
|
||||
result.append(Device(hass, **device))
|
||||
return result
|
||||
@@ -442,9 +564,13 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
return []
|
||||
|
||||
|
||||
def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
scanner: Any, see_device: Callable):
|
||||
"""Helper method to connect scanner-based platform to device tracker."""
|
||||
@asyncio.coroutine
|
||||
def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
scanner: Any, async_see_device: Callable):
|
||||
"""Helper method to connect scanner-based platform to device tracker.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
# Initial scan of each mac we also tell about host name for config
|
||||
@@ -452,18 +578,20 @@ def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
|
||||
def device_tracker_scan(now: dt_util.dt.datetime):
|
||||
"""Called when interval matches."""
|
||||
for mac in scanner.scan_devices():
|
||||
found_devices = scanner.scan_devices()
|
||||
|
||||
for mac in found_devices:
|
||||
if mac in seen:
|
||||
host_name = None
|
||||
else:
|
||||
host_name = scanner.get_device_name(mac)
|
||||
seen.add(mac)
|
||||
see_device(mac=mac, host_name=host_name)
|
||||
hass.add_job(async_see_device(mac=mac, host_name=host_name))
|
||||
|
||||
track_utc_time_change(hass, device_tracker_scan, second=range(0, 60,
|
||||
interval))
|
||||
async_track_utc_time_change(
|
||||
hass, device_tracker_scan, second=range(0, 60, interval))
|
||||
|
||||
device_tracker_scan(None)
|
||||
hass.async_add_job(device_tracker_scan, None)
|
||||
|
||||
|
||||
def update_config(path: str, dev_id: str, device: Device):
|
||||
@@ -474,14 +602,18 @@ def update_config(path: str, dev_id: str, device: Device):
|
||||
'mac': device.mac,
|
||||
'picture': device.config_picture,
|
||||
'track': device.track,
|
||||
CONF_AWAY_HIDE: device.away_hide
|
||||
CONF_AWAY_HIDE: device.away_hide,
|
||||
'vendor': device.vendor,
|
||||
}}
|
||||
out.write('\n')
|
||||
out.write(dump(device))
|
||||
|
||||
|
||||
def get_gravatar_for_email(email: str):
|
||||
"""Return an 80px Gravatar for the given email address."""
|
||||
"""Return an 80px Gravatar for the given email address.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
import hashlib
|
||||
url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar'
|
||||
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())
|
||||
|
||||
@@ -42,6 +42,7 @@ def get_scanner(hass, config):
|
||||
scanner = ActiontecDeviceScanner(config[DOMAIN])
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
Device = namedtuple("Device", ["mac", "ip", "last_update"])
|
||||
|
||||
|
||||
|
||||
@@ -76,6 +76,15 @@ _IP_NEIGH_REGEX = re.compile(
|
||||
r'(\w+\s(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' +
|
||||
r'(?P<status>(\w+))')
|
||||
|
||||
_NVRAM_CMD = 'nvram get client_info_tmp'
|
||||
_NVRAM_REGEX = re.compile(
|
||||
r'.*>.*>' +
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
|
||||
r'>' +
|
||||
r'(?P<mac>(([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})))' +
|
||||
r'>' +
|
||||
r'.*')
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
@@ -84,7 +93,8 @@ def get_scanner(hass, config):
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp')
|
||||
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram')
|
||||
|
||||
|
||||
class AsusWrtDeviceScanner(object):
|
||||
@@ -155,7 +165,8 @@ class AsusWrtDeviceScanner(object):
|
||||
active_clients = [client for client in data.values() if
|
||||
client['status'] == 'REACHABLE' or
|
||||
client['status'] == 'DELAY' or
|
||||
client['status'] == 'STALE']
|
||||
client['status'] == 'STALE' or
|
||||
client['status'] == 'IN_NVRAM']
|
||||
self.last_results = active_clients
|
||||
return True
|
||||
|
||||
@@ -184,13 +195,18 @@ class AsusWrtDeviceScanner(object):
|
||||
ssh.sendline(_WL_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.sendline(_NVRAM_CMD)
|
||||
ssh.prompt()
|
||||
nvram_result = ssh.before.split(b'\n')[1].split(b'<')[1:]
|
||||
else:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
ssh.sendline(_LEASES_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.logout()
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result)
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
except pxssh.ExceptionPxssh as exc:
|
||||
_LOGGER.error('Unexpected response from router: %s', exc)
|
||||
return None
|
||||
@@ -213,13 +229,18 @@ class AsusWrtDeviceScanner(object):
|
||||
telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
|
||||
leases_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii'))
|
||||
nvram_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1].split(b'<')[1:])
|
||||
else:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||
leases_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
telnet.write('exit\n'.encode('ascii'))
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result)
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
except EOFError:
|
||||
_LOGGER.error('Unexpected response from router')
|
||||
return None
|
||||
@@ -277,6 +298,26 @@ class AsusWrtDeviceScanner(object):
|
||||
'ip': arp_match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
# match mac addresses to IP addresses in NVRAM table
|
||||
for nvr in result.nvram:
|
||||
if match.group('mac').upper() in nvr.decode('utf-8'):
|
||||
nvram_match = _NVRAM_REGEX.search(nvr.decode('utf-8'))
|
||||
if not nvram_match:
|
||||
_LOGGER.warning('Could not parse nvr row: %s', nvr)
|
||||
continue
|
||||
|
||||
# skip current check if already in ARP table
|
||||
if nvram_match.group('ip') in devices.keys():
|
||||
continue
|
||||
|
||||
devices[nvram_match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': 'IN_NVRAM',
|
||||
'ip': nvram_match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
else:
|
||||
for lease in result.leases:
|
||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||
|
||||
162
homeassistant/components/device_tracker/cisco_ios.py
Normal file
162
homeassistant/components/device_tracker/cisco_ios.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
Support for Cisco IOS Routers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.cisco_ios/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \
|
||||
CONF_PORT
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=''): cv.string,
|
||||
vol.Optional(CONF_PORT): cv.port,
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Cisco scanner."""
|
||||
scanner = CiscoDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class CiscoDeviceScanner(object):
|
||||
"""This class queries a wireless router running Cisco IOS firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.port = config.get(CONF_PORT)
|
||||
self.password = config.get(CONF_PASSWORD)
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
self.success_init = self._update_info()
|
||||
_LOGGER.info('cisco_ios scanner initialized')
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""The firmware doesn't save the name of the wireless device."""
|
||||
return None
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return self.last_results
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""
|
||||
Ensure the information from the Cisco router is up to date.
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
string_result = self._get_arp_data()
|
||||
|
||||
if string_result:
|
||||
self.last_results = []
|
||||
last_results = []
|
||||
|
||||
lines_result = string_result.splitlines()
|
||||
|
||||
# Remove the first two lines, as they contains the arp command
|
||||
# and the arp table titles e.g.
|
||||
# show ip arp
|
||||
# Protocol Address | Age (min) | Hardware Addr | Type | Interface
|
||||
lines_result = lines_result[2:]
|
||||
|
||||
for line in lines_result:
|
||||
if len(line.split()) is 6:
|
||||
parts = line.split()
|
||||
if len(parts) != 6:
|
||||
continue
|
||||
|
||||
# ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA',
|
||||
# 'GigabitEthernet0']
|
||||
age = parts[2]
|
||||
hw_addr = parts[3]
|
||||
|
||||
if age != "-":
|
||||
mac = _parse_cisco_mac_address(hw_addr)
|
||||
age = int(age)
|
||||
if age < 1:
|
||||
last_results.append(mac)
|
||||
|
||||
self.last_results = last_results
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _get_arp_data(self):
|
||||
"""Open connection to the router and get arp entries."""
|
||||
from pexpect import pxssh
|
||||
import re
|
||||
|
||||
try:
|
||||
cisco_ssh = pxssh.pxssh()
|
||||
cisco_ssh.login(self.host, self.username, self.password,
|
||||
port=self.port, auto_prompt_reset=False)
|
||||
|
||||
# Find the hostname
|
||||
initial_line = cisco_ssh.before.decode('utf-8').splitlines()
|
||||
router_hostname = initial_line[len(initial_line) - 1]
|
||||
router_hostname += "#"
|
||||
# Set the discovered hostname as prompt
|
||||
regex_expression = ('(?i)^%s' % router_hostname).encode()
|
||||
cisco_ssh.PROMPT = re.compile(regex_expression, re.MULTILINE)
|
||||
# Allow full arp table to print at once
|
||||
cisco_ssh.sendline("terminal length 0")
|
||||
cisco_ssh.prompt(1)
|
||||
|
||||
cisco_ssh.sendline("show ip arp")
|
||||
cisco_ssh.prompt(1)
|
||||
|
||||
devices_result = cisco_ssh.before
|
||||
|
||||
return devices_result.decode("utf-8")
|
||||
except pxssh.ExceptionPxssh as px_e:
|
||||
_LOGGER.error("pxssh failed on login.")
|
||||
_LOGGER.error(px_e)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _parse_cisco_mac_address(cisco_hardware_addr):
|
||||
"""
|
||||
Parse a Cisco formatted HW address to normal MAC.
|
||||
|
||||
e.g. convert
|
||||
001d.ec02.07ab
|
||||
|
||||
to:
|
||||
00:1D:EC:02:07:AB
|
||||
|
||||
Takes in cisco_hwaddr: HWAddr String from Cisco ARP table
|
||||
Returns a regular standard MAC address
|
||||
"""
|
||||
cisco_hardware_addr = cisco_hardware_addr.replace('.', '')
|
||||
blocks = [cisco_hardware_addr[x:x + 2]
|
||||
for x in range(0, len(cisco_hardware_addr), 2)]
|
||||
|
||||
return ':'.join(blocks).upper()
|
||||
72
homeassistant/components/device_tracker/gpslogger.py
Normal file
72
homeassistant/components/device_tracker/gpslogger.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
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 asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
# pylint: disable=unused-import
|
||||
from homeassistant.components.device_tracker import ( # NOQA
|
||||
DOMAIN, PLATFORM_SCHEMA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Setup an endpoint for the GPSLogger application."""
|
||||
hass.http.register_view(GPSLoggerView(see))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class GPSLoggerView(HomeAssistantView):
|
||||
"""View to handle gpslogger requests."""
|
||||
|
||||
url = '/api/gpslogger'
|
||||
name = 'api:gpslogger'
|
||||
|
||||
def __init__(self, see):
|
||||
"""Initialize GPSLogger url endpoints."""
|
||||
self.see = see
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""A GPSLogger message received as GET."""
|
||||
res = yield from self._handle(request.app['hass'], request.GET)
|
||||
return res
|
||||
|
||||
@asyncio.coroutine
|
||||
def _handle(self, hass, data):
|
||||
"""Handle gpslogger request."""
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
return ('Latitude and longitude not specified.',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if 'device' not in data:
|
||||
_LOGGER.error('Device id not specified.')
|
||||
return ('Device id not specified.',
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
device = data['device'].replace('-', '')
|
||||
gps_location = (data['latitude'], data['longitude'])
|
||||
accuracy = 200
|
||||
battery = -1
|
||||
|
||||
if 'accuracy' in data:
|
||||
accuracy = int(float(data['accuracy']))
|
||||
if 'battery' in data:
|
||||
battery = float(data['battery'])
|
||||
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, partial(self.see, dev_id=device,
|
||||
gps=gps_location, battery=battery,
|
||||
gps_accuracy=accuracy))
|
||||
|
||||
return 'Setting location for {}'.format(device)
|
||||
@@ -8,7 +8,9 @@ import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
|
||||
from homeassistant.const import (ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
STATE_NOT_HOME,
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
# pylint: disable=unused-import
|
||||
from homeassistant.components.device_tracker import ( # NOQA
|
||||
@@ -21,7 +23,7 @@ DEPENDENCIES = ['http']
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Setup an endpoint for the Locative application."""
|
||||
hass.http.register_view(LocativeView(hass, see))
|
||||
hass.http.register_view(LocativeView(see))
|
||||
|
||||
return True
|
||||
|
||||
@@ -32,27 +34,26 @@ class LocativeView(HomeAssistantView):
|
||||
url = '/api/locative'
|
||||
name = 'api:locative'
|
||||
|
||||
def __init__(self, hass, see):
|
||||
def __init__(self, see):
|
||||
"""Initialize Locative url endpoints."""
|
||||
super().__init__(hass)
|
||||
self.see = see
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Locative message received as GET."""
|
||||
res = yield from self._handle(request.GET)
|
||||
res = yield from self._handle(request.app['hass'], request.GET)
|
||||
return res
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Locative message received."""
|
||||
data = yield from request.post()
|
||||
res = yield from self._handle(data)
|
||||
res = yield from self._handle(request.app['hass'], data)
|
||||
return res
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=too-many-return-statements
|
||||
def _handle(self, data):
|
||||
def _handle(self, hass, data):
|
||||
"""Handle locative request."""
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
return ('Latitude and longitude not specified.',
|
||||
@@ -76,21 +77,25 @@ class LocativeView(HomeAssistantView):
|
||||
device = data['device'].replace('-', '')
|
||||
location_name = data['id'].lower()
|
||||
direction = data['trigger']
|
||||
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
|
||||
|
||||
if direction == 'enter':
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, partial(self.see, dev_id=device,
|
||||
location_name=location_name))
|
||||
location_name=location_name,
|
||||
gps=gps_location))
|
||||
return 'Setting location to {}'.format(location_name)
|
||||
|
||||
elif direction == 'exit':
|
||||
current_state = self.hass.states.get(
|
||||
current_state = hass.states.get(
|
||||
'{}.{}'.format(DOMAIN, device))
|
||||
|
||||
if current_state is None or current_state.state == location_name:
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
location_name = STATE_NOT_HOME
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, partial(self.see, dev_id=device,
|
||||
location_name=STATE_NOT_HOME))
|
||||
location_name=location_name,
|
||||
gps=gps_location))
|
||||
return 'Setting location to not home'
|
||||
else:
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Support for scanning a network with nmap.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.nmap_scanner/
|
||||
https://home-assistant.io/components/device_tracker.nmap_tracker/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
@@ -43,6 +43,7 @@ def get_scanner(hass, config):
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update'])
|
||||
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ def setup_scanner(hass, config, see):
|
||||
data_type, max_gps_accuracy, payload)
|
||||
return None
|
||||
if convert(data.get('acc'), float, 1.0) == 0.0:
|
||||
_LOGGER.warning('Ignoring %s update because GPS accuracy'
|
||||
_LOGGER.warning('Ignoring %s update because GPS accuracy '
|
||||
'is zero: %s',
|
||||
data_type, payload)
|
||||
return None
|
||||
|
||||
108
homeassistant/components/device_tracker/swisscom.py
Normal file
108
homeassistant/components/device_tracker/swisscom.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Support for Swisscom routers (Internet-Box).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.swisscom/
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_IP = '192.168.1.1'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Return the Swisscom device scanner."""
|
||||
scanner = SwisscomDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
class SwisscomDeviceScanner(object):
|
||||
"""This class queries a router running Swisscom Internet-Box firmware."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.last_results = {}
|
||||
|
||||
# Test the router is accessible.
|
||||
data = self.get_swisscom_data()
|
||||
self.success_init = data is not None
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return [client['mac'] for client in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if not self.last_results:
|
||||
return None
|
||||
for client in self.last_results:
|
||||
if client['mac'] == device:
|
||||
return client['host']
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the Swisscom router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info("Loading data from Swisscom Internet Box")
|
||||
data = self.get_swisscom_data()
|
||||
if not data:
|
||||
return False
|
||||
|
||||
active_clients = [client for client in data.values() if
|
||||
client['status']]
|
||||
self.last_results = active_clients
|
||||
return True
|
||||
|
||||
def get_swisscom_data(self):
|
||||
"""Retrieve data from Swisscom and return parsed result."""
|
||||
url = 'http://{}/ws'.format(self.host)
|
||||
headers = {'Content-Type': 'application/x-sah-ws-4-call+json'}
|
||||
data = """
|
||||
{"service":"Devices", "method":"get",
|
||||
"parameters":{"expression":"lan and not self"}}"""
|
||||
|
||||
request = requests.post(url, headers=headers, data=data, timeout=10)
|
||||
|
||||
devices = {}
|
||||
for device in request.json()['status']:
|
||||
try:
|
||||
devices[device['Key']] = {
|
||||
'ip': device['IPAddress'],
|
||||
'mac': device['PhysAddress'],
|
||||
'host': device['Name'],
|
||||
'status': device['Active']
|
||||
}
|
||||
except (KeyError, requests.exceptions.RequestException):
|
||||
pass
|
||||
return devices
|
||||
@@ -55,25 +55,30 @@ def setup_scanner(hass, config, see):
|
||||
"""True if any door/window is opened."""
|
||||
return any([door[key] for key in door if "Open" in key])
|
||||
|
||||
attributes = dict(
|
||||
unlocked=not vehicle["carLocked"],
|
||||
tank_volume=vehicle["fuelTankVolume"],
|
||||
average_fuel_consumption=round(
|
||||
vehicle["averageFuelConsumption"] / 10, 1), # l/100km
|
||||
washer_fluid_low=vehicle["washerFluidLevel"] != "Normal",
|
||||
brake_fluid_low=vehicle["brakeFluid"] != "Normal",
|
||||
service_warning=vehicle["serviceWarningStatus"] != "Normal",
|
||||
bulb_failures=len(vehicle["bulbFailures"]) > 0,
|
||||
doors_open=any_opened(vehicle["doors"]),
|
||||
windows_open=any_opened(vehicle["windows"]),
|
||||
fuel=vehicle["fuelAmount"],
|
||||
odometer=round(vehicle["odometer"] / 1000), # km
|
||||
range=vehicle["distanceToEmpty"])
|
||||
|
||||
if "heater" in vehicle and \
|
||||
"status" in vehicle["heater"]:
|
||||
attributes.update(heater_on=vehicle["heater"]["status"] != "off")
|
||||
|
||||
see(dev_id=dev_id,
|
||||
host_name=host_name,
|
||||
gps=(position["latitude"],
|
||||
position["longitude"]),
|
||||
attributes=dict(
|
||||
unlocked=not vehicle["carLocked"],
|
||||
tank_volume=vehicle["fuelTankVolume"],
|
||||
average_fuel_consumption=round(
|
||||
vehicle["averageFuelConsumption"] / 10, 1), # l/100km
|
||||
washer_fluid_low=vehicle["washerFluidLevel"] != "Normal",
|
||||
brake_fluid_low=vehicle["brakeFluid"] != "Normal",
|
||||
service_warning=vehicle["serviceWarningStatus"] != "Normal",
|
||||
bulb_failures=len(vehicle["bulbFailures"]) > 0,
|
||||
doors_open=any_opened(vehicle["doors"]),
|
||||
windows_open=any_opened(vehicle["windows"]),
|
||||
heater_on=vehicle["heater"]["status"] != "off",
|
||||
fuel=vehicle["fuelAmount"],
|
||||
odometer=round(vehicle["odometer"] / 1000), # km
|
||||
range=vehicle["distanceToEmpty"]))
|
||||
attributes=attributes)
|
||||
|
||||
def update(now):
|
||||
"""Update status from the online service."""
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-digitalocean==1.10.0']
|
||||
REQUIREMENTS = ['python-digitalocean==1.10.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import voluptuous as vol
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
REQUIREMENTS = ['netdisco==0.7.5']
|
||||
REQUIREMENTS = ['netdisco==0.7.7']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant import util, core
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_ON, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||
STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
||||
@@ -75,13 +75,16 @@ def setup(hass, yaml_config):
|
||||
api_password=None,
|
||||
ssl_certificate=None,
|
||||
ssl_key=None,
|
||||
cors_origins=[],
|
||||
trusted_networks=[]
|
||||
cors_origins=None,
|
||||
use_x_forwarded_for=False,
|
||||
trusted_networks=[],
|
||||
login_threshold=0,
|
||||
is_ban_enabled=False
|
||||
)
|
||||
|
||||
server.register_view(DescriptionXmlView(hass, config))
|
||||
server.register_view(HueUsernameView(hass))
|
||||
server.register_view(HueLightsView(hass, config))
|
||||
server.register_view(DescriptionXmlView(config))
|
||||
server.register_view(HueUsernameView)
|
||||
server.register_view(HueLightsView(config))
|
||||
|
||||
upnp_listener = UPNPResponderThread(
|
||||
config.host_ip_addr, config.listen_port)
|
||||
@@ -153,9 +156,8 @@ class DescriptionXmlView(HomeAssistantView):
|
||||
name = 'description:xml'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass, config):
|
||||
def __init__(self, config):
|
||||
"""Initialize the instance of the view."""
|
||||
super().__init__(hass)
|
||||
self.config = config
|
||||
|
||||
@core.callback
|
||||
@@ -197,10 +199,6 @@ class HueUsernameView(HomeAssistantView):
|
||||
extra_urls = ['/api/']
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the instance of the view."""
|
||||
super().__init__(hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle a POST request."""
|
||||
@@ -225,30 +223,33 @@ class HueLightsView(HomeAssistantView):
|
||||
'/api/{username}/lights/{entity_id}/state']
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass, config):
|
||||
def __init__(self, config):
|
||||
"""Initialize the instance of the view."""
|
||||
super().__init__(hass)
|
||||
self.config = config
|
||||
self.cached_states = {}
|
||||
|
||||
@core.callback
|
||||
def get(self, request, username, entity_id=None):
|
||||
"""Handle a GET request."""
|
||||
hass = request.app['hass']
|
||||
|
||||
if entity_id is None:
|
||||
return self.async_get_lights_list()
|
||||
return self.async_get_lights_list(hass)
|
||||
|
||||
if not request.path.endswith('state'):
|
||||
return self.async_get_light_state(entity_id)
|
||||
return self.async_get_light_state(hass, entity_id)
|
||||
|
||||
return web.Response(text="Method not allowed", status=405)
|
||||
|
||||
@asyncio.coroutine
|
||||
def put(self, request, username, entity_id=None):
|
||||
"""Handle a PUT request."""
|
||||
hass = request.app['hass']
|
||||
|
||||
if not request.path.endswith('state'):
|
||||
return web.Response(text="Method not allowed", status=405)
|
||||
|
||||
if entity_id and self.hass.states.get(entity_id) is None:
|
||||
if entity_id and hass.states.get(entity_id) is None:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
try:
|
||||
@@ -256,24 +257,25 @@ class HueLightsView(HomeAssistantView):
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
||||
|
||||
result = yield from self.async_put_light_state(json_data, entity_id)
|
||||
result = yield from self.async_put_light_state(hass, json_data,
|
||||
entity_id)
|
||||
return result
|
||||
|
||||
@core.callback
|
||||
def async_get_lights_list(self):
|
||||
def async_get_lights_list(self, hass):
|
||||
"""Process a request to get the list of available lights."""
|
||||
json_response = {}
|
||||
|
||||
for entity in self.hass.states.async_all():
|
||||
for entity in hass.states.async_all():
|
||||
if self.is_entity_exposed(entity):
|
||||
json_response[entity.entity_id] = entity_to_json(entity)
|
||||
|
||||
return self.json(json_response)
|
||||
|
||||
@core.callback
|
||||
def async_get_light_state(self, entity_id):
|
||||
def async_get_light_state(self, hass, entity_id):
|
||||
"""Process a request to get the state of an individual light."""
|
||||
entity = self.hass.states.get(entity_id)
|
||||
entity = hass.states.get(entity_id)
|
||||
if entity is None or not self.is_entity_exposed(entity):
|
||||
return web.Response(text="Entity not found", status=404)
|
||||
|
||||
@@ -291,12 +293,12 @@ class HueLightsView(HomeAssistantView):
|
||||
return self.json(json_response)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_put_light_state(self, request_json, entity_id):
|
||||
def async_put_light_state(self, hass, request_json, entity_id):
|
||||
"""Process a request to set the state of an individual light."""
|
||||
config = self.config
|
||||
|
||||
# Retrieve the entity from the state machine
|
||||
entity = self.hass.states.get(entity_id)
|
||||
entity = hass.states.get(entity_id)
|
||||
if entity is None:
|
||||
return web.Response(text="Entity not found", status=404)
|
||||
|
||||
@@ -317,7 +319,16 @@ class HueLightsView(HomeAssistantView):
|
||||
# Construct what we need to send to the service
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
|
||||
if brightness is not None:
|
||||
# If the requested entity is a script add some variables
|
||||
if entity.domain.lower() == "script":
|
||||
data['variables'] = {
|
||||
'requested_state': STATE_ON if result else STATE_OFF
|
||||
}
|
||||
|
||||
if brightness is not None:
|
||||
data['variables']['requested_level'] = brightness
|
||||
|
||||
elif brightness is not None:
|
||||
data[ATTR_BRIGHTNESS] = brightness
|
||||
|
||||
if entity.domain.lower() in config.off_maps_to_on_domains:
|
||||
@@ -332,8 +343,8 @@ class HueLightsView(HomeAssistantView):
|
||||
self.cached_states[entity_id] = (result, brightness)
|
||||
|
||||
# Perform the requested action
|
||||
yield from self.hass.services.async_call(core.DOMAIN, service, data,
|
||||
blocking=True)
|
||||
yield from hass.services.async_call(core.DOMAIN, service, data,
|
||||
blocking=True)
|
||||
|
||||
json_response = \
|
||||
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
|
||||
@@ -401,6 +412,13 @@ def parse_hue_api_put_light_body(request_json, entity):
|
||||
|
||||
report_brightness = True
|
||||
result = (brightness > 0)
|
||||
elif entity.domain.lower() == "script":
|
||||
# Convert 0-255 to 0-100
|
||||
level = int(request_json[HUE_API_STATE_BRI]) / 255 * 100
|
||||
|
||||
brightness = round(level)
|
||||
report_brightness = True
|
||||
result = True
|
||||
|
||||
return (result, brightness) if report_brightness else (result, None)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.discovery import load_platform
|
||||
|
||||
REQUIREMENTS = ['pyenvisalink==1.7', 'pydispatcher==2.0.5']
|
||||
REQUIREMENTS = ['pyenvisalink==1.9', 'pydispatcher==2.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'envisalink'
|
||||
|
||||
@@ -75,8 +75,7 @@ def setup(hass, config):
|
||||
descriptions[DOMAIN][SERVICE_CHECKIN],
|
||||
schema=CHECKIN_SERVICE_SCHEMA)
|
||||
|
||||
hass.http.register_view(FoursquarePushReceiver(
|
||||
hass, config[CONF_PUSH_SECRET]))
|
||||
hass.http.register_view(FoursquarePushReceiver(config[CONF_PUSH_SECRET]))
|
||||
|
||||
return True
|
||||
|
||||
@@ -88,9 +87,8 @@ class FoursquarePushReceiver(HomeAssistantView):
|
||||
url = "/api/foursquare"
|
||||
name = "foursquare"
|
||||
|
||||
def __init__(self, hass, push_secret):
|
||||
def __init__(self, push_secret):
|
||||
"""Initialize the OAuth callback view."""
|
||||
super().__init__(hass)
|
||||
self.push_secret = push_secret
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -110,4 +108,4 @@ class FoursquarePushReceiver(HomeAssistantView):
|
||||
"push secret: %s", secret)
|
||||
return self.json_message('Incorrect secret', HTTP_BAD_REQUEST)
|
||||
|
||||
self.hass.bus.async_fire(EVENT_PUSH, data)
|
||||
request.app['hass'].bus.async_fire(EVENT_PUSH, data)
|
||||
|
||||
@@ -8,17 +8,18 @@ import os
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_NOT_FOUND
|
||||
from homeassistant.const import HTTP_NOT_FOUND
|
||||
from homeassistant.components import api, group
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http.auth import is_trusted_ip
|
||||
from homeassistant.components.http.const import KEY_DEVELOPMENT
|
||||
from .version import FINGERPRINTS
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api']
|
||||
DEPENDENCIES = ['api', 'websocket_api']
|
||||
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
|
||||
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
||||
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
|
||||
PANELS = {}
|
||||
MANIFEST_JSON = {
|
||||
"background_color": "#FFFFFF",
|
||||
"description": "Open-source home automation platform running on Python 3.",
|
||||
@@ -32,6 +33,16 @@ MANIFEST_JSON = {
|
||||
"theme_color": "#03A9F4"
|
||||
}
|
||||
|
||||
for size in (192, 384, 512, 1024):
|
||||
MANIFEST_JSON['icons'].append({
|
||||
"src": "/static/icons/favicon-{}x{}.png".format(size, size),
|
||||
"sizes": "{}x{}".format(size, size),
|
||||
"type": "image/png"
|
||||
})
|
||||
|
||||
DATA_PANELS = 'frontend_panels'
|
||||
DATA_INDEX_VIEW = 'frontend_index_view'
|
||||
|
||||
# To keep track we don't register a component twice (gives a warning)
|
||||
_REGISTERED_COMPONENTS = set()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -68,10 +79,14 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
||||
|
||||
Warning: this API will probably change. Use at own risk.
|
||||
"""
|
||||
panels = hass.data.get(DATA_PANELS)
|
||||
if panels is None:
|
||||
panels = hass.data[DATA_PANELS] = {}
|
||||
|
||||
if url_path is None:
|
||||
url_path = component_name
|
||||
|
||||
if url_path in PANELS:
|
||||
if url_path in panels:
|
||||
_LOGGER.warning('Overwriting component %s', url_path)
|
||||
if not os.path.isfile(path):
|
||||
_LOGGER.error('Panel %s component does not exist: %s',
|
||||
@@ -106,7 +121,15 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
||||
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
|
||||
data['url'] = fprinted_url
|
||||
|
||||
PANELS[url_path] = data
|
||||
panels[url_path] = data
|
||||
|
||||
# Register index view for this route if IndexView already loaded
|
||||
# Otherwise it will be done during setup.
|
||||
index_view = hass.data.get(DATA_INDEX_VIEW)
|
||||
|
||||
if index_view:
|
||||
hass.http.app.router.add_route('get', '/{}'.format(url_path),
|
||||
index_view.get)
|
||||
|
||||
|
||||
def add_manifest_json_key(key, val):
|
||||
@@ -134,29 +157,24 @@ def setup(hass, config):
|
||||
if os.path.isdir(local):
|
||||
hass.http.register_static_path("/local", local)
|
||||
|
||||
index_view = hass.data[DATA_INDEX_VIEW] = IndexView()
|
||||
hass.http.register_view(index_view)
|
||||
|
||||
# Components have registered panels before frontend got setup.
|
||||
# Now register their urls.
|
||||
if DATA_PANELS in hass.data:
|
||||
for url_path in hass.data[DATA_PANELS]:
|
||||
hass.http.app.router.add_route('get', '/{}'.format(url_path),
|
||||
index_view.get)
|
||||
else:
|
||||
hass.data[DATA_PANELS] = {}
|
||||
|
||||
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
|
||||
|
||||
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||
'dev-template'):
|
||||
register_built_in_panel(hass, panel)
|
||||
|
||||
def register_frontend_index(event):
|
||||
"""Register the frontend index urls.
|
||||
|
||||
Done when Home Assistant is started so that all panels are known.
|
||||
"""
|
||||
hass.http.register_view(IndexView(
|
||||
hass, ['/{}'.format(name) for name in PANELS]))
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
|
||||
|
||||
for size in (192, 384, 512, 1024):
|
||||
MANIFEST_JSON['icons'].append({
|
||||
"src": "/static/icons/favicon-{}x{}.png".format(size, size),
|
||||
"sizes": "{}x{}".format(size, size),
|
||||
"type": "image/png"
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -169,12 +187,14 @@ class BootstrapView(HomeAssistantView):
|
||||
@callback
|
||||
def get(self, request):
|
||||
"""Return all data needed to bootstrap Home Assistant."""
|
||||
hass = request.app['hass']
|
||||
|
||||
return self.json({
|
||||
'config': self.hass.config.as_dict(),
|
||||
'states': self.hass.states.async_all(),
|
||||
'events': api.async_events_json(self.hass),
|
||||
'services': api.async_services_json(self.hass),
|
||||
'panels': PANELS,
|
||||
'config': hass.config.as_dict(),
|
||||
'states': hass.states.async_all(),
|
||||
'events': api.async_events_json(hass),
|
||||
'services': api.async_services_json(hass),
|
||||
'panels': hass.data[DATA_PANELS],
|
||||
})
|
||||
|
||||
|
||||
@@ -186,13 +206,10 @@ class IndexView(HomeAssistantView):
|
||||
requires_auth = False
|
||||
extra_urls = ['/states', '/states/{entity_id}']
|
||||
|
||||
def __init__(self, hass, extra_urls):
|
||||
def __init__(self):
|
||||
"""Initialize the frontend view."""
|
||||
super().__init__(hass)
|
||||
|
||||
from jinja2 import FileSystemLoader, Environment
|
||||
|
||||
self.extra_urls = self.extra_urls + extra_urls
|
||||
self.templates = Environment(
|
||||
loader=FileSystemLoader(
|
||||
os.path.join(os.path.dirname(__file__), 'templates/')
|
||||
@@ -202,14 +219,16 @@ class IndexView(HomeAssistantView):
|
||||
@asyncio.coroutine
|
||||
def get(self, request, entity_id=None):
|
||||
"""Serve the index view."""
|
||||
hass = request.app['hass']
|
||||
|
||||
if entity_id is not None:
|
||||
state = self.hass.states.get(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
if (not state or state.domain != 'group' or
|
||||
not state.attributes.get(group.ATTR_VIEW)):
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
if self.hass.http.development:
|
||||
if request.app[KEY_DEVELOPMENT]:
|
||||
core_url = '/static/home-assistant-polymer/build/core.js'
|
||||
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
|
||||
else:
|
||||
@@ -223,19 +242,21 @@ class IndexView(HomeAssistantView):
|
||||
else:
|
||||
panel = request.path.split('/')[1]
|
||||
|
||||
panel_url = PANELS[panel]['url'] if panel != 'states' else ''
|
||||
if panel == 'states':
|
||||
panel_url = ''
|
||||
else:
|
||||
panel_url = hass.data[DATA_PANELS][panel]['url']
|
||||
|
||||
no_auth = 'true'
|
||||
if self.hass.config.api.api_password:
|
||||
if hass.config.api.api_password:
|
||||
# require password if set
|
||||
no_auth = 'false'
|
||||
if self.hass.http.is_trusted_ip(
|
||||
self.hass.http.get_real_ip(request)):
|
||||
if is_trusted_ip(request):
|
||||
# bypass for trusted networks
|
||||
no_auth = 'true'
|
||||
|
||||
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
|
||||
template = yield from self.hass.loop.run_in_executor(
|
||||
template = yield from hass.loop.run_in_executor(
|
||||
None, self.templates.get_template, 'index.html')
|
||||
|
||||
# pylint is wrong
|
||||
@@ -244,7 +265,7 @@ class IndexView(HomeAssistantView):
|
||||
resp = template.render(
|
||||
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
|
||||
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
|
||||
panel_url=panel_url, panels=PANELS)
|
||||
panel_url=panel_url, panels=hass.data[DATA_PANELS])
|
||||
|
||||
return web.Response(text=resp, content_type='text/html')
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "5ed5e063d66eb252b5b288738c9c2d16",
|
||||
"frontend.html": "78be2dfedc4e95326cbcd9401fb17b4d",
|
||||
"core.js": "526d7d704ae478c30ae20c1426c2e4f4",
|
||||
"frontend.html": "5baa4dc3b109ca80d4c282fb12c6c23a",
|
||||
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002",
|
||||
"panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e",
|
||||
"panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
|
||||
"panels/ha-panel-dev-service.html": "4a051878b92b002b8b018774ba207769",
|
||||
"panels/ha-panel-dev-service.html": "b3fe49532c5c03198fafb0c6ed58b76a",
|
||||
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
|
||||
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
|
||||
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
|
||||
"panels/ha-panel-map.html": "49ab2d6f180f8bdea7cffaa66b8a5d3e"
|
||||
"panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb",
|
||||
"panels/ha-panel-map.html": "1bf6965b24d76db71a1871865cd4a3a2",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
125
homeassistant/components/frontend/www_static/websocket_test.html
Normal file
125
homeassistant/components/frontend/www_static/websocket_test.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>WebSocket debug</title>
|
||||
<style>
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.controls textarea {
|
||||
height: 160px;
|
||||
min-width: 400px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='controls'>
|
||||
<textarea id="messageinput">
|
||||
{
|
||||
"id": 1, "type": "subscribe_events", "event_type": "state_changed"
|
||||
}
|
||||
</textarea>
|
||||
<pre>
|
||||
Examples:
|
||||
{
|
||||
"id": 2, "type": "subscribe_events", "event_type": "state_changed"
|
||||
}
|
||||
|
||||
{
|
||||
"id": 3, "type": "call_service", "domain": "light", "service": "turn_off"
|
||||
}
|
||||
|
||||
{
|
||||
"id": 4, "type": "unsubscribe_events", "subscription": 2
|
||||
}
|
||||
|
||||
{
|
||||
"id": 5, "type": "get_states"
|
||||
}
|
||||
|
||||
{
|
||||
"id": 6, "type": "get_config"
|
||||
}
|
||||
|
||||
{
|
||||
"id": 7, "type": "get_services"
|
||||
}
|
||||
|
||||
{
|
||||
"id": 8, "type": "get_panels"
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" onclick="openSocket();" >Open</button>
|
||||
<button type="button" onclick="send();" >Send</button>
|
||||
<button type="button" onclick="closeSocket();" >Close</button>
|
||||
</div>
|
||||
<!-- Server responses get written here -->
|
||||
<pre id="messages"></pre>
|
||||
|
||||
<!-- Script to utilise the WebSocket -->
|
||||
<script type="text/javascript">
|
||||
var webSocket;
|
||||
var messages = document.getElementById("messages");
|
||||
|
||||
function openSocket(){
|
||||
var isOpen = false;
|
||||
// Ensures only one connection is open at a time
|
||||
if(webSocket !== undefined && webSocket.readyState !== WebSocket.CLOSED){
|
||||
writeResponse("WebSocket is already opened.");
|
||||
return;
|
||||
}
|
||||
// Create a new instance of the websocket
|
||||
webSocket = new WebSocket("ws://localhost:8123/api/websocket");
|
||||
|
||||
/**
|
||||
* Binds functions to the listeners for the websocket.
|
||||
*/
|
||||
webSocket.onopen = function(event){
|
||||
if (!isOpen) {
|
||||
isOpen = true;
|
||||
writeResponse('Connection opened');
|
||||
}
|
||||
// For reasons I can't determine, onopen gets called twice
|
||||
// and the first time event.data is undefined.
|
||||
// Leave a comment if you know the answer.
|
||||
if(event.data === undefined)
|
||||
return;
|
||||
|
||||
writeResponse(event.data);
|
||||
};
|
||||
|
||||
webSocket.onmessage = function(event){
|
||||
writeResponse(event.data);
|
||||
};
|
||||
|
||||
webSocket.onclose = function(event){
|
||||
writeResponse("Connection closed");
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the value of the text input to the server
|
||||
*/
|
||||
function send(){
|
||||
var text = document.getElementById("messageinput").value;
|
||||
webSocket.send(text);
|
||||
}
|
||||
|
||||
function closeSocket(){
|
||||
webSocket.close();
|
||||
}
|
||||
|
||||
function writeResponse(text){
|
||||
messages.innerHTML += "\n" + text;
|
||||
}
|
||||
|
||||
openSocket();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
292
homeassistant/components/google.py
Normal file
292
homeassistant/components/google.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
Support for Google - Calendar Event Devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/google/
|
||||
|
||||
NOTE TO OTHER DEVELOPERS: IF YOU ADD MORE SCOPES TO THE OAUTH THAN JUST
|
||||
CALENDAR THEN USERS WILL NEED TO DELETE THEIR TOKEN_FILE. THEY WILL LOSE THEIR
|
||||
REFRESH_TOKEN PIECE WHEN RE-AUTHENTICATING TO ADD MORE API ACCESS
|
||||
IT'S BEST TO JUST HAVE SEPARATE OAUTH FOR DIFFERENT PIECES OF GOOGLE
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import yaml
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.error import Error as VoluptuousError
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
from homeassistant.util import convert, dt
|
||||
|
||||
REQUIREMENTS = [
|
||||
'google-api-python-client==1.5.5',
|
||||
'oauth2client==3.0.0',
|
||||
]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'google'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
CONF_CLIENT_ID = 'client_id'
|
||||
CONF_CLIENT_SECRET = 'client_secret'
|
||||
CONF_TRACK_NEW = 'track_new_calendar'
|
||||
|
||||
CONF_CAL_ID = 'cal_id'
|
||||
CONF_DEVICE_ID = 'device_id'
|
||||
CONF_NAME = 'name'
|
||||
CONF_ENTITIES = 'entities'
|
||||
CONF_TRACK = 'track'
|
||||
CONF_SEARCH = 'search'
|
||||
CONF_OFFSET = 'offset'
|
||||
|
||||
DEFAULT_CONF_TRACK_NEW = True
|
||||
DEFAULT_CONF_OFFSET = '!!'
|
||||
|
||||
NOTIFICATION_ID = 'google_calendar_notification'
|
||||
NOTIFICATION_TITLE = 'Google Calendar Setup'
|
||||
GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors"
|
||||
|
||||
SERVICE_SCAN_CALENDARS = 'scan_for_calendars'
|
||||
SERVICE_FOUND_CALENDARS = 'found_calendar'
|
||||
|
||||
DATA_INDEX = 'google_calendars'
|
||||
|
||||
YAML_DEVICES = '{}_calendars.yaml'.format(DOMAIN)
|
||||
SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
|
||||
|
||||
TOKEN_FILE = '.{}.token'.format(DOMAIN)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||
vol.Optional(CONF_TRACK_NEW): cv.boolean,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
_SINGLE_CALSEARCH_CONFIG = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_DEVICE_ID): cv.string,
|
||||
vol.Optional(CONF_TRACK): cv.boolean,
|
||||
vol.Optional(CONF_SEARCH): vol.Any(cv.string, None),
|
||||
vol.Optional(CONF_OFFSET): cv.string,
|
||||
})
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_CAL_ID): cv.string,
|
||||
vol.Required(CONF_ENTITIES, None):
|
||||
vol.All(cv.ensure_list, [_SINGLE_CALSEARCH_CONFIG]),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def do_authentication(hass, config):
|
||||
"""Notify user of actions and authenticate.
|
||||
|
||||
Notify user of user_code and verification_url then poll
|
||||
until we have an access token.
|
||||
"""
|
||||
from oauth2client.client import (
|
||||
OAuth2WebServerFlow,
|
||||
OAuth2DeviceCodeError,
|
||||
FlowExchangeError
|
||||
)
|
||||
from oauth2client.file import Storage
|
||||
|
||||
oauth = OAuth2WebServerFlow(
|
||||
config[CONF_CLIENT_ID],
|
||||
config[CONF_CLIENT_SECRET],
|
||||
'https://www.googleapis.com/auth/calendar.readonly',
|
||||
'Home-Assistant.io',
|
||||
)
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
try:
|
||||
dev_flow = oauth.step1_get_device_and_user_codes()
|
||||
except OAuth2DeviceCodeError as err:
|
||||
persistent_notification.create(
|
||||
hass, 'Error: {}<br />You will need to restart hass after fixing.'
|
||||
''.format(err),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
persistent_notification.create(
|
||||
hass, 'In order to authorize Home-Assistant to view your calendars'
|
||||
'You must visit: <a href="{}" target="_blank">{}</a> and enter'
|
||||
'code: {}'.format(dev_flow.verification_url,
|
||||
dev_flow.verification_url,
|
||||
dev_flow.user_code),
|
||||
title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID
|
||||
)
|
||||
|
||||
def step2_exchange(now):
|
||||
"""Keep trying to validate the user_code until it expires."""
|
||||
if now >= dt.as_local(dev_flow.user_code_expiry):
|
||||
persistent_notification.create(
|
||||
hass, 'Authenication code expired, please restart '
|
||||
'Home-Assistant and try again',
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
listener()
|
||||
|
||||
try:
|
||||
credentials = oauth.step2_exchange(device_flow_info=dev_flow)
|
||||
except FlowExchangeError:
|
||||
# not ready yet, call again
|
||||
return
|
||||
|
||||
storage = Storage(hass.config.path(TOKEN_FILE))
|
||||
storage.put(credentials)
|
||||
do_setup(hass, config)
|
||||
listener()
|
||||
persistent_notification.create(
|
||||
hass, 'We are all setup now. Check {} for calendars that have '
|
||||
'been found'.format(YAML_DEVICES),
|
||||
title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID)
|
||||
|
||||
listener = track_time_change(hass, step2_exchange,
|
||||
second=range(0, 60, dev_flow.interval))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the platform."""
|
||||
if DATA_INDEX not in hass.data:
|
||||
hass.data[DATA_INDEX] = {}
|
||||
|
||||
conf = config.get(DOMAIN, {})
|
||||
|
||||
token_file = hass.config.path(TOKEN_FILE)
|
||||
if not os.path.isfile(token_file):
|
||||
do_authentication(hass, conf)
|
||||
else:
|
||||
do_setup(hass, conf)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def setup_services(hass, track_new_found_calendars, calendar_service):
|
||||
"""Setup service listeners."""
|
||||
def _found_calendar(call):
|
||||
"""Check if we know about a calendar and generate PLATFORM_DISCOVER."""
|
||||
calendar = get_calendar_info(hass, call.data)
|
||||
if hass.data[DATA_INDEX].get(calendar[CONF_CAL_ID], None) is not None:
|
||||
return
|
||||
|
||||
hass.data[DATA_INDEX].update({calendar[CONF_CAL_ID]: calendar})
|
||||
|
||||
update_config(
|
||||
hass.config.path(YAML_DEVICES),
|
||||
hass.data[DATA_INDEX][calendar[CONF_CAL_ID]]
|
||||
)
|
||||
|
||||
discovery.load_platform(hass, 'calendar', DOMAIN,
|
||||
hass.data[DATA_INDEX][calendar[CONF_CAL_ID]])
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar,
|
||||
None, schema=None)
|
||||
|
||||
def _scan_for_calendars(service):
|
||||
"""Scan for new calendars."""
|
||||
service = calendar_service.get()
|
||||
cal_list = service.calendarList() # pylint: disable=no-member
|
||||
calendars = cal_list.list().execute()['items']
|
||||
for calendar in calendars:
|
||||
calendar['track'] = track_new_found_calendars
|
||||
hass.services.call(DOMAIN, SERVICE_FOUND_CALENDARS,
|
||||
calendar)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SCAN_CALENDARS,
|
||||
_scan_for_calendars,
|
||||
None, schema=None)
|
||||
return True
|
||||
|
||||
|
||||
def do_setup(hass, config):
|
||||
"""Run the setup after we have everything configured."""
|
||||
# load calendars the user has configured
|
||||
hass.data[DATA_INDEX] = load_config(hass.config.path(YAML_DEVICES))
|
||||
|
||||
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
||||
track_new_found_calendars = convert(config.get(CONF_TRACK_NEW),
|
||||
bool, DEFAULT_CONF_TRACK_NEW)
|
||||
setup_services(hass, track_new_found_calendars, calendar_service)
|
||||
|
||||
# Ensure component is loaded
|
||||
bootstrap.setup_component(hass, 'calendar', config)
|
||||
|
||||
for calendar in hass.data[DATA_INDEX].values():
|
||||
discovery.load_platform(hass, 'calendar', DOMAIN, calendar)
|
||||
|
||||
# look for any new calendars
|
||||
hass.services.call(DOMAIN, SERVICE_SCAN_CALENDARS, None)
|
||||
return True
|
||||
|
||||
|
||||
class GoogleCalendarService(object):
|
||||
"""Calendar service interface to google."""
|
||||
|
||||
def __init__(self, token_file):
|
||||
"""We just need the token_file."""
|
||||
self.token_file = token_file
|
||||
|
||||
def get(self):
|
||||
"""Get the calendar service from the storage file token."""
|
||||
import httplib2
|
||||
from oauth2client.file import Storage
|
||||
from googleapiclient import discovery as google_discovery
|
||||
credentials = Storage(self.token_file).get()
|
||||
http = credentials.authorize(httplib2.Http())
|
||||
service = google_discovery.build('calendar', 'v3', http=http)
|
||||
return service
|
||||
|
||||
|
||||
def get_calendar_info(hass, calendar):
|
||||
"""Convert data from Google into DEVICE_SCHEMA."""
|
||||
calendar_info = DEVICE_SCHEMA({
|
||||
CONF_CAL_ID: calendar['id'],
|
||||
CONF_ENTITIES: [{
|
||||
CONF_TRACK: calendar['track'],
|
||||
CONF_NAME: calendar['summary'],
|
||||
CONF_DEVICE_ID: generate_entity_id('{}', calendar['summary'],
|
||||
hass=hass),
|
||||
}]
|
||||
})
|
||||
return calendar_info
|
||||
|
||||
|
||||
def load_config(path):
|
||||
"""Load the google_calendar_devices.yaml."""
|
||||
calendars = {}
|
||||
try:
|
||||
with open(path) as file:
|
||||
data = yaml.load(file)
|
||||
for calendar in data:
|
||||
try:
|
||||
calendars.update({calendar[CONF_CAL_ID]:
|
||||
DEVICE_SCHEMA(calendar)})
|
||||
except VoluptuousError as exception:
|
||||
# keep going
|
||||
_LOGGER.warning('Calendar Invalid Data: %s', exception)
|
||||
except FileNotFoundError:
|
||||
# When YAML file could not be loaded/did not contain a dict
|
||||
return {}
|
||||
|
||||
return calendars
|
||||
|
||||
|
||||
def update_config(path, calendar):
|
||||
"""Write the google_calendar_devices.yaml."""
|
||||
with open(path, 'a') as out:
|
||||
out.write('\n')
|
||||
yaml.dump([calendar], out, default_flow_style=False)
|
||||
@@ -184,7 +184,7 @@ def async_setup(hass, config):
|
||||
tasks = [group.async_set_visible(visible) for group
|
||||
in component.async_extract_from_service(service,
|
||||
expand_group=False)]
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler,
|
||||
@@ -207,13 +207,14 @@ def _async_process_config(hass, config, component):
|
||||
icon = conf.get(CONF_ICON)
|
||||
view = conf.get(CONF_VIEW)
|
||||
|
||||
# This order is important as groups get a number based on creation
|
||||
# order.
|
||||
# Don't create tasks and await them all. The order is important as
|
||||
# groups get a number based on creation order.
|
||||
group = yield from Group.async_create_group(
|
||||
hass, name, entity_ids, icon=icon, view=view, object_id=object_id)
|
||||
groups.append(group)
|
||||
|
||||
yield from component.async_add_entities(groups)
|
||||
if groups:
|
||||
yield from component.async_add_entities(groups)
|
||||
|
||||
|
||||
class Group(Entity):
|
||||
@@ -394,7 +395,7 @@ class Group(Entity):
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
self._async_update_group_state(new_state)
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@property
|
||||
def _tracking_states(self):
|
||||
|
||||
@@ -184,8 +184,8 @@ def setup(hass, config):
|
||||
filters.included_entities = include[CONF_ENTITIES]
|
||||
filters.included_domains = include[CONF_DOMAINS]
|
||||
|
||||
hass.http.register_view(Last5StatesView(hass))
|
||||
hass.http.register_view(HistoryPeriodView(hass, filters))
|
||||
hass.http.register_view(Last5StatesView)
|
||||
hass.http.register_view(HistoryPeriodView(filters))
|
||||
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
|
||||
|
||||
return True
|
||||
@@ -197,14 +197,10 @@ class Last5StatesView(HomeAssistantView):
|
||||
url = '/api/history/entity/{entity_id}/recent_states'
|
||||
name = 'api:history:entity-recent-states'
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initilalize the history last 5 states view."""
|
||||
super().__init__(hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, entity_id):
|
||||
"""Retrieve last 5 states of entity."""
|
||||
result = yield from self.hass.loop.run_in_executor(
|
||||
result = yield from request.app['hass'].loop.run_in_executor(
|
||||
None, last_5_states, entity_id)
|
||||
return self.json(result)
|
||||
|
||||
@@ -216,9 +212,8 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
name = 'api:history:view-period'
|
||||
extra_urls = ['/api/history/period/{datetime}']
|
||||
|
||||
def __init__(self, hass, filters):
|
||||
def __init__(self, filters):
|
||||
"""Initilalize the history period view."""
|
||||
super().__init__(hass)
|
||||
self.filters = filters
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -240,7 +235,7 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
end_time = start_time + one_day
|
||||
entity_id = request.GET.get('filter_entity_id')
|
||||
|
||||
result = yield from self.hass.loop.run_in_executor(
|
||||
result = yield from request.app['hass'].loop.run_in_executor(
|
||||
None, get_significant_states, start_time, end_time, entity_id,
|
||||
self.filters)
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ from functools import partial
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN,
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM,
|
||||
ATTR_ENTITY_ID)
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_PLATFORM, CONF_HOSTS, CONF_NAME, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers import discovery
|
||||
@@ -23,13 +23,10 @@ from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
REQUIREMENTS = ["pyhomematic==0.1.16"]
|
||||
|
||||
HOMEMATIC = None
|
||||
HOMEMATIC_LINK_DELAY = 0.5
|
||||
REQUIREMENTS = ["pyhomematic==0.1.18"]
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATE_HUB = timedelta(seconds=300)
|
||||
MIN_TIME_BETWEEN_UPDATE_VAR = timedelta(seconds=60)
|
||||
MIN_TIME_BETWEEN_UPDATE_VAR = timedelta(seconds=30)
|
||||
|
||||
DISCOVER_SWITCHES = 'homematic.switch'
|
||||
DISCOVER_LIGHTS = 'homematic.light'
|
||||
@@ -44,12 +41,15 @@ ATTR_CHANNEL = 'channel'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_ADDRESS = 'address'
|
||||
ATTR_VALUE = 'value'
|
||||
ATTR_PROXY = 'proxy'
|
||||
|
||||
EVENT_KEYPRESS = 'homematic.keypress'
|
||||
EVENT_IMPULSE = 'homematic.impulse'
|
||||
|
||||
SERVICE_VIRTUALKEY = 'virtualkey'
|
||||
SERVICE_SET_VALUE = 'set_value'
|
||||
SERVICE_RECONNECT = 'reconnect'
|
||||
SERVICE_SET_VAR_VALUE = 'set_var_value'
|
||||
SERVICE_SET_DEV_VALUE = 'set_dev_value'
|
||||
|
||||
HM_DEVICE_TYPES = {
|
||||
DISCOVER_SWITCHES: [
|
||||
@@ -109,44 +109,60 @@ CONF_RESOLVENAMES_OPTIONS = [
|
||||
False
|
||||
]
|
||||
|
||||
DATA_HOMEMATIC = 'homematic'
|
||||
DATA_DELAY = 'homematic_delay'
|
||||
DATA_DEVINIT = 'homematic_devinit'
|
||||
DATA_STORE = 'homematic_store'
|
||||
|
||||
CONF_LOCAL_IP = 'local_ip'
|
||||
CONF_LOCAL_PORT = 'local_port'
|
||||
CONF_REMOTE_IP = 'remote_ip'
|
||||
CONF_REMOTE_PORT = 'remote_port'
|
||||
CONF_IP = 'ip'
|
||||
CONF_PORT = 'port'
|
||||
CONF_RESOLVENAMES = 'resolvenames'
|
||||
CONF_DELAY = 'delay'
|
||||
CONF_VARIABLES = 'variables'
|
||||
CONF_DEVICES = 'devices'
|
||||
CONF_DELAY = 'delay'
|
||||
CONF_PRIMARY = 'primary'
|
||||
|
||||
DEFAULT_LOCAL_IP = "0.0.0.0"
|
||||
DEFAULT_LOCAL_PORT = 0
|
||||
DEFAULT_RESOLVENAMES = False
|
||||
DEFAULT_REMOTE_PORT = 2001
|
||||
DEFAULT_PORT = 2001
|
||||
DEFAULT_USERNAME = "Admin"
|
||||
DEFAULT_PASSWORD = ""
|
||||
DEFAULT_VARIABLES = False
|
||||
DEFAULT_DEVICES = True
|
||||
DEFAULT_DELAY = 0.5
|
||||
DEFAULT_PRIMARY = False
|
||||
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): "homematic",
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
vol.Required(ATTR_ADDRESS): cv.string,
|
||||
vol.Required(ATTR_PROXY): cv.string,
|
||||
vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int),
|
||||
vol.Optional(ATTR_PARAM): cv.string,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_REMOTE_IP): cv.string,
|
||||
vol.Required(CONF_HOSTS): {cv.match_all: {
|
||||
vol.Required(CONF_IP): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT):
|
||||
cv.port,
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES):
|
||||
cv.boolean,
|
||||
vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES):
|
||||
vol.In(CONF_RESOLVENAMES_OPTIONS),
|
||||
vol.Optional(CONF_DEVICES, default=DEFAULT_DEVICES): cv.boolean,
|
||||
vol.Optional(CONF_PRIMARY, default=DEFAULT_PRIMARY): cv.boolean,
|
||||
}},
|
||||
vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string,
|
||||
vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port,
|
||||
vol.Optional(CONF_REMOTE_PORT, default=DEFAULT_REMOTE_PORT): cv.port,
|
||||
vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES):
|
||||
vol.In(CONF_RESOLVENAMES_OPTIONS),
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): vol.Coerce(float),
|
||||
vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES): cv.boolean,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -154,105 +170,155 @@ SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({
|
||||
vol.Required(ATTR_ADDRESS): cv.string,
|
||||
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
|
||||
vol.Required(ATTR_PARAM): cv.string,
|
||||
vol.Optional(ATTR_PROXY): cv.string,
|
||||
})
|
||||
|
||||
SCHEMA_SERVICE_SET_VALUE = vol.Schema({
|
||||
SCHEMA_SERVICE_SET_VAR_VALUE = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_VALUE): cv.match_all,
|
||||
})
|
||||
|
||||
SCHEMA_SERVICE_SET_DEV_VALUE = vol.Schema({
|
||||
vol.Required(ATTR_ADDRESS): cv.string,
|
||||
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
|
||||
vol.Required(ATTR_PARAM): cv.string,
|
||||
vol.Required(ATTR_VALUE): cv.match_all,
|
||||
vol.Optional(ATTR_PROXY): cv.string,
|
||||
})
|
||||
|
||||
def virtualkey(hass, address, channel, param):
|
||||
SCHEMA_SERVICE_RECONNECT = vol.Schema({})
|
||||
|
||||
|
||||
def virtualkey(hass, address, channel, param, proxy=None):
|
||||
"""Send virtual keypress to homematic controlller."""
|
||||
data = {
|
||||
ATTR_ADDRESS: address,
|
||||
ATTR_CHANNEL: channel,
|
||||
ATTR_PARAM: param,
|
||||
ATTR_PROXY: proxy,
|
||||
}
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data)
|
||||
|
||||
|
||||
def set_value(hass, entity_id, value):
|
||||
def set_var_value(hass, entity_id, value):
|
||||
"""Change value of homematic system variable."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_VALUE: value,
|
||||
}
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_SET_VALUE, data)
|
||||
hass.services.call(DOMAIN, SERVICE_SET_VAR_VALUE, data)
|
||||
|
||||
|
||||
def set_dev_value(hass, address, channel, param, value, proxy=None):
|
||||
"""Send virtual keypress to homematic controlller."""
|
||||
data = {
|
||||
ATTR_ADDRESS: address,
|
||||
ATTR_CHANNEL: channel,
|
||||
ATTR_PARAM: param,
|
||||
ATTR_VALUE: value,
|
||||
ATTR_PROXY: proxy,
|
||||
}
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_SET_DEV_VALUE, data)
|
||||
|
||||
|
||||
def reconnect(hass):
|
||||
"""Reconnect to CCU/Homegear."""
|
||||
hass.services.call(DOMAIN, SERVICE_RECONNECT, {})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
"""Setup the Homematic component."""
|
||||
global HOMEMATIC, HOMEMATIC_LINK_DELAY
|
||||
from pyhomematic import HMConnection
|
||||
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
local_ip = config[DOMAIN].get(CONF_LOCAL_IP)
|
||||
local_port = config[DOMAIN].get(CONF_LOCAL_PORT)
|
||||
remote_ip = config[DOMAIN].get(CONF_REMOTE_IP)
|
||||
remote_port = config[DOMAIN].get(CONF_REMOTE_PORT)
|
||||
resolvenames = config[DOMAIN].get(CONF_RESOLVENAMES)
|
||||
username = config[DOMAIN].get(CONF_USERNAME)
|
||||
password = config[DOMAIN].get(CONF_PASSWORD)
|
||||
HOMEMATIC_LINK_DELAY = config[DOMAIN].get(CONF_DELAY)
|
||||
use_variables = config[DOMAIN].get(CONF_VARIABLES)
|
||||
hass.data[DATA_DELAY] = config[DOMAIN].get(CONF_DELAY)
|
||||
hass.data[DATA_DEVINIT] = {}
|
||||
hass.data[DATA_STORE] = []
|
||||
|
||||
if remote_ip is None or local_ip is None:
|
||||
_LOGGER.error("Missing remote CCU/Homegear or local address")
|
||||
return False
|
||||
# create hosts list for pyhomematic
|
||||
remotes = {}
|
||||
hosts = {}
|
||||
for rname, rconfig in config[DOMAIN][CONF_HOSTS].items():
|
||||
server = rconfig.get(CONF_IP)
|
||||
|
||||
remotes[rname] = {}
|
||||
remotes[rname][CONF_IP] = server
|
||||
remotes[rname][CONF_PORT] = rconfig.get(CONF_PORT)
|
||||
remotes[rname][CONF_RESOLVENAMES] = rconfig.get(CONF_RESOLVENAMES)
|
||||
remotes[rname][CONF_USERNAME] = rconfig.get(CONF_USERNAME)
|
||||
remotes[rname][CONF_PASSWORD] = rconfig.get(CONF_PASSWORD)
|
||||
|
||||
if server not in hosts or rconfig.get(CONF_PRIMARY):
|
||||
hosts[server] = {
|
||||
CONF_VARIABLES: rconfig.get(CONF_VARIABLES),
|
||||
CONF_NAME: rname,
|
||||
}
|
||||
hass.data[DATA_DEVINIT][rname] = rconfig.get(CONF_DEVICES)
|
||||
|
||||
# Create server thread
|
||||
bound_system_callback = partial(_system_callback_handler, hass, config)
|
||||
HOMEMATIC = HMConnection(local=local_ip,
|
||||
localport=local_port,
|
||||
remote=remote_ip,
|
||||
remoteport=remote_port,
|
||||
systemcallback=bound_system_callback,
|
||||
resolvenames=resolvenames,
|
||||
rpcusername=username,
|
||||
rpcpassword=password,
|
||||
interface_id="homeassistant")
|
||||
hass.data[DATA_HOMEMATIC] = HMConnection(
|
||||
local=config[DOMAIN].get(CONF_LOCAL_IP),
|
||||
localport=config[DOMAIN].get(CONF_LOCAL_PORT),
|
||||
remotes=remotes,
|
||||
systemcallback=bound_system_callback,
|
||||
interface_id="homeassistant"
|
||||
)
|
||||
|
||||
# Start server thread, connect to peer, initialize to receive events
|
||||
HOMEMATIC.start()
|
||||
hass.data[DATA_HOMEMATIC].start()
|
||||
|
||||
# Stops server when Homeassistant is shutting down
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, HOMEMATIC.stop)
|
||||
hass.bus.listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, hass.data[DATA_HOMEMATIC].stop)
|
||||
hass.config.components.append(DOMAIN)
|
||||
|
||||
# init homematic hubs
|
||||
hub_entities = []
|
||||
for _, hub_data in hosts.items():
|
||||
hub_entities.append(HMHub(hass, component, hub_data[CONF_NAME],
|
||||
hub_data[CONF_VARIABLES]))
|
||||
component.add_entities(hub_entities)
|
||||
|
||||
# regeister homematic services
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_VIRTUALKEY,
|
||||
_hm_service_virtualkey,
|
||||
descriptions[DOMAIN][SERVICE_VIRTUALKEY],
|
||||
schema=SCHEMA_SERVICE_VIRTUALKEY)
|
||||
def _hm_service_virtualkey(service):
|
||||
"""Service handle virtualkey services."""
|
||||
address = service.data.get(ATTR_ADDRESS)
|
||||
channel = service.data.get(ATTR_CHANNEL)
|
||||
param = service.data.get(ATTR_PARAM)
|
||||
|
||||
entities = []
|
||||
# device not found
|
||||
hmdevice = _device_from_servicecall(hass, service)
|
||||
if hmdevice is None:
|
||||
_LOGGER.error("%s not found for service virtualkey!", address)
|
||||
return
|
||||
|
||||
##
|
||||
# init HM variable
|
||||
variables = HOMEMATIC.getAllSystemVariables() if use_variables else {}
|
||||
hm_var_store = {}
|
||||
if variables is not None:
|
||||
for key, value in variables.items():
|
||||
varia = HMVariable(key, value)
|
||||
hm_var_store.update({key: varia})
|
||||
entities.append(varia)
|
||||
# if param exists for this device
|
||||
if param not in hmdevice.ACTIONNODE:
|
||||
_LOGGER.error("%s not datapoint in hm device %s", param, address)
|
||||
return
|
||||
|
||||
# add homematic entites
|
||||
entities.append(HMHub(hm_var_store, use_variables))
|
||||
component.add_entities(entities)
|
||||
# channel exists?
|
||||
if channel not in hmdevice.ACTIONNODE[param]:
|
||||
_LOGGER.error("%i is not a channel in hm device %s",
|
||||
channel, address)
|
||||
return
|
||||
|
||||
##
|
||||
# register set_value service if exists variables
|
||||
if not variables:
|
||||
return True
|
||||
# call key
|
||||
hmdevice.actionNodeData(param, True, channel)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_VIRTUALKEY, _hm_service_virtualkey,
|
||||
descriptions[DOMAIN][SERVICE_VIRTUALKEY],
|
||||
schema=SCHEMA_SERVICE_VIRTUALKEY)
|
||||
|
||||
def _service_handle_value(service):
|
||||
"""Set value on homematic variable object."""
|
||||
@@ -261,12 +327,43 @@ def setup(hass, config):
|
||||
value = service.data[ATTR_VALUE]
|
||||
|
||||
for hm_variable in variable_list:
|
||||
hm_variable.hm_set(value)
|
||||
if isinstance(hm_variable, HMVariable):
|
||||
hm_variable.hm_set(value)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SET_VALUE,
|
||||
_service_handle_value,
|
||||
descriptions[DOMAIN][SERVICE_SET_VALUE],
|
||||
schema=SCHEMA_SERVICE_SET_VALUE)
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_VAR_VALUE, _service_handle_value,
|
||||
descriptions[DOMAIN][SERVICE_SET_VAR_VALUE],
|
||||
schema=SCHEMA_SERVICE_SET_VAR_VALUE)
|
||||
|
||||
def _service_handle_reconnect(service):
|
||||
"""Reconnect to all homematic hubs."""
|
||||
hass.data[DATA_HOMEMATIC].reconnect()
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_RECONNECT, _service_handle_reconnect,
|
||||
descriptions[DOMAIN][SERVICE_RECONNECT],
|
||||
schema=SCHEMA_SERVICE_RECONNECT)
|
||||
|
||||
def _service_handle_device(service):
|
||||
"""Service handle set_dev_value services."""
|
||||
address = service.data.get(ATTR_ADDRESS)
|
||||
channel = service.data.get(ATTR_CHANNEL)
|
||||
param = service.data.get(ATTR_PARAM)
|
||||
value = service.data.get(ATTR_VALUE)
|
||||
|
||||
# device not found
|
||||
hmdevice = _device_from_servicecall(hass, service)
|
||||
if hmdevice is None:
|
||||
_LOGGER.error("%s not found!", address)
|
||||
return
|
||||
|
||||
# call key
|
||||
hmdevice.setValue(param, value, channel)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_DEV_VALUE, _service_handle_device,
|
||||
descriptions[DOMAIN][SERVICE_SET_DEV_VALUE],
|
||||
schema=SCHEMA_SERVICE_SET_DEV_VALUE)
|
||||
|
||||
return True
|
||||
|
||||
@@ -274,22 +371,36 @@ def setup(hass, config):
|
||||
def _system_callback_handler(hass, config, src, *args):
|
||||
"""Callback handler."""
|
||||
if src == 'newDevices':
|
||||
_LOGGER.debug("newDevices with: %s", str(args))
|
||||
_LOGGER.debug("newDevices with: %s", args)
|
||||
# pylint: disable=unused-variable
|
||||
(interface_id, dev_descriptions) = args
|
||||
key_dict = {}
|
||||
proxy = interface_id.split('-')[-1]
|
||||
|
||||
# device support active?
|
||||
if not hass.data[DATA_DEVINIT][proxy]:
|
||||
return
|
||||
|
||||
##
|
||||
# Get list of all keys of the devices (ignoring channels)
|
||||
key_dict = {}
|
||||
for dev in dev_descriptions:
|
||||
key_dict[dev['ADDRESS'].split(':')[0]] = True
|
||||
|
||||
##
|
||||
# remove device they allready init by HA
|
||||
tmp_devs = key_dict.copy()
|
||||
for dev in tmp_devs:
|
||||
if dev in hass.data[DATA_STORE]:
|
||||
del key_dict[dev]
|
||||
else:
|
||||
hass.data[DATA_STORE].append(dev)
|
||||
|
||||
# Register EVENTS
|
||||
# Search all device with a EVENTNODE that include data
|
||||
bound_event_callback = partial(_hm_event_handler, hass)
|
||||
bound_event_callback = partial(_hm_event_handler, hass, proxy)
|
||||
for dev in key_dict:
|
||||
if dev not in HOMEMATIC.devices:
|
||||
continue
|
||||
hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(dev)
|
||||
|
||||
hmdevice = HOMEMATIC.devices.get(dev)
|
||||
# have events?
|
||||
if len(hmdevice.EVENTNODE) > 0:
|
||||
_LOGGER.debug("Register Events from %s", dev)
|
||||
@@ -307,7 +418,8 @@ def _system_callback_handler(hass, config, src, *args):
|
||||
('sensor', DISCOVER_SENSORS),
|
||||
('climate', DISCOVER_CLIMATE)):
|
||||
# Get all devices of a specific type
|
||||
found_devices = _get_devices(discovery_type, key_dict)
|
||||
found_devices = _get_devices(
|
||||
hass, discovery_type, key_dict, proxy)
|
||||
|
||||
# When devices of this type are found
|
||||
# they are setup in HA and an event is fired
|
||||
@@ -318,12 +430,12 @@ def _system_callback_handler(hass, config, src, *args):
|
||||
}, config)
|
||||
|
||||
|
||||
def _get_devices(device_type, keys):
|
||||
def _get_devices(hass, device_type, keys, proxy):
|
||||
"""Get the Homematic devices."""
|
||||
device_arr = []
|
||||
|
||||
for key in keys:
|
||||
device = HOMEMATIC.devices[key]
|
||||
device = hass.data[DATA_HOMEMATIC].devices[proxy][key]
|
||||
class_name = device.__class__.__name__
|
||||
metadata = {}
|
||||
|
||||
@@ -357,6 +469,7 @@ def _get_devices(device_type, keys):
|
||||
device_dict = {
|
||||
CONF_PLATFORM: "homematic",
|
||||
ATTR_ADDRESS: key,
|
||||
ATTR_PROXY: proxy,
|
||||
ATTR_NAME: name,
|
||||
ATTR_CHANNEL: channel
|
||||
}
|
||||
@@ -395,28 +508,29 @@ def _create_ha_name(name, channel, param, count):
|
||||
return "{} {} {}".format(name, channel, param)
|
||||
|
||||
|
||||
def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info,
|
||||
def setup_hmdevice_discovery_helper(hass, hmdevicetype, discovery_info,
|
||||
add_callback_devices):
|
||||
"""Helper to setup Homematic devices with discovery info."""
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
_LOGGER.debug("Add device %s from config: %s",
|
||||
str(hmdevicetype), str(config))
|
||||
|
||||
# create object and add to HA
|
||||
new_device = hmdevicetype(config)
|
||||
new_device = hmdevicetype(hass, config)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
add_callback_devices([new_device])
|
||||
|
||||
add_callback_devices(devices)
|
||||
return True
|
||||
|
||||
|
||||
def _hm_event_handler(hass, device, caller, attribute, value):
|
||||
def _hm_event_handler(hass, proxy, device, caller, attribute, value):
|
||||
"""Handle all pyhomematic device events."""
|
||||
try:
|
||||
channel = int(device.split(":")[1])
|
||||
address = device.split(":")[0]
|
||||
hmdevice = HOMEMATIC.devices.get(address)
|
||||
hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(address)
|
||||
except (TypeError, ValueError):
|
||||
_LOGGER.error("Event handling channel convert error!")
|
||||
return
|
||||
@@ -448,46 +562,40 @@ def _hm_event_handler(hass, device, caller, attribute, value):
|
||||
_LOGGER.warning("Event is unknown and not forwarded to HA")
|
||||
|
||||
|
||||
def _hm_service_virtualkey(call):
|
||||
"""Callback for handle virtualkey services."""
|
||||
address = call.data.get(ATTR_ADDRESS)
|
||||
channel = call.data.get(ATTR_CHANNEL)
|
||||
param = call.data.get(ATTR_PARAM)
|
||||
def _device_from_servicecall(hass, service):
|
||||
"""Extract homematic device from service call."""
|
||||
address = service.data.get(ATTR_ADDRESS)
|
||||
proxy = service.data.get(ATTR_PROXY)
|
||||
|
||||
if address not in HOMEMATIC.devices:
|
||||
_LOGGER.error("%s not found for service virtualkey!", address)
|
||||
return
|
||||
hmdevice = HOMEMATIC.devices.get(address)
|
||||
if proxy:
|
||||
return hass.data[DATA_HOMEMATIC].devices[proxy].get(address)
|
||||
|
||||
# if param exists for this device
|
||||
if hmdevice is None or param not in hmdevice.ACTIONNODE:
|
||||
_LOGGER.error("%s not datapoint in hm device %s", param, address)
|
||||
return
|
||||
|
||||
# channel exists?
|
||||
if channel in hmdevice.ACTIONNODE[param]:
|
||||
_LOGGER.error("%i is not a channel in hm device %s", channel, address)
|
||||
return
|
||||
|
||||
# call key
|
||||
hmdevice.actionNodeData(param, 1, channel)
|
||||
for _, devices in hass.data[DATA_HOMEMATIC].devices.items():
|
||||
if address in devices:
|
||||
return devices[address]
|
||||
|
||||
|
||||
class HMHub(Entity):
|
||||
"""The Homematic hub. I.e. CCU2/HomeGear."""
|
||||
|
||||
def __init__(self, variables_store, use_variables=False):
|
||||
def __init__(self, hass, component, name, use_variables):
|
||||
"""Initialize Homematic hub."""
|
||||
self.hass = hass
|
||||
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||
self._component = component
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
self._store = variables_store
|
||||
self._store = {}
|
||||
self._use_variables = use_variables
|
||||
|
||||
self.update()
|
||||
# load data
|
||||
self._update_hub_state()
|
||||
self._init_variables()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return 'Homematic'
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -504,11 +612,6 @@ class HMHub(Entity):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return "mdi:gradient"
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return true if device is available."""
|
||||
return True if HOMEMATIC is not None else False
|
||||
|
||||
def update(self):
|
||||
"""Update Hub data and all HM variables."""
|
||||
self._update_hub_state()
|
||||
@@ -517,30 +620,48 @@ class HMHub(Entity):
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATE_HUB)
|
||||
def _update_hub_state(self):
|
||||
"""Retrieve latest state."""
|
||||
if HOMEMATIC is None:
|
||||
return
|
||||
state = HOMEMATIC.getServiceMessages()
|
||||
state = self._homematic.getServiceMessages(self._name)
|
||||
self._state = STATE_UNKNOWN if state is None else len(state)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATE_VAR)
|
||||
def _update_variables_state(self):
|
||||
"""Retrive all variable data and update hmvariable states."""
|
||||
if HOMEMATIC is None or not self._use_variables:
|
||||
if not self._use_variables:
|
||||
return
|
||||
variables = HOMEMATIC.getAllSystemVariables()
|
||||
if variables is not None:
|
||||
for key, value in variables.items():
|
||||
if key in self._store:
|
||||
self._store.get(key).hm_update(value)
|
||||
|
||||
variables = self._homematic.getAllSystemVariables(self._name)
|
||||
if variables is None:
|
||||
return
|
||||
|
||||
for key, value in variables.items():
|
||||
if key in self._store:
|
||||
self._store.get(key).hm_update(value)
|
||||
|
||||
def _init_variables(self):
|
||||
"""Load variables from hub."""
|
||||
if not self._use_variables:
|
||||
return
|
||||
|
||||
variables = self._homematic.getAllSystemVariables(self._name)
|
||||
if variables is None:
|
||||
return
|
||||
|
||||
entities = []
|
||||
for key, value in variables.items():
|
||||
entities.append(HMVariable(self.hass, self._name, key, value))
|
||||
self._component.add_entities(entities)
|
||||
|
||||
|
||||
class HMVariable(Entity):
|
||||
"""The Homematic system variable."""
|
||||
|
||||
def __init__(self, name, state):
|
||||
def __init__(self, hass, hub_name, name, state):
|
||||
"""Initialize Homematic hub."""
|
||||
self.hass = hass
|
||||
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||
self._state = state
|
||||
self._name = name
|
||||
self._hub_name = hub_name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -562,31 +683,41 @@ class HMVariable(Entity):
|
||||
"""Return false. Homematic Hub object update variable."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = {
|
||||
'hub': self._hub_name,
|
||||
}
|
||||
return attr
|
||||
|
||||
def hm_update(self, value):
|
||||
"""Update variable over Hub object."""
|
||||
if value != self._state:
|
||||
self._state = value
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def hm_set(self, value):
|
||||
"""Set variable on homematic controller."""
|
||||
if HOMEMATIC is not None:
|
||||
if isinstance(self._state, bool):
|
||||
value = cv.boolean(value)
|
||||
else:
|
||||
value = float(value)
|
||||
HOMEMATIC.setSystemVariable(self._name, value)
|
||||
self._state = value
|
||||
self.update_ha_state()
|
||||
if isinstance(self._state, bool):
|
||||
value = cv.boolean(value)
|
||||
else:
|
||||
value = float(value)
|
||||
self._homematic.setSystemVariable(self._hub_name, self._name, value)
|
||||
self._state = value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
class HMDevice(Entity):
|
||||
"""The Homematic device base object."""
|
||||
|
||||
def __init__(self, config):
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a generic Homematic device."""
|
||||
self.hass = hass
|
||||
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||
self._name = config.get(ATTR_NAME)
|
||||
self._address = config.get(ATTR_ADDRESS)
|
||||
self._proxy = config.get(ATTR_PROXY)
|
||||
self._channel = config.get(ATTR_CHANNEL)
|
||||
self._state = config.get(ATTR_PARAM)
|
||||
self._data = {}
|
||||
@@ -636,6 +767,7 @@ class HMDevice(Entity):
|
||||
|
||||
# static attributes
|
||||
attr['ID'] = self._hmdevice.ADDRESS
|
||||
attr['proxy'] = self._proxy
|
||||
|
||||
return attr
|
||||
|
||||
@@ -645,39 +777,31 @@ class HMDevice(Entity):
|
||||
if self._connected:
|
||||
return True
|
||||
|
||||
# pyhomematic is loaded
|
||||
if HOMEMATIC is None:
|
||||
return False
|
||||
# Init
|
||||
self._hmdevice = self._homematic.devices[self._proxy][self._address]
|
||||
self._connected = True
|
||||
|
||||
# Does a HMDevice from pyhomematic exist?
|
||||
if self._address in HOMEMATIC.devices:
|
||||
# Init
|
||||
self._hmdevice = HOMEMATIC.devices[self._address]
|
||||
self._connected = True
|
||||
# Check if Homematic class is okay for HA class
|
||||
_LOGGER.info("Start linking %s to %s", self._address, self._name)
|
||||
try:
|
||||
# Init datapoints of this object
|
||||
self._init_data()
|
||||
if self.hass.data[DATA_DELAY]:
|
||||
# We delay / pause loading of data to avoid overloading
|
||||
# of CCU / Homegear when doing auto detection
|
||||
time.sleep(self.hass.data[DATA_DELAY])
|
||||
self._load_data_from_hm()
|
||||
_LOGGER.debug("%s datastruct: %s", self._name, str(self._data))
|
||||
|
||||
# Check if Homematic class is okay for HA class
|
||||
_LOGGER.info("Start linking %s to %s", self._address, self._name)
|
||||
try:
|
||||
# Init datapoints of this object
|
||||
self._init_data()
|
||||
if HOMEMATIC_LINK_DELAY:
|
||||
# We delay / pause loading of data to avoid overloading
|
||||
# of CCU / Homegear when doing auto detection
|
||||
time.sleep(HOMEMATIC_LINK_DELAY)
|
||||
self._load_data_from_hm()
|
||||
_LOGGER.debug("%s datastruct: %s", self._name, str(self._data))
|
||||
|
||||
# Link events from pyhomatic
|
||||
self._subscribe_homematic_events()
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
_LOGGER.debug("%s linking done", self._name)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
self._connected = False
|
||||
_LOGGER.error("Exception while linking %s: %s",
|
||||
self._address, str(err))
|
||||
else:
|
||||
_LOGGER.debug("%s not found in HOMEMATIC.devices", self._address)
|
||||
# Link events from pyhomatic
|
||||
self._subscribe_homematic_events()
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
_LOGGER.debug("%s linking done", self._name)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
self._connected = False
|
||||
_LOGGER.error("Exception while linking %s: %s",
|
||||
self._address, str(err))
|
||||
|
||||
def _hm_event_callback(self, device, caller, attribute, value):
|
||||
"""Handle all pyhomematic device events."""
|
||||
|
||||
@@ -5,35 +5,38 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/http/
|
||||
"""
|
||||
import asyncio
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import ssl
|
||||
from ipaddress import ip_address, ip_network
|
||||
from ipaddress import ip_network
|
||||
from pathlib import Path
|
||||
|
||||
import os
|
||||
import voluptuous as vol
|
||||
from aiohttp import web, hdrs
|
||||
from aiohttp.file_sender import FileSender
|
||||
from aiohttp.web_exceptions import (
|
||||
HTTPUnauthorized, HTTPMovedPermanently, HTTPNotModified)
|
||||
from aiohttp.web_urldispatcher import StaticRoute
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently
|
||||
|
||||
from homeassistant.core import is_callback
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant import util
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL,
|
||||
CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant.util import get_local_ip
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.core import is_callback
|
||||
from homeassistant.util.logging import HideSensitiveDataFilter
|
||||
|
||||
from .auth import auth_middleware
|
||||
from .ban import ban_middleware, process_wrong_login
|
||||
from .const import (
|
||||
KEY_USE_X_FORWARDED_FOR, KEY_TRUSTED_NETWORKS,
|
||||
KEY_BANS_ENABLED, KEY_LOGIN_THRESHOLD,
|
||||
KEY_DEVELOPMENT, KEY_AUTHENTICATED)
|
||||
from .static import GZIP_FILE_SENDER, staticresource_middleware
|
||||
from .util import get_real_ip
|
||||
|
||||
DOMAIN = 'http'
|
||||
REQUIREMENTS = ('aiohttp_cors==0.4.0',)
|
||||
REQUIREMENTS = ('aiohttp_cors==0.5.0',)
|
||||
|
||||
CONF_API_PASSWORD = 'api_password'
|
||||
CONF_SERVER_HOST = 'server_host'
|
||||
@@ -42,9 +45,11 @@ CONF_DEVELOPMENT = 'development'
|
||||
CONF_SSL_CERTIFICATE = 'ssl_certificate'
|
||||
CONF_SSL_KEY = 'ssl_key'
|
||||
CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
||||
CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for'
|
||||
CONF_TRUSTED_NETWORKS = 'trusted_networks'
|
||||
CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold'
|
||||
CONF_IP_BAN_ENABLED = 'ip_ban_enabled'
|
||||
|
||||
DATA_API_PASSWORD = 'api_password'
|
||||
NOTIFICATION_ID_LOGIN = 'http-login'
|
||||
|
||||
# TLS configuation follows the best-practice guidelines specified here:
|
||||
@@ -68,66 +73,58 @@ CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \
|
||||
"AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \
|
||||
"AES256-SHA:DES-CBC3-SHA:!DSS"
|
||||
|
||||
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_SERVER_HOST = '0.0.0.0'
|
||||
DEFAULT_DEVELOPMENT = '0'
|
||||
DEFAULT_LOGIN_ATTEMPT_THRESHOLD = -1
|
||||
|
||||
HTTP_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_API_PASSWORD, default=None): cv.string,
|
||||
vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string,
|
||||
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_DEVELOPMENT, default=DEFAULT_DEVELOPMENT): cv.string,
|
||||
vol.Optional(CONF_SSL_CERTIFICATE, default=None): cv.isfile,
|
||||
vol.Optional(CONF_SSL_KEY, default=None): cv.isfile,
|
||||
vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.All(cv.ensure_list,
|
||||
[cv.string]),
|
||||
vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean,
|
||||
vol.Optional(CONF_TRUSTED_NETWORKS, default=[]):
|
||||
vol.All(cv.ensure_list, [ip_network]),
|
||||
vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD,
|
||||
default=DEFAULT_LOGIN_ATTEMPT_THRESHOLD): cv.positive_int,
|
||||
vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_API_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_SERVER_HOST): cv.string,
|
||||
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_DEVELOPMENT): cv.string,
|
||||
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
|
||||
vol.Optional(CONF_SSL_KEY): cv.isfile,
|
||||
vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_TRUSTED_NETWORKS):
|
||||
vol.All(cv.ensure_list, [ip_network])
|
||||
}),
|
||||
DOMAIN: HTTP_SCHEMA,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# TEMP TO GET TESTS TO RUN
|
||||
def request_class():
|
||||
"""."""
|
||||
raise Exception('not implemented')
|
||||
|
||||
|
||||
class HideSensitiveFilter(logging.Filter):
|
||||
"""Filter API password calls."""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize sensitive data filter."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
|
||||
def filter(self, record):
|
||||
"""Hide sensitive data in messages."""
|
||||
if self.hass.http.api_password is None:
|
||||
return True
|
||||
|
||||
record.msg = record.msg.replace(self.hass.http.api_password, '*******')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up the HTTP API and debug interface."""
|
||||
logging.getLogger('aiohttp.access').addFilter(HideSensitiveFilter(hass))
|
||||
conf = config.get(DOMAIN)
|
||||
|
||||
conf = config.get(DOMAIN, {})
|
||||
if conf is None:
|
||||
conf = HTTP_SCHEMA({})
|
||||
|
||||
api_password = util.convert(conf.get(CONF_API_PASSWORD), str)
|
||||
server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
|
||||
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
|
||||
development = str(conf.get(CONF_DEVELOPMENT, '')) == '1'
|
||||
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
|
||||
ssl_key = conf.get(CONF_SSL_KEY)
|
||||
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
||||
trusted_networks = [
|
||||
ip_network(trusted_network)
|
||||
for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])]
|
||||
api_password = conf[CONF_API_PASSWORD]
|
||||
server_host = conf[CONF_SERVER_HOST]
|
||||
server_port = conf[CONF_SERVER_PORT]
|
||||
development = conf[CONF_DEVELOPMENT] == '1'
|
||||
ssl_certificate = conf[CONF_SSL_CERTIFICATE]
|
||||
ssl_key = conf[CONF_SSL_KEY]
|
||||
cors_origins = conf[CONF_CORS_ORIGINS]
|
||||
use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR]
|
||||
trusted_networks = conf[CONF_TRUSTED_NETWORKS]
|
||||
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
|
||||
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
|
||||
|
||||
if api_password is not None:
|
||||
logging.getLogger('aiohttp.access').addFilter(
|
||||
HideSensitiveDataFilter(api_password))
|
||||
|
||||
server = HomeAssistantWSGI(
|
||||
hass,
|
||||
@@ -138,7 +135,10 @@ def setup(hass, config):
|
||||
ssl_certificate=ssl_certificate,
|
||||
ssl_key=ssl_key,
|
||||
cors_origins=cors_origins,
|
||||
trusted_networks=trusted_networks
|
||||
use_x_forwarded_for=use_x_forwarded_for,
|
||||
trusted_networks=trusted_networks,
|
||||
login_threshold=login_threshold,
|
||||
is_ban_enabled=is_ban_enabled
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -152,107 +152,40 @@ def setup(hass, config):
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server)
|
||||
yield from server.start()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_server)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server)
|
||||
|
||||
hass.http = server
|
||||
hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
|
||||
else util.get_local_ip(),
|
||||
else get_local_ip(),
|
||||
api_password, server_port,
|
||||
ssl_certificate is not None)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class GzipFileSender(FileSender):
|
||||
"""FileSender class capable of sending gzip version if available."""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
development = False
|
||||
|
||||
@asyncio.coroutine
|
||||
def send(self, request, filepath):
|
||||
"""Send filepath to client using request."""
|
||||
gzip = False
|
||||
if 'gzip' in request.headers[hdrs.ACCEPT_ENCODING]:
|
||||
gzip_path = filepath.with_name(filepath.name + '.gz')
|
||||
|
||||
if gzip_path.is_file():
|
||||
filepath = gzip_path
|
||||
gzip = True
|
||||
|
||||
st = filepath.stat()
|
||||
|
||||
modsince = request.if_modified_since
|
||||
if modsince is not None and st.st_mtime <= modsince.timestamp():
|
||||
raise HTTPNotModified()
|
||||
|
||||
ct, encoding = mimetypes.guess_type(str(filepath))
|
||||
if not ct:
|
||||
ct = 'application/octet-stream'
|
||||
|
||||
resp = self._response_factory()
|
||||
resp.content_type = ct
|
||||
if encoding:
|
||||
resp.headers[hdrs.CONTENT_ENCODING] = encoding
|
||||
if gzip:
|
||||
resp.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING
|
||||
resp.last_modified = st.st_mtime
|
||||
|
||||
# CACHE HACK
|
||||
if not self.development:
|
||||
cache_time = 31 * 86400 # = 1 month
|
||||
resp.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format(
|
||||
cache_time)
|
||||
|
||||
file_size = st.st_size
|
||||
|
||||
resp.content_length = file_size
|
||||
resp.set_tcp_cork(True)
|
||||
try:
|
||||
with filepath.open('rb') as f:
|
||||
yield from self._sendfile(request, resp, f, file_size)
|
||||
|
||||
finally:
|
||||
resp.set_tcp_nodelay(True)
|
||||
|
||||
return resp
|
||||
|
||||
_GZIP_FILE_SENDER = GzipFileSender()
|
||||
|
||||
|
||||
class HAStaticRoute(StaticRoute):
|
||||
"""StaticRoute with support for fingerprinting."""
|
||||
|
||||
def __init__(self, prefix, path):
|
||||
"""Initialize a static route with gzip and cache busting support."""
|
||||
super().__init__(None, prefix, path)
|
||||
self._file_sender = _GZIP_FILE_SENDER
|
||||
|
||||
def match(self, path):
|
||||
"""Match path to filename."""
|
||||
if not path.startswith(self._prefix):
|
||||
return None
|
||||
|
||||
# Extra sauce to remove fingerprinted resource names
|
||||
filename = path[self._prefix_len:]
|
||||
fingerprinted = _FINGERPRINT.match(filename)
|
||||
if fingerprinted:
|
||||
filename = '{}.{}'.format(*fingerprinted.groups())
|
||||
|
||||
return {'filename': filename}
|
||||
|
||||
|
||||
class HomeAssistantWSGI(object):
|
||||
"""WSGI server for Home Assistant."""
|
||||
|
||||
def __init__(self, hass, development, api_password, ssl_certificate,
|
||||
ssl_key, server_host, server_port, cors_origins,
|
||||
trusted_networks):
|
||||
use_x_forwarded_for, trusted_networks,
|
||||
login_threshold, is_ban_enabled):
|
||||
"""Initialize the WSGI Home Assistant server."""
|
||||
import aiohttp_cors
|
||||
|
||||
self.app = web.Application(loop=hass.loop)
|
||||
middlewares = [auth_middleware, staticresource_middleware]
|
||||
|
||||
if is_ban_enabled:
|
||||
middlewares.insert(0, ban_middleware)
|
||||
|
||||
self.app = web.Application(middlewares=middlewares, loop=hass.loop)
|
||||
self.app['hass'] = hass
|
||||
self.app[KEY_USE_X_FORWARDED_FOR] = use_x_forwarded_for
|
||||
self.app[KEY_TRUSTED_NETWORKS] = trusted_networks
|
||||
self.app[KEY_BANS_ENABLED] = is_ban_enabled
|
||||
self.app[KEY_LOGIN_THRESHOLD] = login_threshold
|
||||
self.app[KEY_DEVELOPMENT] = development
|
||||
|
||||
self.hass = hass
|
||||
self.development = development
|
||||
self.api_password = api_password
|
||||
@@ -260,8 +193,6 @@ class HomeAssistantWSGI(object):
|
||||
self.ssl_key = ssl_key
|
||||
self.server_host = server_host
|
||||
self.server_port = server_port
|
||||
self.trusted_networks = trusted_networks
|
||||
self.event_forwarder = None
|
||||
self._handler = None
|
||||
self.server = None
|
||||
|
||||
@@ -275,9 +206,6 @@ class HomeAssistantWSGI(object):
|
||||
else:
|
||||
self.cors = None
|
||||
|
||||
# CACHE HACK
|
||||
_GZIP_FILE_SENDER.development = development
|
||||
|
||||
def register_view(self, view):
|
||||
"""Register a view with the WSGI server.
|
||||
|
||||
@@ -287,7 +215,19 @@ class HomeAssistantWSGI(object):
|
||||
"""
|
||||
if isinstance(view, type):
|
||||
# Instantiate the view, if needed
|
||||
view = view(self.hass)
|
||||
view = view()
|
||||
|
||||
if not hasattr(view, 'url'):
|
||||
class_name = view.__class__.__name__
|
||||
raise AttributeError(
|
||||
'{0} missing required attribute "url"'.format(class_name)
|
||||
)
|
||||
|
||||
if not hasattr(view, 'name'):
|
||||
class_name = view.__class__.__name__
|
||||
raise AttributeError(
|
||||
'{0} missing required attribute "name"'.format(class_name)
|
||||
)
|
||||
|
||||
view.register(self.app.router)
|
||||
|
||||
@@ -312,19 +252,15 @@ class HomeAssistantWSGI(object):
|
||||
Specify optional cache length of asset in days.
|
||||
"""
|
||||
if os.path.isdir(path):
|
||||
assert url_root.startswith('/')
|
||||
if not url_root.endswith('/'):
|
||||
url_root += '/'
|
||||
route = HAStaticRoute(url_root, path)
|
||||
self.app.router.register_route(route)
|
||||
self.app.router.add_static(url_root, path)
|
||||
return
|
||||
|
||||
filepath = Path(path)
|
||||
|
||||
@asyncio.coroutine
|
||||
def serve_file(request):
|
||||
"""Redirect to location."""
|
||||
res = yield from _GZIP_FILE_SENDER.send(request, filepath)
|
||||
"""Serve file from disk."""
|
||||
res = yield from GZIP_FILE_SENDER.send(request, filepath)
|
||||
return res
|
||||
|
||||
# aiohttp supports regex matching for variables. Using that as temp
|
||||
@@ -353,10 +289,21 @@ class HomeAssistantWSGI(object):
|
||||
else:
|
||||
context = None
|
||||
|
||||
# Aiohttp freezes apps after start so that no changes can be made.
|
||||
# However in Home Assistant components can be discovered after boot.
|
||||
# This will now raise a RunTimeError.
|
||||
# To work around this we now fake that we are frozen.
|
||||
# A more appropriate fix would be to create a new app and
|
||||
# re-register all redirects, views, static paths.
|
||||
self.app._frozen = True # pylint: disable=protected-access
|
||||
|
||||
self._handler = self.app.make_handler()
|
||||
|
||||
self.server = yield from self.hass.loop.create_server(
|
||||
self._handler, self.server_host, self.server_port, ssl=context)
|
||||
|
||||
self.app._frozen = False # pylint: disable=protected-access
|
||||
|
||||
@asyncio.coroutine
|
||||
def stop(self):
|
||||
"""Stop the wsgi server."""
|
||||
@@ -366,17 +313,6 @@ class HomeAssistantWSGI(object):
|
||||
yield from self._handler.finish_connections(60.0)
|
||||
yield from self.app.cleanup()
|
||||
|
||||
@staticmethod
|
||||
def get_real_ip(request):
|
||||
"""Return the clients correct ip address, even in proxied setups."""
|
||||
peername = request.transport.get_extra_info('peername')
|
||||
return peername[0] if peername is not None else None
|
||||
|
||||
def is_trusted_ip(self, remote_addr):
|
||||
"""Match an ip address against trusted CIDR networks."""
|
||||
return any(ip_address(remote_addr) in trusted_network
|
||||
for trusted_network in self.hass.http.trusted_networks)
|
||||
|
||||
|
||||
class HomeAssistantView(object):
|
||||
"""Base view for all views."""
|
||||
@@ -385,22 +321,6 @@ class HomeAssistantView(object):
|
||||
extra_urls = []
|
||||
requires_auth = True # Views inheriting from this class can override this
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initilalize the base view."""
|
||||
if not hasattr(self, 'url'):
|
||||
class_name = self.__class__.__name__
|
||||
raise AttributeError(
|
||||
'{0} missing required attribute "url"'.format(class_name)
|
||||
)
|
||||
|
||||
if not hasattr(self, 'name'):
|
||||
class_name = self.__class__.__name__
|
||||
raise AttributeError(
|
||||
'{0} missing required attribute "name"'.format(class_name)
|
||||
)
|
||||
|
||||
self.hass = hass
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def json(self, result, status_code=200):
|
||||
"""Return a JSON response."""
|
||||
@@ -418,7 +338,7 @@ class HomeAssistantView(object):
|
||||
def file(self, request, fil):
|
||||
"""Return a file."""
|
||||
assert isinstance(fil, str), 'only string paths allowed'
|
||||
response = yield from _GZIP_FILE_SENDER.send(request, Path(fil))
|
||||
response = yield from GZIP_FILE_SENDER.send(request, Path(fil))
|
||||
return response
|
||||
|
||||
def register(self, router):
|
||||
@@ -445,50 +365,32 @@ class HomeAssistantView(object):
|
||||
|
||||
|
||||
def request_handler_factory(view, handler):
|
||||
"""Factory to wrap our handler classes.
|
||||
"""Factory to wrap our handler classes."""
|
||||
assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \
|
||||
"Handler should be a coroutine or a callback."
|
||||
|
||||
Eventually authentication should be managed by middleware.
|
||||
"""
|
||||
@asyncio.coroutine
|
||||
def handle(request):
|
||||
"""Handle incoming request."""
|
||||
remote_addr = HomeAssistantWSGI.get_real_ip(request)
|
||||
if not request.app['hass'].is_running:
|
||||
return web.Response(status=503)
|
||||
|
||||
# Auth code verbose on purpose
|
||||
authenticated = False
|
||||
|
||||
if view.hass.http.api_password is None:
|
||||
authenticated = True
|
||||
|
||||
elif view.hass.http.is_trusted_ip(remote_addr):
|
||||
authenticated = True
|
||||
|
||||
elif hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''),
|
||||
view.hass.http.api_password):
|
||||
# A valid auth header has been set
|
||||
authenticated = True
|
||||
|
||||
elif hmac.compare_digest(request.GET.get(DATA_API_PASSWORD, ''),
|
||||
view.hass.http.api_password):
|
||||
authenticated = True
|
||||
remote_addr = get_real_ip(request)
|
||||
authenticated = request.get(KEY_AUTHENTICATED, False)
|
||||
|
||||
if view.requires_auth and not authenticated:
|
||||
yield from process_wrong_login(request)
|
||||
_LOGGER.warning('Login attempt or request with an invalid '
|
||||
'password from %s', remote_addr)
|
||||
persistent_notification.async_create(
|
||||
view.hass,
|
||||
request.app['hass'],
|
||||
'Invalid password used from {}'.format(remote_addr),
|
||||
'Login attempt failed', NOTIFICATION_ID_LOGIN)
|
||||
raise HTTPUnauthorized()
|
||||
|
||||
request.authenticated = authenticated
|
||||
|
||||
_LOGGER.info('Serving %s to %s (auth: %s)',
|
||||
request.path, remote_addr, authenticated)
|
||||
|
||||
assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \
|
||||
"Handler should be a coroutine or a callback."
|
||||
|
||||
result = handler(request, **request.match_info)
|
||||
|
||||
if asyncio.iscoroutine(result):
|
||||
66
homeassistant/components/http/auth.py
Normal file
66
homeassistant/components/http/auth.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Authentication for HTTP component."""
|
||||
import asyncio
|
||||
import hmac
|
||||
import logging
|
||||
|
||||
from homeassistant.const import HTTP_HEADER_HA_AUTH
|
||||
from .util import get_real_ip
|
||||
from .const import KEY_TRUSTED_NETWORKS, KEY_AUTHENTICATED
|
||||
|
||||
DATA_API_PASSWORD = 'api_password'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def auth_middleware(app, handler):
|
||||
"""Authentication middleware."""
|
||||
# If no password set, just always set authenticated=True
|
||||
if app['hass'].http.api_password is None:
|
||||
@asyncio.coroutine
|
||||
def no_auth_middleware_handler(request):
|
||||
"""Auth middleware to approve all requests."""
|
||||
request[KEY_AUTHENTICATED] = True
|
||||
return handler(request)
|
||||
|
||||
return no_auth_middleware_handler
|
||||
|
||||
@asyncio.coroutine
|
||||
def auth_middleware_handler(request):
|
||||
"""Auth middleware to check authentication."""
|
||||
# Auth code verbose on purpose
|
||||
authenticated = False
|
||||
|
||||
if (HTTP_HEADER_HA_AUTH in request.headers and
|
||||
validate_password(request,
|
||||
request.headers[HTTP_HEADER_HA_AUTH])):
|
||||
# A valid auth header has been set
|
||||
authenticated = True
|
||||
|
||||
elif (DATA_API_PASSWORD in request.GET and
|
||||
validate_password(request, request.GET[DATA_API_PASSWORD])):
|
||||
authenticated = True
|
||||
|
||||
elif is_trusted_ip(request):
|
||||
authenticated = True
|
||||
|
||||
request[KEY_AUTHENTICATED] = authenticated
|
||||
|
||||
return handler(request)
|
||||
|
||||
return auth_middleware_handler
|
||||
|
||||
|
||||
def is_trusted_ip(request):
|
||||
"""Test if request is from a trusted ip."""
|
||||
ip_addr = get_real_ip(request)
|
||||
|
||||
return ip_addr and any(
|
||||
ip_addr in trusted_network for trusted_network
|
||||
in request.app[KEY_TRUSTED_NETWORKS])
|
||||
|
||||
|
||||
def validate_password(request, api_password):
|
||||
"""Test if password is valid."""
|
||||
return hmac.compare_digest(api_password,
|
||||
request.app['hass'].http.api_password)
|
||||
132
homeassistant/components/http/ban.py
Normal file
132
homeassistant/components/http/ban.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""Ban logic for HTTP component."""
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
|
||||
from aiohttp.web_exceptions import HTTPForbidden
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.yaml import dump
|
||||
from .const import (
|
||||
KEY_BANS_ENABLED, KEY_BANNED_IPS, KEY_LOGIN_THRESHOLD,
|
||||
KEY_FAILED_LOGIN_ATTEMPTS)
|
||||
from .util import get_real_ip
|
||||
|
||||
NOTIFICATION_ID_BAN = 'ip-ban'
|
||||
|
||||
IP_BANS_FILE = 'ip_bans.yaml'
|
||||
ATTR_BANNED_AT = "banned_at"
|
||||
|
||||
SCHEMA_IP_BAN_ENTRY = vol.Schema({
|
||||
vol.Optional('banned_at'): vol.Any(None, cv.datetime)
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def ban_middleware(app, handler):
|
||||
"""IP Ban middleware."""
|
||||
if not app[KEY_BANS_ENABLED]:
|
||||
return handler
|
||||
|
||||
if KEY_BANNED_IPS not in app:
|
||||
hass = app['hass']
|
||||
app[KEY_BANNED_IPS] = yield from hass.loop.run_in_executor(
|
||||
None, load_ip_bans_config, hass.config.path(IP_BANS_FILE))
|
||||
|
||||
@asyncio.coroutine
|
||||
def ban_middleware_handler(request):
|
||||
"""Verify if IP is not banned."""
|
||||
ip_address_ = get_real_ip(request)
|
||||
|
||||
is_banned = any(ip_ban.ip_address == ip_address_
|
||||
for ip_ban in request.app[KEY_BANNED_IPS])
|
||||
|
||||
if is_banned:
|
||||
raise HTTPForbidden()
|
||||
|
||||
return handler(request)
|
||||
|
||||
return ban_middleware_handler
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def process_wrong_login(request):
|
||||
"""Process a wrong login attempt."""
|
||||
if (not request.app[KEY_BANS_ENABLED] or
|
||||
request.app[KEY_LOGIN_THRESHOLD] < 1):
|
||||
return
|
||||
|
||||
if KEY_FAILED_LOGIN_ATTEMPTS not in request.app:
|
||||
request.app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int)
|
||||
|
||||
remote_addr = get_real_ip(request)
|
||||
|
||||
request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1
|
||||
|
||||
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)
|
||||
|
||||
hass = request.app['hass']
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, update_ip_bans_config, hass.config.path(IP_BANS_FILE),
|
||||
new_ban)
|
||||
|
||||
_LOGGER.warning('Banned IP %s for too many login attempts',
|
||||
remote_addr)
|
||||
|
||||
persistent_notification.async_create(
|
||||
hass,
|
||||
'Too many login attempts from {}'.format(remote_addr),
|
||||
'Banning IP address', NOTIFICATION_ID_BAN)
|
||||
|
||||
|
||||
class IpBan(object):
|
||||
"""Represents banned IP address."""
|
||||
|
||||
def __init__(self, ip_ban: str, banned_at: datetime=None) -> None:
|
||||
"""Initializing Ip Ban object."""
|
||||
self.ip_address = ip_address(ip_ban)
|
||||
self.banned_at = banned_at or datetime.utcnow()
|
||||
|
||||
|
||||
def load_ip_bans_config(path: str):
|
||||
"""Loading list of banned IPs from config file."""
|
||||
ip_list = []
|
||||
|
||||
try:
|
||||
list_ = load_yaml_config_file(path)
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error('Unable to load %s: %s', path, str(err))
|
||||
return []
|
||||
|
||||
for ip_ban, ip_info in list_.items():
|
||||
try:
|
||||
ip_info = SCHEMA_IP_BAN_ENTRY(ip_info)
|
||||
ip_list.append(IpBan(ip_ban, ip_info['banned_at']))
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Failed to load IP ban %s: %s', ip_info, err)
|
||||
continue
|
||||
|
||||
return ip_list
|
||||
|
||||
|
||||
def update_ip_bans_config(path: str, ip_ban: IpBan):
|
||||
"""Update config file with new banned IP address."""
|
||||
with open(path, 'a') as out:
|
||||
ip_ = {str(ip_ban.ip_address): {
|
||||
ATTR_BANNED_AT: ip_ban.banned_at.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
}}
|
||||
out.write('\n')
|
||||
out.write(dump(ip_))
|
||||
12
homeassistant/components/http/const.py
Normal file
12
homeassistant/components/http/const.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""HTTP specific constants."""
|
||||
KEY_AUTHENTICATED = 'ha_authenticated'
|
||||
KEY_USE_X_FORWARDED_FOR = 'ha_use_x_forwarded_for'
|
||||
KEY_TRUSTED_NETWORKS = 'ha_trusted_networks'
|
||||
KEY_REAL_IP = 'ha_real_ip'
|
||||
KEY_BANS_ENABLED = 'ha_bans_enabled'
|
||||
KEY_BANNED_IPS = 'ha_banned_ips'
|
||||
KEY_FAILED_LOGIN_ATTEMPTS = 'ha_failed_login_attempts'
|
||||
KEY_LOGIN_THRESHOLD = 'ha_login_treshold'
|
||||
KEY_DEVELOPMENT = 'ha_development'
|
||||
|
||||
HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For'
|
||||
93
homeassistant/components/http/static.py
Normal file
93
homeassistant/components/http/static.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Static file handling for HTTP component."""
|
||||
import asyncio
|
||||
import mimetypes
|
||||
import re
|
||||
|
||||
from aiohttp import hdrs
|
||||
from aiohttp.file_sender import FileSender
|
||||
from aiohttp.web_urldispatcher import StaticResource
|
||||
from aiohttp.web_exceptions import HTTPNotModified
|
||||
|
||||
from .const import KEY_DEVELOPMENT
|
||||
|
||||
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||
|
||||
|
||||
class GzipFileSender(FileSender):
|
||||
"""FileSender class capable of sending gzip version if available."""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
@asyncio.coroutine
|
||||
def send(self, request, filepath):
|
||||
"""Send filepath to client using request."""
|
||||
gzip = False
|
||||
if 'gzip' in request.headers[hdrs.ACCEPT_ENCODING]:
|
||||
gzip_path = filepath.with_name(filepath.name + '.gz')
|
||||
|
||||
if gzip_path.is_file():
|
||||
filepath = gzip_path
|
||||
gzip = True
|
||||
|
||||
st = filepath.stat()
|
||||
|
||||
modsince = request.if_modified_since
|
||||
if modsince is not None and st.st_mtime <= modsince.timestamp():
|
||||
raise HTTPNotModified()
|
||||
|
||||
ct, encoding = mimetypes.guess_type(str(filepath))
|
||||
if not ct:
|
||||
ct = 'application/octet-stream'
|
||||
|
||||
resp = self._response_factory()
|
||||
resp.content_type = ct
|
||||
if encoding:
|
||||
resp.headers[hdrs.CONTENT_ENCODING] = encoding
|
||||
if gzip:
|
||||
resp.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING
|
||||
resp.last_modified = st.st_mtime
|
||||
|
||||
# CACHE HACK
|
||||
if not request.app[KEY_DEVELOPMENT]:
|
||||
cache_time = 31 * 86400 # = 1 month
|
||||
resp.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format(
|
||||
cache_time)
|
||||
|
||||
file_size = st.st_size
|
||||
|
||||
resp.content_length = file_size
|
||||
with filepath.open('rb') as f:
|
||||
yield from self._sendfile(request, resp, f, file_size)
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
GZIP_FILE_SENDER = GzipFileSender()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def staticresource_middleware(app, handler):
|
||||
"""Enhance StaticResourceHandler middleware.
|
||||
|
||||
Adds gzip encoding and fingerprinting matching.
|
||||
"""
|
||||
inst = getattr(handler, '__self__', None)
|
||||
if not isinstance(inst, StaticResource):
|
||||
return handler
|
||||
|
||||
# pylint: disable=protected-access
|
||||
inst._file_sender = GZIP_FILE_SENDER
|
||||
|
||||
@asyncio.coroutine
|
||||
def static_middleware_handler(request):
|
||||
"""Strip out fingerprints from resource names."""
|
||||
fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
|
||||
|
||||
if fingerprinted:
|
||||
request.match_info['filename'] = \
|
||||
'{}.{}'.format(*fingerprinted.groups())
|
||||
|
||||
resp = yield from handler(request)
|
||||
return resp
|
||||
|
||||
return static_middleware_handler
|
||||
25
homeassistant/components/http/util.py
Normal file
25
homeassistant/components/http/util.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""HTTP utilities."""
|
||||
from ipaddress import ip_address
|
||||
|
||||
from .const import (
|
||||
KEY_REAL_IP, KEY_USE_X_FORWARDED_FOR, HTTP_HEADER_X_FORWARDED_FOR)
|
||||
|
||||
|
||||
def get_real_ip(request):
|
||||
"""Get IP address of client."""
|
||||
if KEY_REAL_IP in request:
|
||||
return request[KEY_REAL_IP]
|
||||
|
||||
if (request.app[KEY_USE_X_FORWARDED_FOR] and
|
||||
HTTP_HEADER_X_FORWARDED_FOR in request.headers):
|
||||
request[KEY_REAL_IP] = ip_address(
|
||||
request.headers.get(HTTP_HEADER_X_FORWARDED_FOR).split(',')[0])
|
||||
else:
|
||||
peername = request.transport.get_extra_info('peername')
|
||||
|
||||
if peername:
|
||||
request[KEY_REAL_IP] = ip_address(peername[0])
|
||||
else:
|
||||
request[KEY_REAL_IP] = None
|
||||
|
||||
return request[KEY_REAL_IP]
|
||||
@@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DB_NAME = 'database'
|
||||
CONF_TAGS = 'tags'
|
||||
CONF_DEFAULT_MEASUREMENT = 'default_measurement'
|
||||
|
||||
DEFAULT_DATABASE = 'home_assistant'
|
||||
DEFAULT_HOST = 'localhost'
|
||||
@@ -40,6 +41,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_TAGS, default={}):
|
||||
vol.Schema({cv.string: cv.string}),
|
||||
vol.Optional(CONF_WHITELIST, default=[]):
|
||||
@@ -65,6 +67,7 @@ def setup(hass, config):
|
||||
blacklist = conf.get(CONF_BLACKLIST)
|
||||
whitelist = conf.get(CONF_WHITELIST)
|
||||
tags = conf.get(CONF_TAGS)
|
||||
default_measurement = conf.get(CONF_DEFAULT_MEASUREMENT)
|
||||
|
||||
try:
|
||||
influx = InfluxDBClient(
|
||||
@@ -96,7 +99,10 @@ def setup(hass, config):
|
||||
|
||||
measurement = state.attributes.get('unit_of_measurement')
|
||||
if measurement in (None, ''):
|
||||
measurement = state.entity_id
|
||||
if default_measurement:
|
||||
measurement = default_measurement
|
||||
else:
|
||||
measurement = state.entity_id
|
||||
|
||||
json_body = [
|
||||
{
|
||||
@@ -114,7 +120,11 @@ def setup(hass, config):
|
||||
|
||||
for key, value in state.attributes.items():
|
||||
if key != 'unit_of_measurement':
|
||||
json_body[0]['fields'][key] = value
|
||||
if isinstance(value, (str, float, bool)):
|
||||
json_body[0]['fields'][key] = value
|
||||
elif isinstance(value, int):
|
||||
# Prevent column data errors in influxDB.
|
||||
json_body[0]['fields'][key] = float(value)
|
||||
|
||||
json_body[0]['tags'].update(tags)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user