mirror of
https://github.com/home-assistant/core.git
synced 2026-01-22 07:26:58 +01:00
Compare commits
298 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ceb57752b | ||
|
|
bd1af8c3d8 | ||
|
|
19a30b0ce6 | ||
|
|
9a659a5d1d | ||
|
|
1cfd770b95 | ||
|
|
3fda97eed7 | ||
|
|
2dc40fe16e | ||
|
|
bf8376ddcb | ||
|
|
8f696193f0 | ||
|
|
70edb2492a | ||
|
|
e35d4beb95 | ||
|
|
919b431a24 | ||
|
|
7f59a8ea0c | ||
|
|
1ac3f0da63 | ||
|
|
12e679c14d | ||
|
|
28ef94c3fa | ||
|
|
27df4cca6c | ||
|
|
a8413249c2 | ||
|
|
f2dacb2570 | ||
|
|
5aaf81f2c9 | ||
|
|
b86cd325fe | ||
|
|
74b7dabf2d | ||
|
|
1ce4c2092a | ||
|
|
875e05ff38 | ||
|
|
fe0e49db4b | ||
|
|
ad86e68c1e | ||
|
|
e7985c970b | ||
|
|
cfac537f51 | ||
|
|
d6e76969cc | ||
|
|
77dca8272c | ||
|
|
3b8ee196be | ||
|
|
4935043f4a | ||
|
|
f5d74e07d5 | ||
|
|
0a724a5473 | ||
|
|
cba8333a13 | ||
|
|
fcbc399809 | ||
|
|
f6eb9e79d5 | ||
|
|
ab3717af76 | ||
|
|
6cd69b413c | ||
|
|
de56a0d021 | ||
|
|
99fdd3e358 | ||
|
|
9a3107aa66 | ||
|
|
d31e01b877 | ||
|
|
f8c8900297 | ||
|
|
ed9cf994c2 | ||
|
|
d4a4938fce | ||
|
|
f7f0138cff | ||
|
|
753ffdaffd | ||
|
|
40aba3d785 | ||
|
|
64f157a036 | ||
|
|
0eddd287c5 | ||
|
|
f32b50cb80 | ||
|
|
a58a566ae8 | ||
|
|
2f1d40e014 | ||
|
|
14ee6178f9 | ||
|
|
753fe8279b | ||
|
|
cc264f415e | ||
|
|
dae90abb34 | ||
|
|
60f692c7bb | ||
|
|
7094d6d61e | ||
|
|
08fc73aa20 | ||
|
|
c14e41f431 | ||
|
|
f1f4d80f24 | ||
|
|
e746b92e0e | ||
|
|
7d2563eb1f | ||
|
|
084b3287ab | ||
|
|
4105429639 | ||
|
|
8c93b484c4 | ||
|
|
3b38de63ea | ||
|
|
eff1d1f14e | ||
|
|
fcb60d472e | ||
|
|
f2a2f2cca5 | ||
|
|
8c7f0669c6 | ||
|
|
d36c7c3de7 | ||
|
|
79efb0e607 | ||
|
|
9bc26e93a4 | ||
|
|
6c3e2021df | ||
|
|
07255a29b4 | ||
|
|
144bb3492a | ||
|
|
6f4dd7b057 | ||
|
|
27f3285d17 | ||
|
|
9a87e62e0e | ||
|
|
9044a9157f | ||
|
|
799ae894a8 | ||
|
|
bff1e1ff6c | ||
|
|
cc2437614b | ||
|
|
0700886d1a | ||
|
|
cd0e321668 | ||
|
|
94a82ab7dc | ||
|
|
b6e4a7771a | ||
|
|
13859388c1 | ||
|
|
2f4c5f949b | ||
|
|
36e8157268 | ||
|
|
2d88f47795 | ||
|
|
5acfe5da68 | ||
|
|
5f9e4ae136 | ||
|
|
a9b0f92afe | ||
|
|
07b2728380 | ||
|
|
a5e66ce6ba | ||
|
|
eae9726bec | ||
|
|
c425afe50e | ||
|
|
a5b9e59cee | ||
|
|
fb447cab82 | ||
|
|
bcde57bff8 | ||
|
|
dfd7ef1fce | ||
|
|
6b9addfeea | ||
|
|
6c62f7231b | ||
|
|
52c21a53b3 | ||
|
|
fdb250d86c | ||
|
|
8de56cfc10 | ||
|
|
7ea25cd360 | ||
|
|
c9498d9f09 | ||
|
|
2f0435ebd8 | ||
|
|
19351fc429 | ||
|
|
bfc16428da | ||
|
|
41fc44b27c | ||
|
|
a55fbd2be7 | ||
|
|
28d6910e56 | ||
|
|
edfc54b2eb | ||
|
|
6ceafabd78 | ||
|
|
48972c7570 | ||
|
|
bf3ead3359 | ||
|
|
b4f8d52fb1 | ||
|
|
143be49c66 | ||
|
|
a9f19a16ee | ||
|
|
d53a8c0823 | ||
|
|
6e5c541a00 | ||
|
|
2cd127921a | ||
|
|
43d2e436b9 | ||
|
|
ef35b8d428 | ||
|
|
69e86c29a6 | ||
|
|
a4d45c46e8 | ||
|
|
45d1d30a8b | ||
|
|
fa9b9105a8 | ||
|
|
4fb4838bde | ||
|
|
3a487e54a2 | ||
|
|
36da82aa8d | ||
|
|
5205354cb7 | ||
|
|
3498234448 | ||
|
|
c13ebacce1 | ||
|
|
ad49942201 | ||
|
|
82770faad7 | ||
|
|
a2f9fdf339 | ||
|
|
a2decdaaa3 | ||
|
|
72a1b7ae3f | ||
|
|
2753dd0c5e | ||
|
|
118c49ecaa | ||
|
|
0d9b3bea10 | ||
|
|
23afdec767 | ||
|
|
4671bd95c6 | ||
|
|
1bc916927c | ||
|
|
2f8865d6cb | ||
|
|
cfdea8d20f | ||
|
|
6e941af9b2 | ||
|
|
ba9bb90cf7 | ||
|
|
2ff61786bc | ||
|
|
9791c6b21b | ||
|
|
a183043d5d | ||
|
|
0589379de5 | ||
|
|
ee7e59fe68 | ||
|
|
b489519930 | ||
|
|
4395217031 | ||
|
|
c8ad9c4daa | ||
|
|
c050eb4100 | ||
|
|
c8a53c564a | ||
|
|
c316d5b0b9 | ||
|
|
e88fc33eef | ||
|
|
8854efd685 | ||
|
|
b0e850ba5d | ||
|
|
74f1f08ab5 | ||
|
|
aa51bb6cb9 | ||
|
|
8deb462471 | ||
|
|
46dc9322a2 | ||
|
|
daf8143d01 | ||
|
|
54dfe045b2 | ||
|
|
f2dfc84d52 | ||
|
|
e55f7ebf81 | ||
|
|
8d06469efe | ||
|
|
c1127133ea | ||
|
|
25970027c6 | ||
|
|
d7640e6ec3 | ||
|
|
12e76ef7c1 | ||
|
|
d36996c8f0 | ||
|
|
e929f45ab8 | ||
|
|
4c328baaa6 | ||
|
|
cc5edf69e3 | ||
|
|
909f2448ca | ||
|
|
97076aa3fd | ||
|
|
a3777c4ea8 | ||
|
|
1c3293ac85 | ||
|
|
f06a0ba373 | ||
|
|
9afc2634c6 | ||
|
|
ed3efc8712 | ||
|
|
144524fbbb | ||
|
|
298d31e42b | ||
|
|
3e7d4fc902 | ||
|
|
64223cea72 | ||
|
|
1053473111 | ||
|
|
25dcddfeef | ||
|
|
e20f88c143 | ||
|
|
1533a68c06 | ||
|
|
5ff5c73e2b | ||
|
|
6ba49e12a2 | ||
|
|
852ce9f990 | ||
|
|
df69680d24 | ||
|
|
2e7b5dcd19 | ||
|
|
e49e0b5a13 | ||
|
|
de50d5d9c1 | ||
|
|
612a37b2dd | ||
|
|
d47006c98f | ||
|
|
16bf10b1a2 | ||
|
|
710533ae8a | ||
|
|
11c57f9345 | ||
|
|
7562b4164b | ||
|
|
cf44b77225 | ||
|
|
44e9783c7c | ||
|
|
1b5c02ff67 | ||
|
|
2f74ffcf81 | ||
|
|
954e4796b8 | ||
|
|
fb501282cc | ||
|
|
c06351f2a9 | ||
|
|
6b9c65c9ce | ||
|
|
8ae3caa292 | ||
|
|
391e3196ea | ||
|
|
cb709931e4 | ||
|
|
a750f8444e | ||
|
|
a5bff4cd8d | ||
|
|
e0bc894cbb | ||
|
|
3ec56d55c5 | ||
|
|
b904a4e770 | ||
|
|
146a9492ec | ||
|
|
e5d714ef52 | ||
|
|
4d63baf705 | ||
|
|
234bf1f0ea | ||
|
|
843789528e | ||
|
|
ea2c073612 | ||
|
|
d1228d5cf4 | ||
|
|
7aec098a05 | ||
|
|
70af7e5fad | ||
|
|
99e272fc8d | ||
|
|
990f476ac9 | ||
|
|
9abc13aaa6 | ||
|
|
d17186a8b7 | ||
|
|
6fedad7890 | ||
|
|
b371bf700f | ||
|
|
01ce43ec7c | ||
|
|
b903bbc042 | ||
|
|
304137e7ff | ||
|
|
e80628d45b | ||
|
|
d6b81fb345 | ||
|
|
621c653fed | ||
|
|
48d70e520f | ||
|
|
528ad56530 | ||
|
|
be3b227a87 | ||
|
|
ef8fc1f201 | ||
|
|
8fcf085829 | ||
|
|
6843893d9f | ||
|
|
e963fc5acf | ||
|
|
bc664c276c | ||
|
|
f192ef8219 | ||
|
|
db31cdf075 | ||
|
|
f168226be9 | ||
|
|
ea01b127c2 | ||
|
|
6e831138b4 | ||
|
|
eb2671f4bb | ||
|
|
8d017b7678 | ||
|
|
5ec7fc7ddb | ||
|
|
0f3ec94fba | ||
|
|
2c566072f5 | ||
|
|
cf8562a030 | ||
|
|
a91c1bc668 | ||
|
|
d43e6a2888 | ||
|
|
50cea77887 | ||
|
|
6231394614 | ||
|
|
f516cc7dc6 | ||
|
|
9c7523d7b0 | ||
|
|
10505d542a | ||
|
|
e12994a0cd | ||
|
|
ff01aa40c9 | ||
|
|
6199e50e80 | ||
|
|
c664c20165 | ||
|
|
eb551a6d5a | ||
|
|
4343659742 | ||
|
|
230bd3929c | ||
|
|
ba7333e804 | ||
|
|
48b13cc865 | ||
|
|
e7c7b9b2a9 | ||
|
|
c7166241f7 | ||
|
|
6318178a8b | ||
|
|
a2b8ad50f2 | ||
|
|
5c95c53c6c | ||
|
|
e60d066514 | ||
|
|
91fe6e4e56 | ||
|
|
34727be5ac | ||
|
|
107769ab81 | ||
|
|
63cc179ea2 | ||
|
|
2bb1a95098 | ||
|
|
f3411f8db2 |
26
.coveragerc
26
.coveragerc
@@ -4,6 +4,8 @@ source = homeassistant
|
||||
omit =
|
||||
homeassistant/__main__.py
|
||||
homeassistant/scripts/*.py
|
||||
homeassistant/util/async.py
|
||||
homeassistant/monkey_patch.py
|
||||
homeassistant/helpers/typing.py
|
||||
homeassistant/helpers/signal.py
|
||||
|
||||
@@ -121,13 +123,16 @@ omit =
|
||||
homeassistant/components/homematicip_cloud.py
|
||||
homeassistant/components/*/homematicip_cloud.py
|
||||
|
||||
homeassistant/components/hydrawise.py
|
||||
homeassistant/components/*/hydrawise.py
|
||||
|
||||
homeassistant/components/ihc/*
|
||||
homeassistant/components/*/ihc.py
|
||||
|
||||
homeassistant/components/insteon_local.py
|
||||
homeassistant/components/*/insteon_local.py
|
||||
|
||||
homeassistant/components/insteon_plm.py
|
||||
homeassistant/components/insteon_plm/*
|
||||
homeassistant/components/*/insteon_plm.py
|
||||
|
||||
homeassistant/components/ios.py
|
||||
@@ -151,6 +156,9 @@ omit =
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/konnected.py
|
||||
homeassistant/components/*/konnected.py
|
||||
|
||||
homeassistant/components/lametric.py
|
||||
homeassistant/components/*/lametric.py
|
||||
|
||||
@@ -211,7 +219,7 @@ omit =
|
||||
homeassistant/components/raincloud.py
|
||||
homeassistant/components/*/raincloud.py
|
||||
|
||||
homeassistant/components/rainmachine.py
|
||||
homeassistant/components/rainmachine/*
|
||||
homeassistant/components/*/rainmachine.py
|
||||
|
||||
homeassistant/components/raspihats.py
|
||||
@@ -226,6 +234,9 @@ omit =
|
||||
homeassistant/components/rpi_pfio.py
|
||||
homeassistant/components/*/rpi_pfio.py
|
||||
|
||||
homeassistant/components/sabnzbd.py
|
||||
homeassistant/components/*/sabnzbd.py
|
||||
|
||||
homeassistant/components/satel_integra.py
|
||||
homeassistant/components/*/satel_integra.py
|
||||
|
||||
@@ -342,6 +353,7 @@ omit =
|
||||
homeassistant/components/calendar/todoist.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/canary.py
|
||||
homeassistant/components/camera/familyhub.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
@@ -373,6 +385,7 @@ omit =
|
||||
homeassistant/components/cover/myq.py
|
||||
homeassistant/components/cover/opengarage.py
|
||||
homeassistant/components/cover/rpi_gpio.py
|
||||
homeassistant/components/cover/ryobi_gdo.py
|
||||
homeassistant/components/cover/scsgate.py
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
@@ -412,7 +425,6 @@ omit =
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/emulated_hue/upnp.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/folder_watcher.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/goalfeed.py
|
||||
@@ -435,6 +447,7 @@ omit =
|
||||
homeassistant/components/light/lifx_legacy.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/lw12wifi.py
|
||||
homeassistant/components/light/mystrom.py
|
||||
homeassistant/components/light/nanoleaf_aurora.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
@@ -510,9 +523,10 @@ omit =
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.py
|
||||
homeassistant/components/notify/clickatell.py
|
||||
homeassistant/components/notify/clicksend_tts.py
|
||||
homeassistant/components/notify/clicksend.py
|
||||
homeassistant/components/notify/clicksend_tts.py
|
||||
homeassistant/components/notify/discord.py
|
||||
homeassistant/components/notify/flock.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/group.py
|
||||
@@ -525,7 +539,6 @@ omit =
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/mycroft.py
|
||||
homeassistant/components/notify/nfandroidtv.py
|
||||
homeassistant/components/notify/nma.py
|
||||
homeassistant/components/notify/prowl.py
|
||||
homeassistant/components/notify/pushbullet.py
|
||||
homeassistant/components/notify/pushetta.py
|
||||
@@ -592,6 +605,7 @@ omit =
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fedex.py
|
||||
homeassistant/components/sensor/filesize.py
|
||||
homeassistant/components/sensor/fints.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/folder.py
|
||||
@@ -611,6 +625,7 @@ omit =
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
homeassistant/components/sensor/iperf3.py
|
||||
homeassistant/components/sensor/irish_rail_transport.py
|
||||
homeassistant/components/sensor/kwb.py
|
||||
homeassistant/components/sensor/lacrosse.py
|
||||
@@ -650,7 +665,6 @@ omit =
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/rainbird.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sense.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -20,7 +20,7 @@ If user exposed functionality or configuration variables are added/changed:
|
||||
If the code communicates with devices, web services, or third-party tools:
|
||||
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
|
||||
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
|
||||
- [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
|
||||
- [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
|
||||
- [ ] New files were added to `.coveragerc`.
|
||||
|
||||
If the code does not interact with devices:
|
||||
|
||||
@@ -10,8 +10,8 @@ matrix:
|
||||
env: TOXENV=lint
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=pylint
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=typing
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
|
||||
@@ -78,7 +78,6 @@ homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/tibber.py @danielhiversen
|
||||
homeassistant/components/sensor/upnp.py @dgomes
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
homeassistant/components/switch/rainmachine.py @bachya
|
||||
homeassistant/components/switch/tplink.py @rytilahti
|
||||
homeassistant/components/vacuum/roomba.py @pschmitt
|
||||
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
@@ -94,10 +93,14 @@ homeassistant/components/*/hive.py @Rendili @KJonline
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
homeassistant/components/knx.py @Julius2342
|
||||
homeassistant/components/*/knx.py @Julius2342
|
||||
homeassistant/components/konnected.py @heythisisnate
|
||||
homeassistant/components/*/konnected.py @heythisisnate
|
||||
homeassistant/components/matrix.py @tinloaf
|
||||
homeassistant/components/*/matrix.py @tinloaf
|
||||
homeassistant/components/qwikswitch.py @kellerza
|
||||
homeassistant/components/*/qwikswitch.py @kellerza
|
||||
homeassistant/components/rainmachine/* @bachya
|
||||
homeassistant/components/*/rainmachine.py @bachya
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
homeassistant/components/tahoma.py @philklei
|
||||
homeassistant/components/*/tahoma.py @philklei
|
||||
|
||||
@@ -12,6 +12,7 @@ LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
|
||||
#ENV INSTALL_LIBCEC no
|
||||
#ENV INSTALL_PHANTOMJS no
|
||||
#ENV INSTALL_SSOCR no
|
||||
#ENV INSTALL_IPERF3 no
|
||||
|
||||
VOLUME /config
|
||||
|
||||
|
||||
@@ -1,606 +0,0 @@
|
||||
swagger: '2.0'
|
||||
info:
|
||||
title: Home Assistant
|
||||
description: Home Assistant REST API
|
||||
version: "1.0.1"
|
||||
# the domain of the service
|
||||
host: localhost:8123
|
||||
|
||||
# array of all schemes that your API supports
|
||||
schemes:
|
||||
- http
|
||||
- https
|
||||
|
||||
securityDefinitions:
|
||||
#api_key:
|
||||
# type: apiKey
|
||||
# description: API password
|
||||
# name: api_password
|
||||
# in: query
|
||||
|
||||
api_key:
|
||||
type: apiKey
|
||||
description: API password
|
||||
name: x-ha-access
|
||||
in: header
|
||||
|
||||
# will be prefixed to all paths
|
||||
basePath: /api
|
||||
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- application/json
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
summary: API alive message
|
||||
description: Returns message if API is up and running.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: API is up and running
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/config:
|
||||
get:
|
||||
summary: API alive message
|
||||
description: Returns the current configuration as JSON.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Current configuration
|
||||
schema:
|
||||
$ref: '#/definitions/ApiConfig'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/discovery_info:
|
||||
get:
|
||||
summary: Basic information about Home Assistant instance
|
||||
tags:
|
||||
- Core
|
||||
responses:
|
||||
200:
|
||||
description: Basic information
|
||||
schema:
|
||||
$ref: '#/definitions/DiscoveryInfo'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/bootstrap:
|
||||
get:
|
||||
summary: Returns all data needed to bootstrap Home Assistant.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Bootstrap information
|
||||
schema:
|
||||
$ref: '#/definitions/BootstrapInfo'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/events:
|
||||
get:
|
||||
summary: Array of event objects.
|
||||
description: Returns an array of event objects. Each event object contain event name and listener count.
|
||||
tags:
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Events
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Event'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/services:
|
||||
get:
|
||||
summary: Array of service objects.
|
||||
description: Returns an array of service objects. Each object contains the domain and which services it contains.
|
||||
tags:
|
||||
- Services
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Services
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Service'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/history:
|
||||
get:
|
||||
summary: Array of state changes in the past.
|
||||
description: Returns an array of state changes in the past. Each object contains further detail for the entities.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: State changes
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/History'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/states:
|
||||
get:
|
||||
summary: Array of state objects.
|
||||
description: |
|
||||
Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: States
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/State'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/states/{entity_id}:
|
||||
get:
|
||||
summary: Specific state object.
|
||||
description: |
|
||||
Returns a state object for specified entity_id.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
description: entity_id of the entity to query
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: State
|
||||
schema:
|
||||
$ref: '#/definitions/State'
|
||||
404:
|
||||
description: Not found
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
post:
|
||||
description: |
|
||||
Updates or creates the current state of an entity.
|
||||
tags:
|
||||
- State
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
description: entity_id to set the state of
|
||||
required: true
|
||||
type: string
|
||||
- $ref: '#/parameters/State'
|
||||
responses:
|
||||
200:
|
||||
description: State of existing entity was set
|
||||
schema:
|
||||
$ref: '#/definitions/State'
|
||||
201:
|
||||
description: State of new entity was set
|
||||
schema:
|
||||
$ref: '#/definitions/State'
|
||||
headers:
|
||||
location:
|
||||
type: string
|
||||
description: location of the new entity
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/error_log:
|
||||
get:
|
||||
summary: Error log
|
||||
description: |
|
||||
Retrieve all errors logged during the current session of Home Assistant as a plaintext response.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
200:
|
||||
description: Plain text error log
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/camera_proxy/camera.{entity_id}:
|
||||
get:
|
||||
summary: Camera image.
|
||||
description: |
|
||||
Returns the data (image) from the specified camera entity_id.
|
||||
tags:
|
||||
- Camera
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- image/jpeg
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
description: entity_id of the camera to query
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Camera image
|
||||
schema:
|
||||
type: file
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/events/{event_type}:
|
||||
post:
|
||||
description: |
|
||||
Fires an event with event_type
|
||||
tags:
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- name: event_type
|
||||
in: path
|
||||
description: event_type to fire event with
|
||||
required: true
|
||||
type: string
|
||||
- $ref: '#/parameters/EventData'
|
||||
responses:
|
||||
200:
|
||||
description: Response message
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/services/{domain}/{service}:
|
||||
post:
|
||||
description: |
|
||||
Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first.
|
||||
tags:
|
||||
- Services
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- name: domain
|
||||
in: path
|
||||
description: domain of the service
|
||||
required: true
|
||||
type: string
|
||||
- name: service
|
||||
in: path
|
||||
description: service to call
|
||||
required: true
|
||||
type: string
|
||||
- $ref: '#/parameters/ServiceData'
|
||||
responses:
|
||||
200:
|
||||
description: List of states that have changed while the service was being executed. The result will include any changed states that changed while the service was being executed, even if their change was the result of something else happening in the system.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/State'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/template:
|
||||
post:
|
||||
description: |
|
||||
Render a Home Assistant template.
|
||||
tags:
|
||||
- Template
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- text/plain
|
||||
parameters:
|
||||
- $ref: '#/parameters/Template'
|
||||
responses:
|
||||
200:
|
||||
description: Returns the rendered template in plain text.
|
||||
schema:
|
||||
type: string
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/event_forwarding:
|
||||
post:
|
||||
description: |
|
||||
Setup event forwarding to another Home Assistant instance.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- $ref: '#/parameters/EventForwarding'
|
||||
responses:
|
||||
200:
|
||||
description: It will return a message if event forwarding was setup successful.
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
delete:
|
||||
description: |
|
||||
Cancel event forwarding to another Home Assistant instance.
|
||||
tags:
|
||||
- Core
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- $ref: '#/parameters/EventForwarding'
|
||||
responses:
|
||||
200:
|
||||
description: It will return a message if event forwarding was cancelled successful.
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
default:
|
||||
description: Error
|
||||
schema:
|
||||
$ref: '#/definitions/Message'
|
||||
/stream:
|
||||
get:
|
||||
summary: Server-sent events
|
||||
description: The server-sent events feature is a one-way channel from your Home Assistant server to a client which is acting as a consumer.
|
||||
tags:
|
||||
- Core
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- text/event-stream
|
||||
parameters:
|
||||
- name: restrict
|
||||
in: query
|
||||
description: comma-separated list of event_types to filter
|
||||
required: false
|
||||
type: string
|
||||
responses:
|
||||
default:
|
||||
description: Stream of events
|
||||
schema:
|
||||
type: object
|
||||
x-events:
|
||||
state_changed:
|
||||
type: object
|
||||
properties:
|
||||
entity_id:
|
||||
type: string
|
||||
old_state:
|
||||
$ref: '#/definitions/State'
|
||||
new_state:
|
||||
$ref: '#/definitions/State'
|
||||
definitions:
|
||||
ApiConfig:
|
||||
type: object
|
||||
properties:
|
||||
components:
|
||||
type: array
|
||||
description: List of component types
|
||||
items:
|
||||
type: string
|
||||
description: Component type
|
||||
latitude:
|
||||
type: number
|
||||
format: float
|
||||
description: Latitude of Home Assistant server
|
||||
longitude:
|
||||
type: number
|
||||
format: float
|
||||
description: Longitude of Home Assistant server
|
||||
location_name:
|
||||
type: string
|
||||
unit_system:
|
||||
type: object
|
||||
properties:
|
||||
length:
|
||||
type: string
|
||||
mass:
|
||||
type: string
|
||||
temperature:
|
||||
type: string
|
||||
volume:
|
||||
type: string
|
||||
time_zone:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
DiscoveryInfo:
|
||||
type: object
|
||||
properties:
|
||||
base_url:
|
||||
type: string
|
||||
location_name:
|
||||
type: string
|
||||
requires_api_password:
|
||||
type: boolean
|
||||
version:
|
||||
type: string
|
||||
BootstrapInfo:
|
||||
type: object
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/definitions/ApiConfig'
|
||||
events:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Event'
|
||||
services:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Service'
|
||||
states:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/State'
|
||||
Event:
|
||||
type: object
|
||||
properties:
|
||||
event:
|
||||
type: string
|
||||
listener_count:
|
||||
type: integer
|
||||
Service:
|
||||
type: object
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
||||
services:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/DomainService'
|
||||
DomainService:
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
fields:
|
||||
type: object
|
||||
description: Object with service fields that can be called
|
||||
State:
|
||||
type: object
|
||||
properties:
|
||||
attributes:
|
||||
$ref: '#/definitions/StateAttributes'
|
||||
state:
|
||||
type: string
|
||||
entity_id:
|
||||
type: string
|
||||
last_changed:
|
||||
type: string
|
||||
format: date-time
|
||||
StateAttributes:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
History:
|
||||
allOf:
|
||||
- $ref: '#/definitions/State'
|
||||
- type: object
|
||||
properties:
|
||||
last_updated:
|
||||
type: string
|
||||
format: date-time
|
||||
Message:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
parameters:
|
||||
State:
|
||||
name: body
|
||||
in: body
|
||||
description: State parameter
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- state
|
||||
properties:
|
||||
attributes:
|
||||
$ref: '#/definitions/StateAttributes'
|
||||
state:
|
||||
type: string
|
||||
EventData:
|
||||
name: body
|
||||
in: body
|
||||
description: event_data
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
ServiceData:
|
||||
name: body
|
||||
in: body
|
||||
description: service_data
|
||||
required: false
|
||||
schema:
|
||||
type: object
|
||||
Template:
|
||||
name: body
|
||||
in: body
|
||||
description: Template to render
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- template
|
||||
properties:
|
||||
template:
|
||||
description: Jinja2 template string
|
||||
type: string
|
||||
EventForwarding:
|
||||
name: body
|
||||
in: body
|
||||
description: Event Forwarding parameter
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- host
|
||||
- api_password
|
||||
properties:
|
||||
host:
|
||||
type: string
|
||||
api_password:
|
||||
type: string
|
||||
port:
|
||||
type: integer
|
||||
@@ -8,7 +8,8 @@ import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Dict, Any # noqa #pylint: disable=unused-import
|
||||
|
||||
|
||||
from homeassistant import monkey_patch
|
||||
from homeassistant.const import (
|
||||
@@ -259,7 +260,7 @@ def setup_and_run_hass(config_dir: str,
|
||||
config = {
|
||||
'frontend': {},
|
||||
'demo': {}
|
||||
}
|
||||
} # type: Dict[str, Any]
|
||||
hass = bootstrap.from_config_dict(
|
||||
config, config_dir=config_dir, verbose=args.verbose,
|
||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
|
||||
|
||||
@@ -15,7 +15,6 @@ from voluptuous.humanize import humanize_error
|
||||
from homeassistant import data_entry_flow, requirements
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -36,23 +35,7 @@ ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||
DATA_REQS = 'auth_reqs_processed'
|
||||
|
||||
|
||||
class AuthError(HomeAssistantError):
|
||||
"""Generic authentication error."""
|
||||
|
||||
|
||||
class InvalidUser(AuthError):
|
||||
"""Raised when an invalid user has been specified."""
|
||||
|
||||
|
||||
class InvalidPassword(AuthError):
|
||||
"""Raised when an invalid password has been supplied."""
|
||||
|
||||
|
||||
class UnknownError(AuthError):
|
||||
"""When an unknown error occurs."""
|
||||
|
||||
|
||||
def generate_secret(entropy=32):
|
||||
def generate_secret(entropy: int = 32) -> str:
|
||||
"""Generate a secret.
|
||||
|
||||
Backport of secrets.token_hex from Python 3.6
|
||||
@@ -69,8 +52,9 @@ class AuthProvider:
|
||||
|
||||
initialized = False
|
||||
|
||||
def __init__(self, store, config):
|
||||
def __init__(self, hass, store, config):
|
||||
"""Initialize an auth provider."""
|
||||
self.hass = hass
|
||||
self.store = store
|
||||
self.config = config
|
||||
|
||||
@@ -210,6 +194,7 @@ class Client:
|
||||
name = attr.ib(type=str)
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
secret = attr.ib(type=str, default=attr.Factory(generate_secret))
|
||||
redirect_uris = attr.ib(type=list, default=attr.Factory(list))
|
||||
|
||||
|
||||
async def load_auth_provider_module(hass, provider):
|
||||
@@ -283,7 +268,7 @@ async def _auth_provider_from_config(hass, store, config):
|
||||
provider_name, humanize_error(config, err))
|
||||
return None
|
||||
|
||||
return AUTH_PROVIDERS[provider_name](store, config)
|
||||
return AUTH_PROVIDERS[provider_name](hass, store, config)
|
||||
|
||||
|
||||
class AuthManager:
|
||||
@@ -340,9 +325,11 @@ class AuthManager:
|
||||
"""Get an access token."""
|
||||
return self.access_tokens.get(token)
|
||||
|
||||
async def async_create_client(self, name):
|
||||
async def async_create_client(self, name, *, redirect_uris=None,
|
||||
no_secret=False):
|
||||
"""Create a new client."""
|
||||
return await self._store.async_create_client(name)
|
||||
return await self._store.async_create_client(
|
||||
name, redirect_uris, no_secret)
|
||||
|
||||
async def async_get_client(self, client_id):
|
||||
"""Get a client."""
|
||||
@@ -360,6 +347,9 @@ class AuthManager:
|
||||
|
||||
async def _async_finish_login_flow(self, result):
|
||||
"""Result of a credential login flow."""
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
return None
|
||||
|
||||
auth_provider = self._providers[result['handler']]
|
||||
return await auth_provider.async_get_or_create_credentials(
|
||||
result['data'])
|
||||
@@ -477,12 +467,20 @@ class AuthStore:
|
||||
|
||||
return None
|
||||
|
||||
async def async_create_client(self, name):
|
||||
async def async_create_client(self, name, redirect_uris, no_secret):
|
||||
"""Create a new client."""
|
||||
if self.clients is None:
|
||||
await self.async_load()
|
||||
|
||||
client = Client(name)
|
||||
kwargs = {
|
||||
'name': name,
|
||||
'redirect_uris': redirect_uris
|
||||
}
|
||||
|
||||
if no_secret:
|
||||
kwargs['secret'] = None
|
||||
|
||||
client = Client(**kwargs)
|
||||
self.clients[client.id] = client
|
||||
await self.async_save()
|
||||
return client
|
||||
|
||||
181
homeassistant/auth_providers/homeassistant.py
Normal file
181
homeassistant/auth_providers/homeassistant.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""Home Assistant auth provider."""
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import auth, data_entry_flow
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util import json
|
||||
|
||||
|
||||
PATH_DATA = '.users.json'
|
||||
|
||||
CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Raised when we encounter invalid authentication."""
|
||||
|
||||
|
||||
class InvalidUser(HomeAssistantError):
|
||||
"""Raised when invalid user is specified.
|
||||
|
||||
Will not be raised when validating authentication.
|
||||
"""
|
||||
|
||||
|
||||
class Data:
|
||||
"""Hold the user data."""
|
||||
|
||||
def __init__(self, path, data):
|
||||
"""Initialize the user data store."""
|
||||
self.path = path
|
||||
if data is None:
|
||||
data = {
|
||||
'salt': auth.generate_secret(),
|
||||
'users': []
|
||||
}
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
"""Return users."""
|
||||
return self._data['users']
|
||||
|
||||
def validate_login(self, username, password):
|
||||
"""Validate a username and password.
|
||||
|
||||
Raises InvalidAuth if auth invalid.
|
||||
"""
|
||||
password = self.hash_password(password)
|
||||
|
||||
found = None
|
||||
|
||||
# Compare all users to avoid timing attacks.
|
||||
for user in self._data['users']:
|
||||
if username == user['username']:
|
||||
found = user
|
||||
|
||||
if found is None:
|
||||
# Do one more compare to make timing the same as if user was found.
|
||||
hmac.compare_digest(password, password)
|
||||
raise InvalidAuth
|
||||
|
||||
if not hmac.compare_digest(password,
|
||||
base64.b64decode(found['password'])):
|
||||
raise InvalidAuth
|
||||
|
||||
def hash_password(self, password, for_storage=False):
|
||||
"""Encode a password."""
|
||||
hashed = hashlib.pbkdf2_hmac(
|
||||
'sha512', password.encode(), self._data['salt'].encode(), 100000)
|
||||
if for_storage:
|
||||
hashed = base64.b64encode(hashed).decode()
|
||||
return hashed
|
||||
|
||||
def add_user(self, username, password):
|
||||
"""Add a user."""
|
||||
if any(user['username'] == username for user in self.users):
|
||||
raise InvalidUser
|
||||
|
||||
self.users.append({
|
||||
'username': username,
|
||||
'password': self.hash_password(password, True),
|
||||
})
|
||||
|
||||
def change_password(self, username, new_password):
|
||||
"""Update the password of a user.
|
||||
|
||||
Raises InvalidUser if user cannot be found.
|
||||
"""
|
||||
for user in self.users:
|
||||
if user['username'] == username:
|
||||
user['password'] = self.hash_password(new_password, True)
|
||||
break
|
||||
else:
|
||||
raise InvalidUser
|
||||
|
||||
def save(self):
|
||||
"""Save data."""
|
||||
json.save_json(self.path, self._data)
|
||||
|
||||
|
||||
def load_data(path):
|
||||
"""Load auth data."""
|
||||
return Data(path, json.load_json(path, None))
|
||||
|
||||
|
||||
@auth.AUTH_PROVIDERS.register('homeassistant')
|
||||
class HassAuthProvider(auth.AuthProvider):
|
||||
"""Auth provider based on a local storage of users in HASS config dir."""
|
||||
|
||||
DEFAULT_TITLE = 'Home Assistant Local'
|
||||
|
||||
async def async_credential_flow(self):
|
||||
"""Return a flow to login."""
|
||||
return LoginFlow(self)
|
||||
|
||||
async def async_validate_login(self, username, password):
|
||||
"""Helper to validate a username and password."""
|
||||
def validate():
|
||||
"""Validate creds."""
|
||||
data = self._auth_data()
|
||||
data.validate_login(username, password)
|
||||
|
||||
await self.hass.async_add_job(validate)
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result['username']
|
||||
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == username:
|
||||
return credential
|
||||
|
||||
# Create new credentials.
|
||||
return self.async_create_credentials({
|
||||
'username': username
|
||||
})
|
||||
|
||||
def _auth_data(self):
|
||||
"""Return the auth provider data."""
|
||||
return load_data(self.hass.config.path(PATH_DATA))
|
||||
|
||||
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
def __init__(self, auth_provider):
|
||||
"""Initialize the login flow."""
|
||||
self._auth_provider = auth_provider
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await self._auth_provider.async_validate_login(
|
||||
user_input['username'], user_input['password'])
|
||||
except InvalidAuth:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=self._auth_provider.name,
|
||||
data=user_input
|
||||
)
|
||||
|
||||
schema = OrderedDict()
|
||||
schema['username'] = str
|
||||
schema['password'] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
)
|
||||
@@ -4,6 +4,7 @@ import hmac
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import auth, data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
|
||||
@@ -20,6 +21,10 @@ CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
@auth.AUTH_PROVIDERS.register('insecure_example')
|
||||
class ExampleAuthProvider(auth.AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
@@ -43,18 +48,15 @@ class ExampleAuthProvider(auth.AuthProvider):
|
||||
# Do one more compare to make timing the same as if user was found.
|
||||
hmac.compare_digest(password.encode('utf-8'),
|
||||
password.encode('utf-8'))
|
||||
raise auth.InvalidUser
|
||||
raise InvalidAuthError
|
||||
|
||||
if not hmac.compare_digest(user['password'].encode('utf-8'),
|
||||
password.encode('utf-8')):
|
||||
raise auth.InvalidPassword
|
||||
raise InvalidAuthError
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result['username']
|
||||
password = flow_result['password']
|
||||
|
||||
self.async_validate_login(username, password)
|
||||
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == username:
|
||||
@@ -96,7 +98,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
try:
|
||||
self._auth_provider.async_validate_login(
|
||||
user_input['username'], user_input['password'])
|
||||
except (auth.InvalidUser, auth.InvalidPassword):
|
||||
except InvalidAuthError:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
|
||||
@@ -278,7 +278,8 @@ def async_enable_logging(hass: core.HomeAssistant,
|
||||
|
||||
if log_rotate_days:
|
||||
err_handler = logging.handlers.TimedRotatingFileHandler(
|
||||
err_log_path, when='midnight', backupCount=log_rotate_days)
|
||||
err_log_path, when='midnight',
|
||||
backupCount=log_rotate_days) # type: logging.FileHandler
|
||||
else:
|
||||
err_handler = logging.FileHandler(
|
||||
err_log_path, mode='w', delay=True)
|
||||
@@ -297,7 +298,7 @@ def async_enable_logging(hass: core.HomeAssistant,
|
||||
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
|
||||
|
||||
logger = logging.getLogger('')
|
||||
logger.addHandler(async_handler)
|
||||
logger.addHandler(async_handler) # type: ignore
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Save the log file location for access by other components.
|
||||
|
||||
@@ -100,8 +100,8 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the regex for code format or None if no code is required."""
|
||||
return '^\\d{4,6}$'
|
||||
"""Return one or more digits/characters."""
|
||||
return 'Number'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -6,6 +6,7 @@ https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -79,8 +80,12 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -80,7 +80,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the characters if code is defined."""
|
||||
return '[0-9]{4}([0-9]{2})?'
|
||||
return 'Number'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -106,7 +106,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
return '^\\d{4,6}$'
|
||||
return 'Number'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.ifttt/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -124,8 +125,12 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more characters."""
|
||||
return None if self._code is None else '.+'
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -7,6 +7,7 @@ https://home-assistant.io/components/alarm_control_panel.manual/
|
||||
import copy
|
||||
import datetime
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -201,8 +202,12 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more characters."""
|
||||
return None if self._code is None else '.+'
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -8,6 +8,7 @@ import asyncio
|
||||
import copy
|
||||
import datetime
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -237,8 +238,12 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more characters."""
|
||||
return None if self._code is None else '.+'
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -6,6 +6,7 @@ https://home-assistant.io/components/alarm_control_panel.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -117,8 +118,12 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""One or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
|
||||
@@ -69,8 +69,8 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return che characters if code is defined."""
|
||||
return '[0-9]{4}([0-9]{2})?'
|
||||
"""Return one or more digits/characters."""
|
||||
return 'Number'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -66,7 +66,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the regex for code format or None if no code is required."""
|
||||
return '^\\d{4,6}$'
|
||||
return 'Number'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.simplisafe/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -83,8 +84,12 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.17']
|
||||
REQUIREMENTS = ['total_connect_client==0.18']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the code format as regex."""
|
||||
return '^\\d{%s}$' % self._digits
|
||||
"""Return one or more digits/characters."""
|
||||
return 'Number'
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Rest API for Home Assistant.
|
||||
|
||||
For more details about the RESTful API, please refer to the documentation at
|
||||
https://home-assistant.io/developers/api/
|
||||
https://developers.home-assistant.io/docs/en/external_api_rest.html
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
@@ -11,31 +11,34 @@ import logging
|
||||
from aiohttp import web
|
||||
import async_timeout
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant.bootstrap import DATA_LOGGING
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
||||
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND,
|
||||
MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
|
||||
URL_API_EVENTS, URL_API_SERVICES,
|
||||
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
|
||||
__version__)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.state import AsyncTrackStates
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST,
|
||||
HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS,
|
||||
URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM,
|
||||
URL_API_TEMPLATE, __version__)
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
from homeassistant.helpers.state import AsyncTrackStates
|
||||
import homeassistant.remote as rem
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_BASE_URL = 'base_url'
|
||||
ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_REQUIRES_API_PASSWORD = 'requires_api_password'
|
||||
ATTR_VERSION = 'version'
|
||||
|
||||
DOMAIN = 'api'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
STREAM_PING_PAYLOAD = "ping"
|
||||
STREAM_PING_PAYLOAD = 'ping'
|
||||
STREAM_PING_INTERVAL = 50 # seconds
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Register the API with the HTTP interface."""
|
||||
@@ -62,19 +65,19 @@ class APIStatusView(HomeAssistantView):
|
||||
"""View to handle Status requests."""
|
||||
|
||||
url = URL_API
|
||||
name = "api:status"
|
||||
name = 'api:status'
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Retrieve if API is running."""
|
||||
return self.json_message('API running.')
|
||||
return self.json_message("API running.")
|
||||
|
||||
|
||||
class APIEventStream(HomeAssistantView):
|
||||
"""View to handle EventStream requests."""
|
||||
|
||||
url = URL_API_STREAM
|
||||
name = "api:stream"
|
||||
name = 'api:stream'
|
||||
|
||||
async def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
@@ -95,7 +98,7 @@ class APIEventStream(HomeAssistantView):
|
||||
if restrict and event.event_type not in restrict:
|
||||
return
|
||||
|
||||
_LOGGER.debug('STREAM %s FORWARDING %s', id(stop_obj), event)
|
||||
_LOGGER.debug("STREAM %s FORWARDING %s", id(stop_obj), event)
|
||||
|
||||
if event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||
data = stop_obj
|
||||
@@ -111,7 +114,7 @@ class APIEventStream(HomeAssistantView):
|
||||
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
|
||||
|
||||
try:
|
||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||
_LOGGER.debug("STREAM %s ATTACHED", id(stop_obj))
|
||||
|
||||
# Fire off one message so browsers fire open event right away
|
||||
await to_write.put(STREAM_PING_PAYLOAD)
|
||||
@@ -126,25 +129,25 @@ class APIEventStream(HomeAssistantView):
|
||||
break
|
||||
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||
msg.strip())
|
||||
await response.write(msg.encode("UTF-8"))
|
||||
_LOGGER.debug(
|
||||
"STREAM %s WRITING %s", id(stop_obj), msg.strip())
|
||||
await response.write(msg.encode('UTF-8'))
|
||||
except asyncio.TimeoutError:
|
||||
await to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug('STREAM %s ABORT', id(stop_obj))
|
||||
_LOGGER.debug("STREAM %s ABORT", id(stop_obj))
|
||||
|
||||
finally:
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
_LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj))
|
||||
unsub_stream()
|
||||
|
||||
|
||||
class APIConfigView(HomeAssistantView):
|
||||
"""View to handle Config requests."""
|
||||
"""View to handle Configuration requests."""
|
||||
|
||||
url = URL_API_CONFIG
|
||||
name = "api:config"
|
||||
name = 'api:config'
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
@@ -153,22 +156,22 @@ class APIConfigView(HomeAssistantView):
|
||||
|
||||
|
||||
class APIDiscoveryView(HomeAssistantView):
|
||||
"""View to provide discovery info."""
|
||||
"""View to provide Discovery information."""
|
||||
|
||||
requires_auth = False
|
||||
url = URL_API_DISCOVERY_INFO
|
||||
name = "api:discovery"
|
||||
name = 'api:discovery'
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get discovery info."""
|
||||
"""Get discovery information."""
|
||||
hass = request.app['hass']
|
||||
needs_auth = hass.config.api.api_password is not None
|
||||
return self.json({
|
||||
'base_url': hass.config.api.base_url,
|
||||
'location_name': hass.config.location_name,
|
||||
'requires_api_password': needs_auth,
|
||||
'version': __version__
|
||||
ATTR_BASE_URL: hass.config.api.base_url,
|
||||
ATTR_LOCATION_NAME: hass.config.location_name,
|
||||
ATTR_REQUIRES_API_PASSWORD: needs_auth,
|
||||
ATTR_VERSION: __version__,
|
||||
})
|
||||
|
||||
|
||||
@@ -187,8 +190,8 @@ class APIStatesView(HomeAssistantView):
|
||||
class APIEntityStateView(HomeAssistantView):
|
||||
"""View to handle EntityState requests."""
|
||||
|
||||
url = "/api/states/{entity_id}"
|
||||
name = "api:entity-state"
|
||||
url = '/api/states/{entity_id}'
|
||||
name = 'api:entity-state'
|
||||
|
||||
@ha.callback
|
||||
def get(self, request, entity_id):
|
||||
@@ -196,7 +199,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
state = request.app['hass'].states.get(entity_id)
|
||||
if state:
|
||||
return self.json(state)
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
|
||||
|
||||
async def post(self, request, entity_id):
|
||||
"""Update state of entity."""
|
||||
@@ -204,13 +207,13 @@ class APIEntityStateView(HomeAssistantView):
|
||||
try:
|
||||
data = await request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON specified',
|
||||
HTTP_BAD_REQUEST)
|
||||
return self.json_message(
|
||||
"Invalid JSON specified.", HTTP_BAD_REQUEST)
|
||||
|
||||
new_state = data.get('state')
|
||||
|
||||
if new_state is None:
|
||||
return self.json_message('No state specified', HTTP_BAD_REQUEST)
|
||||
return self.json_message("No state specified.", HTTP_BAD_REQUEST)
|
||||
|
||||
attributes = data.get('attributes')
|
||||
force_update = data.get('force_update', False)
|
||||
@@ -232,15 +235,15 @@ class APIEntityStateView(HomeAssistantView):
|
||||
def delete(self, request, entity_id):
|
||||
"""Remove entity."""
|
||||
if request.app['hass'].states.async_remove(entity_id):
|
||||
return self.json_message('Entity removed')
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
return self.json_message("Entity removed.")
|
||||
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
|
||||
|
||||
|
||||
class APIEventListenersView(HomeAssistantView):
|
||||
"""View to handle EventListeners requests."""
|
||||
|
||||
url = URL_API_EVENTS
|
||||
name = "api:event-listeners"
|
||||
name = 'api:event-listeners'
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
@@ -252,7 +255,7 @@ class APIEventView(HomeAssistantView):
|
||||
"""View to handle Event requests."""
|
||||
|
||||
url = '/api/events/{event_type}'
|
||||
name = "api:event"
|
||||
name = 'api:event'
|
||||
|
||||
async def post(self, request, event_type):
|
||||
"""Fire events."""
|
||||
@@ -260,12 +263,12 @@ class APIEventView(HomeAssistantView):
|
||||
try:
|
||||
event_data = json.loads(body) if body else None
|
||||
except ValueError:
|
||||
return self.json_message('Event data should be valid JSON',
|
||||
HTTP_BAD_REQUEST)
|
||||
return self.json_message(
|
||||
"Event data should be valid JSON.", HTTP_BAD_REQUEST)
|
||||
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
return self.json_message('Event data should be a JSON object',
|
||||
HTTP_BAD_REQUEST)
|
||||
return self.json_message(
|
||||
"Event data should be a JSON object", HTTP_BAD_REQUEST)
|
||||
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
@@ -276,8 +279,8 @@ class APIEventView(HomeAssistantView):
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
request.app['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))
|
||||
|
||||
@@ -286,7 +289,7 @@ class APIServicesView(HomeAssistantView):
|
||||
"""View to handle Services requests."""
|
||||
|
||||
url = URL_API_SERVICES
|
||||
name = "api:services"
|
||||
name = 'api:services'
|
||||
|
||||
async def get(self, request):
|
||||
"""Get registered services."""
|
||||
@@ -297,8 +300,8 @@ class APIServicesView(HomeAssistantView):
|
||||
class APIDomainServicesView(HomeAssistantView):
|
||||
"""View to handle DomainServices requests."""
|
||||
|
||||
url = "/api/services/{domain}/{service}"
|
||||
name = "api:domain-services"
|
||||
url = '/api/services/{domain}/{service}'
|
||||
name = 'api:domain-services'
|
||||
|
||||
async def post(self, request, domain, service):
|
||||
"""Call a service.
|
||||
@@ -310,8 +313,8 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
try:
|
||||
data = json.loads(body) if body else None
|
||||
except ValueError:
|
||||
return self.json_message('Data should be valid JSON',
|
||||
HTTP_BAD_REQUEST)
|
||||
return self.json_message(
|
||||
"Data should be valid JSON.", HTTP_BAD_REQUEST)
|
||||
|
||||
with AsyncTrackStates(hass) as changed_states:
|
||||
await hass.services.async_call(domain, service, data, True)
|
||||
@@ -323,7 +326,7 @@ class APIComponentsView(HomeAssistantView):
|
||||
"""View to handle Components requests."""
|
||||
|
||||
url = URL_API_COMPONENTS
|
||||
name = "api:components"
|
||||
name = 'api:components'
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
@@ -332,10 +335,10 @@ class APIComponentsView(HomeAssistantView):
|
||||
|
||||
|
||||
class APITemplateView(HomeAssistantView):
|
||||
"""View to handle requests."""
|
||||
"""View to handle Template requests."""
|
||||
|
||||
url = URL_API_TEMPLATE
|
||||
name = "api:template"
|
||||
name = 'api:template'
|
||||
|
||||
async def post(self, request):
|
||||
"""Render a template."""
|
||||
@@ -344,29 +347,29 @@ class APITemplateView(HomeAssistantView):
|
||||
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),
|
||||
HTTP_BAD_REQUEST)
|
||||
return self.json_message(
|
||||
"Error rendering template: {}".format(ex), HTTP_BAD_REQUEST)
|
||||
|
||||
|
||||
class APIErrorLog(HomeAssistantView):
|
||||
"""View to fetch the error log."""
|
||||
"""View to fetch the API error log."""
|
||||
|
||||
url = URL_API_ERROR_LOG
|
||||
name = "api:error_log"
|
||||
name = 'api:error_log'
|
||||
|
||||
async def get(self, request):
|
||||
"""Retrieve API error log."""
|
||||
return await self.file(request, request.app['hass'].data[DATA_LOGGING])
|
||||
return web.FileResponse(request.app['hass'].data[DATA_LOGGING])
|
||||
|
||||
|
||||
async def async_services_json(hass):
|
||||
"""Generate services data to JSONify."""
|
||||
descriptions = await async_get_all_descriptions(hass)
|
||||
return [{"domain": key, "services": value}
|
||||
return [{'domain': key, 'services': value}
|
||||
for key, value in descriptions.items()]
|
||||
|
||||
|
||||
def async_events_json(hass):
|
||||
"""Generate event data to JSONify."""
|
||||
return [{"event": key, "listener_count": value}
|
||||
return [{'event': key, 'listener_count': value}
|
||||
for key, value in hass.bus.async_listeners().items()]
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyatv==0.3.9']
|
||||
REQUIREMENTS = ['pyatv==0.3.10']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ class AuthProvidersView(HomeAssistantView):
|
||||
requires_auth = False
|
||||
|
||||
@verify_client
|
||||
async def get(self, request, client_id):
|
||||
async def get(self, request, client):
|
||||
"""Get available auth providers."""
|
||||
return self.json([{
|
||||
'name': provider.name,
|
||||
@@ -166,8 +166,15 @@ class LoginFlowIndexView(FlowManagerIndexView):
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
@verify_client
|
||||
async def post(self, request, client_id):
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('handler'): vol.Any(str, list),
|
||||
vol.Required('redirect_uri'): str,
|
||||
}))
|
||||
async def post(self, request, client, data):
|
||||
"""Create a new login flow."""
|
||||
if data['redirect_uri'] not in client.redirect_uris:
|
||||
return self.json_message('invalid redirect uri', )
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
return await super().post(request)
|
||||
|
||||
@@ -192,7 +199,7 @@ class LoginFlowResourceView(FlowManagerResourceView):
|
||||
# pylint: disable=arguments-differ
|
||||
@verify_client
|
||||
@RequestDataValidator(vol.Schema(dict), allow_empty=True)
|
||||
async def post(self, request, client_id, flow_id, data):
|
||||
async def post(self, request, client, flow_id, data):
|
||||
"""Handle progressing a login flow request."""
|
||||
try:
|
||||
result = await self._flow_mgr.async_configure(flow_id, data)
|
||||
@@ -205,7 +212,7 @@ class LoginFlowResourceView(FlowManagerResourceView):
|
||||
return self.json(self._prepare_result_json(result))
|
||||
|
||||
result.pop('data')
|
||||
result['result'] = self._store_credentials(client_id, result['result'])
|
||||
result['result'] = self._store_credentials(client.id, result['result'])
|
||||
|
||||
return self.json(result)
|
||||
|
||||
@@ -222,7 +229,7 @@ class GrantTokenView(HomeAssistantView):
|
||||
self._retrieve_credentials = retrieve_credentials
|
||||
|
||||
@verify_client
|
||||
async def post(self, request, client_id):
|
||||
async def post(self, request, client):
|
||||
"""Grant a token."""
|
||||
hass = request.app['hass']
|
||||
data = await request.post()
|
||||
@@ -230,11 +237,11 @@ class GrantTokenView(HomeAssistantView):
|
||||
|
||||
if grant_type == 'authorization_code':
|
||||
return await self._async_handle_auth_code(
|
||||
hass, client_id, data)
|
||||
hass, client.id, data)
|
||||
|
||||
elif grant_type == 'refresh_token':
|
||||
return await self._async_handle_refresh_token(
|
||||
hass, client_id, data)
|
||||
hass, client.id, data)
|
||||
|
||||
return self.json({
|
||||
'error': 'unsupported_grant_type',
|
||||
|
||||
@@ -11,15 +11,15 @@ def verify_client(method):
|
||||
@wraps(method)
|
||||
async def wrapper(view, request, *args, **kwargs):
|
||||
"""Verify client id/secret before doing request."""
|
||||
client_id = await _verify_client(request)
|
||||
client = await _verify_client(request)
|
||||
|
||||
if client_id is None:
|
||||
if client is None:
|
||||
return view.json({
|
||||
'error': 'invalid_client',
|
||||
}, status_code=401)
|
||||
|
||||
return await method(
|
||||
view, request, *args, client_id=client_id, **kwargs)
|
||||
view, request, *args, **kwargs, client=client)
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -46,18 +46,34 @@ async def _verify_client(request):
|
||||
client_id, client_secret = decoded.split(':', 1)
|
||||
except ValueError:
|
||||
# If no ':' in decoded
|
||||
return None
|
||||
client_id, client_secret = decoded, None
|
||||
|
||||
client = await request.app['hass'].auth.async_get_client(client_id)
|
||||
return await async_secure_get_client(
|
||||
request.app['hass'], client_id, client_secret)
|
||||
|
||||
|
||||
async def async_secure_get_client(hass, client_id, client_secret):
|
||||
"""Get a client id/secret in consistent time."""
|
||||
client = await hass.auth.async_get_client(client_id)
|
||||
|
||||
if client is None:
|
||||
# Still do a compare so we run same time as if a client was found.
|
||||
hmac.compare_digest(client_secret.encode('utf-8'),
|
||||
client_secret.encode('utf-8'))
|
||||
if client_secret is not None:
|
||||
# Still do a compare so we run same time as if a client was found.
|
||||
hmac.compare_digest(client_secret.encode('utf-8'),
|
||||
client_secret.encode('utf-8'))
|
||||
return None
|
||||
|
||||
if hmac.compare_digest(client_secret.encode('utf-8'),
|
||||
client.secret.encode('utf-8')):
|
||||
return client_id
|
||||
if client.secret is None:
|
||||
return client
|
||||
|
||||
elif client_secret is None:
|
||||
# Still do a compare so we run same time as if a secret was passed.
|
||||
hmac.compare_digest(client.secret.encode('utf-8'),
|
||||
client.secret.encode('utf-8'))
|
||||
return None
|
||||
|
||||
elif hmac.compare_digest(client_secret.encode('utf-8'),
|
||||
client.secret.encode('utf-8')):
|
||||
return client
|
||||
|
||||
return None
|
||||
|
||||
@@ -98,7 +98,7 @@ SERVICE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
TRIGGER_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_VARIABLES, default={}): dict,
|
||||
})
|
||||
|
||||
|
||||
@@ -217,4 +217,4 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
self._deviation = bool(self.probability > self._probability_threshold)
|
||||
self._deviation = bool(self.probability >= self._probability_threshold)
|
||||
|
||||
@@ -17,9 +17,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES = {
|
||||
'lids': ['Doors', 'opening'],
|
||||
'windows': ['Windows', 'opening'],
|
||||
'door_lock_state': ['Door lock state', 'safety']
|
||||
'door_lock_state': ['Door lock state', 'safety'],
|
||||
'lights_parking': ['Parking lights', 'light'],
|
||||
'condition_based_services': ['Condition based services', 'problem'],
|
||||
'check_control_messages': ['Control messages', 'problem']
|
||||
}
|
||||
|
||||
SENSOR_TYPES_ELEC = {
|
||||
'charging_status': ['Charging status', 'power'],
|
||||
'connection_status': ['Connection status', 'plug']
|
||||
}
|
||||
|
||||
SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the BMW sensors."""
|
||||
@@ -29,10 +39,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
devices = []
|
||||
for account in accounts:
|
||||
for vehicle in account.account.vehicles:
|
||||
for key, value in sorted(SENSOR_TYPES.items()):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
devices.append(device)
|
||||
if vehicle.has_hv_battery:
|
||||
_LOGGER.debug('BMW with a high voltage battery')
|
||||
for key, value in sorted(SENSOR_TYPES_ELEC.items()):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
devices.append(device)
|
||||
elif vehicle.has_internal_combustion_engine:
|
||||
_LOGGER.debug('BMW with an internal combustion engine')
|
||||
for key, value in sorted(SENSOR_TYPES.items()):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
devices.append(device)
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
@@ -92,12 +110,34 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
result[window.name] = window.state.value
|
||||
elif self._attribute == 'door_lock_state':
|
||||
result['door_lock_state'] = vehicle_state.door_lock_state.value
|
||||
result['last_update_reason'] = vehicle_state.last_update_reason
|
||||
elif self._attribute == 'lights_parking':
|
||||
result['lights_parking'] = vehicle_state.parking_lights.value
|
||||
elif self._attribute == 'condition_based_services':
|
||||
for report in vehicle_state.condition_based_services:
|
||||
result.update(self._format_cbs_report(report))
|
||||
elif self._attribute == 'check_control_messages':
|
||||
check_control_messages = vehicle_state.check_control_messages
|
||||
if not check_control_messages:
|
||||
result['check_control_messages'] = 'OK'
|
||||
else:
|
||||
result['check_control_messages'] = check_control_messages
|
||||
elif self._attribute == 'charging_status':
|
||||
result['charging_status'] = vehicle_state.charging_status.value
|
||||
# pylint: disable=W0212
|
||||
result['last_charging_end_result'] = \
|
||||
vehicle_state._attributes['lastChargingEndResult']
|
||||
if self._attribute == 'connection_status':
|
||||
# pylint: disable=W0212
|
||||
result['connection_status'] = \
|
||||
vehicle_state._attributes['connectionStatus']
|
||||
|
||||
return result
|
||||
return sorted(result.items())
|
||||
|
||||
def update(self):
|
||||
"""Read new state data from the library."""
|
||||
from bimmer_connected.state import LockState
|
||||
from bimmer_connected.state import ChargingState
|
||||
vehicle_state = self._vehicle.state
|
||||
|
||||
# device class opening: On means open, Off means closed
|
||||
@@ -111,6 +151,37 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
||||
self._state = vehicle_state.door_lock_state not in \
|
||||
[LockState.LOCKED, LockState.SECURED]
|
||||
# device class light: On means light detected, Off means no light
|
||||
if self._attribute == 'lights_parking':
|
||||
self._state = vehicle_state.are_parking_lights_on
|
||||
# device class problem: On means problem detected, Off means no problem
|
||||
if self._attribute == 'condition_based_services':
|
||||
self._state = not vehicle_state.are_all_cbs_ok
|
||||
if self._attribute == 'check_control_messages':
|
||||
self._state = vehicle_state.has_check_control_messages
|
||||
# device class power: On means power detected, Off means no power
|
||||
if self._attribute == 'charging_status':
|
||||
self._state = vehicle_state.charging_status in \
|
||||
[ChargingState.CHARGING]
|
||||
# device class plug: On means device is plugged in,
|
||||
# Off means device is unplugged
|
||||
if self._attribute == 'connection_status':
|
||||
# pylint: disable=W0212
|
||||
self._state = (vehicle_state._attributes['connectionStatus'] ==
|
||||
'CONNECTED')
|
||||
|
||||
@staticmethod
|
||||
def _format_cbs_report(report):
|
||||
result = {}
|
||||
service_type = report.service_type.lower().replace('_', ' ')
|
||||
result['{} status'.format(service_type)] = report.state.value
|
||||
if report.due_date is not None:
|
||||
result['{} date'.format(service_type)] = \
|
||||
report.due_date.strftime('%Y-%m-%d')
|
||||
if report.due_distance is not None:
|
||||
result['{} distance'.format(service_type)] = \
|
||||
'{} km'.format(report.due_distance)
|
||||
return result
|
||||
|
||||
def update_callback(self):
|
||||
"""Schedule a state update."""
|
||||
|
||||
@@ -6,7 +6,8 @@ https://home-assistant.io/components/binary_sensor.deconz/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz import (
|
||||
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
|
||||
CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID,
|
||||
DATA_DECONZ_UNSUB)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -27,10 +28,13 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Add binary sensor from deCONZ."""
|
||||
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
||||
entities = []
|
||||
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
|
||||
for sensor in sensors:
|
||||
if sensor.type in DECONZ_BINARY_SENSOR:
|
||||
if sensor.type in DECONZ_BINARY_SENSOR and \
|
||||
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
|
||||
entities.append(DeconzBinarySensor(sensor))
|
||||
async_add_devices(entities, True)
|
||||
|
||||
hass.data[DATA_DECONZ_UNSUB].append(
|
||||
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
|
||||
|
||||
@@ -103,6 +107,6 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
attr = {}
|
||||
if self._sensor.battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
|
||||
if self._sensor.type in PRESENCE and self._sensor.dark:
|
||||
if self._sensor.type in PRESENCE and self._sensor.dark is not None:
|
||||
attr['dark'] = self._sensor.dark
|
||||
return attr
|
||||
|
||||
@@ -6,6 +6,7 @@ https://home-assistant.io/components/binary_sensor.envisalink/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -14,6 +15,7 @@ from homeassistant.components.envisalink import (
|
||||
DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice,
|
||||
SIGNAL_ZONE_UPDATE)
|
||||
from homeassistant.const import ATTR_LAST_TRIP_TIME
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -63,7 +65,25 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault']
|
||||
|
||||
# The Envisalink library returns a "last_fault" value that's the
|
||||
# number of seconds since the last fault, up to a maximum of 327680
|
||||
# seconds (65536 5-second ticks).
|
||||
#
|
||||
# We don't want the HA event log to fill up with a bunch of no-op
|
||||
# "state changes" that are just that number ticking up once per poll
|
||||
# interval, so we subtract it from the current second-accurate time
|
||||
# unless it is already at the maximum value, in which case we set it
|
||||
# to None since we can't determine the actual value.
|
||||
seconds_ago = self._info['last_fault']
|
||||
if seconds_ago < 65536 * 5:
|
||||
now = dt_util.now().replace(microsecond=0)
|
||||
delta = datetime.timedelta(seconds=seconds_ago)
|
||||
last_trip_time = (now - delta).isoformat()
|
||||
else:
|
||||
last_trip_time = None
|
||||
|
||||
attr[ATTR_LAST_TRIP_TIME] = last_trip_time
|
||||
return attr
|
||||
|
||||
@property
|
||||
|
||||
85
homeassistant/components/binary_sensor/homematicip_cloud.py
Normal file
85
homeassistant/components/binary_sensor/homematicip_cloud.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Support for HomematicIP binary sensor.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.homematicip_cloud/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
||||
ATTR_HOME_ID)
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_WINDOW_STATE = 'window_state'
|
||||
ATTR_EVENT_DELAY = 'event_delay'
|
||||
ATTR_MOTION_DETECTED = 'motion_detected'
|
||||
ATTR_ILLUMINATION = 'illumination'
|
||||
|
||||
HMIP_OPEN = 'open'
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP binary sensor devices."""
|
||||
from homematicip.device import (ShutterContact, MotionDetectorIndoor)
|
||||
|
||||
if discovery_info is None:
|
||||
return
|
||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
||||
devices = []
|
||||
for device in home.devices:
|
||||
if isinstance(device, ShutterContact):
|
||||
devices.append(HomematicipShutterContact(home, device))
|
||||
elif isinstance(device, MotionDetectorIndoor):
|
||||
devices.append(HomematicipMotionDetector(home, device))
|
||||
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
|
||||
"""HomematicIP shutter contact."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the shutter contact."""
|
||||
super().__init__(home, device)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return 'door'
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the shutter contact is on/open."""
|
||||
if self._device.sabotage:
|
||||
return True
|
||||
if self._device.windowState is None:
|
||||
return None
|
||||
return self._device.windowState.lower() == HMIP_OPEN
|
||||
|
||||
|
||||
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
"""MomematicIP motion detector."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the shutter contact."""
|
||||
super().__init__(home, device)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return 'motion'
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if motion is detected."""
|
||||
if self._device.sabotage:
|
||||
return True
|
||||
return self._device.motionDetected
|
||||
81
homeassistant/components/binary_sensor/hydrawise.py
Normal file
81
homeassistant/components/binary_sensor/hydrawise.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Support for Hydrawise sprinkler.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.hydrawise/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.hydrawise import (
|
||||
BINARY_SENSORS, DATA_HYDRAWISE, HydrawiseEntity, DEVICE_MAP,
|
||||
DEVICE_MAP_INDEX)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
|
||||
DEPENDENCIES = ['hydrawise']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=BINARY_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a sensor for a Hydrawise device."""
|
||||
hydrawise = hass.data[DATA_HYDRAWISE].data
|
||||
|
||||
sensors = []
|
||||
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
|
||||
if sensor_type in ['status', 'rain_sensor']:
|
||||
sensors.append(
|
||||
HydrawiseBinarySensor(
|
||||
hydrawise.controller_status, sensor_type))
|
||||
|
||||
else:
|
||||
# create a sensor for each zone
|
||||
for zone in hydrawise.relays:
|
||||
zone_data = zone
|
||||
zone_data['running'] = \
|
||||
hydrawise.controller_status.get('running', False)
|
||||
sensors.append(HydrawiseBinarySensor(zone_data, sensor_type))
|
||||
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorDevice):
|
||||
"""A sensor implementation for Hydrawise device."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
_LOGGER.debug("Updating Hydrawise binary sensor: %s", self._name)
|
||||
mydata = self.hass.data[DATA_HYDRAWISE].data
|
||||
if self._sensor_type == 'status':
|
||||
self._state = mydata.status == 'All good!'
|
||||
elif self._sensor_type == 'rain_sensor':
|
||||
for sensor in mydata.sensors:
|
||||
if sensor['name'] == 'Rain':
|
||||
self._state = sensor['active'] == 1
|
||||
elif self._sensor_type == 'is_watering':
|
||||
if not mydata.running:
|
||||
self._state = False
|
||||
elif int(mydata.running[0]['relay']) == self.data['relay']:
|
||||
self._state = True
|
||||
else:
|
||||
self._state = False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of the sensor type."""
|
||||
return DEVICE_MAP[self._sensor_type][
|
||||
DEVICE_MAP_INDEX.index('DEVICE_CLASS_INDEX')]
|
||||
@@ -117,8 +117,10 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
# pylint: disable=protected-access
|
||||
if _is_val_unknown(self._node.status._val):
|
||||
self._computed_state = None
|
||||
self._status_was_unknown = True
|
||||
else:
|
||||
self._computed_state = bool(self._node.status._val)
|
||||
self._status_was_unknown = False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self) -> None:
|
||||
@@ -156,9 +158,13 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
# pylint: disable=protected-access
|
||||
if not _is_val_unknown(self._negative_node.status._val):
|
||||
# If the negative node has a value, it means the negative node is
|
||||
# in use for this device. Therefore, we cannot determine the state
|
||||
# of the sensor until we receive our first ON event.
|
||||
self._computed_state = None
|
||||
# in use for this device. Next we need to check to see if the
|
||||
# negative and positive nodes disagree on the state (both ON or
|
||||
# both OFF).
|
||||
if self._negative_node.status._val == self._node.status._val:
|
||||
# The states disagree, therefore we cannot determine the state
|
||||
# of the sensor until we receive our first ON event.
|
||||
self._computed_state = None
|
||||
|
||||
def _negative_node_control_handler(self, event: object) -> None:
|
||||
"""Handle an "On" control event from the "negative" node."""
|
||||
@@ -189,14 +195,21 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Ignore primary node status updates.
|
||||
"""Primary node status updates.
|
||||
|
||||
We listen directly to the Control events on all nodes for this
|
||||
device.
|
||||
We MOSTLY ignore these updates, as we listen directly to the Control
|
||||
events on all nodes for this device. However, there is one edge case:
|
||||
If a leak sensor is unknown, due to a recent reboot of the ISY, the
|
||||
status will get updated to dry upon the first heartbeat. This status
|
||||
update is the only way that a leak sensor's status changes without
|
||||
an accompanying Control event, so we need to watch for it.
|
||||
"""
|
||||
pass
|
||||
if self._status_was_unknown and self._computed_state is None:
|
||||
self._computed_state = bool(int(self._node.status))
|
||||
self._status_was_unknown = False
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
|
||||
@property
|
||||
def value(self) -> object:
|
||||
|
||||
82
homeassistant/components/binary_sensor/konnected.py
Normal file
82
homeassistant/components/binary_sensor/konnected.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Support for wired binary sensors attached to a Konnected device.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.konnected/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.konnected import (
|
||||
DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE)
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_BINARY_SENSORS, ATTR_ENTITY_ID,
|
||||
ATTR_STATE)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['konnected']
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up binary sensors attached to a Konnected device."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = hass.data[KONNECTED_DOMAIN]
|
||||
device_id = discovery_info['device_id']
|
||||
sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data)
|
||||
for pin_num, pin_data in
|
||||
data[CONF_DEVICES][device_id][CONF_BINARY_SENSORS].items()]
|
||||
async_add_devices(sensors)
|
||||
|
||||
|
||||
class KonnectedBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Konnected binary sensor."""
|
||||
|
||||
def __init__(self, device_id, pin_num, data):
|
||||
"""Initialize the binary sensor."""
|
||||
self._data = data
|
||||
self._device_id = device_id
|
||||
self._pin_num = pin_num
|
||||
self._state = self._data.get(ATTR_STATE)
|
||||
self._device_class = self._data.get(CONF_TYPE)
|
||||
self._name = self._data.get(CONF_NAME, 'Konnected {} Zone {}'.format(
|
||||
device_id, PIN_TO_ZONE[pin_num]))
|
||||
_LOGGER.debug('Created new Konnected sensor: %s', self._name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Store entity_id and register state change callback."""
|
||||
self._data[ATTR_ENTITY_ID] = self.entity_id
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_SENSOR_UPDATE.format(self.entity_id),
|
||||
self.async_set_state)
|
||||
|
||||
@callback
|
||||
def async_set_state(self, state):
|
||||
"""Update the sensor's state."""
|
||||
self._state = state
|
||||
self.async_schedule_update_ha_state()
|
||||
@@ -31,7 +31,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
sensors = []
|
||||
hub = hass.data[MYCHEVY_DOMAIN]
|
||||
for sconfig in SENSORS:
|
||||
sensors.append(EVBinarySensor(hub, sconfig))
|
||||
for car in hub.cars:
|
||||
sensors.append(EVBinarySensor(hub, sconfig, car.vid))
|
||||
|
||||
async_add_devices(sensors)
|
||||
|
||||
@@ -45,16 +46,18 @@ class EVBinarySensor(BinarySensorDevice):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, connection, config):
|
||||
def __init__(self, connection, config, car_vid):
|
||||
"""Initialize sensor with car connection."""
|
||||
self._conn = connection
|
||||
self._name = config.name
|
||||
self._attr = config.attr
|
||||
self._type = config.device_class
|
||||
self._is_on = None
|
||||
|
||||
self._car_vid = car_vid
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(
|
||||
'{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name)))
|
||||
'{}_{}_{}'.format(MYCHEVY_DOMAIN,
|
||||
slugify(self._car.name),
|
||||
slugify(self._name)))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -66,6 +69,11 @@ class EVBinarySensor(BinarySensorDevice):
|
||||
"""Return if on."""
|
||||
return self._is_on
|
||||
|
||||
@property
|
||||
def _car(self):
|
||||
"""Return the car."""
|
||||
return self._conn.get_car(self._car_vid)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@@ -75,8 +83,8 @@ class EVBinarySensor(BinarySensorDevice):
|
||||
@callback
|
||||
def async_update_callback(self):
|
||||
"""Update state."""
|
||||
if self._conn.car is not None:
|
||||
self._is_on = getattr(self._conn.car, self._attr, None)
|
||||
if self._car is not None:
|
||||
self._is_on = getattr(self._car, self._attr, None)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
|
||||
@@ -7,27 +7,36 @@ https://home-assistant.io/components/binary_sensor.nest/
|
||||
from itertools import chain
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
from homeassistant.components.sensor.nest import NestSensor
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.nest import DATA_NEST, NestSensorDevice
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.components.nest import DATA_NEST
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
|
||||
BINARY_TYPES = ['online']
|
||||
BINARY_TYPES = {'online': 'connectivity'}
|
||||
|
||||
CLIMATE_BINARY_TYPES = [
|
||||
'fan',
|
||||
'is_using_emergency_heat',
|
||||
'is_locked',
|
||||
'has_leaf',
|
||||
]
|
||||
CLIMATE_BINARY_TYPES = {
|
||||
'fan': None,
|
||||
'is_using_emergency_heat': 'heat',
|
||||
'is_locked': None,
|
||||
'has_leaf': None,
|
||||
}
|
||||
|
||||
CAMERA_BINARY_TYPES = [
|
||||
'motion_detected',
|
||||
'sound_detected',
|
||||
'person_detected',
|
||||
]
|
||||
CAMERA_BINARY_TYPES = {
|
||||
'motion_detected': 'motion',
|
||||
'sound_detected': 'sound',
|
||||
'person_detected': 'occupancy',
|
||||
}
|
||||
|
||||
STRUCTURE_BINARY_TYPES = {
|
||||
'away': None,
|
||||
# 'security_state', # pending python-nest update
|
||||
}
|
||||
|
||||
STRUCTURE_BINARY_STATE_MAP = {
|
||||
'away': {'away': True, 'home': False},
|
||||
'security_state': {'deter': True, 'ok': False},
|
||||
}
|
||||
|
||||
_BINARY_TYPES_DEPRECATED = [
|
||||
'hvac_ac_state',
|
||||
@@ -40,8 +49,8 @@ _BINARY_TYPES_DEPRECATED = [
|
||||
'hvac_emer_heat_state',
|
||||
]
|
||||
|
||||
_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \
|
||||
+ CAMERA_BINARY_TYPES
|
||||
_VALID_BINARY_SENSOR_TYPES = {**BINARY_TYPES, **CLIMATE_BINARY_TYPES,
|
||||
**CAMERA_BINARY_TYPES, **STRUCTURE_BINARY_TYPES}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -68,6 +77,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error(wstr)
|
||||
|
||||
sensors = []
|
||||
for structure in nest.structures():
|
||||
sensors += [NestBinarySensor(structure, None, variable)
|
||||
for variable in conditions
|
||||
if variable in STRUCTURE_BINARY_TYPES]
|
||||
device_chain = chain(nest.thermostats(),
|
||||
nest.smoke_co_alarms(),
|
||||
nest.cameras())
|
||||
@@ -88,11 +101,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
sensors += [NestActivityZoneSensor(structure,
|
||||
device,
|
||||
activity_zone)]
|
||||
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||
class NestBinarySensor(NestSensorDevice, BinarySensorDevice):
|
||||
"""Represents a Nest binary sensor."""
|
||||
|
||||
@property
|
||||
@@ -100,9 +112,19 @@ class NestBinarySensor(NestSensor, BinarySensorDevice):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of the binary sensor."""
|
||||
return _VALID_BINARY_SENSOR_TYPES.get(self.variable)
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._state = bool(getattr(self.device, self.variable))
|
||||
value = getattr(self.device, self.variable)
|
||||
if self.variable in STRUCTURE_BINARY_TYPES:
|
||||
self._state = bool(STRUCTURE_BINARY_STATE_MAP
|
||||
[self.variable][value])
|
||||
else:
|
||||
self._state = bool(value)
|
||||
|
||||
|
||||
class NestActivityZoneSensor(NestBinarySensor):
|
||||
@@ -115,9 +137,9 @@ class NestActivityZoneSensor(NestBinarySensor):
|
||||
self._name = "{} {} activity".format(self._name, self.zone.name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
return self._name
|
||||
def device_class(self):
|
||||
"""Return the device class of the binary sensor."""
|
||||
return 'motion'
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
|
||||
102
homeassistant/components/binary_sensor/rainmachine.py
Normal file
102
homeassistant/components/binary_sensor/rainmachine.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
This platform provides binary sensors for key RainMachine data.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rainmachine/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.rainmachine import (
|
||||
BINARY_SENSORS, DATA_RAINMACHINE, DATA_UPDATE_TOPIC, TYPE_FREEZE,
|
||||
TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH,
|
||||
TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity)
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['rainmachine']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the RainMachine Switch platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
rainmachine = hass.data[DATA_RAINMACHINE]
|
||||
|
||||
binary_sensors = []
|
||||
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
|
||||
name, icon = BINARY_SENSORS[sensor_type]
|
||||
binary_sensors.append(
|
||||
RainMachineBinarySensor(rainmachine, sensor_type, name, icon))
|
||||
|
||||
add_devices(binary_sensors, True)
|
||||
|
||||
|
||||
class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
|
||||
"""A sensor implementation for raincloud device."""
|
||||
|
||||
def __init__(self, rainmachine, sensor_type, name, icon):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(rainmachine)
|
||||
|
||||
self._icon = icon
|
||||
self._name = name
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the icon."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the status of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, HASS-friendly identifier for this entity."""
|
||||
return '{0}_{1}'.format(
|
||||
self.rainmachine.device_mac.replace(':', ''), self._sensor_type)
|
||||
|
||||
@callback
|
||||
def update_data(self):
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC,
|
||||
self.update_data)
|
||||
|
||||
def update(self):
|
||||
"""Update the state."""
|
||||
if self._sensor_type == TYPE_FREEZE:
|
||||
self._state = self.rainmachine.restrictions['current']['freeze']
|
||||
elif self._sensor_type == TYPE_FREEZE_PROTECTION:
|
||||
self._state = self.rainmachine.restrictions['global'][
|
||||
'freezeProtectEnabled']
|
||||
elif self._sensor_type == TYPE_HOT_DAYS:
|
||||
self._state = self.rainmachine.restrictions['global'][
|
||||
'hotDaysExtraWatering']
|
||||
elif self._sensor_type == TYPE_HOURLY:
|
||||
self._state = self.rainmachine.restrictions['current']['hourly']
|
||||
elif self._sensor_type == TYPE_MONTH:
|
||||
self._state = self.rainmachine.restrictions['current']['month']
|
||||
elif self._sensor_type == TYPE_RAINDELAY:
|
||||
self._state = self.rainmachine.restrictions['current']['rainDelay']
|
||||
elif self._sensor_type == TYPE_RAINSENSOR:
|
||||
self._state = self.rainmachine.restrictions['current'][
|
||||
'rainSensor']
|
||||
elif self._sensor_type == TYPE_WEEKDAY:
|
||||
self._state = self.rainmachine.restrictions['current']['weekDay']
|
||||
@@ -4,7 +4,6 @@ Support for showing random states.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.random/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -24,8 +23,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Random binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
@@ -57,8 +56,7 @@ class RandomSensor(BinarySensorDevice):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._device_class
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
async def async_update(self):
|
||||
"""Get new state and update the sensor's state."""
|
||||
from random import getrandbits
|
||||
self._state = bool(getrandbits(1))
|
||||
|
||||
@@ -10,7 +10,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import rfxtrx
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, BinarySensorDevice)
|
||||
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.components.rfxtrx import (
|
||||
ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES,
|
||||
CONF_FIRE_EVENT, CONF_OFF_DELAY)
|
||||
@@ -29,8 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_DEVICES, default={}): {
|
||||
cv.string: vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS):
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_OFF_DELAY):
|
||||
vol.Any(cv.time_period, cv.positive_timedelta),
|
||||
|
||||
@@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
'channel_1', hass, gateway))
|
||||
devices.append(XiaomiButton(device, 'Wall Switch (Both)',
|
||||
'dual_channel', hass, gateway))
|
||||
elif model in ['cube', 'sensor_cube']:
|
||||
elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']:
|
||||
devices.append(XiaomiCube(device, hass, gateway))
|
||||
add_devices(devices)
|
||||
|
||||
@@ -330,6 +330,8 @@ class XiaomiButton(XiaomiBinarySensor):
|
||||
click_type = 'both'
|
||||
elif value == 'shake':
|
||||
click_type = 'shake'
|
||||
elif value == 'long_click':
|
||||
return False
|
||||
else:
|
||||
_LOGGER.warning("Unsupported click_type detected: %s", value)
|
||||
return False
|
||||
|
||||
@@ -108,7 +108,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
if self._state == 'unknown':
|
||||
if self._state is None:
|
||||
return False
|
||||
return bool(self._state)
|
||||
|
||||
@@ -133,7 +133,8 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
from bellows.types.basic import uint16_t
|
||||
|
||||
result = await zha.safe_read(self._endpoint.ias_zone,
|
||||
['zone_status'])
|
||||
['zone_status'],
|
||||
allow_cache=False)
|
||||
state = result.get('zone_status', self._state)
|
||||
if isinstance(state, (int, uint16_t)):
|
||||
self._state = result.get('zone_status', self._state) & 3
|
||||
@@ -202,14 +203,19 @@ class Switch(zha.Entity, BinarySensorDevice):
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize Switch."""
|
||||
super().__init__(**kwargs)
|
||||
self._state = True
|
||||
self._level = 255
|
||||
self._state = False
|
||||
self._level = 0
|
||||
from zigpy.zcl.clusters import general
|
||||
self._out_listeners = {
|
||||
general.OnOff.cluster_id: self.OnOffListener(self),
|
||||
general.LevelControl.cluster_id: self.LevelListener(self),
|
||||
}
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Let zha handle polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the binary sensor is on."""
|
||||
@@ -218,7 +224,10 @@ class Switch(zha.Entity, BinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
return {'level': self._state and self._level or 0}
|
||||
self._device_state_attributes.update({
|
||||
'level': self._state and self._level or 0
|
||||
})
|
||||
return self._device_state_attributes
|
||||
|
||||
def move_level(self, change):
|
||||
"""Increment the level, setting state if appropriate."""
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['bimmer_connected==0.5.0']
|
||||
REQUIREMENTS = ['bimmer_connected==0.5.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ activate_air_conditioning:
|
||||
description: >
|
||||
Start the air conditioning of the vehicle. What exactly is started here
|
||||
depends on the type of vehicle. It might range from just ventilation over
|
||||
auxilary heating to real air conditioning. The vehicle is identified via
|
||||
auxiliary heating to real air conditioning. The vehicle is identified via
|
||||
the vin (see below).
|
||||
fields:
|
||||
vin:
|
||||
@@ -39,4 +39,4 @@ update_state:
|
||||
description: >
|
||||
Fetch the last state of the vehicles of all your accounts from the BMW
|
||||
server. This does *not* trigger an update from the vehicle, it just gets
|
||||
the data from the BMW servers. This service does not require any attributes.
|
||||
the data from the BMW servers. This service does not require any attributes.
|
||||
|
||||
@@ -256,6 +256,11 @@ class Camera(Entity):
|
||||
"""Return the camera model."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def frame_interval(self):
|
||||
"""Return the interval between frames of the mjpeg stream."""
|
||||
return 0.5
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
raise NotImplementedError()
|
||||
@@ -272,10 +277,6 @@ class Camera(Entity):
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
if interval < MIN_STREAM_INTERVAL:
|
||||
raise ValueError("Stream interval must be be > {}"
|
||||
.format(MIN_STREAM_INTERVAL))
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = ('multipart/x-mixed-replace; '
|
||||
'boundary=--frameboundary')
|
||||
@@ -325,8 +326,7 @@ class Camera(Entity):
|
||||
a direct stream from the camera.
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
await self.handle_async_still_stream(request,
|
||||
FALLBACK_STREAM_INTERVAL)
|
||||
await self.handle_async_still_stream(request, self.frame_interval)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -448,6 +448,9 @@ class CameraMjpegStream(CameraView):
|
||||
try:
|
||||
# Compose camera stream from stills
|
||||
interval = float(request.query.get('interval'))
|
||||
if interval < MIN_STREAM_INTERVAL:
|
||||
raise ValueError("Stream interval must be be > {}"
|
||||
.format(MIN_STREAM_INTERVAL))
|
||||
await camera.handle_async_still_stream(request, interval)
|
||||
return
|
||||
except ValueError:
|
||||
|
||||
58
homeassistant/components/camera/familyhub.py
Normal file
58
homeassistant/components/camera/familyhub.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Family Hub camera for Samsung Refrigerators.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/camera.familyhub/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['python-family-hub-local==0.0.2']
|
||||
|
||||
DEFAULT_NAME = 'FamilyHub Camera'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_IP_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Family Hub Camera."""
|
||||
from pyfamilyhublocal import FamilyHubCam
|
||||
address = config.get(CONF_IP_ADDRESS)
|
||||
name = config.get(CONF_NAME)
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
family_hub_cam = FamilyHubCam(address, hass.loop, session)
|
||||
|
||||
async_add_devices([FamilyHubCamera(name, family_hub_cam)], True)
|
||||
|
||||
|
||||
class FamilyHubCamera(Camera):
|
||||
"""The representation of a Family Hub camera."""
|
||||
|
||||
def __init__(self, name, family_hub_cam):
|
||||
"""Initialize camera component."""
|
||||
super().__init__()
|
||||
self._name = name
|
||||
self.family_hub_cam = family_hub_cam
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response."""
|
||||
return await self.family_hub_cam.async_get_cam_image()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
@@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_CONTENT_TYPE = 'content_type'
|
||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
|
||||
CONF_STILL_IMAGE_URL = 'still_image_url'
|
||||
CONF_FRAMERATE = 'framerate'
|
||||
|
||||
DEFAULT_NAME = 'Generic Camera'
|
||||
|
||||
@@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string,
|
||||
vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
@@ -62,6 +64,7 @@ class GenericCamera(Camera):
|
||||
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
|
||||
self._still_image_url.hass = hass
|
||||
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
|
||||
self._frame_interval = 1 / device_info[CONF_FRAMERATE]
|
||||
self.content_type = device_info[CONF_CONTENT_TYPE]
|
||||
|
||||
username = device_info.get(CONF_USERNAME)
|
||||
@@ -78,6 +81,11 @@ class GenericCamera(Camera):
|
||||
self._last_url = None
|
||||
self._last_image = None
|
||||
|
||||
@property
|
||||
def frame_interval(self):
|
||||
"""Return the interval between frames of the mjpeg stream."""
|
||||
return self._frame_interval
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
return run_coroutine_threadsafe(
|
||||
|
||||
@@ -22,6 +22,12 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE,
|
||||
PRECISION_TENTHS, )
|
||||
|
||||
DEFAULT_MIN_TEMP = 7
|
||||
DEFAULT_MAX_TEMP = 35
|
||||
DEFAULT_MIN_HUMITIDY = 30
|
||||
DEFAULT_MAX_HUMIDITY = 99
|
||||
|
||||
DOMAIN = 'climate'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
@@ -778,19 +784,21 @@ class ClimateDevice(Entity):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return convert_temperature(7, TEMP_CELSIUS, self.temperature_unit)
|
||||
return convert_temperature(DEFAULT_MIN_TEMP, TEMP_CELSIUS,
|
||||
self.temperature_unit)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return convert_temperature(35, TEMP_CELSIUS, self.temperature_unit)
|
||||
return convert_temperature(DEFAULT_MAX_TEMP, TEMP_CELSIUS,
|
||||
self.temperature_unit)
|
||||
|
||||
@property
|
||||
def min_humidity(self):
|
||||
"""Return the minimum humidity."""
|
||||
return 30
|
||||
return DEFAULT_MIN_HUMITIDY
|
||||
|
||||
@property
|
||||
def max_humidity(self):
|
||||
"""Return the maximum humidity."""
|
||||
return 99
|
||||
return DEFAULT_MAX_HUMIDITY
|
||||
|
||||
@@ -14,7 +14,8 @@ from homeassistant.core import DOMAIN as HA_DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice,
|
||||
ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
|
||||
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA,
|
||||
DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
|
||||
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
@@ -267,8 +268,7 @@ class GenericThermostat(ClimateDevice):
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
|
||||
# get default temp from super class
|
||||
return ClimateDevice.min_temp.fget(self)
|
||||
return DEFAULT_MIN_TEMP
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
@@ -277,8 +277,7 @@ class GenericThermostat(ClimateDevice):
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
|
||||
# Get default temp from super class
|
||||
return ClimateDevice.max_temp.fget(self)
|
||||
return DEFAULT_MAX_TEMP
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_sensor_changed(self, entity_id, old_state, new_state):
|
||||
|
||||
101
homeassistant/components/climate/homematicip_cloud.py
Normal file
101
homeassistant/components/climate/homematicip_cloud.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Support for HomematicIP climate.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.homematicip_cloud/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, ATTR_TEMPERATURE,
|
||||
STATE_AUTO, STATE_MANUAL)
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
||||
ATTR_HOME_ID)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_BOOST = 'Boost'
|
||||
|
||||
HA_STATE_TO_HMIP = {
|
||||
STATE_AUTO: 'AUTOMATIC',
|
||||
STATE_MANUAL: 'MANUAL',
|
||||
}
|
||||
|
||||
HMIP_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_HMIP.items()}
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP climate devices."""
|
||||
from homematicip.group import HeatingGroup
|
||||
|
||||
if discovery_info is None:
|
||||
return
|
||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
||||
|
||||
devices = []
|
||||
for device in home.groups:
|
||||
if isinstance(device, HeatingGroup):
|
||||
devices.append(HomematicipHeatingGroup(home, device))
|
||||
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
|
||||
"""Representation of a MomematicIP heating group."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize heating group."""
|
||||
device.modelType = 'Group-Heating'
|
||||
super().__init__(home, device)
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._device.setPointTemperature
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._device.actualTemperature
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
return self._device.humidity
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. automatic or manual."""
|
||||
return HMIP_STATE_TO_HA.get(self._device.controlMode)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._device.minTemperature
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._device.maxTemperature
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
await self._device.set_point_temperature(temperature)
|
||||
@@ -115,7 +115,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
"""List of available fan modes."""
|
||||
return ['Auto', 'Min', 'Normal', 'Max']
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
@@ -143,9 +143,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that device has changed state
|
||||
self._values[value_type] = value
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def set_fan_mode(self, fan_mode):
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target temperature."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
@@ -153,9 +153,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that device has changed state
|
||||
self._values[set_req.V_HVAC_SPEED] = fan_mode
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
async def async_set_operation_mode(self, operation_mode):
|
||||
"""Set new target temperature."""
|
||||
self.gateway.set_child_value(
|
||||
self.node_id, self.child_id, self.value_type,
|
||||
@@ -163,7 +163,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that device has changed state
|
||||
self._values[self.value_type] = operation_mode
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.nest import DATA_NEST
|
||||
from homeassistant.components.nest import DATA_NEST, SIGNAL_NEST_UPDATE
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice,
|
||||
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
@@ -18,6 +18,7 @@ from homeassistant.components.climate import (
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -37,11 +38,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
|
||||
add_devices(
|
||||
[NestThermostat(structure, device, temp_unit)
|
||||
for structure, device in hass.data[DATA_NEST].thermostats()],
|
||||
True
|
||||
)
|
||||
all_devices = [NestThermostat(structure, device, temp_unit)
|
||||
for structure, device in hass.data[DATA_NEST].thermostats()]
|
||||
|
||||
add_devices(all_devices, True)
|
||||
|
||||
|
||||
class NestThermostat(ClimateDevice):
|
||||
@@ -97,6 +97,20 @@ class NestThermostat(ClimateDevice):
|
||||
self._min_temperature = None
|
||||
self._max_temperature = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Do not need poll thanks using Nest streaming API."""
|
||||
return False
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register update signal handler."""
|
||||
async def async_update_state():
|
||||
"""Update device state."""
|
||||
await self.async_update_ha_state(True)
|
||||
|
||||
async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE,
|
||||
async_update_state)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
@@ -134,7 +148,9 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._mode != NEST_MODE_HEAT_COOL and not self.is_away_mode_on:
|
||||
if self._mode != NEST_MODE_HEAT_COOL and \
|
||||
self._mode != STATE_ECO and \
|
||||
not self.is_away_mode_on:
|
||||
return self._target_temperature
|
||||
return None
|
||||
|
||||
@@ -168,18 +184,24 @@ class NestThermostat(ClimateDevice):
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
import nest
|
||||
temp = None
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if self._mode == NEST_MODE_HEAT_COOL:
|
||||
if target_temp_low is not None and target_temp_high is not None:
|
||||
temp = (target_temp_low, target_temp_high)
|
||||
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
|
||||
else:
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
|
||||
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
|
||||
try:
|
||||
self.device.target = temp
|
||||
except nest.nest.APIError:
|
||||
_LOGGER.error("An error occurred while setting the temperature")
|
||||
if temp is not None:
|
||||
self.device.target = temp
|
||||
except nest.nest.APIError as api_error:
|
||||
_LOGGER.error("An error occurred while setting temperature: %s",
|
||||
api_error)
|
||||
# restore target temperature
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
|
||||
@@ -19,13 +19,13 @@ from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_FAN_MODE, SUPPORT_SWING_MODE,
|
||||
SUPPORT_ON_OFF)
|
||||
SUPPORT_ON_OFF, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
|
||||
REQUIREMENTS = ['pysensibo==1.0.2']
|
||||
REQUIREMENTS = ['pysensibo==1.0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -154,7 +154,8 @@ class SensiboClimate(ClimateDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {ATTR_CURRENT_HUMIDITY: self.current_humidity}
|
||||
return {ATTR_CURRENT_HUMIDITY: self.current_humidity,
|
||||
'battery': self.current_battery}
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
@@ -191,6 +192,11 @@ class SensiboClimate(ClimateDevice):
|
||||
"""Return the current humidity."""
|
||||
return self._measurements['humidity']
|
||||
|
||||
@property
|
||||
def current_battery(self):
|
||||
"""Return the current battery voltage."""
|
||||
return self._measurements.get('batteryVoltage')
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
@@ -240,13 +246,13 @@ class SensiboClimate(ClimateDevice):
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._temperatures_list[0] \
|
||||
if self._temperatures_list else super().min_temp
|
||||
if self._temperatures_list else DEFAULT_MIN_TEMP
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._temperatures_list[-1] \
|
||||
if self._temperatures_list else super().max_temp
|
||||
if self._temperatures_list else DEFAULT_MAX_TEMP
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
||||
@@ -8,7 +8,8 @@ import logging
|
||||
|
||||
from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS)
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
|
||||
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
|
||||
DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
from homeassistant.components.tado import DATA_TADO
|
||||
|
||||
@@ -232,16 +233,16 @@ class TadoClimate(ClimateDevice):
|
||||
"""Return the minimum temperature."""
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
|
||||
return DEFAULT_MIN_TEMP
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
|
||||
return DEFAULT_MAX_TEMP
|
||||
|
||||
def update(self):
|
||||
"""Update the state of this climate device."""
|
||||
|
||||
@@ -11,9 +11,11 @@ import voluptuous as vol
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE,
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY,
|
||||
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH,
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice)
|
||||
SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_AWAY_MODE,
|
||||
SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW,
|
||||
SUPPORT_HOLD_MODE, SUPPORT_TARGET_TEMPERATURE,
|
||||
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
|
||||
ClimateDevice)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_TIMEOUT,
|
||||
CONF_USERNAME, PRECISION_WHOLE, STATE_OFF, STATE_ON, TEMP_CELSIUS,
|
||||
@@ -27,14 +29,20 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ATTR_FAN_STATE = 'fan_state'
|
||||
ATTR_HVAC_STATE = 'hvac_state'
|
||||
|
||||
CONF_HUMIDIFIER = 'humidifier'
|
||||
|
||||
DEFAULT_SSL = False
|
||||
|
||||
VALID_FAN_STATES = [STATE_ON, STATE_AUTO]
|
||||
VALID_THERMOSTAT_MODES = [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_AUTO]
|
||||
|
||||
HOLD_MODE_OFF = 'off'
|
||||
HOLD_MODE_TEMPERATURE = 'temperature'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_HUMIDIFIER, default=True): cv.boolean,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
vol.Optional(CONF_TIMEOUT, default=5):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
@@ -50,6 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
password = config.get(CONF_PASSWORD)
|
||||
host = config.get(CONF_HOST)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
humidifier = config.get(CONF_HUMIDIFIER)
|
||||
|
||||
if config.get(CONF_SSL):
|
||||
proto = 'https'
|
||||
@@ -60,15 +69,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
addr=host, timeout=timeout, user=username, password=password,
|
||||
proto=proto)
|
||||
|
||||
add_devices([VenstarThermostat(client)], True)
|
||||
add_devices([VenstarThermostat(client, humidifier)], True)
|
||||
|
||||
|
||||
class VenstarThermostat(ClimateDevice):
|
||||
"""Representation of a Venstar thermostat."""
|
||||
|
||||
def __init__(self, client):
|
||||
def __init__(self, client, humidifier):
|
||||
"""Initialize the thermostat."""
|
||||
self._client = client
|
||||
self._humidifier = humidifier
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
@@ -81,14 +91,18 @@ class VenstarThermostat(ClimateDevice):
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE |
|
||||
SUPPORT_OPERATION_MODE)
|
||||
SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE |
|
||||
SUPPORT_HOLD_MODE)
|
||||
|
||||
if self._client.mode == self._client.MODE_AUTO:
|
||||
features |= (SUPPORT_TARGET_TEMPERATURE_HIGH |
|
||||
SUPPORT_TARGET_TEMPERATURE_LOW)
|
||||
|
||||
if self._client.hum_active == 1:
|
||||
features |= SUPPORT_TARGET_HUMIDITY
|
||||
if (self._humidifier and
|
||||
hasattr(self._client, 'hum_active')):
|
||||
features |= (SUPPORT_TARGET_HUMIDITY |
|
||||
SUPPORT_TARGET_HUMIDITY_HIGH |
|
||||
SUPPORT_TARGET_HUMIDITY_LOW)
|
||||
|
||||
return features
|
||||
|
||||
@@ -197,6 +211,18 @@ class VenstarThermostat(ClimateDevice):
|
||||
"""Return the maximum humidity. Hardcoded to 60 in API."""
|
||||
return 60
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return the status of away mode."""
|
||||
return self._client.away == self._client.AWAY_AWAY
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return the status of hold mode."""
|
||||
if self._client.schedule == 0:
|
||||
return HOLD_MODE_TEMPERATURE
|
||||
return HOLD_MODE_OFF
|
||||
|
||||
def _set_operation_mode(self, operation_mode):
|
||||
"""Change the operation mode (internal)."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
@@ -259,3 +285,30 @@ class VenstarThermostat(ClimateDevice):
|
||||
|
||||
if not success:
|
||||
_LOGGER.error("Failed to change the target humidity level")
|
||||
|
||||
def set_hold_mode(self, hold_mode):
|
||||
"""Set the hold mode."""
|
||||
if hold_mode == HOLD_MODE_TEMPERATURE:
|
||||
success = self._client.set_schedule(0)
|
||||
elif hold_mode == HOLD_MODE_OFF:
|
||||
success = self._client.set_schedule(1)
|
||||
else:
|
||||
_LOGGER.error("Unknown hold mode: %s", hold_mode)
|
||||
success = False
|
||||
|
||||
if not success:
|
||||
_LOGGER.error("Failed to change the schedule/hold state")
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Activate away mode."""
|
||||
success = self._client.set_away(self._client.AWAY_AWAY)
|
||||
|
||||
if not success:
|
||||
_LOGGER.error("Failed to activate away mode")
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Deactivate away mode."""
|
||||
success = self._client.set_away(self._client.AWAY_HOME)
|
||||
|
||||
if not success:
|
||||
_LOGGER.error("Failed to deactivate away mode")
|
||||
|
||||
@@ -190,7 +190,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
@property
|
||||
def cool_on(self):
|
||||
"""Return whether or not the heat is actually heating."""
|
||||
return self.wink.heat_on()
|
||||
return self.wink.cool_on()
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
||||
@@ -185,7 +185,7 @@ class CloudIoT:
|
||||
yield from client.send_json(response)
|
||||
|
||||
except client_exceptions.WSServerHandshakeError as err:
|
||||
if err.code == 401:
|
||||
if err.status == 401:
|
||||
disconnect_warn = 'Invalid auth.'
|
||||
self.close_requested = True
|
||||
# Should we notify user?
|
||||
|
||||
@@ -21,7 +21,7 @@ ON_DEMAND = ('zwave',)
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the config component."""
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
'config', 'config', 'mdi:settings')
|
||||
'config', 'config', 'hass:settings')
|
||||
|
||||
async def setup_panel(panel_name):
|
||||
"""Set up a panel."""
|
||||
|
||||
@@ -96,6 +96,7 @@ async def async_setup(hass, config):
|
||||
async def process(service):
|
||||
"""Parse text into commands."""
|
||||
text = service.data[ATTR_TEXT]
|
||||
_LOGGER.debug('Processing: <%s>', text)
|
||||
try:
|
||||
await _process(hass, text)
|
||||
except intent.IntentHandleError as err:
|
||||
10
homeassistant/components/conversation/services.yaml
Normal file
10
homeassistant/components/conversation/services.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
# Describes the format for available component services
|
||||
|
||||
process:
|
||||
description: Launch a conversation from a transcribed text.
|
||||
fields:
|
||||
text:
|
||||
description: Transcribed text
|
||||
example: Turn all lights on
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
@@ -94,9 +94,8 @@ def async_reset(hass, entity_id):
|
||||
DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id}))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up a counter."""
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the counters."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
entities = []
|
||||
@@ -115,8 +114,7 @@ def async_setup(hass, config):
|
||||
if not entities:
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handler_service(service):
|
||||
async def async_handler_service(service):
|
||||
"""Handle a call to the counter services."""
|
||||
target_counters = component.async_extract_from_service(service)
|
||||
|
||||
@@ -129,7 +127,7 @@ def async_setup(hass, config):
|
||||
|
||||
tasks = [getattr(counter, attr)() for counter in target_counters]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_INCREMENT, async_handler_service)
|
||||
@@ -138,7 +136,7 @@ def async_setup(hass, config):
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESET, async_handler_service)
|
||||
|
||||
yield from component.async_add_entities(entities)
|
||||
await component.async_add_entities(entities)
|
||||
return True
|
||||
|
||||
|
||||
@@ -181,30 +179,26 @@ class Counter(Entity):
|
||||
ATTR_STEP: self._step,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to Home Assistant."""
|
||||
# If not None, we got an initial value.
|
||||
if self._state is not None:
|
||||
return
|
||||
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
state = await async_get_last_state(self.hass, self.entity_id)
|
||||
self._state = state and state.state == state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_decrement(self):
|
||||
async def async_decrement(self):
|
||||
"""Decrement the counter."""
|
||||
self._state -= self._step
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_increment(self):
|
||||
async def async_increment(self):
|
||||
"""Increment a counter."""
|
||||
self._state += self._step
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_reset(self):
|
||||
async def async_reset(self):
|
||||
"""Reset a counter."""
|
||||
self._state = self._initial
|
||||
yield from self.async_update_ha_state()
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for Gogogate2 Garage Doors.
|
||||
Support for Gogogate2 garage Doors.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/cover.gogogate2/
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
CONF_IP_ADDRESS, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pygogogate2==0.0.7']
|
||||
REQUIREMENTS = ['pygogogate2==0.1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,9 +25,9 @@ NOTIFICATION_ID = 'gogogate2_notification'
|
||||
NOTIFICATION_TITLE = 'Gogogate2 Cover Setup'
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_IP_ADDRESS): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
@@ -36,10 +36,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Gogogate2 component."""
|
||||
from pygogogate2 import Gogogate2API as pygogogate2
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
ip_address = config.get(CONF_IP_ADDRESS)
|
||||
name = config.get(CONF_NAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
username = config.get(CONF_USERNAME)
|
||||
|
||||
mygogogate2 = pygogogate2(username, password, ip_address)
|
||||
|
||||
try:
|
||||
|
||||
@@ -235,6 +235,11 @@ class MqttCover(MqttAvailability, CoverDevice):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return true if we do optimistic updates."""
|
||||
return self._optimistic
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
|
||||
@@ -69,6 +69,11 @@ class MyQDevice(CoverDevice):
|
||||
self._name = device['name']
|
||||
self._status = STATE_CLOSED
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Define this cover as a garage door."""
|
||||
return 'garage'
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Poll for state."""
|
||||
|
||||
@@ -42,7 +42,7 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
set_req = self.gateway.const.SetReq
|
||||
return self._values.get(set_req.V_DIMMER)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
@@ -53,9 +53,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
self._values[set_req.V_DIMMER] = 100
|
||||
else:
|
||||
self._values[set_req.V_LIGHT] = STATE_ON
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
@@ -66,9 +66,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
self._values[set_req.V_DIMMER] = 0
|
||||
else:
|
||||
self._values[set_req.V_LIGHT] = STATE_OFF
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
position = kwargs.get(ATTR_POSITION)
|
||||
set_req = self.gateway.const.SetReq
|
||||
@@ -77,9 +77,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
|
||||
if self.gateway.optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._values[set_req.V_DIMMER] = position
|
||||
self.schedule_update_ha_state()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Stop the device."""
|
||||
set_req = self.gateway.const.SetReq
|
||||
self.gateway.set_child_value(
|
||||
|
||||
103
homeassistant/components/cover/ryobi_gdo.py
Normal file
103
homeassistant/components/cover/ryobi_gdo.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Ryobi platform for the cover component.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/cover.ryobi_gdo/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, STATE_UNKNOWN, STATE_CLOSED)
|
||||
|
||||
REQUIREMENTS = ['py_ryobi_gdo==0.0.10']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DEVICE_ID = 'device_id'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
})
|
||||
|
||||
SUPPORTED_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Ryobi covers."""
|
||||
from py_ryobi_gdo import RyobiGDO as ryobi_door
|
||||
covers = []
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
devices = config.get(CONF_DEVICE_ID)
|
||||
|
||||
for device_id in devices:
|
||||
my_door = ryobi_door(username, password, device_id)
|
||||
_LOGGER.debug("Getting the API key")
|
||||
if my_door.get_api_key() is False:
|
||||
_LOGGER.error("Wrong credentials, no API key retrieved")
|
||||
return
|
||||
_LOGGER.debug("Checking if the device ID is present")
|
||||
if my_door.check_device_id() is False:
|
||||
_LOGGER.error("%s not in your device list", device_id)
|
||||
return
|
||||
_LOGGER.debug("Adding device %s to covers", device_id)
|
||||
covers.append(RyobiCover(hass, my_door))
|
||||
if covers:
|
||||
_LOGGER.debug("Adding covers")
|
||||
add_devices(covers, True)
|
||||
|
||||
|
||||
class RyobiCover(CoverDevice):
|
||||
"""Representation of a ryobi cover."""
|
||||
|
||||
def __init__(self, hass, ryobi_door):
|
||||
"""Initialize the cover."""
|
||||
self.ryobi_door = ryobi_door
|
||||
self._name = 'ryobi_gdo_{}'.format(ryobi_door.get_device_id())
|
||||
self._door_state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self._door_state == STATE_UNKNOWN:
|
||||
return False
|
||||
return self._door_state == STATE_CLOSED
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'garage'
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORTED_FEATURES
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
_LOGGER.debug("Closing garage door")
|
||||
self.ryobi_door.close_device()
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
_LOGGER.debug("Opening garage door")
|
||||
self.ryobi_door.open_device()
|
||||
|
||||
def update(self):
|
||||
"""Update status from the door."""
|
||||
_LOGGER.debug("Updating RyobiGDO status")
|
||||
self.ryobi_door.update()
|
||||
self._door_state = self.ryobi_door.get_door_status()
|
||||
@@ -79,7 +79,9 @@ class TahomaCover(TahomaDevice, CoverDevice):
|
||||
if self.tahoma_device.type == \
|
||||
'io:RollerShutterWithLowSpeedManagementIOComponent':
|
||||
self.apply_action('setPosition', 'secured')
|
||||
elif self.tahoma_device.type == 'rts:BlindRTSComponent':
|
||||
elif self.tahoma_device.type in \
|
||||
('rts:BlindRTSComponent',
|
||||
'io:ExteriorVenetianBlindIOComponent'):
|
||||
self.apply_action('my')
|
||||
else:
|
||||
self.apply_action('stopIdentify')
|
||||
|
||||
@@ -19,8 +19,14 @@
|
||||
"link": {
|
||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button",
|
||||
"title": "Link with deCONZ"
|
||||
},
|
||||
"options": {
|
||||
"title": "Extra configuration options for deCONZ",
|
||||
"data": {
|
||||
"allow_clip_sensor": "Allow importing virtual sensors"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
"title": "deCONZ Zigbee gateway"
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,10 @@ from homeassistant.util.json import load_json
|
||||
# Loading the config flow file will register the flow
|
||||
from .config_flow import configured_hosts
|
||||
from .const import (
|
||||
CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID,
|
||||
DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
|
||||
CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT,
|
||||
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
|
||||
|
||||
REQUIREMENTS = ['pydeconz==37']
|
||||
REQUIREMENTS = ['pydeconz==38']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@@ -104,8 +104,10 @@ async def async_setup_entry(hass, config_entry):
|
||||
def async_add_remote(sensors):
|
||||
"""Setup remote from deCONZ."""
|
||||
from pydeconz.sensor import SWITCH as DECONZ_REMOTE
|
||||
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
|
||||
for sensor in sensors:
|
||||
if sensor.type in DECONZ_REMOTE:
|
||||
if sensor.type in DECONZ_REMOTE and \
|
||||
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
|
||||
hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor))
|
||||
hass.data[DATA_DECONZ_UNSUB].append(
|
||||
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote))
|
||||
|
||||
@@ -8,13 +8,15 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.util.json import load_json
|
||||
|
||||
from .const import CONFIG_FILE, DOMAIN
|
||||
from .const import CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN
|
||||
|
||||
CONF_BRIDGEID = 'bridgeid'
|
||||
|
||||
|
||||
@callback
|
||||
def configured_hosts(hass):
|
||||
"""Return a set of the configured hosts."""
|
||||
return set(entry.data['host'] for entry
|
||||
return set(entry.data[CONF_HOST] for entry
|
||||
in hass.config_entries.async_entries(DOMAIN))
|
||||
|
||||
|
||||
@@ -30,7 +32,12 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
|
||||
self.deconz_config = {}
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle a deCONZ config flow start."""
|
||||
"""Handle a deCONZ config flow start.
|
||||
|
||||
Only allows one instance to be set up.
|
||||
If only one bridge is found go to link step.
|
||||
If more than one bridge is found let user choose bridge to link.
|
||||
"""
|
||||
from pydeconz.utils import async_discovery
|
||||
|
||||
if configured_hosts(self.hass):
|
||||
@@ -65,7 +72,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
|
||||
|
||||
async def async_step_link(self, user_input=None):
|
||||
"""Attempt to link with the deCONZ bridge."""
|
||||
from pydeconz.utils import async_get_api_key, async_get_bridgeid
|
||||
from pydeconz.utils import async_get_api_key
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
@@ -75,13 +82,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
|
||||
api_key = await async_get_api_key(session, **self.deconz_config)
|
||||
if api_key:
|
||||
self.deconz_config[CONF_API_KEY] = api_key
|
||||
if 'bridgeid' not in self.deconz_config:
|
||||
self.deconz_config['bridgeid'] = await async_get_bridgeid(
|
||||
session, **self.deconz_config)
|
||||
return self.async_create_entry(
|
||||
title='deCONZ-' + self.deconz_config['bridgeid'],
|
||||
data=self.deconz_config
|
||||
)
|
||||
return await self.async_step_options()
|
||||
errors['base'] = 'no_key'
|
||||
|
||||
return self.async_show_form(
|
||||
@@ -89,6 +90,34 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_options(self, user_input=None):
|
||||
"""Extra options for deCONZ.
|
||||
|
||||
CONF_CLIP_SENSOR -- Allow user to choose if they want clip sensors.
|
||||
"""
|
||||
from pydeconz.utils import async_get_bridgeid
|
||||
|
||||
if user_input is not None:
|
||||
self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = \
|
||||
user_input[CONF_ALLOW_CLIP_SENSOR]
|
||||
|
||||
if CONF_BRIDGEID not in self.deconz_config:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
self.deconz_config[CONF_BRIDGEID] = await async_get_bridgeid(
|
||||
session, **self.deconz_config)
|
||||
|
||||
return self.async_create_entry(
|
||||
title='deCONZ-' + self.deconz_config[CONF_BRIDGEID],
|
||||
data=self.deconz_config
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='options',
|
||||
data_schema=vol.Schema({
|
||||
vol.Optional(CONF_ALLOW_CLIP_SENSOR): bool,
|
||||
}),
|
||||
)
|
||||
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
"""Prepare configuration for a discovered deCONZ bridge.
|
||||
|
||||
@@ -97,7 +126,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
|
||||
deconz_config = {}
|
||||
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
|
||||
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
|
||||
deconz_config['bridgeid'] = discovery_info.get('serial')
|
||||
deconz_config[CONF_BRIDGEID] = discovery_info.get('serial')
|
||||
|
||||
config_file = await self.hass.async_add_job(
|
||||
load_json, self.hass.config.path(CONFIG_FILE))
|
||||
@@ -121,19 +150,15 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
|
||||
Otherwise we will delegate to `link` step which
|
||||
will ask user to link the bridge.
|
||||
"""
|
||||
from pydeconz.utils import async_get_bridgeid
|
||||
|
||||
if configured_hosts(self.hass):
|
||||
return self.async_abort(reason='one_instance_only')
|
||||
elif CONF_API_KEY not in import_config:
|
||||
self.deconz_config = import_config
|
||||
|
||||
self.deconz_config = import_config
|
||||
if CONF_API_KEY not in import_config:
|
||||
return await self.async_step_link()
|
||||
|
||||
if 'bridgeid' not in import_config:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
import_config['bridgeid'] = await async_get_bridgeid(
|
||||
session, **import_config)
|
||||
self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True
|
||||
return self.async_create_entry(
|
||||
title='deCONZ-' + import_config['bridgeid'],
|
||||
data=import_config
|
||||
title='deCONZ-' + self.deconz_config[CONF_BRIDGEID],
|
||||
data=self.deconz_config
|
||||
)
|
||||
|
||||
@@ -8,3 +8,5 @@ CONFIG_FILE = 'deconz.conf'
|
||||
DATA_DECONZ_EVENT = 'deconz_events'
|
||||
DATA_DECONZ_ID = 'deconz_entities'
|
||||
DATA_DECONZ_UNSUB = 'deconz_dispatchers'
|
||||
|
||||
CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "deCONZ",
|
||||
"title": "deCONZ Zigbee gateway",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Define deCONZ gateway",
|
||||
@@ -12,6 +12,12 @@
|
||||
"link": {
|
||||
"title": "Link with deCONZ",
|
||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
|
||||
},
|
||||
"options": {
|
||||
"title": "Extra configuration options for deCONZ",
|
||||
"data":{
|
||||
"allow_clip_sensor": "Allow importing virtual sensors"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -33,7 +33,7 @@ from homeassistant.helpers.event import async_track_utc_time_change
|
||||
from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC,
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID,
|
||||
CONF_ICON, ATTR_ICON)
|
||||
CONF_ICON, ATTR_ICON, ATTR_NAME)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -71,7 +71,6 @@ ATTR_GPS = 'gps'
|
||||
ATTR_HOST_NAME = 'host_name'
|
||||
ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_MAC = 'mac'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_SOURCE_TYPE = 'source_type'
|
||||
ATTR_CONSIDER_HOME = 'consider_home'
|
||||
|
||||
|
||||
@@ -4,22 +4,27 @@ Support for Google Maps location sharing.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.google_maps/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, SOURCE_TYPE_GPS)
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.const import ATTR_ID, CONF_PASSWORD, CONF_USERNAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import slugify
|
||||
|
||||
REQUIREMENTS = ['locationsharinglib==2.0.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['locationsharinglib==1.2.2']
|
||||
ATTR_ADDRESS = 'address'
|
||||
ATTR_FULL_NAME = 'full_name'
|
||||
ATTR_LAST_SEEN = 'last_seen'
|
||||
ATTR_NICKNAME = 'nickname'
|
||||
|
||||
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
|
||||
|
||||
@@ -60,19 +65,23 @@ class GoogleMapsScanner(object):
|
||||
self.success_init = True
|
||||
|
||||
except InvalidUser:
|
||||
_LOGGER.error('You have specified invalid login credentials')
|
||||
_LOGGER.error("You have specified invalid login credentials")
|
||||
self.success_init = False
|
||||
|
||||
def _update_info(self, now=None):
|
||||
for person in self.service.get_all_people():
|
||||
dev_id = 'google_maps_{0}'.format(slugify(person.id))
|
||||
try:
|
||||
dev_id = 'google_maps_{0}'.format(slugify(person.id))
|
||||
except TypeError:
|
||||
_LOGGER.warning("No location(s) shared with this account")
|
||||
return
|
||||
|
||||
attrs = {
|
||||
'id': person.id,
|
||||
'nickname': person.nickname,
|
||||
'full_name': person.full_name,
|
||||
'last_seen': person.datetime,
|
||||
'address': person.address
|
||||
ATTR_ADDRESS: person.address,
|
||||
ATTR_FULL_NAME: person.full_name,
|
||||
ATTR_ID: person.id,
|
||||
ATTR_LAST_SEEN: person.datetime,
|
||||
ATTR_NICKNAME: person.nickname,
|
||||
}
|
||||
self.see(
|
||||
dev_id=dev_id,
|
||||
@@ -80,5 +89,5 @@ class GoogleMapsScanner(object):
|
||||
picture=person.picture_url,
|
||||
source_type=SOURCE_TYPE_GPS,
|
||||
gps_accuracy=person.accuracy,
|
||||
attributes=attrs
|
||||
attributes=attrs,
|
||||
)
|
||||
|
||||
@@ -14,15 +14,18 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_TYPE
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_TYPE = "rogers"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -49,6 +52,11 @@ class HitronCODADeviceScanner(DeviceScanner):
|
||||
self._username = config.get(CONF_USERNAME)
|
||||
self._password = config.get(CONF_PASSWORD)
|
||||
|
||||
if config.get(CONF_TYPE) == "shaw":
|
||||
self._type = 'pwd'
|
||||
else:
|
||||
self._type = 'pws'
|
||||
|
||||
self._userid = None
|
||||
|
||||
self.success_init = self._update_info()
|
||||
@@ -74,7 +82,7 @@ class HitronCODADeviceScanner(DeviceScanner):
|
||||
try:
|
||||
data = [
|
||||
('user', self._username),
|
||||
('pws', self._password),
|
||||
(self._type, self._password),
|
||||
]
|
||||
res = requests.post(self._loginurl, data=data, timeout=10)
|
||||
except requests.exceptions.Timeout:
|
||||
|
||||
@@ -24,8 +24,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pyicloud==0.9.1']
|
||||
|
||||
CONF_IGNORED_DEVICES = 'ignored_devices'
|
||||
CONF_ACCOUNTNAME = 'account_name'
|
||||
CONF_MAX_INTERVAL = 'max_interval'
|
||||
CONF_GPS_ACCURACY_THRESHOLD = 'gps_accuracy_threshold'
|
||||
|
||||
# entity attributes
|
||||
ATTR_ACCOUNTNAME = 'account_name'
|
||||
@@ -64,13 +65,15 @@ DEVICESTATUSCODES = {
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]),
|
||||
vol.Optional(ATTR_DEVICENAME): cv.slugify,
|
||||
vol.Optional(ATTR_INTERVAL): cv.positive_int,
|
||||
vol.Optional(ATTR_INTERVAL): cv.positive_int
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(ATTR_ACCOUNTNAME): cv.slugify,
|
||||
vol.Optional(CONF_MAX_INTERVAL, default=30): cv.positive_int,
|
||||
vol.Optional(CONF_GPS_ACCURACY_THRESHOLD, default=1000): cv.positive_int
|
||||
})
|
||||
|
||||
|
||||
@@ -79,8 +82,11 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0]))
|
||||
max_interval = config.get(CONF_MAX_INTERVAL)
|
||||
gps_accuracy_threshold = config.get(CONF_GPS_ACCURACY_THRESHOLD)
|
||||
|
||||
icloudaccount = Icloud(hass, username, password, account, see)
|
||||
icloudaccount = Icloud(hass, username, password, account, max_interval,
|
||||
gps_accuracy_threshold, see)
|
||||
|
||||
if icloudaccount.api is not None:
|
||||
ICLOUDTRACKERS[account] = icloudaccount
|
||||
@@ -96,6 +102,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
for account in accounts:
|
||||
if account in ICLOUDTRACKERS:
|
||||
ICLOUDTRACKERS[account].lost_iphone(devicename)
|
||||
|
||||
hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone,
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
@@ -106,6 +113,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
for account in accounts:
|
||||
if account in ICLOUDTRACKERS:
|
||||
ICLOUDTRACKERS[account].update_icloud(devicename)
|
||||
|
||||
hass.services.register(DOMAIN, 'icloud_update', update_icloud,
|
||||
schema=SERVICE_SCHEMA)
|
||||
|
||||
@@ -115,6 +123,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
for account in accounts:
|
||||
if account in ICLOUDTRACKERS:
|
||||
ICLOUDTRACKERS[account].reset_account_icloud()
|
||||
|
||||
hass.services.register(DOMAIN, 'icloud_reset_account',
|
||||
reset_account_icloud, schema=SERVICE_SCHEMA)
|
||||
|
||||
@@ -137,7 +146,8 @@ def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
class Icloud(DeviceScanner):
|
||||
"""Representation of an iCloud account."""
|
||||
|
||||
def __init__(self, hass, username, password, name, see):
|
||||
def __init__(self, hass, username, password, name, max_interval,
|
||||
gps_accuracy_threshold, see):
|
||||
"""Initialize an iCloud account."""
|
||||
self.hass = hass
|
||||
self.username = username
|
||||
@@ -148,6 +158,8 @@ class Icloud(DeviceScanner):
|
||||
self.seen_devices = {}
|
||||
self._overridestates = {}
|
||||
self._intervals = {}
|
||||
self._max_interval = max_interval
|
||||
self._gps_accuracy_threshold = gps_accuracy_threshold
|
||||
self.see = see
|
||||
|
||||
self._trusted_device = None
|
||||
@@ -348,7 +360,7 @@ class Icloud(DeviceScanner):
|
||||
self._overridestates[devicename] = None
|
||||
|
||||
if currentzone is not None:
|
||||
self._intervals[devicename] = 30
|
||||
self._intervals[devicename] = self._max_interval
|
||||
return
|
||||
|
||||
if mindistance is None:
|
||||
@@ -363,7 +375,6 @@ class Icloud(DeviceScanner):
|
||||
|
||||
if interval > 180:
|
||||
# Three hour drive? This is far enough that they might be flying
|
||||
# home - check every half hour
|
||||
interval = 30
|
||||
|
||||
if battery is not None and battery <= 33 and mindistance > 3:
|
||||
@@ -403,22 +414,24 @@ class Icloud(DeviceScanner):
|
||||
status = device.status(DEVICESTATUSSET)
|
||||
battery = status.get('batteryLevel', 0) * 100
|
||||
location = status['location']
|
||||
if location:
|
||||
self.determine_interval(
|
||||
devicename, location['latitude'],
|
||||
location['longitude'], battery)
|
||||
interval = self._intervals.get(devicename, 1)
|
||||
attrs[ATTR_INTERVAL] = interval
|
||||
accuracy = location['horizontalAccuracy']
|
||||
kwargs['dev_id'] = dev_id
|
||||
kwargs['host_name'] = status['name']
|
||||
kwargs['gps'] = (location['latitude'],
|
||||
location['longitude'])
|
||||
kwargs['battery'] = battery
|
||||
kwargs['gps_accuracy'] = accuracy
|
||||
kwargs[ATTR_ATTRIBUTES] = attrs
|
||||
self.see(**kwargs)
|
||||
self.seen_devices[devicename] = True
|
||||
if location and location['horizontalAccuracy']:
|
||||
horizontal_accuracy = int(location['horizontalAccuracy'])
|
||||
if horizontal_accuracy < self._gps_accuracy_threshold:
|
||||
self.determine_interval(
|
||||
devicename, location['latitude'],
|
||||
location['longitude'], battery)
|
||||
interval = self._intervals.get(devicename, 1)
|
||||
attrs[ATTR_INTERVAL] = interval
|
||||
accuracy = location['horizontalAccuracy']
|
||||
kwargs['dev_id'] = dev_id
|
||||
kwargs['host_name'] = status['name']
|
||||
kwargs['gps'] = (location['latitude'],
|
||||
location['longitude'])
|
||||
kwargs['battery'] = battery
|
||||
kwargs['gps_accuracy'] = accuracy
|
||||
kwargs[ATTR_ATTRIBUTES] = attrs
|
||||
self.see(**kwargs)
|
||||
self.seen_devices[devicename] = True
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error("No iCloud Devices found")
|
||||
|
||||
@@ -434,7 +447,7 @@ class Icloud(DeviceScanner):
|
||||
device.play_sound()
|
||||
|
||||
def update_icloud(self, devicename=None):
|
||||
"""Authenticate against iCloud and scan for devices."""
|
||||
"""Request device information from iCloud and update device_tracker."""
|
||||
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||
|
||||
if self.api is None:
|
||||
@@ -443,13 +456,13 @@ class Icloud(DeviceScanner):
|
||||
try:
|
||||
if devicename is not None:
|
||||
if devicename in self.devices:
|
||||
self.devices[devicename].location()
|
||||
self.update_device(devicename)
|
||||
else:
|
||||
_LOGGER.error("devicename %s unknown for account %s",
|
||||
devicename, self._attrs[ATTR_ACCOUNTNAME])
|
||||
else:
|
||||
for device in self.devices:
|
||||
self.devices[device].location()
|
||||
self.update_device(device)
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error("No iCloud Devices found")
|
||||
|
||||
|
||||
@@ -15,14 +15,18 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_SSL)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_SSL = False
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean
|
||||
})
|
||||
|
||||
|
||||
@@ -44,7 +48,9 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
host = config[CONF_HOST]
|
||||
protocol = 'http' if not config[CONF_SSL] else 'https'
|
||||
self.origin = '{}://{}'.format(protocol, host)
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
@@ -57,7 +63,7 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
|
||||
def refresh_token(self):
|
||||
"""Get a new token."""
|
||||
self.token = _get_token(self.host, self.username, self.password)
|
||||
self.token = _get_token(self.origin, self.username, self.password)
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
@@ -67,9 +73,9 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if self.mac2name is None:
|
||||
url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host)
|
||||
result = _req_json_rpc(url, 'get_all', 'dhcp',
|
||||
params={'auth': self.token})
|
||||
url = '{}/cgi-bin/luci/rpc/uci'.format(self.origin)
|
||||
result = _req_json_rpc(
|
||||
url, 'get_all', 'dhcp', params={'auth': self.token})
|
||||
if result:
|
||||
hosts = [x for x in result.values()
|
||||
if x['.type'] == 'host' and
|
||||
@@ -92,11 +98,11 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
|
||||
_LOGGER.info("Checking ARP")
|
||||
|
||||
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
|
||||
url = '{}/cgi-bin/luci/rpc/sys'.format(self.origin)
|
||||
|
||||
try:
|
||||
result = _req_json_rpc(url, 'net.arptable',
|
||||
params={'auth': self.token})
|
||||
result = _req_json_rpc(
|
||||
url, 'net.arptable', params={'auth': self.token})
|
||||
except InvalidLuciTokenError:
|
||||
_LOGGER.info("Refreshing token")
|
||||
self.refresh_token()
|
||||
@@ -146,10 +152,10 @@ def _req_json_rpc(url, method, *args, **kwargs):
|
||||
raise InvalidLuciTokenError
|
||||
|
||||
else:
|
||||
_LOGGER.error('Invalid response from luci: %s', res)
|
||||
_LOGGER.error("Invalid response from luci: %s", res)
|
||||
|
||||
|
||||
def _get_token(host, username, password):
|
||||
"""Get authentication token for the given host+username+password."""
|
||||
url = 'http://{}/cgi-bin/luci/rpc/auth'.format(host)
|
||||
def _get_token(origin, username, password):
|
||||
"""Get authentication token for the given configuration."""
|
||||
url = '{}/cgi-bin/luci/rpc/auth'.format(origin)
|
||||
return _req_json_rpc(url, 'login', username, password)
|
||||
|
||||
@@ -37,8 +37,10 @@ SERVICE_WINK = 'wink'
|
||||
SERVICE_XIAOMI_GW = 'xiaomi_gw'
|
||||
SERVICE_TELLDUSLIVE = 'tellstick'
|
||||
SERVICE_HUE = 'philips_hue'
|
||||
SERVICE_KONNECTED = 'konnected'
|
||||
SERVICE_DECONZ = 'deconz'
|
||||
SERVICE_DAIKIN = 'daikin'
|
||||
SERVICE_SABNZBD = 'sabnzbd'
|
||||
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
|
||||
SERVICE_HOMEKIT = 'homekit'
|
||||
|
||||
@@ -59,7 +61,9 @@ SERVICE_HANDLERS = {
|
||||
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
|
||||
SERVICE_TELLDUSLIVE: ('tellduslive', None),
|
||||
SERVICE_DAIKIN: ('daikin', None),
|
||||
SERVICE_SABNZBD: ('sabnzbd', None),
|
||||
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
|
||||
SERVICE_KONNECTED: ('konnected', None),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
'plex_mediaserver': ('media_player', 'plex'),
|
||||
@@ -74,12 +78,12 @@ SERVICE_HANDLERS = {
|
||||
'frontier_silicon': ('media_player', 'frontier_silicon'),
|
||||
'openhome': ('media_player', 'openhome'),
|
||||
'harmony': ('remote', 'harmony'),
|
||||
'sabnzbd': ('sensor', 'sabnzbd'),
|
||||
'bose_soundtouch': ('media_player', 'soundtouch'),
|
||||
'bluesound': ('media_player', 'bluesound'),
|
||||
'songpal': ('media_player', 'songpal'),
|
||||
'kodi': ('media_player', 'kodi'),
|
||||
'volumio': ('media_player', 'volumio'),
|
||||
'nanoleaf_aurora': ('light', 'nanoleaf_aurora'),
|
||||
}
|
||||
|
||||
OPTIONAL_SERVICE_HANDLERS = {
|
||||
@@ -190,6 +194,7 @@ def _discover(netdisco):
|
||||
for disc in netdisco.discover():
|
||||
for service in netdisco.get_info(disc):
|
||||
results.append((disc, service))
|
||||
|
||||
finally:
|
||||
netdisco.stop()
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
REQUIREMENTS = ['lakeside==0.5']
|
||||
REQUIREMENTS = ['lakeside==0.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@ set_direction:
|
||||
description: Name(s) of the entities to toggle
|
||||
example: 'fan.living_room'
|
||||
direction:
|
||||
description: The direction to rotate
|
||||
example: 'left'
|
||||
description: The direction to rotate. Either 'forward' or 'reverse'
|
||||
example: 'forward'
|
||||
|
||||
dyson_set_night_mode:
|
||||
description: Set the fan in night mode.
|
||||
|
||||
@@ -18,11 +18,10 @@ from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
|
||||
SPEED_HIGH, SUPPORT_SET_SPEED,
|
||||
SUPPORT_OSCILLATE, FanEntity,
|
||||
ATTR_SPEED, ATTR_OSCILLATING,
|
||||
ENTITY_ID_FORMAT)
|
||||
from homeassistant.components.fan import (
|
||||
SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE,
|
||||
FanEntity, ATTR_SPEED, ATTR_OSCILLATING, ENTITY_ID_FORMAT,
|
||||
SUPPORT_DIRECTION, DIRECTION_FORWARD, DIRECTION_REVERSE, ATTR_DIRECTION)
|
||||
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.script import Script
|
||||
@@ -33,25 +32,30 @@ CONF_FANS = 'fans'
|
||||
CONF_SPEED_LIST = 'speeds'
|
||||
CONF_SPEED_TEMPLATE = 'speed_template'
|
||||
CONF_OSCILLATING_TEMPLATE = 'oscillating_template'
|
||||
CONF_DIRECTION_TEMPLATE = 'direction_template'
|
||||
CONF_ON_ACTION = 'turn_on'
|
||||
CONF_OFF_ACTION = 'turn_off'
|
||||
CONF_SET_SPEED_ACTION = 'set_speed'
|
||||
CONF_SET_OSCILLATING_ACTION = 'set_oscillating'
|
||||
CONF_SET_DIRECTION_ACTION = 'set_direction'
|
||||
|
||||
_VALID_STATES = [STATE_ON, STATE_OFF]
|
||||
_VALID_OSC = [True, False]
|
||||
_VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE]
|
||||
|
||||
FAN_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
|
||||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_SPEED_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template,
|
||||
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
|
||||
vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA,
|
||||
|
||||
vol.Optional(
|
||||
CONF_SPEED_LIST,
|
||||
@@ -80,18 +84,21 @@ async def async_setup_platform(
|
||||
oscillating_template = device_config.get(
|
||||
CONF_OSCILLATING_TEMPLATE
|
||||
)
|
||||
direction_template = device_config.get(CONF_DIRECTION_TEMPLATE)
|
||||
|
||||
on_action = device_config[CONF_ON_ACTION]
|
||||
off_action = device_config[CONF_OFF_ACTION]
|
||||
set_speed_action = device_config.get(CONF_SET_SPEED_ACTION)
|
||||
set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION)
|
||||
set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION)
|
||||
|
||||
speed_list = device_config[CONF_SPEED_LIST]
|
||||
|
||||
entity_ids = set()
|
||||
manual_entity_ids = device_config.get(CONF_ENTITY_ID)
|
||||
|
||||
for template in (state_template, speed_template, oscillating_template):
|
||||
for template in (state_template, speed_template, oscillating_template,
|
||||
direction_template):
|
||||
if template is None:
|
||||
continue
|
||||
template.hass = hass
|
||||
@@ -114,8 +121,9 @@ async def async_setup_platform(
|
||||
TemplateFan(
|
||||
hass, device, friendly_name,
|
||||
state_template, speed_template, oscillating_template,
|
||||
on_action, off_action, set_speed_action,
|
||||
set_oscillating_action, speed_list, entity_ids
|
||||
direction_template, on_action, off_action, set_speed_action,
|
||||
set_oscillating_action, set_direction_action, speed_list,
|
||||
entity_ids
|
||||
)
|
||||
)
|
||||
|
||||
@@ -127,8 +135,9 @@ class TemplateFan(FanEntity):
|
||||
|
||||
def __init__(self, hass, device_id, friendly_name,
|
||||
state_template, speed_template, oscillating_template,
|
||||
on_action, off_action, set_speed_action,
|
||||
set_oscillating_action, speed_list, entity_ids):
|
||||
direction_template, on_action, off_action, set_speed_action,
|
||||
set_oscillating_action, set_direction_action, speed_list,
|
||||
entity_ids):
|
||||
"""Initialize the fan."""
|
||||
self.hass = hass
|
||||
self.entity_id = async_generate_entity_id(
|
||||
@@ -138,6 +147,7 @@ class TemplateFan(FanEntity):
|
||||
self._template = state_template
|
||||
self._speed_template = speed_template
|
||||
self._oscillating_template = oscillating_template
|
||||
self._direction_template = direction_template
|
||||
self._supported_features = 0
|
||||
|
||||
self._on_script = Script(hass, on_action)
|
||||
@@ -151,9 +161,14 @@ class TemplateFan(FanEntity):
|
||||
if set_oscillating_action:
|
||||
self._set_oscillating_script = Script(hass, set_oscillating_action)
|
||||
|
||||
self._set_direction_script = None
|
||||
if set_direction_action:
|
||||
self._set_direction_script = Script(hass, set_direction_action)
|
||||
|
||||
self._state = STATE_OFF
|
||||
self._speed = None
|
||||
self._oscillating = None
|
||||
self._direction = None
|
||||
|
||||
self._template.hass = self.hass
|
||||
if self._speed_template:
|
||||
@@ -162,6 +177,9 @@ class TemplateFan(FanEntity):
|
||||
if self._oscillating_template:
|
||||
self._oscillating_template.hass = self.hass
|
||||
self._supported_features |= SUPPORT_OSCILLATE
|
||||
if self._direction_template:
|
||||
self._direction_template.hass = self.hass
|
||||
self._supported_features |= SUPPORT_DIRECTION
|
||||
|
||||
self._entities = entity_ids
|
||||
# List of valid speeds
|
||||
@@ -197,6 +215,11 @@ class TemplateFan(FanEntity):
|
||||
"""Return the oscillation state."""
|
||||
return self._oscillating
|
||||
|
||||
@property
|
||||
def direction(self):
|
||||
"""Return the oscillation state."""
|
||||
return self._direction
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
@@ -236,10 +259,30 @@ class TemplateFan(FanEntity):
|
||||
if self._set_oscillating_script is None:
|
||||
return
|
||||
|
||||
await self._set_oscillating_script.async_run(
|
||||
{ATTR_OSCILLATING: oscillating}
|
||||
)
|
||||
self._oscillating = oscillating
|
||||
if oscillating in _VALID_OSC:
|
||||
self._oscillating = oscillating
|
||||
await self._set_oscillating_script.async_run(
|
||||
{ATTR_OSCILLATING: oscillating})
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Received invalid oscillating value: %s. ' +
|
||||
'Expected: %s.',
|
||||
oscillating, ', '.join(_VALID_OSC))
|
||||
|
||||
async def async_set_direction(self, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
if self._set_direction_script is None:
|
||||
return
|
||||
|
||||
if direction in _VALID_DIRECTIONS:
|
||||
self._direction = direction
|
||||
await self._set_direction_script.async_run(
|
||||
{ATTR_DIRECTION: direction})
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Received invalid direction: %s. ' +
|
||||
'Expected: %s.',
|
||||
direction, ', '.join(_VALID_DIRECTIONS))
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@@ -308,6 +351,7 @@ class TemplateFan(FanEntity):
|
||||
oscillating = self._oscillating_template.async_render()
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
oscillating = None
|
||||
self._state = None
|
||||
|
||||
# Validate osc
|
||||
@@ -322,3 +366,24 @@ class TemplateFan(FanEntity):
|
||||
'Received invalid oscillating: %s. ' +
|
||||
'Expected: True/False.', oscillating)
|
||||
self._oscillating = None
|
||||
|
||||
# Update direction if 'direction_template' is configured
|
||||
if self._direction_template is not None:
|
||||
try:
|
||||
direction = self._direction_template.async_render()
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
direction = None
|
||||
self._state = None
|
||||
|
||||
# Validate speed
|
||||
if direction in _VALID_DIRECTIONS:
|
||||
self._direction = direction
|
||||
elif direction == STATE_UNKNOWN:
|
||||
self._direction = None
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Received invalid direction: %s. ' +
|
||||
'Expected: %s.',
|
||||
direction, ', '.join(_VALID_DIRECTIONS))
|
||||
self._direction = None
|
||||
|
||||
@@ -10,7 +10,6 @@ from homeassistant.components import zha
|
||||
from homeassistant.components.fan import (
|
||||
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
SUPPORT_SET_SPEED)
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
DEPENDENCIES = ['zha']
|
||||
|
||||
@@ -72,7 +71,7 @@ class ZhaFan(zha.Entity, FanEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if entity is on."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
if self._state is None:
|
||||
return False
|
||||
return self._state != SPEED_OFF
|
||||
|
||||
@@ -103,7 +102,7 @@ class ZhaFan(zha.Entity, FanEntity):
|
||||
"""Retrieve latest state."""
|
||||
result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode'])
|
||||
new_value = result.get('fan_mode', None)
|
||||
self._state = VALUE_TO_SPEED.get(new_value, STATE_UNKNOWN)
|
||||
self._state = VALUE_TO_SPEED.get(new_value, None)
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
|
||||
@@ -4,7 +4,7 @@ Support for RSS/Atom feeds.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/feedreader/
|
||||
"""
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from logging import getLogger
|
||||
from os.path import exists
|
||||
from threading import Lock
|
||||
@@ -12,8 +12,8 @@ import pickle
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['feedparser==5.2.1']
|
||||
@@ -21,16 +21,22 @@ REQUIREMENTS = ['feedparser==5.2.1']
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
CONF_URLS = 'urls'
|
||||
CONF_MAX_ENTRIES = 'max_entries'
|
||||
|
||||
DEFAULT_MAX_ENTRIES = 20
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(hours=1)
|
||||
|
||||
DOMAIN = 'feedreader'
|
||||
|
||||
EVENT_FEEDREADER = 'feedreader'
|
||||
|
||||
MAX_ENTRIES = 20
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]),
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES):
|
||||
cv.positive_int
|
||||
}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -38,33 +44,50 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
def setup(hass, config):
|
||||
"""Set up the Feedreader component."""
|
||||
urls = config.get(DOMAIN)[CONF_URLS]
|
||||
scan_interval = config.get(DOMAIN).get(CONF_SCAN_INTERVAL)
|
||||
max_entries = config.get(DOMAIN).get(CONF_MAX_ENTRIES)
|
||||
data_file = hass.config.path("{}.pickle".format(DOMAIN))
|
||||
storage = StoredData(data_file)
|
||||
feeds = [FeedManager(url, hass, storage) for url in urls]
|
||||
feeds = [FeedManager(url, scan_interval, max_entries, hass, storage) for
|
||||
url in urls]
|
||||
return len(feeds) > 0
|
||||
|
||||
|
||||
class FeedManager(object):
|
||||
"""Abstraction over Feedparser module."""
|
||||
|
||||
def __init__(self, url, hass, storage):
|
||||
"""Initialize the FeedManager object, poll every hour."""
|
||||
def __init__(self, url, scan_interval, max_entries, hass, storage):
|
||||
"""Initialize the FeedManager object, poll as per scan interval."""
|
||||
self._url = url
|
||||
self._scan_interval = scan_interval
|
||||
self._max_entries = max_entries
|
||||
self._feed = None
|
||||
self._hass = hass
|
||||
self._firstrun = True
|
||||
self._storage = storage
|
||||
self._last_entry_timestamp = None
|
||||
self._last_update_successful = False
|
||||
self._has_published_parsed = False
|
||||
self._event_type = EVENT_FEEDREADER
|
||||
self._feed_id = url
|
||||
hass.bus.listen_once(
|
||||
EVENT_HOMEASSISTANT_START, lambda _: self._update())
|
||||
track_utc_time_change(
|
||||
hass, lambda now: self._update(), minute=0, second=0)
|
||||
self._init_regular_updates(hass)
|
||||
|
||||
def _log_no_entries(self):
|
||||
"""Send no entries log at debug level."""
|
||||
_LOGGER.debug("No new entries to be published in feed %s", self._url)
|
||||
|
||||
def _init_regular_updates(self, hass):
|
||||
"""Schedule regular updates at the top of the clock."""
|
||||
track_time_interval(hass, lambda now: self._update(),
|
||||
self._scan_interval)
|
||||
|
||||
@property
|
||||
def last_update_successful(self):
|
||||
"""Return True if the last feed update was successful."""
|
||||
return self._last_update_successful
|
||||
|
||||
def _update(self):
|
||||
"""Update the feed and publish new entries to the event bus."""
|
||||
import feedparser
|
||||
@@ -76,26 +99,39 @@ class FeedManager(object):
|
||||
else self._feed.get('modified'))
|
||||
if not self._feed:
|
||||
_LOGGER.error("Error fetching feed data from %s", self._url)
|
||||
self._last_update_successful = False
|
||||
else:
|
||||
# The 'bozo' flag really only indicates that there was an issue
|
||||
# during the initial parsing of the XML, but it doesn't indicate
|
||||
# whether this is an unrecoverable error. In this case the
|
||||
# feedparser lib is trying a less strict parsing approach.
|
||||
# If an error is detected here, log error message but continue
|
||||
# processing the feed entries if present.
|
||||
if self._feed.bozo != 0:
|
||||
_LOGGER.error("Error parsing feed %s", self._url)
|
||||
_LOGGER.error("Error parsing feed %s: %s", self._url,
|
||||
self._feed.bozo_exception)
|
||||
# Using etag and modified, if there's no new data available,
|
||||
# the entries list will be empty
|
||||
elif self._feed.entries:
|
||||
if self._feed.entries:
|
||||
_LOGGER.debug("%s entri(es) available in feed %s",
|
||||
len(self._feed.entries), self._url)
|
||||
if len(self._feed.entries) > MAX_ENTRIES:
|
||||
_LOGGER.debug("Processing only the first %s entries "
|
||||
"in feed %s", MAX_ENTRIES, self._url)
|
||||
self._feed.entries = self._feed.entries[0:MAX_ENTRIES]
|
||||
self._filter_entries()
|
||||
self._publish_new_entries()
|
||||
if self._has_published_parsed:
|
||||
self._storage.put_timestamp(
|
||||
self._url, self._last_entry_timestamp)
|
||||
self._feed_id, self._last_entry_timestamp)
|
||||
else:
|
||||
self._log_no_entries()
|
||||
self._last_update_successful = True
|
||||
_LOGGER.info("Fetch from feed %s completed", self._url)
|
||||
|
||||
def _filter_entries(self):
|
||||
"""Filter the entries provided and return the ones to keep."""
|
||||
if len(self._feed.entries) > self._max_entries:
|
||||
_LOGGER.debug("Processing only the first %s entries "
|
||||
"in feed %s", self._max_entries, self._url)
|
||||
self._feed.entries = self._feed.entries[0:self._max_entries]
|
||||
|
||||
def _update_and_fire_entry(self, entry):
|
||||
"""Update last_entry_timestamp and fire entry."""
|
||||
# We are lucky, `published_parsed` data available, let's make use of
|
||||
@@ -109,12 +145,12 @@ class FeedManager(object):
|
||||
_LOGGER.debug("No published_parsed info available for entry %s",
|
||||
entry.title)
|
||||
entry.update({'feed_url': self._url})
|
||||
self._hass.bus.fire(EVENT_FEEDREADER, entry)
|
||||
self._hass.bus.fire(self._event_type, entry)
|
||||
|
||||
def _publish_new_entries(self):
|
||||
"""Publish new entries to the event bus."""
|
||||
new_entries = False
|
||||
self._last_entry_timestamp = self._storage.get_timestamp(self._url)
|
||||
self._last_entry_timestamp = self._storage.get_timestamp(self._feed_id)
|
||||
if self._last_entry_timestamp:
|
||||
self._firstrun = False
|
||||
else:
|
||||
@@ -157,18 +193,18 @@ class StoredData(object):
|
||||
_LOGGER.error("Error loading data from pickled file %s",
|
||||
self._data_file)
|
||||
|
||||
def get_timestamp(self, url):
|
||||
"""Return stored timestamp for given url."""
|
||||
def get_timestamp(self, feed_id):
|
||||
"""Return stored timestamp for given feed id (usually the url)."""
|
||||
self._fetch_data()
|
||||
return self._data.get(url)
|
||||
return self._data.get(feed_id)
|
||||
|
||||
def put_timestamp(self, url, timestamp):
|
||||
"""Update timestamp for given URL."""
|
||||
def put_timestamp(self, feed_id, timestamp):
|
||||
"""Update timestamp for given feed id (usually the url)."""
|
||||
self._fetch_data()
|
||||
with self._lock, open(self._data_file, 'wb') as myfile:
|
||||
self._data.update({url: timestamp})
|
||||
self._data.update({feed_id: timestamp})
|
||||
_LOGGER.debug("Overwriting feed %s timestamp in storage file %s",
|
||||
url, self._data_file)
|
||||
feed_id, self._data_file)
|
||||
try:
|
||||
pickle.dump(self._data, myfile)
|
||||
except: # noqa: E722 # pylint: disable=bare-except
|
||||
|
||||
@@ -43,7 +43,7 @@ def setup(hass, config):
|
||||
|
||||
|
||||
def create_event_handler(patterns, hass):
|
||||
""""Return the Watchdog EventHandler object."""
|
||||
"""Return the Watchdog EventHandler object."""
|
||||
from watchdog.events import PatternMatchingEventHandler
|
||||
|
||||
class EventHandler(PatternMatchingEventHandler):
|
||||
|
||||
@@ -25,7 +25,7 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180509.0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20180608.0b0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||
@@ -147,21 +147,6 @@ class AbstractPanel:
|
||||
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
|
||||
index_view.get)
|
||||
|
||||
def to_response(self, hass, request):
|
||||
"""Panel as dictionary."""
|
||||
result = {
|
||||
'component_name': self.component_name,
|
||||
'icon': self.sidebar_icon,
|
||||
'title': self.sidebar_title,
|
||||
'url_path': self.frontend_url_path,
|
||||
'config': self.config,
|
||||
}
|
||||
if _is_latest(hass.data[DATA_JS_VERSION], request):
|
||||
result['url'] = self.webcomponent_url_latest
|
||||
else:
|
||||
result['url'] = self.webcomponent_url_es5
|
||||
return result
|
||||
|
||||
|
||||
class BuiltInPanel(AbstractPanel):
|
||||
"""Panel that is part of hass_frontend."""
|
||||
@@ -175,30 +160,15 @@ class BuiltInPanel(AbstractPanel):
|
||||
self.frontend_url_path = frontend_url_path or component_name
|
||||
self.config = config
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_finalize(self, hass, frontend_repository_path):
|
||||
"""Finalize this panel for usage.
|
||||
|
||||
If frontend_repository_path is set, will be prepended to path of
|
||||
built-in components.
|
||||
"""
|
||||
if frontend_repository_path is None:
|
||||
import hass_frontend
|
||||
import hass_frontend_es5
|
||||
|
||||
self.webcomponent_url_latest = \
|
||||
'/frontend_latest/panels/ha-panel-{}-{}.html'.format(
|
||||
self.component_name,
|
||||
hass_frontend.FINGERPRINTS[self.component_name])
|
||||
self.webcomponent_url_es5 = \
|
||||
'/frontend_es5/panels/ha-panel-{}-{}.html'.format(
|
||||
self.component_name,
|
||||
hass_frontend_es5.FINGERPRINTS[self.component_name])
|
||||
else:
|
||||
# Dev mode
|
||||
self.webcomponent_url_es5 = self.webcomponent_url_latest = \
|
||||
'/home-assistant-polymer/panels/{}/ha-panel-{}.html'.format(
|
||||
self.component_name, self.component_name)
|
||||
def to_response(self, hass, request):
|
||||
"""Panel as dictionary."""
|
||||
return {
|
||||
'component_name': self.component_name,
|
||||
'icon': self.sidebar_icon,
|
||||
'title': self.sidebar_title,
|
||||
'config': self.config,
|
||||
'url_path': self.frontend_url_path,
|
||||
}
|
||||
|
||||
|
||||
class ExternalPanel(AbstractPanel):
|
||||
@@ -244,6 +214,21 @@ class ExternalPanel(AbstractPanel):
|
||||
frontend_repository_path is None)
|
||||
self.REGISTERED_COMPONENTS.add(self.component_name)
|
||||
|
||||
def to_response(self, hass, request):
|
||||
"""Panel as dictionary."""
|
||||
result = {
|
||||
'component_name': self.component_name,
|
||||
'icon': self.sidebar_icon,
|
||||
'title': self.sidebar_title,
|
||||
'url_path': self.frontend_url_path,
|
||||
'config': self.config,
|
||||
}
|
||||
if _is_latest(hass.data[DATA_JS_VERSION], request):
|
||||
result['url'] = self.webcomponent_url_latest
|
||||
else:
|
||||
result['url'] = self.webcomponent_url_es5
|
||||
return result
|
||||
|
||||
|
||||
@bind_hass
|
||||
@asyncio.coroutine
|
||||
@@ -296,6 +281,15 @@ def add_manifest_json_key(key, val):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up the serving of the frontend."""
|
||||
if list(hass.auth.async_auth_providers):
|
||||
client = yield from hass.auth.async_create_client(
|
||||
'Home Assistant Frontend',
|
||||
redirect_uris=['/'],
|
||||
no_secret=True,
|
||||
)
|
||||
else:
|
||||
client = None
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS)
|
||||
hass.http.register_view(ManifestJSONView)
|
||||
@@ -307,59 +301,40 @@ def async_setup(hass, config):
|
||||
hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION)
|
||||
|
||||
if is_dev:
|
||||
for subpath in ["src", "build-translations", "build-temp", "build",
|
||||
"hass_frontend", "bower_components", "panels",
|
||||
"hassio"]:
|
||||
hass.http.register_static_path(
|
||||
"/home-assistant-polymer/{}".format(subpath),
|
||||
os.path.join(repo_path, subpath),
|
||||
False)
|
||||
|
||||
hass.http.register_static_path(
|
||||
"/static/translations",
|
||||
os.path.join(repo_path, "build-translations/output"), False)
|
||||
sw_path_es5 = os.path.join(repo_path, "build-es5/service_worker.js")
|
||||
sw_path_latest = os.path.join(repo_path, "build/service_worker.js")
|
||||
static_path = os.path.join(repo_path, 'hass_frontend')
|
||||
frontend_es5_path = os.path.join(repo_path, 'build-es5')
|
||||
frontend_latest_path = os.path.join(repo_path, 'build')
|
||||
hass_frontend_path = os.path.join(repo_path, 'hass_frontend')
|
||||
hass_frontend_es5_path = os.path.join(repo_path, 'hass_frontend_es5')
|
||||
else:
|
||||
import hass_frontend
|
||||
import hass_frontend_es5
|
||||
sw_path_es5 = os.path.join(hass_frontend_es5.where(),
|
||||
"service_worker.js")
|
||||
sw_path_latest = os.path.join(hass_frontend.where(),
|
||||
"service_worker.js")
|
||||
# /static points to dir with files that are JS-type agnostic.
|
||||
# ES5 files are served from /frontend_es5.
|
||||
# ES6 files are served from /frontend_latest.
|
||||
static_path = hass_frontend.where()
|
||||
frontend_es5_path = hass_frontend_es5.where()
|
||||
frontend_latest_path = static_path
|
||||
hass_frontend_path = hass_frontend.where()
|
||||
hass_frontend_es5_path = hass_frontend_es5.where()
|
||||
|
||||
hass.http.register_static_path(
|
||||
"/service_worker_es5.js", sw_path_es5, False)
|
||||
"/service_worker_es5.js",
|
||||
os.path.join(hass_frontend_es5_path, "service_worker.js"), False)
|
||||
hass.http.register_static_path(
|
||||
"/service_worker.js", sw_path_latest, False)
|
||||
"/service_worker.js",
|
||||
os.path.join(hass_frontend_path, "service_worker.js"), False)
|
||||
hass.http.register_static_path(
|
||||
"/robots.txt", os.path.join(static_path, "robots.txt"), not is_dev)
|
||||
hass.http.register_static_path("/static", static_path, not is_dev)
|
||||
"/robots.txt",
|
||||
os.path.join(hass_frontend_path, "robots.txt"), False)
|
||||
hass.http.register_static_path("/static", hass_frontend_path, not is_dev)
|
||||
hass.http.register_static_path(
|
||||
"/frontend_latest", frontend_latest_path, not is_dev)
|
||||
"/frontend_latest", hass_frontend_path, not is_dev)
|
||||
hass.http.register_static_path(
|
||||
"/frontend_es5", frontend_es5_path, not is_dev)
|
||||
"/frontend_es5", hass_frontend_es5_path, not is_dev)
|
||||
|
||||
local = hass.config.path('www')
|
||||
if os.path.isdir(local):
|
||||
hass.http.register_static_path("/local", local, not is_dev)
|
||||
|
||||
index_view = IndexView(repo_path, js_version)
|
||||
index_view = IndexView(repo_path, js_version, client)
|
||||
hass.http.register_view(index_view)
|
||||
|
||||
@asyncio.coroutine
|
||||
def finalize_panel(panel):
|
||||
async def finalize_panel(panel):
|
||||
"""Finalize setup of a panel."""
|
||||
yield from panel.async_finalize(hass, repo_path)
|
||||
if hasattr(panel, 'async_finalize'):
|
||||
await panel.async_finalize(hass, repo_path)
|
||||
panel.async_register_index_routes(hass.http.app.router, index_view)
|
||||
|
||||
yield from asyncio.wait([
|
||||
@@ -451,10 +426,11 @@ class IndexView(HomeAssistantView):
|
||||
requires_auth = False
|
||||
extra_urls = ['/states', '/states/{extra}']
|
||||
|
||||
def __init__(self, repo_path, js_option):
|
||||
def __init__(self, repo_path, js_option, client):
|
||||
"""Initialize the frontend view."""
|
||||
self.repo_path = repo_path
|
||||
self.js_option = js_option
|
||||
self.client = client
|
||||
self._template_cache = {}
|
||||
|
||||
def get_template(self, latest):
|
||||
@@ -508,7 +484,7 @@ class IndexView(HomeAssistantView):
|
||||
|
||||
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5
|
||||
|
||||
resp = template.render(
|
||||
template_params = dict(
|
||||
no_auth=no_auth,
|
||||
panel_url=panel_url,
|
||||
panels=hass.data[DATA_PANELS],
|
||||
@@ -516,7 +492,11 @@ class IndexView(HomeAssistantView):
|
||||
extra_urls=hass.data[extra_key],
|
||||
)
|
||||
|
||||
return web.Response(text=resp, content_type='text/html')
|
||||
if self.client is not None:
|
||||
template_params['client_id'] = self.client.id
|
||||
|
||||
return web.Response(text=template.render(**template_params),
|
||||
content_type='text/html')
|
||||
|
||||
|
||||
class ManifestJSONView(HomeAssistantView):
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
|
||||
STATE_UNLOCKED, STATE_OK, STATE_PROBLEM, STATE_UNKNOWN,
|
||||
ATTR_ASSUMED_STATE, SERVICE_RELOAD)
|
||||
ATTR_ASSUMED_STATE, SERVICE_RELOAD, ATTR_NAME, ATTR_ICON)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
||||
@@ -35,8 +35,6 @@ ATTR_ADD_ENTITIES = 'add_entities'
|
||||
ATTR_AUTO = 'auto'
|
||||
ATTR_CONTROL = 'control'
|
||||
ATTR_ENTITIES = 'entities'
|
||||
ATTR_ICON = 'icon'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_OBJECT_ID = 'object_id'
|
||||
ATTR_ORDER = 'order'
|
||||
ATTR_VIEW = 'view'
|
||||
|
||||
@@ -13,12 +13,13 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import SERVICE_CHECK_CONFIG
|
||||
from homeassistant.const import (
|
||||
SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP)
|
||||
ATTR_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP)
|
||||
from homeassistant.core import DOMAIN as HASS_DOMAIN
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .handler import HassIO
|
||||
from .http import HassIOView
|
||||
|
||||
@@ -27,6 +28,15 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'hassio'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
CONF_FRONTEND_REPO = 'development_repo'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(DOMAIN): vol.Schema({
|
||||
vol.Optional(CONF_FRONTEND_REPO): cv.isdir,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
DATA_HOMEASSISTANT_VERSION = 'hassio_hass_version'
|
||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=55)
|
||||
|
||||
@@ -47,7 +57,6 @@ ATTR_SNAPSHOT = 'snapshot'
|
||||
ATTR_ADDONS = 'addons'
|
||||
ATTR_FOLDERS = 'folders'
|
||||
ATTR_HOMEASSISTANT = 'homeassistant'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_PASSWORD = 'password'
|
||||
|
||||
SCHEMA_NO_DATA = vol.Schema({})
|
||||
@@ -142,7 +151,13 @@ def async_setup(hass, config):
|
||||
try:
|
||||
host = os.environ['HASSIO']
|
||||
except KeyError:
|
||||
_LOGGER.error("No Hass.io supervisor detect")
|
||||
_LOGGER.error("Missing HASSIO environment variable.")
|
||||
return False
|
||||
|
||||
try:
|
||||
os.environ['HASSIO_TOKEN']
|
||||
except KeyError:
|
||||
_LOGGER.error("Missing HASSIO_TOKEN environment variable.")
|
||||
return False
|
||||
|
||||
websession = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||
@@ -152,11 +167,18 @@ def async_setup(hass, config):
|
||||
_LOGGER.error("Not connected with Hass.io")
|
||||
return False
|
||||
|
||||
# This overrides the normal API call that would be forwarded
|
||||
development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO)
|
||||
if development_repo is not None:
|
||||
hass.http.register_static_path(
|
||||
'/api/hassio/app-es5',
|
||||
os.path.join(development_repo, 'hassio/build-es5'), False)
|
||||
|
||||
hass.http.register_view(HassIOView(host, websession))
|
||||
|
||||
if 'frontend' in hass.config.components:
|
||||
yield from hass.components.frontend.async_register_built_in_panel(
|
||||
'hassio', 'Hass.io', 'mdi:home-assistant')
|
||||
'hassio', 'Hass.io', 'hass:home-assistant')
|
||||
|
||||
if 'http' in config:
|
||||
yield from hassio.update_hass_api(config['http'])
|
||||
|
||||
@@ -33,7 +33,7 @@ def _api_bool(funct):
|
||||
|
||||
|
||||
def _api_data(funct):
|
||||
"""Return a api data."""
|
||||
"""Return data of an api."""
|
||||
@asyncio.coroutine
|
||||
def _wrapper(*argv, **kwargs):
|
||||
"""Wrap function."""
|
||||
|
||||
@@ -36,7 +36,7 @@ NO_TIMEOUT = {
|
||||
}
|
||||
|
||||
NO_AUTH = {
|
||||
re.compile(r'^app-(es5|latest)/(index|hassio-app).html$'),
|
||||
re.compile(r'^app-(es5|latest)/.+$'),
|
||||
re.compile(r'^addons/[^/]*/logo$')
|
||||
}
|
||||
|
||||
|
||||
@@ -274,7 +274,7 @@ async def async_setup(hass, config):
|
||||
|
||||
hass.http.register_view(HistoryPeriodView(filters, use_include_order))
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
'history', 'history', 'mdi:poll-box')
|
||||
'history', 'history', 'hass:poll-box')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -9,28 +9,28 @@ from zlib import adler32
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION)
|
||||
import homeassistant.components.cover as cover
|
||||
from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
||||
ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE)
|
||||
ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, CONF_TYPE, DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
|
||||
from homeassistant.util import get_local_ip
|
||||
from homeassistant.util.decorator import Registry
|
||||
from .const import (
|
||||
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER,
|
||||
DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START,
|
||||
DEVICE_CLASS_CO2, DEVICE_CLASS_PM25)
|
||||
CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, CONF_FILTER,
|
||||
DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25,
|
||||
DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, TYPE_OUTLET, TYPE_SWITCH)
|
||||
from .util import (
|
||||
validate_entity_config, show_setup_message)
|
||||
show_setup_message, validate_entity_config, validate_media_player_features)
|
||||
|
||||
TYPES = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['HAP-python==2.0.0']
|
||||
REQUIREMENTS = ['HAP-python==2.2.2']
|
||||
|
||||
# #### Driver Status ####
|
||||
STATUS_READY = 0
|
||||
@@ -38,6 +38,8 @@ STATUS_RUNNING = 1
|
||||
STATUS_STOPPED = 2
|
||||
STATUS_WAIT = 3
|
||||
|
||||
SWITCH_TYPES = {TYPE_OUTLET: 'Outlet',
|
||||
TYPE_SWITCH: 'Switch'}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All({
|
||||
@@ -84,7 +86,7 @@ async def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
def get_accessory(hass, state, aid, config):
|
||||
def get_accessory(hass, driver, state, aid, config):
|
||||
"""Take state and return an accessory object if supported."""
|
||||
if not aid:
|
||||
_LOGGER.warning('The entitiy "%s" is not supported, since it '
|
||||
@@ -93,7 +95,7 @@ def get_accessory(hass, state, aid, config):
|
||||
return None
|
||||
|
||||
a_type = None
|
||||
config = config or {}
|
||||
name = config.get(CONF_NAME, state.name)
|
||||
|
||||
if state.domain == 'alarm_control_panel':
|
||||
a_type = 'SecuritySystem'
|
||||
@@ -109,19 +111,28 @@ def get_accessory(hass, state, aid, config):
|
||||
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
|
||||
if device_class == 'garage' and \
|
||||
features & (SUPPORT_OPEN | SUPPORT_CLOSE):
|
||||
features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE):
|
||||
a_type = 'GarageDoorOpener'
|
||||
elif features & SUPPORT_SET_POSITION:
|
||||
elif features & cover.SUPPORT_SET_POSITION:
|
||||
a_type = 'WindowCovering'
|
||||
elif features & (SUPPORT_OPEN | SUPPORT_CLOSE):
|
||||
elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE):
|
||||
a_type = 'WindowCoveringBasic'
|
||||
|
||||
elif state.domain == 'fan':
|
||||
a_type = 'Fan'
|
||||
|
||||
elif state.domain == 'light':
|
||||
a_type = 'Light'
|
||||
|
||||
elif state.domain == 'lock':
|
||||
a_type = 'Lock'
|
||||
|
||||
elif state.domain == 'media_player':
|
||||
feature_list = config.get(CONF_FEATURE_LIST)
|
||||
if feature_list and \
|
||||
validate_media_player_features(state, feature_list):
|
||||
a_type = 'MediaPlayer'
|
||||
|
||||
elif state.domain == 'sensor':
|
||||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
@@ -140,14 +151,18 @@ def get_accessory(hass, state, aid, config):
|
||||
elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'):
|
||||
a_type = 'LightSensor'
|
||||
|
||||
elif state.domain in ('switch', 'remote', 'input_boolean', 'script'):
|
||||
elif state.domain == 'switch':
|
||||
switch_type = config.get(CONF_TYPE, TYPE_SWITCH)
|
||||
a_type = SWITCH_TYPES[switch_type]
|
||||
|
||||
elif state.domain in ('automation', 'input_boolean', 'remote', 'script'):
|
||||
a_type = 'Switch'
|
||||
|
||||
if a_type is None:
|
||||
return None
|
||||
|
||||
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type)
|
||||
return TYPES[a_type](hass, state.name, state.entity_id, aid, config=config)
|
||||
return TYPES[a_type](hass, driver, name, state.entity_id, aid, config)
|
||||
|
||||
|
||||
def generate_aid(entity_id):
|
||||
@@ -182,8 +197,9 @@ class HomeKit():
|
||||
|
||||
ip_addr = self._ip_address or get_local_ip()
|
||||
path = self.hass.config.path(HOMEKIT_FILE)
|
||||
self.bridge = HomeBridge(self.hass)
|
||||
self.driver = HomeDriver(self.bridge, self._port, ip_addr, path)
|
||||
self.driver = HomeDriver(self.hass, address=ip_addr,
|
||||
port=self._port, persist_file=path)
|
||||
self.bridge = HomeBridge(self.hass, self.driver)
|
||||
|
||||
def add_bridge_accessory(self, state):
|
||||
"""Try adding accessory to bridge if configured beforehand."""
|
||||
@@ -191,7 +207,7 @@ class HomeKit():
|
||||
return
|
||||
aid = generate_aid(state.entity_id)
|
||||
conf = self._config.pop(state.entity_id, {})
|
||||
acc = get_accessory(self.hass, state, aid, conf)
|
||||
acc = get_accessory(self.hass, self.driver, state, aid, conf)
|
||||
if acc is not None:
|
||||
self.bridge.add_accessory(acc)
|
||||
|
||||
@@ -203,15 +219,16 @@ class HomeKit():
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
from . import ( # noqa F401
|
||||
type_covers, type_lights, type_locks, type_security_systems,
|
||||
type_sensors, type_switches, type_thermostats)
|
||||
type_covers, type_fans, type_lights, type_locks,
|
||||
type_media_players, type_security_systems, type_sensors,
|
||||
type_switches, type_thermostats)
|
||||
|
||||
for state in self.hass.states.all():
|
||||
self.add_bridge_accessory(state)
|
||||
self.bridge.set_driver(self.driver)
|
||||
self.driver.add_accessory(self.bridge)
|
||||
|
||||
if not self.bridge.paired:
|
||||
show_setup_message(self.hass, self.bridge)
|
||||
if not self.driver.state.paired:
|
||||
show_setup_message(self.hass, self.driver.state.pincode)
|
||||
|
||||
_LOGGER.debug('Driver start')
|
||||
self.hass.add_job(self.driver.start)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Extend the basic Accessory and Bridge functions."""
|
||||
from datetime import timedelta
|
||||
from functools import wraps
|
||||
from functools import partial, wraps
|
||||
from inspect import getmodule
|
||||
import logging
|
||||
|
||||
@@ -16,8 +16,8 @@ from homeassistant.helpers.event import (
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME,
|
||||
BRIDGE_SERIAL_NUMBER, MANUFACTURER)
|
||||
BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER,
|
||||
DEBOUNCE_TIMEOUT, MANUFACTURER)
|
||||
from .util import (
|
||||
show_setup_message, dismiss_setup_message)
|
||||
|
||||
@@ -27,35 +27,25 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def debounce(func):
|
||||
"""Decorator function. Debounce callbacks form HomeKit."""
|
||||
@ha_callback
|
||||
def call_later_listener(*args):
|
||||
def call_later_listener(self, *args):
|
||||
"""Callback listener called from call_later."""
|
||||
# pylint: disable=unsubscriptable-object
|
||||
nonlocal lastargs, remove_listener
|
||||
hass = lastargs['hass']
|
||||
hass.async_add_job(func, *lastargs['args'])
|
||||
lastargs = remove_listener = None
|
||||
debounce_params = self.debounce.pop(func.__name__, None)
|
||||
if debounce_params:
|
||||
self.hass.async_add_job(func, self, *debounce_params[1:])
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args):
|
||||
"""Wrapper starts async timer.
|
||||
|
||||
The accessory must have 'self.hass' and 'self.entity_id' as attributes.
|
||||
"""
|
||||
# pylint: disable=not-callable
|
||||
hass = args[0].hass
|
||||
nonlocal lastargs, remove_listener
|
||||
if remove_listener:
|
||||
remove_listener()
|
||||
lastargs = remove_listener = None
|
||||
lastargs = {'hass': hass, 'args': [*args]}
|
||||
def wrapper(self, *args):
|
||||
"""Wrapper starts async timer."""
|
||||
debounce_params = self.debounce.pop(func.__name__, None)
|
||||
if debounce_params:
|
||||
debounce_params[0]() # remove listener
|
||||
remove_listener = track_point_in_utc_time(
|
||||
hass, call_later_listener,
|
||||
self.hass, partial(call_later_listener, self),
|
||||
dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT))
|
||||
logger.debug('%s: Start %s timeout', args[0].entity_id,
|
||||
self.debounce[func.__name__] = (remove_listener, *args)
|
||||
logger.debug('%s: Start %s timeout', self.entity_id,
|
||||
func.__name__.replace('set_', ''))
|
||||
|
||||
remove_listener = None
|
||||
lastargs = None
|
||||
name = getmodule(func).__name__
|
||||
logger = logging.getLogger(name)
|
||||
return wrapper
|
||||
@@ -64,46 +54,53 @@ def debounce(func):
|
||||
class HomeAccessory(Accessory):
|
||||
"""Adapter class for Accessory."""
|
||||
|
||||
def __init__(self, hass, name, entity_id, aid, category=CATEGORY_OTHER):
|
||||
def __init__(self, hass, driver, name, entity_id, aid, config,
|
||||
category=CATEGORY_OTHER):
|
||||
"""Initialize a Accessory object."""
|
||||
super().__init__(name, aid=aid)
|
||||
domain = split_entity_id(entity_id)[0].replace("_", " ").title()
|
||||
super().__init__(driver, name, aid=aid)
|
||||
model = split_entity_id(entity_id)[0].replace("_", " ").title()
|
||||
self.set_info_service(
|
||||
firmware_revision=__version__, manufacturer=MANUFACTURER,
|
||||
model=domain, serial_number=entity_id)
|
||||
model=model, serial_number=entity_id)
|
||||
self.category = category
|
||||
self.config = config
|
||||
self.entity_id = entity_id
|
||||
self.hass = hass
|
||||
self.debounce = {}
|
||||
|
||||
def run(self):
|
||||
"""Method called by accessory after driver is started."""
|
||||
async def run(self):
|
||||
"""Method called by accessory after driver is started.
|
||||
|
||||
Run inside the HAP-python event loop.
|
||||
"""
|
||||
state = self.hass.states.get(self.entity_id)
|
||||
self.update_state_callback(new_state=state)
|
||||
self.hass.add_job(self.update_state_callback, None, None, state)
|
||||
async_track_state_change(
|
||||
self.hass, self.entity_id, self.update_state_callback)
|
||||
|
||||
@ha_callback
|
||||
def update_state_callback(self, entity_id=None, old_state=None,
|
||||
new_state=None):
|
||||
"""Callback from state change listener."""
|
||||
_LOGGER.debug('New_state: %s', new_state)
|
||||
if new_state is None:
|
||||
return
|
||||
self.update_state(new_state)
|
||||
self.hass.async_add_job(self.update_state, new_state)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Method called on state change to update HomeKit value.
|
||||
|
||||
Overridden by accessory types.
|
||||
"""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class HomeBridge(Bridge):
|
||||
"""Adapter class for Bridge."""
|
||||
|
||||
def __init__(self, hass, name=BRIDGE_NAME):
|
||||
def __init__(self, hass, driver, name=BRIDGE_NAME):
|
||||
"""Initialize a Bridge object."""
|
||||
super().__init__(name)
|
||||
super().__init__(driver, name)
|
||||
self.set_info_service(
|
||||
firmware_revision=__version__, manufacturer=MANUFACTURER,
|
||||
model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER)
|
||||
@@ -113,20 +110,23 @@ class HomeBridge(Bridge):
|
||||
"""Prevent print of pyhap setup message to terminal."""
|
||||
pass
|
||||
|
||||
def add_paired_client(self, client_uuid, client_public):
|
||||
"""Override super function to dismiss setup message if paired."""
|
||||
super().add_paired_client(client_uuid, client_public)
|
||||
dismiss_setup_message(self.hass)
|
||||
|
||||
def remove_paired_client(self, client_uuid):
|
||||
"""Override super function to show setup message if unpaired."""
|
||||
super().remove_paired_client(client_uuid)
|
||||
show_setup_message(self.hass, self)
|
||||
|
||||
|
||||
class HomeDriver(AccessoryDriver):
|
||||
"""Adapter class for AccessoryDriver."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, hass, **kwargs):
|
||||
"""Initialize a AccessoryDriver object."""
|
||||
super().__init__(*args, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
self.hass = hass
|
||||
|
||||
def pair(self, client_uuid, client_public):
|
||||
"""Override super function to dismiss setup message if paired."""
|
||||
success = super().pair(client_uuid, client_public)
|
||||
if success:
|
||||
dismiss_setup_message(self.hass)
|
||||
return success
|
||||
|
||||
def unpair(self, client_uuid):
|
||||
"""Override super function to show setup message if unpaired."""
|
||||
super().unpair(client_uuid)
|
||||
show_setup_message(self.hass, self.state.pincode)
|
||||
|
||||
@@ -1,54 +1,68 @@
|
||||
"""Constants used be the HomeKit component."""
|
||||
# #### MISC ####
|
||||
# #### Misc ####
|
||||
DEBOUNCE_TIMEOUT = 0.5
|
||||
DOMAIN = 'homekit'
|
||||
HOMEKIT_FILE = '.homekit.state'
|
||||
HOMEKIT_NOTIFY_ID = 4663548
|
||||
|
||||
# #### CONFIG ####
|
||||
# #### Config ####
|
||||
CONF_AUTO_START = 'auto_start'
|
||||
CONF_ENTITY_CONFIG = 'entity_config'
|
||||
CONF_FEATURE = 'feature'
|
||||
CONF_FEATURE_LIST = 'feature_list'
|
||||
CONF_FILTER = 'filter'
|
||||
|
||||
# #### CONFIG DEFAULTS ####
|
||||
# #### Config Defaults ####
|
||||
DEFAULT_AUTO_START = True
|
||||
DEFAULT_PORT = 51827
|
||||
|
||||
# #### HOMEKIT COMPONENT SERVICES ####
|
||||
# #### Features ####
|
||||
FEATURE_ON_OFF = 'on_off'
|
||||
FEATURE_PLAY_PAUSE = 'play_pause'
|
||||
FEATURE_PLAY_STOP = 'play_stop'
|
||||
FEATURE_TOGGLE_MUTE = 'toggle_mute'
|
||||
|
||||
# #### HomeKit Component Services ####
|
||||
SERVICE_HOMEKIT_START = 'start'
|
||||
|
||||
# #### STRING CONSTANTS ####
|
||||
# #### String Constants ####
|
||||
BRIDGE_MODEL = 'Bridge'
|
||||
BRIDGE_NAME = 'Home Assistant Bridge'
|
||||
BRIDGE_SERIAL_NUMBER = 'homekit.bridge'
|
||||
MANUFACTURER = 'Home Assistant'
|
||||
|
||||
# #### Switch Types ####
|
||||
TYPE_OUTLET = 'outlet'
|
||||
TYPE_SWITCH = 'switch'
|
||||
|
||||
# #### Services ####
|
||||
SERV_ACCESSORY_INFO = 'AccessoryInformation'
|
||||
SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor'
|
||||
SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor'
|
||||
SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor'
|
||||
SERV_CONTACT_SENSOR = 'ContactSensor'
|
||||
SERV_FANV2 = 'Fanv2'
|
||||
SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener'
|
||||
SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity
|
||||
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
|
||||
SERV_LEAK_SENSOR = 'LeakSensor'
|
||||
SERV_LIGHT_SENSOR = 'LightSensor'
|
||||
SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name
|
||||
SERV_LIGHTBULB = 'Lightbulb'
|
||||
SERV_LOCK = 'LockMechanism'
|
||||
SERV_MOTION_SENSOR = 'MotionSensor'
|
||||
SERV_OCCUPANCY_SENSOR = 'OccupancySensor'
|
||||
SERV_OUTLET = 'Outlet'
|
||||
SERV_SECURITY_SYSTEM = 'SecuritySystem'
|
||||
SERV_SMOKE_SENSOR = 'SmokeSensor'
|
||||
SERV_SWITCH = 'Switch'
|
||||
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
|
||||
SERV_THERMOSTAT = 'Thermostat'
|
||||
SERV_WINDOW_COVERING = 'WindowCovering'
|
||||
# CurrentPosition, TargetPosition, PositionState
|
||||
|
||||
# #### Characteristics ####
|
||||
CHAR_ACTIVE = 'Active'
|
||||
CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
|
||||
CHAR_AIR_QUALITY = 'AirQuality'
|
||||
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
|
||||
CHAR_BRIGHTNESS = 'Brightness'
|
||||
CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected'
|
||||
CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel'
|
||||
CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel'
|
||||
@@ -59,13 +73,13 @@ CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
|
||||
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel'
|
||||
CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState'
|
||||
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
|
||||
CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100]
|
||||
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
|
||||
CHAR_CURRENT_POSITION = 'CurrentPosition'
|
||||
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity'
|
||||
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
|
||||
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
|
||||
CHAR_FIRMWARE_REVISION = 'FirmwareRevision'
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
|
||||
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
|
||||
CHAR_HUE = 'Hue'
|
||||
CHAR_LEAK_DETECTED = 'LeakDetected'
|
||||
CHAR_LOCK_CURRENT_STATE = 'LockCurrentState'
|
||||
CHAR_LOCK_TARGET_STATE = 'LockTargetState'
|
||||
@@ -75,33 +89,35 @@ CHAR_MODEL = 'Model'
|
||||
CHAR_MOTION_DETECTED = 'MotionDetected'
|
||||
CHAR_NAME = 'Name'
|
||||
CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
|
||||
CHAR_ON = 'On' # boolean
|
||||
CHAR_OUTLET_IN_USE = 'OutletInUse'
|
||||
CHAR_ON = 'On'
|
||||
CHAR_POSITION_STATE = 'PositionState'
|
||||
CHAR_SATURATION = 'Saturation' # percent
|
||||
CHAR_ROTATION_DIRECTION = 'RotationDirection'
|
||||
CHAR_SATURATION = 'Saturation'
|
||||
CHAR_SERIAL_NUMBER = 'SerialNumber'
|
||||
CHAR_SMOKE_DETECTED = 'SmokeDetected'
|
||||
CHAR_SWING_MODE = 'SwingMode'
|
||||
CHAR_TARGET_DOOR_STATE = 'TargetDoorState'
|
||||
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
|
||||
CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100]
|
||||
CHAR_TARGET_POSITION = 'TargetPosition'
|
||||
CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
|
||||
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
|
||||
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
|
||||
|
||||
# #### Properties ####
|
||||
PROP_MAX_VALUE = 'maxValue'
|
||||
PROP_MIN_VALUE = 'minValue'
|
||||
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}
|
||||
|
||||
# #### Device Class ####
|
||||
# #### Device Classes ####
|
||||
DEVICE_CLASS_CO2 = 'co2'
|
||||
DEVICE_CLASS_DOOR = 'door'
|
||||
DEVICE_CLASS_GARAGE_DOOR = 'garage_door'
|
||||
DEVICE_CLASS_GAS = 'gas'
|
||||
DEVICE_CLASS_HUMIDITY = 'humidity'
|
||||
DEVICE_CLASS_LIGHT = 'light'
|
||||
DEVICE_CLASS_MOISTURE = 'moisture'
|
||||
DEVICE_CLASS_MOTION = 'motion'
|
||||
DEVICE_CLASS_OCCUPANCY = 'occupancy'
|
||||
DEVICE_CLASS_OPENING = 'opening'
|
||||
DEVICE_CLASS_PM25 = 'pm25'
|
||||
DEVICE_CLASS_SMOKE = 'smoke'
|
||||
DEVICE_CLASS_TEMPERATURE = 'temperature'
|
||||
DEVICE_CLASS_WINDOW = 'window'
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
"""Class to hold all cover accessories."""
|
||||
import logging
|
||||
|
||||
from pyhap.const import CATEGORY_WINDOW_COVERING, CATEGORY_GARAGE_DOOR_OPENER
|
||||
from pyhap.const import CATEGORY_GARAGE_DOOR_OPENER, CATEGORY_WINDOW_COVERING
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED,
|
||||
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER,
|
||||
ATTR_SUPPORTED_FEATURES)
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_CLOSE_COVER,
|
||||
SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER,
|
||||
STATE_CLOSED, STATE_OPEN)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, debounce
|
||||
from .accessories import debounce, HomeAccessory
|
||||
from .const import (
|
||||
SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION,
|
||||
CHAR_TARGET_POSITION, CHAR_POSITION_STATE,
|
||||
SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
|
||||
CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, CHAR_POSITION_STATE,
|
||||
CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION,
|
||||
SERV_GARAGE_DOOR_OPENER, SERV_WINDOW_COVERING)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,7 +28,7 @@ class GarageDoorOpener(HomeAccessory):
|
||||
and support no more than open, close, and stop.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a GarageDoorOpener accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER)
|
||||
self.flag_target_state = False
|
||||
@@ -44,12 +44,13 @@ class GarageDoorOpener(HomeAccessory):
|
||||
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
|
||||
self.flag_target_state = True
|
||||
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
if value == 0:
|
||||
self.char_current_state.set_value(3)
|
||||
self.hass.components.cover.open_cover(self.entity_id)
|
||||
self.hass.services.call(DOMAIN, SERVICE_OPEN_COVER, params)
|
||||
elif value == 1:
|
||||
self.char_current_state.set_value(2)
|
||||
self.hass.components.cover.close_cover(self.entity_id)
|
||||
self.hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, params)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update cover state after state changed."""
|
||||
@@ -69,7 +70,7 @@ class WindowCovering(HomeAccessory):
|
||||
The cover entity must support: set_cover_position.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a WindowCovering accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
|
||||
self.homekit_target = None
|
||||
@@ -108,7 +109,7 @@ class WindowCoveringBasic(HomeAccessory):
|
||||
stop_cover (optional).
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a WindowCovering accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
|
||||
features = self.hass.states.get(self.entity_id) \
|
||||
@@ -141,8 +142,8 @@ class WindowCoveringBasic(HomeAccessory):
|
||||
else:
|
||||
service, position = (SERVICE_CLOSE_COVER, 0)
|
||||
|
||||
self.hass.services.call(DOMAIN, service,
|
||||
{ATTR_ENTITY_ID: self.entity_id})
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
self.hass.services.call(DOMAIN, service, params)
|
||||
|
||||
# Snap the current/target position to the expected final position.
|
||||
self.char_current_position.set_value(position)
|
||||
|
||||
115
homeassistant/components/homekit/type_fans.py
Normal file
115
homeassistant/components/homekit/type_fans.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Class to hold all light accessories."""
|
||||
import logging
|
||||
|
||||
from pyhap.const import CATEGORY_FAN
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE,
|
||||
DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SUPPORT_DIRECTION,
|
||||
SUPPORT_OSCILLATE)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON, STATE_OFF, STATE_ON)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory
|
||||
from .const import (
|
||||
CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_SWING_MODE, SERV_FANV2)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@TYPES.register('Fan')
|
||||
class Fan(HomeAccessory):
|
||||
"""Generate a Fan accessory for a fan entity.
|
||||
|
||||
Currently supports: state, speed, oscillate, direction.
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""Initialize a new Light accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_FAN)
|
||||
self._flag = {CHAR_ACTIVE: False,
|
||||
CHAR_ROTATION_DIRECTION: False,
|
||||
CHAR_SWING_MODE: False}
|
||||
self._state = 0
|
||||
|
||||
self.chars = []
|
||||
features = self.hass.states.get(self.entity_id) \
|
||||
.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
if features & SUPPORT_DIRECTION:
|
||||
self.chars.append(CHAR_ROTATION_DIRECTION)
|
||||
if features & SUPPORT_OSCILLATE:
|
||||
self.chars.append(CHAR_SWING_MODE)
|
||||
|
||||
serv_fan = self.add_preload_service(SERV_FANV2, self.chars)
|
||||
self.char_active = serv_fan.configure_char(
|
||||
CHAR_ACTIVE, value=0, setter_callback=self.set_state)
|
||||
|
||||
if CHAR_ROTATION_DIRECTION in self.chars:
|
||||
self.char_direction = serv_fan.configure_char(
|
||||
CHAR_ROTATION_DIRECTION, value=0,
|
||||
setter_callback=self.set_direction)
|
||||
|
||||
if CHAR_SWING_MODE in self.chars:
|
||||
self.char_swing = serv_fan.configure_char(
|
||||
CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating)
|
||||
|
||||
def set_state(self, value):
|
||||
"""Set state if call came from HomeKit."""
|
||||
if self._state == value:
|
||||
return
|
||||
|
||||
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
|
||||
self._flag[CHAR_ACTIVE] = True
|
||||
service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
self.hass.services.call(DOMAIN, service, params)
|
||||
|
||||
def set_direction(self, value):
|
||||
"""Set state if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set direction to %d', self.entity_id, value)
|
||||
self._flag[CHAR_ROTATION_DIRECTION] = True
|
||||
direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD
|
||||
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_DIRECTION: direction}
|
||||
self.hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, params)
|
||||
|
||||
def set_oscillating(self, value):
|
||||
"""Set state if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set oscillating to %d', self.entity_id, value)
|
||||
self._flag[CHAR_SWING_MODE] = True
|
||||
oscillating = True if value == 1 else False
|
||||
params = {ATTR_ENTITY_ID: self.entity_id,
|
||||
ATTR_OSCILLATING: oscillating}
|
||||
self.hass.services.call(DOMAIN, SERVICE_OSCILLATE, params)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update fan after state change."""
|
||||
# Handle State
|
||||
state = new_state.state
|
||||
if state in (STATE_ON, STATE_OFF):
|
||||
self._state = 1 if state == STATE_ON else 0
|
||||
if not self._flag[CHAR_ACTIVE] and \
|
||||
self.char_active.value != self._state:
|
||||
self.char_active.set_value(self._state)
|
||||
self._flag[CHAR_ACTIVE] = False
|
||||
|
||||
# Handle Direction
|
||||
if CHAR_ROTATION_DIRECTION in self.chars:
|
||||
direction = new_state.attributes.get(ATTR_DIRECTION)
|
||||
if not self._flag[CHAR_ROTATION_DIRECTION] and \
|
||||
direction in (DIRECTION_FORWARD, DIRECTION_REVERSE):
|
||||
hk_direction = 1 if direction == DIRECTION_REVERSE else 0
|
||||
if self.char_direction.value != hk_direction:
|
||||
self.char_direction.set_value(hk_direction)
|
||||
self._flag[CHAR_ROTATION_DIRECTION] = False
|
||||
|
||||
# Handle Oscillating
|
||||
if CHAR_SWING_MODE in self.chars:
|
||||
oscillating = new_state.attributes.get(ATTR_OSCILLATING)
|
||||
if not self._flag[CHAR_SWING_MODE] and \
|
||||
oscillating in (True, False):
|
||||
hk_oscillating = 1 if oscillating else 0
|
||||
if self.char_swing.value != hk_oscillating:
|
||||
self.char_swing.set_value(hk_oscillating)
|
||||
self._flag[CHAR_SWING_MODE] = False
|
||||
@@ -4,15 +4,18 @@ import logging
|
||||
from pyhap.const import CATEGORY_LIGHTBULB
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS,
|
||||
ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS)
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
|
||||
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR,
|
||||
ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, DOMAIN,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON,
|
||||
SERVICE_TURN_OFF, STATE_OFF, STATE_ON)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, debounce
|
||||
from .accessories import debounce, HomeAccessory
|
||||
from .const import (
|
||||
SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
|
||||
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
|
||||
CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, CHAR_ON,
|
||||
CHAR_SATURATION, SERV_LIGHTBULB, PROP_MAX_VALUE, PROP_MIN_VALUE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,7 +29,7 @@ class Light(HomeAccessory):
|
||||
Currently supports: state, brightness, color temperature, rgb_color.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a new Light accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_LIGHTBULB)
|
||||
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
|
||||
@@ -61,7 +64,8 @@ class Light(HomeAccessory):
|
||||
.attributes.get(ATTR_MAX_MIREDS, 500)
|
||||
self.char_color_temperature = serv_light.configure_char(
|
||||
CHAR_COLOR_TEMPERATURE, value=min_mireds,
|
||||
properties={'minValue': min_mireds, 'maxValue': max_mireds},
|
||||
properties={PROP_MIN_VALUE: min_mireds,
|
||||
PROP_MAX_VALUE: max_mireds},
|
||||
setter_callback=self.set_color_temperature)
|
||||
if CHAR_HUE in self.chars:
|
||||
self.char_hue = serv_light.configure_char(
|
||||
@@ -77,28 +81,27 @@ class Light(HomeAccessory):
|
||||
|
||||
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
|
||||
self._flag[CHAR_ON] = True
|
||||
|
||||
if value == 1:
|
||||
self.hass.components.light.turn_on(self.entity_id)
|
||||
elif value == 0:
|
||||
self.hass.components.light.turn_off(self.entity_id)
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF
|
||||
self.hass.services.call(DOMAIN, service, params)
|
||||
|
||||
@debounce
|
||||
def set_brightness(self, value):
|
||||
"""Set brightness if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set brightness to %d', self.entity_id, value)
|
||||
self._flag[CHAR_BRIGHTNESS] = True
|
||||
if value != 0:
|
||||
self.hass.components.light.turn_on(
|
||||
self.entity_id, brightness_pct=value)
|
||||
else:
|
||||
self.hass.components.light.turn_off(self.entity_id)
|
||||
if value == 0:
|
||||
self.set_state(0) # Turn off light
|
||||
return
|
||||
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value}
|
||||
self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
|
||||
|
||||
def set_color_temperature(self, value):
|
||||
"""Set color temperature if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set color temp to %s', self.entity_id, value)
|
||||
self._flag[CHAR_COLOR_TEMPERATURE] = True
|
||||
self.hass.components.light.turn_on(self.entity_id, color_temp=value)
|
||||
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value}
|
||||
self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
|
||||
|
||||
def set_saturation(self, value):
|
||||
"""Set saturation if call came from HomeKit."""
|
||||
@@ -116,15 +119,14 @@ class Light(HomeAccessory):
|
||||
|
||||
def set_color(self):
|
||||
"""Set color if call came from HomeKit."""
|
||||
# Handle Color
|
||||
if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \
|
||||
self._flag[CHAR_SATURATION]:
|
||||
color = (self._hue, self._saturation)
|
||||
_LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color)
|
||||
self._flag.update({
|
||||
CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True})
|
||||
self.hass.components.light.turn_on(
|
||||
self.entity_id, hs_color=color)
|
||||
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color}
|
||||
self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update light after state change."""
|
||||
|
||||
@@ -4,12 +4,12 @@ import logging
|
||||
from pyhap.const import CATEGORY_DOOR_LOCK
|
||||
|
||||
from homeassistant.components.lock import (
|
||||
ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN)
|
||||
ATTR_ENTITY_ID, DOMAIN, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN)
|
||||
from homeassistant.const import ATTR_CODE
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory
|
||||
from .const import (
|
||||
SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE)
|
||||
from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,9 +29,10 @@ class Lock(HomeAccessory):
|
||||
The lock entity must support: unlock and lock.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, config):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a Lock accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_DOOR_LOCK)
|
||||
self._code = self.config.get(ATTR_CODE)
|
||||
self.flag_target_state = False
|
||||
|
||||
serv_lock_mechanism = self.add_preload_service(SERV_LOCK)
|
||||
@@ -51,7 +52,9 @@ class Lock(HomeAccessory):
|
||||
service = STATE_TO_SERVICE[hass_value]
|
||||
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
self.hass.services.call('lock', service, params)
|
||||
if self._code:
|
||||
params[ATTR_CODE] = self._code
|
||||
self.hass.services.call(DOMAIN, service, params)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update lock after state changed."""
|
||||
|
||||
142
homeassistant/components/homekit/type_media_players.py
Normal file
142
homeassistant/components/homekit/type_media_players.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""Class to hold all media player accessories."""
|
||||
import logging
|
||||
|
||||
from pyhap.const import CATEGORY_SWITCH
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE,
|
||||
STATE_OFF, STATE_PLAYING, STATE_UNKNOWN)
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_VOLUME_MUTED, DOMAIN)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory
|
||||
from .const import (
|
||||
CHAR_NAME, CHAR_ON, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE,
|
||||
FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, SERV_SWITCH)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MODE_FRIENDLY_NAME = {FEATURE_ON_OFF: 'Power',
|
||||
FEATURE_PLAY_PAUSE: 'Play/Pause',
|
||||
FEATURE_PLAY_STOP: 'Play/Stop',
|
||||
FEATURE_TOGGLE_MUTE: 'Mute'}
|
||||
|
||||
|
||||
@TYPES.register('MediaPlayer')
|
||||
class MediaPlayer(HomeAccessory):
|
||||
"""Generate a Media Player accessory."""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""Initialize a Switch accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_SWITCH)
|
||||
self._flag = {FEATURE_ON_OFF: False, FEATURE_PLAY_PAUSE: False,
|
||||
FEATURE_PLAY_STOP: False, FEATURE_TOGGLE_MUTE: False}
|
||||
self.chars = {FEATURE_ON_OFF: None, FEATURE_PLAY_PAUSE: None,
|
||||
FEATURE_PLAY_STOP: None, FEATURE_TOGGLE_MUTE: None}
|
||||
feature_list = self.config[CONF_FEATURE_LIST]
|
||||
|
||||
if FEATURE_ON_OFF in feature_list:
|
||||
name = self.generate_service_name(FEATURE_ON_OFF)
|
||||
serv_on_off = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
|
||||
serv_on_off.configure_char(CHAR_NAME, value=name)
|
||||
self.chars[FEATURE_ON_OFF] = serv_on_off.configure_char(
|
||||
CHAR_ON, value=False, setter_callback=self.set_on_off)
|
||||
|
||||
if FEATURE_PLAY_PAUSE in feature_list:
|
||||
name = self.generate_service_name(FEATURE_PLAY_PAUSE)
|
||||
serv_play_pause = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
|
||||
serv_play_pause.configure_char(CHAR_NAME, value=name)
|
||||
self.chars[FEATURE_PLAY_PAUSE] = serv_play_pause.configure_char(
|
||||
CHAR_ON, value=False, setter_callback=self.set_play_pause)
|
||||
|
||||
if FEATURE_PLAY_STOP in feature_list:
|
||||
name = self.generate_service_name(FEATURE_PLAY_STOP)
|
||||
serv_play_stop = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
|
||||
serv_play_stop.configure_char(CHAR_NAME, value=name)
|
||||
self.chars[FEATURE_PLAY_STOP] = serv_play_stop.configure_char(
|
||||
CHAR_ON, value=False, setter_callback=self.set_play_stop)
|
||||
|
||||
if FEATURE_TOGGLE_MUTE in feature_list:
|
||||
name = self.generate_service_name(FEATURE_TOGGLE_MUTE)
|
||||
serv_toggle_mute = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
|
||||
serv_toggle_mute.configure_char(CHAR_NAME, value=name)
|
||||
self.chars[FEATURE_TOGGLE_MUTE] = serv_toggle_mute.configure_char(
|
||||
CHAR_ON, value=False, setter_callback=self.set_toggle_mute)
|
||||
|
||||
def generate_service_name(self, mode):
|
||||
"""Generate name for individual service."""
|
||||
return '{} {}'.format(self.display_name, MODE_FRIENDLY_NAME[mode])
|
||||
|
||||
def set_on_off(self, value):
|
||||
"""Move switch state to value if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set switch state for "on_off" to %s',
|
||||
self.entity_id, value)
|
||||
self._flag[FEATURE_ON_OFF] = True
|
||||
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
self.hass.services.call(DOMAIN, service, params)
|
||||
|
||||
def set_play_pause(self, value):
|
||||
"""Move switch state to value if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set switch state for "play_pause" to %s',
|
||||
self.entity_id, value)
|
||||
self._flag[FEATURE_PLAY_PAUSE] = True
|
||||
service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
self.hass.services.call(DOMAIN, service, params)
|
||||
|
||||
def set_play_stop(self, value):
|
||||
"""Move switch state to value if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set switch state for "play_stop" to %s',
|
||||
self.entity_id, value)
|
||||
self._flag[FEATURE_PLAY_STOP] = True
|
||||
service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
self.hass.services.call(DOMAIN, service, params)
|
||||
|
||||
def set_toggle_mute(self, value):
|
||||
"""Move switch state to value if call came from HomeKit."""
|
||||
_LOGGER.debug('%s: Set switch state for "toggle_mute" to %s',
|
||||
self.entity_id, value)
|
||||
self._flag[FEATURE_TOGGLE_MUTE] = True
|
||||
params = {ATTR_ENTITY_ID: self.entity_id,
|
||||
ATTR_MEDIA_VOLUME_MUTED: value}
|
||||
self.hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, params)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update switch state after state changed."""
|
||||
current_state = new_state.state
|
||||
|
||||
if self.chars[FEATURE_ON_OFF]:
|
||||
hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN, 'None')
|
||||
if not self._flag[FEATURE_ON_OFF]:
|
||||
_LOGGER.debug('%s: Set current state for "on_off" to %s',
|
||||
self.entity_id, hk_state)
|
||||
self.chars[FEATURE_ON_OFF].set_value(hk_state)
|
||||
self._flag[FEATURE_ON_OFF] = False
|
||||
|
||||
if self.chars[FEATURE_PLAY_PAUSE]:
|
||||
hk_state = current_state == STATE_PLAYING
|
||||
if not self._flag[FEATURE_PLAY_PAUSE]:
|
||||
_LOGGER.debug('%s: Set current state for "play_pause" to %s',
|
||||
self.entity_id, hk_state)
|
||||
self.chars[FEATURE_PLAY_PAUSE].set_value(hk_state)
|
||||
self._flag[FEATURE_PLAY_PAUSE] = False
|
||||
|
||||
if self.chars[FEATURE_PLAY_STOP]:
|
||||
hk_state = current_state == STATE_PLAYING
|
||||
if not self._flag[FEATURE_PLAY_STOP]:
|
||||
_LOGGER.debug('%s: Set current state for "play_stop" to %s',
|
||||
self.entity_id, hk_state)
|
||||
self.chars[FEATURE_PLAY_STOP].set_value(hk_state)
|
||||
self._flag[FEATURE_PLAY_STOP] = False
|
||||
|
||||
if self.chars[FEATURE_TOGGLE_MUTE]:
|
||||
current_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)
|
||||
if not self._flag[FEATURE_TOGGLE_MUTE]:
|
||||
_LOGGER.debug('%s: Set current state for "toggle_mute" to %s',
|
||||
self.entity_id, current_state)
|
||||
self.chars[FEATURE_TOGGLE_MUTE].set_value(current_state)
|
||||
self._flag[FEATURE_TOGGLE_MUTE] = False
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user