Merge pull request #23864 from home-assistant/rc

0.93.0
This commit is contained in:
Paulus Schoutsen 2019-05-16 07:08:27 +02:00 committed by GitHub
commit 584bfbaa76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
565 changed files with 23840 additions and 14595 deletions

View File

@ -90,7 +90,7 @@ jobs:
name: run static check name: run static check
command: | command: |
. venv/bin/activate . venv/bin/activate
flake8 flake8 homeassistant tests script
- run: - run:
name: run static type check name: run static type check

View File

@ -13,3 +13,4 @@ coverage:
url: "secret:TgWDUM4Jw0w7wMJxuxNF/yhSOHglIo1fGwInJnRLEVPy2P2aLimkoK1mtKCowH5TFw+baUXVXT3eAqefbdvIuM8BjRR4aRji95C6CYyD0QHy4N8i7nn1SQkWDPpS8IthYTg07rUDF7s5guurkKv2RrgoCdnnqjAMSzHoExMOF7xUmblMdhBTWJgBpWEhASJy85w/xxjlsE1xoTkzeJu9Q67pTXtRcn+5kb5/vIzPSYg=" url: "secret:TgWDUM4Jw0w7wMJxuxNF/yhSOHglIo1fGwInJnRLEVPy2P2aLimkoK1mtKCowH5TFw+baUXVXT3eAqefbdvIuM8BjRR4aRji95C6CYyD0QHy4N8i7nn1SQkWDPpS8IthYTg07rUDF7s5guurkKv2RrgoCdnnqjAMSzHoExMOF7xUmblMdhBTWJgBpWEhASJy85w/xxjlsE1xoTkzeJu9Q67pTXtRcn+5kb5/vIzPSYg="
comment: comment:
require_changes: yes require_changes: yes
branches: master

View File

@ -22,6 +22,7 @@ omit =
homeassistant/components/alarmdotcom/alarm_control_panel.py homeassistant/components/alarmdotcom/alarm_control_panel.py
homeassistant/components/alpha_vantage/sensor.py homeassistant/components/alpha_vantage/sensor.py
homeassistant/components/amazon_polly/tts.py homeassistant/components/amazon_polly/tts.py
homeassistant/components/ambiclimate/climate.py
homeassistant/components/ambient_station/* homeassistant/components/ambient_station/*
homeassistant/components/amcrest/* homeassistant/components/amcrest/*
homeassistant/components/ampio/* homeassistant/components/ampio/*
@ -52,6 +53,7 @@ omit =
homeassistant/components/bbox/sensor.py homeassistant/components/bbox/sensor.py
homeassistant/components/bh1750/sensor.py homeassistant/components/bh1750/sensor.py
homeassistant/components/bitcoin/sensor.py homeassistant/components/bitcoin/sensor.py
homeassistant/components/bizkaibus/sensor.py
homeassistant/components/blink/* homeassistant/components/blink/*
homeassistant/components/blinksticklight/light.py homeassistant/components/blinksticklight/light.py
homeassistant/components/blinkt/light.py homeassistant/components/blinkt/light.py
@ -173,6 +175,7 @@ omit =
homeassistant/components/esphome/light.py homeassistant/components/esphome/light.py
homeassistant/components/esphome/sensor.py homeassistant/components/esphome/sensor.py
homeassistant/components/esphome/switch.py homeassistant/components/esphome/switch.py
homeassistant/components/essent/sensor.py
homeassistant/components/etherscan/sensor.py homeassistant/components/etherscan/sensor.py
homeassistant/components/eufy/* homeassistant/components/eufy/*
homeassistant/components/everlights/light.py homeassistant/components/everlights/light.py
@ -275,9 +278,11 @@ omit =
homeassistant/components/imap_email_content/sensor.py homeassistant/components/imap_email_content/sensor.py
homeassistant/components/influxdb/sensor.py homeassistant/components/influxdb/sensor.py
homeassistant/components/insteon/* homeassistant/components/insteon/*
homeassistant/components/incomfort/*
homeassistant/components/ios/* homeassistant/components/ios/*
homeassistant/components/iota/* homeassistant/components/iota/*
homeassistant/components/iperf3/* homeassistant/components/iperf3/*
homeassistant/components/iqvia/*
homeassistant/components/irish_rail_transport/sensor.py homeassistant/components/irish_rail_transport/sensor.py
homeassistant/components/iss/binary_sensor.py homeassistant/components/iss/binary_sensor.py
homeassistant/components/isy994/* homeassistant/components/isy994/*
@ -344,6 +349,7 @@ omit =
homeassistant/components/message_bird/notify.py homeassistant/components/message_bird/notify.py
homeassistant/components/met/weather.py homeassistant/components/met/weather.py
homeassistant/components/meteo_france/* homeassistant/components/meteo_france/*
homeassistant/components/meteoalarm/*
homeassistant/components/metoffice/sensor.py homeassistant/components/metoffice/sensor.py
homeassistant/components/metoffice/weather.py homeassistant/components/metoffice/weather.py
homeassistant/components/microsoft/tts.py homeassistant/components/microsoft/tts.py
@ -418,6 +424,7 @@ omit =
homeassistant/components/openweathermap/sensor.py homeassistant/components/openweathermap/sensor.py
homeassistant/components/openweathermap/weather.py homeassistant/components/openweathermap/weather.py
homeassistant/components/opple/light.py homeassistant/components/opple/light.py
homeassistant/components/orangepi_gpio/*
homeassistant/components/orvibo/switch.py homeassistant/components/orvibo/switch.py
homeassistant/components/osramlightify/light.py homeassistant/components/osramlightify/light.py
homeassistant/components/otp/sensor.py homeassistant/components/otp/sensor.py
@ -440,7 +447,6 @@ omit =
homeassistant/components/plum_lightpad/* homeassistant/components/plum_lightpad/*
homeassistant/components/pocketcasts/sensor.py homeassistant/components/pocketcasts/sensor.py
homeassistant/components/point/* homeassistant/components/point/*
homeassistant/components/pollen/sensor.py
homeassistant/components/postnl/sensor.py homeassistant/components/postnl/sensor.py
homeassistant/components/prezzibenzina/sensor.py homeassistant/components/prezzibenzina/sensor.py
homeassistant/components/proliphix/climate.py homeassistant/components/proliphix/climate.py
@ -449,6 +455,7 @@ omit =
homeassistant/components/proxy/camera.py homeassistant/components/proxy/camera.py
homeassistant/components/ps4/__init__.py homeassistant/components/ps4/__init__.py
homeassistant/components/ps4/media_player.py homeassistant/components/ps4/media_player.py
homeassistant/components/ptvsd/*
homeassistant/components/pulseaudio_loopback/switch.py homeassistant/components/pulseaudio_loopback/switch.py
homeassistant/components/pushbullet/notify.py homeassistant/components/pushbullet/notify.py
homeassistant/components/pushbullet/sensor.py homeassistant/components/pushbullet/sensor.py
@ -534,9 +541,7 @@ omit =
homeassistant/components/smappee/* homeassistant/components/smappee/*
homeassistant/components/smtp/notify.py homeassistant/components/smtp/notify.py
homeassistant/components/snapcast/media_player.py homeassistant/components/snapcast/media_player.py
homeassistant/components/snmp/device_tracker.py homeassistant/components/snmp/*
homeassistant/components/snmp/sensor.py
homeassistant/components/snmp/switch.py
homeassistant/components/sochain/sensor.py homeassistant/components/sochain/sensor.py
homeassistant/components/socialblade/sensor.py homeassistant/components/socialblade/sensor.py
homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge/sensor.py
@ -561,6 +566,7 @@ omit =
homeassistant/components/swiss_public_transport/sensor.py homeassistant/components/swiss_public_transport/sensor.py
homeassistant/components/swisscom/device_tracker.py homeassistant/components/swisscom/device_tracker.py
homeassistant/components/switchbot/switch.py homeassistant/components/switchbot/switch.py
homeassistant/components/switcher_kis/switch.py
homeassistant/components/switchmate/switch.py homeassistant/components/switchmate/switch.py
homeassistant/components/syncthru/sensor.py homeassistant/components/syncthru/sensor.py
homeassistant/components/synology/camera.py homeassistant/components/synology/camera.py

View File

@ -7,7 +7,7 @@
**Related issue (if applicable):** fixes #<home-assistant issue number goes here> **Related issue (if applicable):** fixes #<home-assistant issue number goes here>
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#<home-assistant.io PR number goes here> **Pull request with documentation for [home-assistant.io](https://github.com/home-assistant/home-assistant.io) (if applicable):** home-assistant/home-assistant.io#<home-assistant.io PR number goes here>
## Example entry for `configuration.yaml` (if applicable): ## Example entry for `configuration.yaml` (if applicable):
```yaml ```yaml
@ -18,21 +18,18 @@
- [ ] The code change is tested and works locally. - [ ] The code change is tested and works locally.
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** - [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
- [ ] There is no commented out code in this PR. - [ ] There is no commented out code in this PR.
- [ ] I have followed the [development checklist][dev-checklist]
If user exposed functionality or configuration variables are added/changed: If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) - [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
If the code communicates with devices, web services, or third-party tools: If the code communicates with devices, web services, or third-party tools:
- [ ] [_The manifest file_][manifest-docs] has all fields filled out correctly ([example][ex-manifest]). - [ ] [_The manifest file_][manifest-docs] has all fields filled out correctly. Update and include derived files by running `python3 -m script.hassfest`.
- [ ] New dependencies have been added to `requirements` in the manifest ([example][ex-requir]). - [ ] New or updated dependencies have been added to `requirements_all.txt` by running `python3 -m script.gen_requirements_all`.
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - [ ] Untested files have been added to `.coveragerc`.
- [ ] 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: If the code does not interact with devices:
- [ ] Tests have been added to verify that the new code works. - [ ] Tests have been added to verify that the new code works.
[ex-manifest]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mobile_app/manifest.json [dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mobile_app/manifest.json#L5 [manifest-docs]: https://developers.home-assistant.io/docs/en/creating_integration_manifest.html
[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L23
[manifest-docs]: https://developers.home-assistant.io/docs/en/development_checklist.html#_the-manifest-file_

14
.github/main.workflow vendored
View File

@ -1,14 +0,0 @@
workflow "Mention CODEOWNERS of integrations when integration label is added to an issue" {
on = "issues"
resolves = "codeowners-mention"
}
workflow "Mention CODEOWNERS of integrations when integration label is added to an PRs" {
on = "pull_request"
resolves = "codeowners-mention"
}
action "codeowners-mention" {
uses = "home-assistant/codeowners-mention@master"
secrets = ["GITHUB_TOKEN"]
}

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
config/* config/*
config2/*
tests/testing_config/deps tests/testing_config/deps
tests/testing_config/home-assistant.log tests/testing_config/home-assistant.log
@ -84,7 +85,7 @@ Scripts/
# vimmy stuff # vimmy stuff
*.swp *.swp
*.swo *.swo
tags
ctags.tmp ctags.tmp
# vagrant stuff # vagrant stuff

33
.travis.yml Normal file
View File

@ -0,0 +1,33 @@
sudo: false
dist: xenial
addons:
apt:
sources:
- sourceline: "ppa:jonathonf/ffmpeg-4"
packages:
- libudev-dev
- libavformat-dev
- libavcodec-dev
- libavdevice-dev
- libavutil-dev
- libswscale-dev
- libswresample-dev
- libavfilter-dev
matrix:
fast_finish: true
include:
- python: "3.5.3"
env: TOXENV=lint
- python: "3.5.3"
env: TOXENV=pylint
- python: "3.5.3"
env: TOXENV=typing
- python: "3.5.3"
env: TOXENV=py35
- python: "3.7"
env: TOXENV=py37
cache: pip
install: pip install -U tox
language: python
script: travis_wait 40 tox --develop

View File

@ -21,6 +21,7 @@ homeassistant/components/airvisual/* @bachya
homeassistant/components/alarm_control_panel/* @colinodell homeassistant/components/alarm_control_panel/* @colinodell
homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/alpha_vantage/* @fabaff
homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/amazon_polly/* @robbiet480
homeassistant/components/ambiclimate/* @danielhiversen
homeassistant/components/ambient_station/* @bachya homeassistant/components/ambient_station/* @bachya
homeassistant/components/api/* @home-assistant/core homeassistant/components/api/* @home-assistant/core
homeassistant/components/arduino/* @fabaff homeassistant/components/arduino/* @fabaff
@ -32,6 +33,7 @@ homeassistant/components/automation/* @home-assistant/core
homeassistant/components/aws/* @awarecan @robbiet480 homeassistant/components/aws/* @awarecan @robbiet480
homeassistant/components/axis/* @kane610 homeassistant/components/axis/* @kane610
homeassistant/components/bitcoin/* @fabaff homeassistant/components/bitcoin/* @fabaff
homeassistant/components/bizkaibus/* @UgaitzEtxebarria
homeassistant/components/blink/* @fronzbot homeassistant/components/blink/* @fronzbot
homeassistant/components/bmw_connected_drive/* @ChristianKuehnel homeassistant/components/bmw_connected_drive/* @ChristianKuehnel
homeassistant/components/braviatv/* @robbiet480 homeassistant/components/braviatv/* @robbiet480
@ -66,10 +68,13 @@ homeassistant/components/egardia/* @jeroenterheerdt
homeassistant/components/eight_sleep/* @mezz64 homeassistant/components/eight_sleep/* @mezz64
homeassistant/components/emby/* @mezz64 homeassistant/components/emby/* @mezz64
homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enigma2/* @fbradyirl
homeassistant/components/enocean/* @bdurrer
homeassistant/components/ephember/* @ttroy50 homeassistant/components/ephember/* @ttroy50
homeassistant/components/epsonworkforce/* @ThaStealth homeassistant/components/epsonworkforce/* @ThaStealth
homeassistant/components/eq3btsmart/* @rytilahti homeassistant/components/eq3btsmart/* @rytilahti
homeassistant/components/esphome/* @OttoWinter homeassistant/components/esphome/* @OttoWinter
homeassistant/components/essent/* @TheLastProject
homeassistant/components/evohome/* @zxdavb
homeassistant/components/file/* @fabaff homeassistant/components/file/* @fabaff
homeassistant/components/filter/* @dgomes homeassistant/components/filter/* @dgomes
homeassistant/components/fitbit/* @robbiet480 homeassistant/components/fitbit/* @robbiet480
@ -80,6 +85,7 @@ homeassistant/components/foursquare/* @robbiet480
homeassistant/components/freebox/* @snoof85 homeassistant/components/freebox/* @snoof85
homeassistant/components/frontend/* @home-assistant/core homeassistant/components/frontend/* @home-assistant/core
homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/gearbest/* @HerrHofrat
homeassistant/components/geniushub/* @zxdavb
homeassistant/components/gitter/* @fabaff homeassistant/components/gitter/* @fabaff
homeassistant/components/glances/* @fabaff homeassistant/components/glances/* @fabaff
homeassistant/components/gntp/* @robbiet480 homeassistant/components/gntp/* @robbiet480
@ -99,6 +105,7 @@ homeassistant/components/history_graph/* @andrey-git
homeassistant/components/hive/* @Rendili @KJonline homeassistant/components/hive/* @Rendili @KJonline
homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homeassistant/* @home-assistant/core
homeassistant/components/homekit/* @cdce8p homeassistant/components/homekit/* @cdce8p
homeassistant/components/homekit_controller/* @Jc2k
homeassistant/components/homematic/* @pvizeli @danielperna84 homeassistant/components/homematic/* @pvizeli @danielperna84
homeassistant/components/html5/* @robbiet480 homeassistant/components/html5/* @robbiet480
homeassistant/components/http/* @home-assistant/core homeassistant/components/http/* @home-assistant/core
@ -106,6 +113,7 @@ homeassistant/components/huawei_lte/* @scop
homeassistant/components/huawei_router/* @abmantis homeassistant/components/huawei_router/* @abmantis
homeassistant/components/hue/* @balloob homeassistant/components/hue/* @balloob
homeassistant/components/ign_sismologia/* @exxamalte homeassistant/components/ign_sismologia/* @exxamalte
homeassistant/components/incomfort/* @zxdavb
homeassistant/components/influxdb/* @fabaff homeassistant/components/influxdb/* @fabaff
homeassistant/components/input_boolean/* @home-assistant/core homeassistant/components/input_boolean/* @home-assistant/core
homeassistant/components/input_datetime/* @home-assistant/core homeassistant/components/input_datetime/* @home-assistant/core
@ -115,6 +123,7 @@ homeassistant/components/input_text/* @home-assistant/core
homeassistant/components/integration/* @dgomes homeassistant/components/integration/* @dgomes
homeassistant/components/ios/* @robbiet480 homeassistant/components/ios/* @robbiet480
homeassistant/components/ipma/* @dgomes homeassistant/components/ipma/* @dgomes
homeassistant/components/iqvia/* @bachya
homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/irish_rail_transport/* @ttroy50
homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/jewish_calendar/* @tsvi
homeassistant/components/knx/* @Julius2342 homeassistant/components/knx/* @Julius2342
@ -137,6 +146,7 @@ homeassistant/components/matrix/* @tinloaf
homeassistant/components/mediaroom/* @dgomes homeassistant/components/mediaroom/* @dgomes
homeassistant/components/melissa/* @kennedyshead homeassistant/components/melissa/* @kennedyshead
homeassistant/components/met/* @danielhiversen homeassistant/components/met/* @danielhiversen
homeassistant/components/meteoalarm/* @rolfberkenbosch
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
homeassistant/components/mill/* @danielhiversen homeassistant/components/mill/* @danielhiversen
homeassistant/components/min_max/* @fabaff homeassistant/components/min_max/* @fabaff
@ -150,24 +160,28 @@ homeassistant/components/nello/* @pschmitt
homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/ness_alarm/* @nickw444
homeassistant/components/nest/* @awarecan homeassistant/components/nest/* @awarecan
homeassistant/components/netdata/* @fabaff homeassistant/components/netdata/* @fabaff
homeassistant/components/nextbus/* @vividboarder
homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nissan_leaf/* @filcole
homeassistant/components/nmbs/* @thibmaek homeassistant/components/nmbs/* @thibmaek
homeassistant/components/no_ip/* @fabaff homeassistant/components/no_ip/* @fabaff
homeassistant/components/notify/* @flowolf homeassistant/components/notify/* @home-assistant/core
homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_fuel_station/* @nickw444
homeassistant/components/nuki/* @pschmitt homeassistant/components/nuki/* @pschmitt
homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/ohmconnect/* @robbiet480
homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/onboarding/* @home-assistant/core
homeassistant/components/openuv/* @bachya homeassistant/components/openuv/* @bachya
homeassistant/components/openweathermap/* @fabaff homeassistant/components/openweathermap/* @fabaff
homeassistant/components/orangepi_gpio/* @pascallj
homeassistant/components/owlet/* @oblogic7 homeassistant/components/owlet/* @oblogic7
homeassistant/components/panel_custom/* @home-assistant/core homeassistant/components/panel_custom/* @home-assistant/core
homeassistant/components/panel_iframe/* @home-assistant/core homeassistant/components/panel_iframe/* @home-assistant/core
homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/persistent_notification/* @home-assistant/core
homeassistant/components/philips_js/* @elupus
homeassistant/components/pi_hole/* @fabaff homeassistant/components/pi_hole/* @fabaff
homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plant/* @ChristianKuehnel
homeassistant/components/point/* @fredrike homeassistant/components/point/* @fredrike
homeassistant/components/pollen/* @bachya homeassistant/components/ps4/* @ktnrg45
homeassistant/components/ptvsd/* @swamp-ig
homeassistant/components/push/* @dgomes homeassistant/components/push/* @dgomes
homeassistant/components/pvoutput/* @fabaff homeassistant/components/pvoutput/* @fabaff
homeassistant/components/qnap/* @colinodell homeassistant/components/qnap/* @colinodell
@ -204,7 +218,9 @@ homeassistant/components/supla/* @mwegrzynek
homeassistant/components/swiss_hydrological_data/* @fabaff homeassistant/components/swiss_hydrological_data/* @fabaff
homeassistant/components/swiss_public_transport/* @fabaff homeassistant/components/swiss_public_transport/* @fabaff
homeassistant/components/switchbot/* @danielhiversen homeassistant/components/switchbot/* @danielhiversen
homeassistant/components/switcher_kis/* @tomerfi
homeassistant/components/switchmate/* @danielhiversen homeassistant/components/switchmate/* @danielhiversen
homeassistant/components/syncthru/* @nielstron
homeassistant/components/synology_srm/* @aerialls homeassistant/components/synology_srm/* @aerialls
homeassistant/components/syslog/* @fabaff homeassistant/components/syslog/* @fabaff
homeassistant/components/sytadin/* @gautric homeassistant/components/sytadin/* @gautric
@ -235,6 +251,7 @@ homeassistant/components/uptimerobot/* @ludeeus
homeassistant/components/utility_meter/* @dgomes homeassistant/components/utility_meter/* @dgomes
homeassistant/components/velux/* @Julius2342 homeassistant/components/velux/* @Julius2342
homeassistant/components/version/* @fabaff homeassistant/components/version/* @fabaff
homeassistant/components/vizio/* @raman325
homeassistant/components/waqi/* @andrey-git homeassistant/components/waqi/* @andrey-git
homeassistant/components/weather/* @fabaff homeassistant/components/weather/* @fabaff
homeassistant/components/weblink/* @home-assistant/core homeassistant/components/weblink/* @home-assistant/core
@ -245,7 +262,7 @@ homeassistant/components/xfinity/* @cisasteelersfan
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
homeassistant/components/xiaomi_miio/* @rytilahti @syssi homeassistant/components/xiaomi_miio/* @rytilahti @syssi
homeassistant/components/xiaomi_tv/* @fattdev homeassistant/components/xiaomi_tv/* @fattdev
homeassistant/components/xmpp/* @fabaff homeassistant/components/xmpp/* @fabaff @flowolf
homeassistant/components/yamaha_musiccast/* @jalmeroth homeassistant/components/yamaha_musiccast/* @jalmeroth
homeassistant/components/yeelight/* @rytilahti @zewelor homeassistant/components/yeelight/* @rytilahti @zewelor
homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yeelightsunflower/* @lindsaymarkward

186
azure-pipelines.yml Normal file
View File

@ -0,0 +1,186 @@
# https://dev.azure.com/home-assistant
trigger:
batch: true
branches:
include:
- dev
tags:
include:
- '*'
variables:
- name: versionBuilder
value: '3.2'
- name: versionWheels
value: '0.3'
- group: docker
- group: wheels
- group: github
jobs:
- job: 'Wheels'
condition: eq(variables['Build.SourceBranchName'], 'dev')
timeoutInMinutes: 360
pool:
vmImage: 'ubuntu-16.04'
strategy:
maxParallel: 3
matrix:
amd64:
buildArch: 'amd64'
i386:
buildArch: 'i386'
armhf:
buildArch: 'armhf'
armv7:
buildArch: 'armv7'
aarch64:
buildArch: 'aarch64'
steps:
- script: |
sudo apt-get install -y --no-install-recommends \
qemu-user-static \
binfmt-support
sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
sudo update-binfmts --enable qemu-arm
sudo update-binfmts --enable qemu-aarch64
displayName: 'Initial cross build'
- script: |
mkdir -p .ssh
echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa
ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts
chmod 600 .ssh/*
displayName: 'Install ssh key'
- script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels)
displayName: 'Install wheels builder'
- script: |
cp requirements_all.txt requirements_hassio.txt
# Enable because we can build it
sed -i "s|# pytradfri|pytradfri|g" requirements_hassio.txt
sed -i "s|# pybluez|pybluez|g" requirements_hassio.txt
sed -i "s|# bluepy|bluepy|g" requirements_hassio.txt
sed -i "s|# beacontools|beacontools|g" requirements_hassio.txt
sed -i "s|# RPi.GPIO|RPi.GPIO|g" requirements_hassio.txt
sed -i "s|# raspihats|raspihats|g" requirements_hassio.txt
sed -i "s|# rpi-rf|rpi-rf|g" requirements_hassio.txt
sed -i "s|# blinkt|blinkt|g" requirements_hassio.txt
sed -i "s|# fritzconnection|fritzconnection|g" requirements_hassio.txt
sed -i "s|# pyuserinput|pyuserinput|g" requirements_hassio.txt
sed -i "s|# evdev|evdev|g" requirements_hassio.txt
sed -i "s|# smbus-cffi|smbus-cffi|g" requirements_hassio.txt
sed -i "s|# i2csense|i2csense|g" requirements_hassio.txt
sed -i "s|# python-eq3bt|python-eq3bt|g" requirements_hassio.txt
sed -i "s|# pycups|pycups|g" requirements_hassio.txt
sed -i "s|# homekit|homekit|g" requirements_hassio.txt
sed -i "s|# decora_wifi|decora_wifi|g" requirements_hassio.txt
sed -i "s|# decora|decora|g" requirements_hassio.txt
sed -i "s|# PySwitchbot|PySwitchbot|g" requirements_hassio.txt
sed -i "s|# pySwitchmate|pySwitchmate|g" requirements_hassio.txt
# Disable because of error
sed -i "s|insteonplm|# insteonplm|g" requirements_hassio.txt
displayName: 'Prepare requirements files for Hass.io'
- script: |
sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \
homeassistant/$(buildArch)-wheels:$(versionWheels) \
--apk "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;linux-headers;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev" \
--index https://wheels.hass.io \
--requirement requirements_hassio.txt \
--upload rsync \
--remote wheels@$(wheelsHost):/opt/wheels
displayName: 'Run wheels build'
- job: 'Release'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
timeoutInMinutes: 120
pool:
vmImage: 'ubuntu-16.04'
strategy:
maxParallel: 5
matrix:
amd64:
buildArch: 'amd64'
buildMachine: 'qemux86-64,intel-nuc'
i386:
buildArch: 'i386'
buildMachine: 'qemux86'
armhf:
buildArch: 'armhf'
buildMachine: 'qemuarm,raspberrypi'
armv7:
buildArch: 'armv7'
buildMachine: 'raspberrypi2,raspberrypi3,odroid-xu,tinker'
aarch64:
buildArch: 'aarch64'
buildMachine: 'qemuarm-64,raspberrypi3-64,odroid-c2,orangepi-prime'
steps:
- script: sudo docker login -u $(dockerUser) -p $(dockerPassword)
displayName: 'Docker hub login'
- script: sudo docker pull homeassistant/amd64-builder:$(versionBuilder)
displayName: 'Install Builder'
- script: |
set -e
sudo docker run --rm --privileged \
-v ~/.docker:/root/.docker \
-v /run/docker.sock:/run/docker.sock:rw \
homeassistant/amd64-builder:$(versionBuilder) \
--homeassistant $(Build.SourceBranchName) "--$(buildArch)" \
-r https://github.com/home-assistant/hassio-homeassistant \
-t generic --docker-hub homeassistant
sudo docker run --rm --privileged \
-v ~/.docker:/root/.docker \
-v /run/docker.sock:/run/docker.sock:rw \
homeassistant/amd64-builder:$(versionBuilder) \
--homeassistant-machine "$(Build.SourceBranchName)=$(buildMachine)" \
-r https://github.com/home-assistant/hassio-homeassistant \
-t machine --docker-hub homeassistant
displayName: 'Build Release'
- job: 'ReleasePublish'
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('Release'))
dependsOn:
- 'Release'
pool:
vmImage: 'ubuntu-16.04'
steps:
- script: |
sudo apt-get install -y --no-install-recommends \
git jq
git config --global user.name "Pascal Vizeli"
git config --global user.email "pvizeli@syshack.ch"
git config --global credential.helper store
echo "https://$(githubToken):x-oauth-basic@github.com > $HOME\.git-credentials
displayName: 'Install requirements'
- script: |
set -e
version="$(Build.SourceBranchName)"
git clone https://github.com/home-assistant/hassio-version
cd hassio-version
dev_version="$(jq --raw-output '.homeassistant.default' dev.json)"
beta_version="$(jq --raw-output '.homeassistant.default' beta.json)"
stable_version="$(jq --raw-output '.homeassistant.default' stable.json)"
if [[ "$version" =~ b ]]; then
sed -i "s|$dev_version|$version|g" dev.json
sed -i "s|$beta_version|$version|g" beta.json
else
sed -i "s|$dev_version|$version|g" dev.json
sed -i "s|$beta_version|$version|g" beta.json
sed -i "s|$stable_version|$version|g" stable.json
fi
git commit -am "Bump Home Assistant $version"
git push

View File

@ -7,8 +7,9 @@ import platform
import subprocess import subprocess
import sys import sys
import threading import threading
from typing import List, Dict, Any # noqa pylint: disable=unused-import from typing import ( # noqa pylint: disable=unused-import
List, Dict, Any, TYPE_CHECKING
)
from homeassistant import monkey_patch from homeassistant import monkey_patch
from homeassistant.const import ( from homeassistant.const import (
@ -18,6 +19,9 @@ from homeassistant.const import (
RESTART_EXIT_CODE, RESTART_EXIT_CODE,
) )
if TYPE_CHECKING:
from homeassistant import core
def set_loop() -> None: def set_loop() -> None:
"""Attempt to use uvloop.""" """Attempt to use uvloop."""
@ -86,10 +90,12 @@ def ensure_config_path(config_dir: str) -> None:
sys.exit(1) sys.exit(1)
def ensure_config_file(config_dir: str) -> str: async def ensure_config_file(hass: 'core.HomeAssistant', config_dir: str) \
-> str:
"""Ensure configuration file exists.""" """Ensure configuration file exists."""
import homeassistant.config as config_util import homeassistant.config as config_util
config_path = config_util.ensure_config_exists(config_dir) config_path = await config_util.async_ensure_config_exists(
hass, config_dir)
if config_path is None: if config_path is None:
print('Error getting configuration path') print('Error getting configuration path')
@ -261,6 +267,7 @@ def cmdline() -> List[str]:
async def setup_and_run_hass(config_dir: str, async def setup_and_run_hass(config_dir: str,
args: argparse.Namespace) -> int: args: argparse.Namespace) -> int:
"""Set up HASS and run.""" """Set up HASS and run."""
# pylint: disable=redefined-outer-name
from homeassistant import bootstrap, core from homeassistant import bootstrap, core
hass = core.HomeAssistant() hass = core.HomeAssistant()
@ -275,7 +282,7 @@ async def setup_and_run_hass(config_dir: str,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days, skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
log_file=args.log_file, log_no_color=args.log_no_color) log_file=args.log_file, log_no_color=args.log_no_color)
else: else:
config_file = ensure_config_file(config_dir) config_file = await ensure_config_file(hass, config_dir)
print('Config directory:', config_dir) print('Config directory:', config_dir)
await bootstrap.async_from_config_file( await bootstrap.async_from_config_file(
config_file, hass, verbose=args.verbose, skip_pip=args.skip_pip, config_file, hass, verbose=args.verbose, skip_pip=args.skip_pip,
@ -390,7 +397,7 @@ def main() -> int:
if exit_code == RESTART_EXIT_CODE and not args.runner: if exit_code == RESTART_EXIT_CODE and not args.runner:
try_to_restart() try_to_restart()
return exit_code # type: ignore # mypy cannot yet infer it return exit_code # type: ignore
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \ from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
REQUIREMENTS = ['pyotp==2.2.6'] REQUIREMENTS = ['pyotp==2.2.7']
CONF_MESSAGE = 'message' CONF_MESSAGE = 'message'

View File

@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \ from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
REQUIREMENTS = ['pyotp==2.2.6', 'PyQRCode==1.2.1'] REQUIREMENTS = ['pyotp==2.2.7', 'PyQRCode==1.2.1']
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({ CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
}, extra=vol.PREVENT_EXTRA) }, extra=vol.PREVENT_EXTRA)

View File

@ -11,6 +11,7 @@ from .models import PermissionLookup
from .types import PolicyType from .types import PolicyType
from .entities import ENTITY_POLICY_SCHEMA, compile_entities from .entities import ENTITY_POLICY_SCHEMA, compile_entities
from .merge import merge_policies # noqa from .merge import merge_policies # noqa
from .util import test_all
POLICY_SCHEMA = vol.Schema({ POLICY_SCHEMA = vol.Schema({
@ -29,6 +30,10 @@ class AbstractPermissions:
"""Return a function that can test entity access.""" """Return a function that can test entity access."""
raise NotImplementedError raise NotImplementedError
def access_all_entities(self, key: str) -> bool:
"""Check if we have a certain access to all entities."""
raise NotImplementedError
def check_entity(self, entity_id: str, key: str) -> bool: def check_entity(self, entity_id: str, key: str) -> bool:
"""Check if we can access entity.""" """Check if we can access entity."""
entity_func = self._cached_entity_func entity_func = self._cached_entity_func
@ -48,6 +53,10 @@ class PolicyPermissions(AbstractPermissions):
self._policy = policy self._policy = policy
self._perm_lookup = perm_lookup self._perm_lookup = perm_lookup
def access_all_entities(self, key: str) -> bool:
"""Check if we have a certain access to all entities."""
return test_all(self._policy.get(CAT_ENTITIES), key)
def _entity_func(self) -> Callable[[str, str], bool]: def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access.""" """Return a function that can test entity access."""
return compile_entities(self._policy.get(CAT_ENTITIES), return compile_entities(self._policy.get(CAT_ENTITIES),
@ -65,6 +74,10 @@ class _OwnerPermissions(AbstractPermissions):
# pylint: disable=no-self-use # pylint: disable=no-self-use
def access_all_entities(self, key: str) -> bool:
"""Check if we have a certain access to all entities."""
return True
def _entity_func(self) -> Callable[[str, str], bool]: def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access.""" """Return a function that can test entity access."""
return lambda entity_id, key: True return lambda entity_id, key: True

View File

@ -3,6 +3,7 @@ from functools import wraps
from typing import Callable, Dict, List, Optional, Union, cast # noqa: F401 from typing import Callable, Dict, List, Optional, Union, cast # noqa: F401
from .const import SUBCAT_ALL
from .models import PermissionLookup from .models import PermissionLookup
from .types import CategoryType, SubCategoryDict, ValueType from .types import CategoryType, SubCategoryDict, ValueType
@ -96,3 +97,16 @@ def _gen_dict_test_func(
return schema.get(key) return schema.get(key)
return test_value return test_value
def test_all(policy: CategoryType, key: str) -> bool:
"""Test if a policy has an ALL access for a specific key."""
if not isinstance(policy, dict):
return bool(policy)
all_policy = policy.get(SUBCAT_ALL)
if not isinstance(all_policy, dict):
return bool(all_policy)
return all_policy.get(key, False)

View File

@ -26,6 +26,7 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
# hass.data key for logging information. # hass.data key for logging information.
DATA_LOGGING = 'logging' DATA_LOGGING = 'logging'
DEBUGGER_INTEGRATIONS = {'ptvsd', }
CORE_INTEGRATIONS = ('homeassistant', 'persistent_notification') CORE_INTEGRATIONS = ('homeassistant', 'persistent_notification')
LOGGING_INTEGRATIONS = {'logger', 'system_log'} LOGGING_INTEGRATIONS = {'logger', 'system_log'}
STAGE_1_INTEGRATIONS = { STAGE_1_INTEGRATIONS = {
@ -306,6 +307,15 @@ async def _async_set_up_integrations(
"""Set up all the integrations.""" """Set up all the integrations."""
domains = _get_domains(hass, config) domains = _get_domains(hass, config)
# Start up debuggers. Start these first in case they want to wait.
debuggers = domains & DEBUGGER_INTEGRATIONS
if debuggers:
_LOGGER.debug("Starting up debuggers %s", debuggers)
await asyncio.gather(*[
async_setup_component(hass, domain, config)
for domain in debuggers])
domains -= DEBUGGER_INTEGRATIONS
# Resolve all dependencies of all components so we can find the logging # Resolve all dependencies of all components so we can find the logging
# and integrations that need faster initialization. # and integrations that need faster initialization.
resolved_domains_task = asyncio.gather(*[ resolved_domains_task = asyncio.gather(*[
@ -339,7 +349,7 @@ async def _async_set_up_integrations(
stage_2_domains = domains - logging_domains - stage_1_domains stage_2_domains = domains - logging_domains - stage_1_domains
if logging_domains: if logging_domains:
_LOGGER.debug("Setting up %s", logging_domains) _LOGGER.info("Setting up %s", logging_domains)
await asyncio.gather(*[ await asyncio.gather(*[
async_setup_component(hass, domain, config) async_setup_component(hass, domain, config)

View File

@ -31,9 +31,11 @@ CONF_ADS_TYPE = 'adstype'
CONF_ADS_VALUE = 'value' CONF_ADS_VALUE = 'value'
CONF_ADS_VAR = 'adsvar' CONF_ADS_VAR = 'adsvar'
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness' CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
CONF_ADS_VAR_POSITION = 'adsvar_position'
STATE_KEY_STATE = 'state' STATE_KEY_STATE = 'state'
STATE_KEY_BRIGHTNESS = 'brightness' STATE_KEY_BRIGHTNESS = 'brightness'
STATE_KEY_POSITION = 'position'
DOMAIN = 'ads' DOMAIN = 'ads'

View File

@ -0,0 +1,165 @@
"""Support for ADS covers."""
import logging
import voluptuous as vol
from homeassistant.components.cover import (
PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP,
SUPPORT_SET_POSITION, ATTR_POSITION, DEVICE_CLASSES_SCHEMA,
CoverDevice)
from homeassistant.const import (
CONF_NAME, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv
from . import CONF_ADS_VAR, CONF_ADS_VAR_POSITION, DATA_ADS, \
AdsEntity, STATE_KEY_STATE, STATE_KEY_POSITION
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'ADS Cover'
CONF_ADS_VAR_SET_POS = 'adsvar_set_position'
CONF_ADS_VAR_OPEN = 'adsvar_open'
CONF_ADS_VAR_CLOSE = 'adsvar_close'
CONF_ADS_VAR_STOP = 'adsvar_stop'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
vol.Optional(CONF_ADS_VAR_SET_POS): cv.string,
vol.Optional(CONF_ADS_VAR_CLOSE): cv.string,
vol.Optional(CONF_ADS_VAR_OPEN): cv.string,
vol.Optional(CONF_ADS_VAR_STOP): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the cover platform for ADS."""
ads_hub = hass.data[DATA_ADS]
ads_var_is_closed = config.get(CONF_ADS_VAR)
ads_var_position = config.get(CONF_ADS_VAR_POSITION)
ads_var_pos_set = config.get(CONF_ADS_VAR_SET_POS)
ads_var_open = config.get(CONF_ADS_VAR_OPEN)
ads_var_close = config.get(CONF_ADS_VAR_CLOSE)
ads_var_stop = config.get(CONF_ADS_VAR_STOP)
name = config[CONF_NAME]
device_class = config.get(CONF_DEVICE_CLASS)
add_entities([AdsCover(ads_hub,
ads_var_is_closed,
ads_var_position,
ads_var_pos_set,
ads_var_open,
ads_var_close,
ads_var_stop,
name,
device_class)])
class AdsCover(AdsEntity, CoverDevice):
"""Representation of ADS cover."""
def __init__(self, ads_hub,
ads_var_is_closed, ads_var_position,
ads_var_pos_set, ads_var_open,
ads_var_close, ads_var_stop, name, device_class):
"""Initialize AdsCover entity."""
super().__init__(ads_hub, name, ads_var_is_closed)
if self._ads_var is None:
if ads_var_position is not None:
self._unique_id = ads_var_position
elif ads_var_pos_set is not None:
self._unique_id = ads_var_pos_set
elif ads_var_open is not None:
self._unique_id = ads_var_open
self._state_dict[STATE_KEY_POSITION] = None
self._ads_var_position = ads_var_position
self._ads_var_pos_set = ads_var_pos_set
self._ads_var_open = ads_var_open
self._ads_var_close = ads_var_close
self._ads_var_stop = ads_var_stop
self._device_class = device_class
async def async_added_to_hass(self):
"""Register device notification."""
if self._ads_var is not None:
await self.async_initialize_device(self._ads_var,
self._ads_hub.PLCTYPE_BOOL)
if self._ads_var_position is not None:
await self.async_initialize_device(self._ads_var_position,
self._ads_hub.PLCTYPE_BYTE,
STATE_KEY_POSITION)
@property
def device_class(self):
"""Return the class of this cover."""
return self._device_class
@property
def is_closed(self):
"""Return if the cover is closed."""
if self._ads_var is not None:
return self._state_dict[STATE_KEY_STATE]
if self._ads_var_position is not None:
return self._state_dict[STATE_KEY_POSITION] == 0
return None
@property
def current_cover_position(self):
"""Return current position of cover."""
return self._state_dict[STATE_KEY_POSITION]
@property
def supported_features(self):
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
if self._ads_var_stop is not None:
supported_features |= SUPPORT_STOP
if self._ads_var_pos_set is not None:
supported_features |= SUPPORT_SET_POSITION
return supported_features
def stop_cover(self, **kwargs):
"""Fire the stop action."""
if self._ads_var_stop:
self._ads_hub.write_by_name(self._ads_var_stop, True,
self._ads_hub.PLCTYPE_BOOL)
def set_cover_position(self, **kwargs):
"""Set cover position."""
position = kwargs[ATTR_POSITION]
if self._ads_var_pos_set is not None:
self._ads_hub.write_by_name(self._ads_var_pos_set, position,
self._ads_hub.PLCTYPE_BYTE)
def open_cover(self, **kwargs):
"""Move the cover up."""
if self._ads_var_open is not None:
self._ads_hub.write_by_name(self._ads_var_open, True,
self._ads_hub.PLCTYPE_BOOL)
elif self._ads_var_pos_set is not None:
self.set_cover_position(position=100)
def close_cover(self, **kwargs):
"""Move the cover down."""
if self._ads_var_close is not None:
self._ads_hub.write_by_name(self._ads_var_close, True,
self._ads_hub.PLCTYPE_BOOL)
elif self._ads_var_pos_set is not None:
self.set_cover_position(position=0)
@property
def available(self):
"""Return False if state has not been updated yet."""
if self._ads_var is not None or self._ads_var_position is not None:
return self._state_dict[STATE_KEY_STATE] is not None or \
self._state_dict[STATE_KEY_POSITION] is not None
return True

View File

@ -449,9 +449,9 @@ class _AlexaPowerController(_AlexaInterface):
if name != 'powerState': if name != 'powerState':
raise _UnsupportedProperty(name) raise _UnsupportedProperty(name)
if self.entity.state == STATE_ON: if self.entity.state == STATE_OFF:
return 'ON'
return 'OFF' return 'OFF'
return 'ON'
class _AlexaLockController(_AlexaInterface): class _AlexaLockController(_AlexaInterface):

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "S'ha produ\u00eft un error desconegut al generat un testimoni d'acc\u00e9s.",
"already_setup": "El compte d\u2019Ambi Climate est\u00e0 configurat.",
"no_config": "Necessites configurar Ambi Climate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Autenticaci\u00f3 exitosa amb Ambi Climate."
},
"error": {
"follow_link": "V\u00e9s a l'enlla\u00e7 i autentica't abans de pr\u00e9mer Envia",
"no_token": "No autenticat amb Ambi Climate"
},
"step": {
"auth": {
"description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i <b>Permet</b> l'acc\u00e9s al teu compte de Ambi Climate, despr\u00e9s torna i prem <b>Envia</b> (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})",
"title": "Autenticaci\u00f3 amb Ambi Climate"
}
},
"title": "Ambi Climate"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"error": {
"follow_link": "N\u00e1sledujte odkaz a prove\u010fte ov\u011b\u0159en\u00ed p\u0159ed stisknut\u00edm tla\u010d\u00edtka Odeslat.",
"no_token": "Nen\u00ed ov\u011b\u0159en s Ambiclimate"
},
"step": {
"auth": {
"description": "N\u00e1sledujte tento [odkaz]({authorization_url}) a <b> Povolit </b> p\u0159\u00edstup k va\u0161emu \u00fa\u010dtu Ambiclimate, pot\u00e9 se vra\u0165te a stiskn\u011bte <b> Odeslat </b> n\u00ed\u017ee. \n (Ujist\u011bte se, \u017ee zadan\u00e1 adresa URL zp\u011btn\u00e9ho vol\u00e1n\u00ed je {cb_url} )",
"title": "Ov\u011b\u0159it Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Unbekannter Fehler beim Generieren eines Zugriffstokens.",
"already_setup": "Das Ambiclimate Konto ist konfiguriert.",
"no_config": "Ambiclimate muss konfiguriert sein, bevor die Authentifizierund durchgef\u00fchrt werden kann. [Bitte lies die Anleitung] (https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Erfolgreiche Authentifizierung mit Ambiclimate"
},
"error": {
"follow_link": "Bitte folge dem Link und authentifizieren dich, bevor du auf Senden klickst",
"no_token": "Nicht authentifiziert mit Ambiclimate"
},
"step": {
"auth": {
"description": "Bitte folge diesem [link] ({authorization_url}) und <b> Erlaube </b> Zugriff auf dein Ambiclimate-Konto, komme dann zur\u00fcck und dr\u00fccke <b> Senden </b> darunter.\n (Pr\u00fcfe, dass die Callback-URL {cb_url} ist.)",
"title": "Ambiclimate authentifizieren"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Unknown error generating an access token.",
"already_setup": "The Ambiclimate account is configured.",
"no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "Successfully authenticated with Ambiclimate"
},
"error": {
"follow_link": "Please follow the link and authenticate before pressing Submit",
"no_token": "Not authenticated with Ambiclimate"
},
"step": {
"auth": {
"description": "Please follow this [link]({authorization_url}) and <b>Allow</b> access to your Ambiclimate account, then come back and press <b>Submit</b> below.\n(Make sure the specified callback url is {cb_url})",
"title": "Authenticate Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070 \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
"already_setup": "Ambi Climate \uacc4\uc815\uc774 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"no_config": "Ambi Climate \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Ambi Climate \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/ambiclimate/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694."
},
"create_entry": {
"default": "Ambi Climate \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
"error": {
"follow_link": "Submit \ubc84\ud2bc\uc744 \ub204\ub974\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694",
"no_token": "Ambi Climate \ub85c \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4"
},
"step": {
"auth": {
"description": "[\ub9c1\ud06c]({authorization_url}) \ub97c \ud074\ub9ad\ud558\uc5ec Ambi Climate \uacc4\uc815\uc5d0 \ub300\ud574 <b>\ud5c8\uc6a9</b> \ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 <b>Submit</b> \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694. \n(\ucf5c\ubc31 url \uc744 {cb_url} \ub85c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)",
"title": "Ambi Climate \uc778\uc99d"
}
},
"title": "Ambi Climate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "Onbekannte Feeler beim gener\u00e9ieren vum Acc\u00e8s Jeton.",
"already_setup": "Den Ambiclimate Kont ass konfigur\u00e9iert.",
"no_config": "Dir musst Ambiclimate konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/ambiclimatet/)."
},
"create_entry": {
"default": "Erfollegr\u00e4ich mat Ambiclimate authentifiz\u00e9iert."
},
"error": {
"follow_link": "Follegt w.e.g. dem Link an authentifiz\u00e9iert de Kont ier dir op ofsch\u00e9cken dr\u00e9ckt.",
"no_token": "Net mat Ambiclimate authentifiz\u00e9iert"
},
"step": {
"auth": {
"description": "Follegt d\u00ebsem [Link]({authorization_url}) an <b>erlaabtt</b> den Acc\u00e8s zu \u00e4rem Ambiclimate Kont , a kommt dann zer\u00e9ck heihin an dr\u00e9ck op <b>ofsch\u00e9cken</b> hei \u00ebnnen.\n(Stellt s\u00e9cher dass den Type vun Callback {cb_url} ass.)",
"title": "Ambiclimate authentifiz\u00e9ieren"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.",
"already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.",
"no_config": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
},
"error": {
"follow_link": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e, \u043f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043d\u0430\u0436\u0430\u0442\u044c \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\".",
"no_token": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430."
},
"step": {
"auth": {
"description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 <b>\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435</b> \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 <b>\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c</b>. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})",
"title": "Ambi Climate"
}
},
"title": "Ambi Climate"
}
}

View File

@ -0,0 +1,23 @@
{
"config": {
"abort": {
"access_token": "\u7522\u751f\u5b58\u53d6\u8a8d\u8b49\u78bc\u672a\u77e5\u932f\u8aa4\u3002",
"already_setup": "Ambiclimate \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"no_config": "\u5fc5\u9808\u5148\u8a2d\u5b9a Ambiclimate \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/ambiclimate/\uff09\u3002"
},
"create_entry": {
"default": "\u5df2\u6210\u529f\u8a8d\u8b49 Ambiclimate \u88dd\u7f6e\u3002"
},
"error": {
"follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002",
"no_token": "Ambiclimate \u672a\u6388\u6b0a"
},
"step": {
"auth": {
"description": "\u8acb\u4f7f\u7528\u6b64[\u9023\u7d50]\uff08{authorization_url}\uff09\u4e26\u9ede\u9078<b>\u5141\u8a31</b>\u4ee5\u5b58\u53d6 Ambiclimate \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684<b>\u50b3\u9001</b>\u3002\n\uff08\u78ba\u5b9a Callback url \u70ba {cb_url}\uff09",
"title": "\u8a8d\u8b49 Ambiclimate"
}
},
"title": "Ambiclimate"
}
}

View File

@ -0,0 +1,44 @@
"""Support for Ambiclimate devices."""
import logging
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from . import config_flow
from .const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN:
vol.Schema({
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
})
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass, config):
"""Set up Ambiclimate components."""
if DOMAIN not in config:
return True
conf = config[DOMAIN]
config_flow.register_flow_implementation(
hass, conf[CONF_CLIENT_ID],
conf[CONF_CLIENT_SECRET])
return True
async def async_setup_entry(hass, entry):
"""Set up Ambiclimate from a config entry."""
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
entry, 'climate'))
return True

View File

@ -0,0 +1,230 @@
"""Support for Ambiclimate ac."""
import asyncio
import logging
import ambiclimate
import voluptuous as vol
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_ON_OFF, STATE_HEAT)
from homeassistant.const import ATTR_NAME
from homeassistant.const import (ATTR_TEMPERATURE,
STATE_OFF, TEMP_CELSIUS)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (ATTR_VALUE, CONF_CLIENT_ID, CONF_CLIENT_SECRET,
DOMAIN, SERVICE_COMFORT_FEEDBACK, SERVICE_COMFORT_MODE,
SERVICE_TEMPERATURE_MODE, STORAGE_KEY, STORAGE_VERSION)
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
SUPPORT_ON_OFF)
SEND_COMFORT_FEEDBACK_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_VALUE): cv.string,
})
SET_COMFORT_MODE_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
})
SET_TEMPERATURE_MODE_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_VALUE): cv.string,
})
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Ambicliamte device."""
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Ambicliamte device from config entry."""
config = entry.data
websession = async_get_clientsession(hass)
store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
token_info = await store.async_load()
oauth = ambiclimate.AmbiclimateOAuth(config[CONF_CLIENT_ID],
config[CONF_CLIENT_SECRET],
config['callback_url'],
websession)
try:
_token_info = await oauth.refresh_access_token(token_info)
except ambiclimate.AmbiclimateOauthError:
_LOGGER.error("Failed to refresh access token")
return
if _token_info:
await store.async_save(token_info)
token_info = _token_info
data_connection = ambiclimate.AmbiclimateConnection(oauth,
token_info=token_info,
websession=websession)
if not await data_connection.find_devices():
_LOGGER.error("No devices found")
return
tasks = []
for heater in data_connection.get_devices():
tasks.append(heater.update_device_info())
await asyncio.wait(tasks)
devs = []
for heater in data_connection.get_devices():
devs.append(AmbiclimateEntity(heater, store))
async_add_entities(devs, True)
async def send_comfort_feedback(service):
"""Send comfort feedback."""
device_name = service.data[ATTR_NAME]
device = data_connection.find_device_by_room_name(device_name)
if device:
await device.set_comfort_feedback(service.data[ATTR_VALUE])
hass.services.async_register(DOMAIN,
SERVICE_COMFORT_FEEDBACK,
send_comfort_feedback,
schema=SEND_COMFORT_FEEDBACK_SCHEMA)
async def set_comfort_mode(service):
"""Set comfort mode."""
device_name = service.data[ATTR_NAME]
device = data_connection.find_device_by_room_name(device_name)
if device:
await device.set_comfort_mode()
hass.services.async_register(DOMAIN,
SERVICE_COMFORT_MODE,
set_comfort_mode,
schema=SET_COMFORT_MODE_SCHEMA)
async def set_temperature_mode(service):
"""Set temperature mode."""
device_name = service.data[ATTR_NAME]
device = data_connection.find_device_by_room_name(device_name)
if device:
await device.set_temperature_mode(service.data[ATTR_VALUE])
hass.services.async_register(DOMAIN,
SERVICE_TEMPERATURE_MODE,
set_temperature_mode,
schema=SET_TEMPERATURE_MODE_SCHEMA)
class AmbiclimateEntity(ClimateDevice):
"""Representation of a Ambiclimate Thermostat device."""
def __init__(self, heater, store):
"""Initialize the thermostat."""
self._heater = heater
self._store = store
self._data = {}
@property
def unique_id(self):
"""Return a unique ID."""
return self._heater.device_id
@property
def name(self):
"""Return the name of the entity."""
return self._heater.name
@property
def device_info(self):
"""Return the device info."""
return {
'identifiers': {
(DOMAIN, self.unique_id)
},
'name': self.name,
'manufacturer': 'Ambiclimate',
}
@property
def temperature_unit(self):
"""Return the unit of measurement which this thermostat uses."""
return TEMP_CELSIUS
@property
def target_temperature(self):
"""Return the target temperature."""
return self._data.get('target_temperature')
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return 1
@property
def current_temperature(self):
"""Return the current temperature."""
return self._data.get('temperature')
@property
def current_humidity(self):
"""Return the current humidity."""
return self._data.get('humidity')
@property
def is_on(self):
"""Return true if heater is on."""
return self._data.get('power', '').lower() == 'on'
@property
def min_temp(self):
"""Return the minimum temperature."""
return self._heater.get_min_temp()
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._heater.get_max_temp()
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def current_operation(self):
"""Return current operation."""
return STATE_HEAT if self.is_on else STATE_OFF
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
await self._heater.set_target_temperature(temperature)
async def async_turn_on(self):
"""Turn device on."""
await self._heater.turn_on()
async def async_turn_off(self):
"""Turn device off."""
await self._heater.turn_off()
async def async_update(self):
"""Retrieve latest state."""
try:
token_info = await self._heater.control.refresh_access_token()
except ambiclimate.AmbiclimateOauthError:
_LOGGER.error("Failed to refresh access token")
return
if token_info:
await self._store.async_save(token_info)
self._data = await self._heater.update_device()

View File

@ -0,0 +1,153 @@
"""Config flow for Ambiclimate."""
import logging
import ambiclimate
from homeassistant import config_entries
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, CONF_CLIENT_ID,
CONF_CLIENT_SECRET, DOMAIN, STORAGE_VERSION, STORAGE_KEY)
DATA_AMBICLIMATE_IMPL = 'ambiclimate_flow_implementation'
_LOGGER = logging.getLogger(__name__)
@callback
def register_flow_implementation(hass, client_id, client_secret):
"""Register a ambiclimate implementation.
client_id: Client id.
client_secret: Client secret.
"""
hass.data.setdefault(DATA_AMBICLIMATE_IMPL, {})
hass.data[DATA_AMBICLIMATE_IMPL] = {
CONF_CLIENT_ID: client_id,
CONF_CLIENT_SECRET: client_secret,
}
@config_entries.HANDLERS.register('ambiclimate')
class AmbiclimateFlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize flow."""
self._registered_view = False
self._oauth = None
async def async_step_user(self, user_input=None):
"""Handle external yaml configuration."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='already_setup')
config = self.hass.data.get(DATA_AMBICLIMATE_IMPL, {})
if not config:
_LOGGER.debug("No config")
return self.async_abort(reason='no_config')
return await self.async_step_auth()
async def async_step_auth(self, user_input=None):
"""Handle a flow start."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='already_setup')
errors = {}
if user_input is not None:
errors['base'] = 'follow_link'
if not self._registered_view:
self._generate_view()
return self.async_show_form(
step_id='auth',
description_placeholders={'authorization_url':
await self._get_authorize_url(),
'cb_url': self._cb_url()},
errors=errors,
)
async def async_step_code(self, code=None):
"""Received code for authentication."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='already_setup')
token_info = await self._get_token_info(code)
if token_info is None:
return self.async_abort(reason='access_token')
config = self.hass.data[DATA_AMBICLIMATE_IMPL].copy()
config['callback_url'] = self._cb_url()
return self.async_create_entry(
title="Ambiclimate",
data=config,
)
async def _get_token_info(self, code):
oauth = self._generate_oauth()
try:
token_info = await oauth.get_access_token(code)
except ambiclimate.AmbiclimateOauthError:
_LOGGER.error("Failed to get access token", exc_info=True)
return None
store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
await store.async_save(token_info)
return token_info
def _generate_view(self):
self.hass.http.register_view(AmbiclimateAuthCallbackView())
self._registered_view = True
def _generate_oauth(self):
config = self.hass.data[DATA_AMBICLIMATE_IMPL]
clientsession = async_get_clientsession(self.hass)
callback_url = self._cb_url()
oauth = ambiclimate.AmbiclimateOAuth(config.get(CONF_CLIENT_ID),
config.get(CONF_CLIENT_SECRET),
callback_url,
clientsession)
return oauth
def _cb_url(self):
return '{}{}'.format(self.hass.config.api.base_url,
AUTH_CALLBACK_PATH)
async def _get_authorize_url(self):
oauth = self._generate_oauth()
return oauth.get_authorize_url()
class AmbiclimateAuthCallbackView(HomeAssistantView):
"""Ambiclimate Authorization Callback View."""
requires_auth = False
url = AUTH_CALLBACK_PATH
name = AUTH_CALLBACK_NAME
async def get(self, request):
"""Receive authorization token."""
code = request.query.get('code')
if code is None:
return "No code"
hass = request.app['hass']
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={'source': 'code'},
data=code,
))
return "OK!"

View File

@ -0,0 +1,14 @@
"""Constants used by the Ambiclimate component."""
ATTR_VALUE = 'value'
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
DOMAIN = 'ambiclimate'
SERVICE_COMFORT_FEEDBACK = 'send_comfort_feedback'
SERVICE_COMFORT_MODE = 'set_comfort_mode'
SERVICE_TEMPERATURE_MODE = 'set_temperature_mode'
STORAGE_KEY = 'ambiclimate_auth'
STORAGE_VERSION = 1
AUTH_CALLBACK_NAME = 'api:ambiclimate'
AUTH_CALLBACK_PATH = '/api/ambiclimate'

View File

@ -0,0 +1,12 @@
{
"domain": "ambiclimate",
"name": "Ambiclimate",
"documentation": "https://www.home-assistant.io/components/ambiclimate",
"requirements": [
"ambiclimate==0.1.1"
],
"dependencies": [],
"codeowners": [
"@danielhiversen"
]
}

View File

@ -0,0 +1,36 @@
# Describes the format for available services for ambiclimate
set_comfort_mode:
description: >
Enable comfort mode on your AC
fields:
Name:
description: >
String with device name.
example: Bedroom
send_comfort_feedback:
description: >
Send feedback for comfort mode
fields:
Name:
description: >
String with device name.
example: Bedroom
Value:
description: >
Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing
example: bit_warm
set_temperature_mode:
description: >
Enable temperature mode on your AC
fields:
Name:
description: >
String with device name.
example: Bedroom
Value:
description: >
Target value in celsius
example: 22

View File

@ -0,0 +1,23 @@
{
"config": {
"title": "Ambiclimate",
"step": {
"auth": {
"title": "Authenticate Ambiclimate",
"description": "Please follow this [link]({authorization_url}) and <b>Allow</b> access to your Ambiclimate account, then come back and press <b>Submit</b> below.\n(Make sure the specified callback url is {cb_url})"
}
},
"create_entry": {
"default": "Successfully authenticated with Ambiclimate"
},
"error": {
"no_token": "Not authenticated with Ambiclimate",
"follow_link": "Please follow the link and authenticate before pressing Submit"
},
"abort": {
"already_setup": "The Ambiclimate account is configured.",
"no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/).",
"access_token": "Unknown error generating an access token."
}
}
}

View File

@ -5,16 +5,30 @@ from datetime import timedelta
import aiohttp import aiohttp
import voluptuous as vol import voluptuous as vol
from homeassistant.auth.permissions.const import POLICY_CONTROL
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.camera import DOMAIN as CAMERA
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, ATTR_ENTITY_ID, CONF_AUTHENTICATION, CONF_BINARY_SENSORS, CONF_HOST,
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS,
HTTP_BASIC_AUTHENTICATION) CONF_SWITCHES, CONF_USERNAME, ENTITY_MATCH_ALL, HTTP_BASIC_AUTHENTICATION)
from homeassistant.exceptions import Unauthorized, UnknownUser
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.service import async_extract_entity_ids
from .binary_sensor import BINARY_SENSORS
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
from .const import DOMAIN, DATA_AMCREST
from .helpers import service_signal
from .sensor import SENSOR_MOTION_DETECTOR, SENSORS
from .switch import SWITCHES
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_AUTHENTICATION = 'authentication'
CONF_RESOLUTION = 'resolution' CONF_RESOLUTION = 'resolution'
CONF_STREAM_SOURCE = 'stream_source' CONF_STREAM_SOURCE = 'stream_source'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
@ -22,12 +36,7 @@ CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
DEFAULT_NAME = 'Amcrest Camera' DEFAULT_NAME = 'Amcrest Camera'
DEFAULT_PORT = 80 DEFAULT_PORT = 80
DEFAULT_RESOLUTION = 'high' DEFAULT_RESOLUTION = 'high'
DEFAULT_STREAM_SOURCE = 'snapshot'
DEFAULT_ARGUMENTS = '-pred 1' DEFAULT_ARGUMENTS = '-pred 1'
TIMEOUT = 10
DATA_AMCREST = 'amcrest'
DOMAIN = 'amcrest'
NOTIFICATION_ID = 'amcrest_notification' NOTIFICATION_ID = 'amcrest_notification'
NOTIFICATION_TITLE = 'Amcrest Camera Setup' NOTIFICATION_TITLE = 'Amcrest Camera Setup'
@ -43,48 +52,35 @@ AUTHENTICATION_LIST = {
'basic': 'basic' 'basic': 'basic'
} }
STREAM_SOURCE_LIST = {
'mjpeg': 0,
'snapshot': 1,
'rtsp': 2,
}
BINARY_SENSORS = { def _deprecated_sensor_values(sensors):
'motion_detected': 'Motion Detected' if SENSOR_MOTION_DETECTOR in sensors:
}
# Sensor types are defined like: Name, units, icon
SENSOR_MOTION_DETECTOR = 'motion_detector'
SENSORS = {
SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'],
'sdcard': ['SD Used', '%', 'mdi:sd'],
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
}
# Switch types are defined like: Name, icon
SWITCHES = {
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
'motion_recording': ['Motion Recording', 'mdi:record-rec']
}
def _deprecated_sensors(value):
if SENSOR_MOTION_DETECTOR in value:
_LOGGER.warning( _LOGGER.warning(
'sensors option %s is deprecated. ' "The 'sensors' option value '%s' is deprecated, "
'Please remove from your configuration and ' "please remove it from your configuration and use "
'use binary_sensors option motion_detected instead.', "the 'binary_sensors' option with value 'motion_detected' "
SENSOR_MOTION_DETECTOR) "instead.", SENSOR_MOTION_DETECTOR)
return value return sensors
def _has_unique_names(value): def _deprecated_switches(config):
names = [camera[CONF_NAME] for camera in value] if CONF_SWITCHES in config:
_LOGGER.warning(
"The 'switches' option (with value %s) is deprecated, "
"please remove it from your configuration and use "
"camera services and attributes instead.",
config[CONF_SWITCHES])
return config
def _has_unique_names(devices):
names = [device[CONF_NAME] for device in devices]
vol.Schema(vol.Unique())(names) vol.Schema(vol.Unique())(names)
return value return devices
AMCREST_SCHEMA = vol.Schema({ AMCREST_SCHEMA = vol.All(
vol.Schema({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
@ -94,7 +90,7 @@ AMCREST_SCHEMA = vol.Schema({
vol.All(vol.In(AUTHENTICATION_LIST)), vol.All(vol.In(AUTHENTICATION_LIST)),
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
vol.All(vol.In(RESOLUTION_LIST)), vol.All(vol.In(RESOLUTION_LIST)),
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE): vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]):
vol.All(vol.In(STREAM_SOURCE_LIST)), vol.All(vol.In(STREAM_SOURCE_LIST)),
vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS):
cv.string, cv.string,
@ -103,10 +99,13 @@ AMCREST_SCHEMA = vol.Schema({
vol.Optional(CONF_BINARY_SENSORS): vol.Optional(CONF_BINARY_SENSORS):
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]), vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
vol.Optional(CONF_SENSORS): vol.Optional(CONF_SENSORS):
vol.All(cv.ensure_list, [vol.In(SENSORS)], _deprecated_sensors), vol.All(cv.ensure_list, [vol.In(SENSORS)],
_deprecated_sensor_values),
vol.Optional(CONF_SWITCHES): vol.Optional(CONF_SWITCHES):
vol.All(cv.ensure_list, [vol.In(SWITCHES)]), vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
}) }),
_deprecated_switches
)
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names) DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names)
@ -117,21 +116,22 @@ def setup(hass, config):
"""Set up the Amcrest IP Camera component.""" """Set up the Amcrest IP Camera component."""
from amcrest import AmcrestCamera, AmcrestError from amcrest import AmcrestCamera, AmcrestError
hass.data.setdefault(DATA_AMCREST, {}) hass.data.setdefault(DATA_AMCREST, {'devices': {}, 'cameras': []})
amcrest_cams = config[DOMAIN] devices = config[DOMAIN]
for device in amcrest_cams: for device in devices:
name = device[CONF_NAME] name = device[CONF_NAME]
username = device[CONF_USERNAME] username = device[CONF_USERNAME]
password = device[CONF_PASSWORD] password = device[CONF_PASSWORD]
try: try:
camera = AmcrestCamera(device[CONF_HOST], api = AmcrestCamera(device[CONF_HOST],
device[CONF_PORT], device[CONF_PORT],
username, username,
password).camera password).camera
# pylint: disable=pointless-statement # pylint: disable=pointless-statement
camera.current_time # Test camera communications.
api.current_time
except AmcrestError as ex: except AmcrestError as ex:
_LOGGER.error("Unable to connect to %s camera: %s", name, str(ex)) _LOGGER.error("Unable to connect to %s camera: %s", name, str(ex))
@ -148,7 +148,7 @@ def setup(hass, config):
binary_sensors = device.get(CONF_BINARY_SENSORS) binary_sensors = device.get(CONF_BINARY_SENSORS)
sensors = device.get(CONF_SENSORS) sensors = device.get(CONF_SENSORS)
switches = device.get(CONF_SWITCHES) switches = device.get(CONF_SWITCHES)
stream_source = STREAM_SOURCE_LIST[device[CONF_STREAM_SOURCE]] stream_source = device[CONF_STREAM_SOURCE]
# currently aiohttp only works with basic authentication # currently aiohttp only works with basic authentication
# only valid for mjpeg streaming # only valid for mjpeg streaming
@ -157,47 +157,97 @@ def setup(hass, config):
else: else:
authentication = None authentication = None
hass.data[DATA_AMCREST][name] = AmcrestDevice( hass.data[DATA_AMCREST]['devices'][name] = AmcrestDevice(
camera, name, authentication, ffmpeg_arguments, stream_source, api, authentication, ffmpeg_arguments, stream_source,
resolution) resolution)
discovery.load_platform( discovery.load_platform(
hass, 'camera', DOMAIN, { hass, CAMERA, DOMAIN, {
CONF_NAME: name, CONF_NAME: name,
}, config) }, config)
if binary_sensors: if binary_sensors:
discovery.load_platform( discovery.load_platform(
hass, 'binary_sensor', DOMAIN, { hass, BINARY_SENSOR, DOMAIN, {
CONF_NAME: name, CONF_NAME: name,
CONF_BINARY_SENSORS: binary_sensors CONF_BINARY_SENSORS: binary_sensors
}, config) }, config)
if sensors: if sensors:
discovery.load_platform( discovery.load_platform(
hass, 'sensor', DOMAIN, { hass, SENSOR, DOMAIN, {
CONF_NAME: name, CONF_NAME: name,
CONF_SENSORS: sensors, CONF_SENSORS: sensors,
}, config) }, config)
if switches: if switches:
discovery.load_platform( discovery.load_platform(
hass, 'switch', DOMAIN, { hass, SWITCH, DOMAIN, {
CONF_NAME: name, CONF_NAME: name,
CONF_SWITCHES: switches CONF_SWITCHES: switches
}, config) }, config)
return len(hass.data[DATA_AMCREST]) >= 1 if not hass.data[DATA_AMCREST]['devices']:
return False
def have_permission(user, entity_id):
return not user or user.permissions.check_entity(
entity_id, POLICY_CONTROL)
async def async_extract_from_service(call):
if call.context.user_id:
user = await hass.auth.async_get_user(call.context.user_id)
if user is None:
raise UnknownUser(context=call.context)
else:
user = None
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
# Return all entity_ids user has permission to control.
return [
entity_id for entity_id in hass.data[DATA_AMCREST]['cameras']
if have_permission(user, entity_id)
]
call_ids = await async_extract_entity_ids(hass, call)
entity_ids = []
for entity_id in hass.data[DATA_AMCREST]['cameras']:
if entity_id not in call_ids:
continue
if not have_permission(user, entity_id):
raise Unauthorized(
context=call.context,
entity_id=entity_id,
permission=POLICY_CONTROL
)
entity_ids.append(entity_id)
return entity_ids
async def async_service_handler(call):
args = []
for arg in CAMERA_SERVICES[call.service][2]:
args.append(call.data[arg])
for entity_id in await async_extract_from_service(call):
async_dispatcher_send(
hass,
service_signal(call.service, entity_id),
*args
)
for service, params in CAMERA_SERVICES.items():
hass.services.async_register(
DOMAIN, service, async_service_handler, params[0])
return True
class AmcrestDevice: class AmcrestDevice:
"""Representation of a base Amcrest discovery device.""" """Representation of a base Amcrest discovery device."""
def __init__(self, camera, name, authentication, ffmpeg_arguments, def __init__(self, api, authentication, ffmpeg_arguments,
stream_source, resolution): stream_source, resolution):
"""Initialize the entity.""" """Initialize the entity."""
self.device = camera self.api = api
self.name = name
self.authentication = authentication self.authentication = authentication
self.ffmpeg_arguments = ffmpeg_arguments self.ffmpeg_arguments = ffmpeg_arguments
self.stream_source = stream_source self.stream_source = stream_source

View File

@ -5,38 +5,39 @@ import logging
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, DEVICE_CLASS_MOTION) BinarySensorDevice, DEVICE_CLASS_MOTION)
from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS
from . import DATA_AMCREST, BINARY_SENSORS
from .const import BINARY_SENSOR_SCAN_INTERVAL_SECS, DATA_AMCREST
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5) SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS)
BINARY_SENSORS = {
'motion_detected': 'Motion Detected'
}
async def async_setup_platform(hass, config, async_add_devices, async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None): discovery_info=None):
"""Set up a binary sensor for an Amcrest IP Camera.""" """Set up a binary sensor for an Amcrest IP Camera."""
if discovery_info is None: if discovery_info is None:
return return
device_name = discovery_info[CONF_NAME] name = discovery_info[CONF_NAME]
binary_sensors = discovery_info[CONF_BINARY_SENSORS] device = hass.data[DATA_AMCREST]['devices'][name]
amcrest = hass.data[DATA_AMCREST][device_name] async_add_entities(
[AmcrestBinarySensor(name, device, sensor_type)
amcrest_binary_sensors = [] for sensor_type in discovery_info[CONF_BINARY_SENSORS]],
for sensor_type in binary_sensors: True)
amcrest_binary_sensors.append(
AmcrestBinarySensor(amcrest.name, amcrest.device, sensor_type))
async_add_devices(amcrest_binary_sensors, True)
class AmcrestBinarySensor(BinarySensorDevice): class AmcrestBinarySensor(BinarySensorDevice):
"""Binary sensor for Amcrest camera.""" """Binary sensor for Amcrest camera."""
def __init__(self, name, camera, sensor_type): def __init__(self, name, device, sensor_type):
"""Initialize entity.""" """Initialize entity."""
self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type]) self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type])
self._camera = camera self._api = device.api
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._state = None self._state = None
@ -62,7 +63,7 @@ class AmcrestBinarySensor(BinarySensorDevice):
_LOGGER.debug('Pulling data from %s binary sensor', self._name) _LOGGER.debug('Pulling data from %s binary sensor', self._name)
try: try:
self._state = self._camera.is_motion_detected self._state = self._api.is_motion_detected
except AmcrestError as error: except AmcrestError as error:
_LOGGER.error( _LOGGER.error(
'Could not update %s binary sensor due to error: %s', 'Could not update %s binary sensor due to error: %s',

View File

@ -2,18 +2,72 @@
import asyncio import asyncio
import logging import logging
import voluptuous as vol
from homeassistant.components.camera import ( from homeassistant.components.camera import (
Camera, SUPPORT_ON_OFF, SUPPORT_STREAM) Camera, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, SUPPORT_STREAM)
from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import CONF_NAME from homeassistant.const import (
CONF_NAME, STATE_ON, STATE_OFF)
from homeassistant.helpers.aiohttp_client import ( from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_stream, async_aiohttp_proxy_web, async_aiohttp_proxy_stream, async_aiohttp_proxy_web,
async_get_clientsession) async_get_clientsession)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT from .const import CAMERA_WEB_SESSION_TIMEOUT, DATA_AMCREST
from .helpers import service_signal
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
STREAM_SOURCE_LIST = [
'snapshot',
'mjpeg',
'rtsp',
]
_SRV_EN_REC = 'enable_recording'
_SRV_DS_REC = 'disable_recording'
_SRV_EN_AUD = 'enable_audio'
_SRV_DS_AUD = 'disable_audio'
_SRV_EN_MOT_REC = 'enable_motion_recording'
_SRV_DS_MOT_REC = 'disable_motion_recording'
_SRV_GOTO = 'goto_preset'
_SRV_CBW = 'set_color_bw'
_SRV_TOUR_ON = 'start_tour'
_SRV_TOUR_OFF = 'stop_tour'
_ATTR_PRESET = 'preset'
_ATTR_COLOR_BW = 'color_bw'
_CBW_COLOR = 'color'
_CBW_AUTO = 'auto'
_CBW_BW = 'bw'
_CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW]
_SRV_GOTO_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1)),
})
_SRV_CBW_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(_ATTR_COLOR_BW): vol.In(_CBW),
})
CAMERA_SERVICES = {
_SRV_EN_REC: (CAMERA_SERVICE_SCHEMA, 'async_enable_recording', ()),
_SRV_DS_REC: (CAMERA_SERVICE_SCHEMA, 'async_disable_recording', ()),
_SRV_EN_AUD: (CAMERA_SERVICE_SCHEMA, 'async_enable_audio', ()),
_SRV_DS_AUD: (CAMERA_SERVICE_SCHEMA, 'async_disable_audio', ()),
_SRV_EN_MOT_REC: (
CAMERA_SERVICE_SCHEMA, 'async_enable_motion_recording', ()),
_SRV_DS_MOT_REC: (
CAMERA_SERVICE_SCHEMA, 'async_disable_motion_recording', ()),
_SRV_GOTO: (_SRV_GOTO_SCHEMA, 'async_goto_preset', (_ATTR_PRESET,)),
_SRV_CBW: (_SRV_CBW_SCHEMA, 'async_set_color_bw', (_ATTR_COLOR_BW,)),
_SRV_TOUR_ON: (CAMERA_SERVICE_SCHEMA, 'async_start_tour', ()),
_SRV_TOUR_OFF: (CAMERA_SERVICE_SCHEMA, 'async_stop_tour', ()),
}
_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF}
async def async_setup_platform(hass, config, async_add_entities, async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None): discovery_info=None):
@ -21,28 +75,33 @@ async def async_setup_platform(hass, config, async_add_entities,
if discovery_info is None: if discovery_info is None:
return return
device_name = discovery_info[CONF_NAME] name = discovery_info[CONF_NAME]
amcrest = hass.data[DATA_AMCREST][device_name] device = hass.data[DATA_AMCREST]['devices'][name]
async_add_entities([
async_add_entities([AmcrestCam(hass, amcrest)], True) AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True)
class AmcrestCam(Camera): class AmcrestCam(Camera):
"""An implementation of an Amcrest IP camera.""" """An implementation of an Amcrest IP camera."""
def __init__(self, hass, amcrest): def __init__(self, name, device, ffmpeg):
"""Initialize an Amcrest camera.""" """Initialize an Amcrest camera."""
super(AmcrestCam, self).__init__() super().__init__()
self._name = amcrest.name self._name = name
self._camera = amcrest.device self._api = device.api
self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg = ffmpeg
self._ffmpeg_arguments = amcrest.ffmpeg_arguments self._ffmpeg_arguments = device.ffmpeg_arguments
self._stream_source = amcrest.stream_source self._stream_source = device.stream_source
self._resolution = amcrest.resolution self._resolution = device.resolution
self._token = self._auth = amcrest.authentication self._token = self._auth = device.authentication
self._is_recording = False self._is_recording = False
self._motion_detection_enabled = None
self._model = None self._model = None
self._audio_enabled = None
self._motion_recording_enabled = None
self._color_bw = None
self._snapshot_lock = asyncio.Lock() self._snapshot_lock = asyncio.Lock()
self._unsub_dispatcher = []
async def async_camera_image(self): async def async_camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
@ -56,7 +115,7 @@ class AmcrestCam(Camera):
try: try:
# Send the request to snap a picture and return raw jpg data # Send the request to snap a picture and return raw jpg data
response = await self.hass.async_add_executor_job( response = await self.hass.async_add_executor_job(
self._camera.snapshot, self._resolution) self._api.snapshot, self._resolution)
return response.data return response.data
except AmcrestError as error: except AmcrestError as error:
_LOGGER.error( _LOGGER.error(
@ -67,15 +126,16 @@ class AmcrestCam(Camera):
async def handle_async_mjpeg_stream(self, request): async def handle_async_mjpeg_stream(self, request):
"""Return an MJPEG stream.""" """Return an MJPEG stream."""
# The snapshot implementation is handled by the parent class # The snapshot implementation is handled by the parent class
if self._stream_source == STREAM_SOURCE_LIST['snapshot']: if self._stream_source == 'snapshot':
return await super().handle_async_mjpeg_stream(request) return await super().handle_async_mjpeg_stream(request)
if self._stream_source == STREAM_SOURCE_LIST['mjpeg']: if self._stream_source == 'mjpeg':
# stream an MJPEG image stream directly from the camera # stream an MJPEG image stream directly from the camera
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
streaming_url = self._camera.mjpeg_url(typeno=self._resolution) streaming_url = self._api.mjpeg_url(typeno=self._resolution)
stream_coro = websession.get( stream_coro = websession.get(
streaming_url, auth=self._token, timeout=TIMEOUT) streaming_url, auth=self._token,
timeout=CAMERA_WEB_SESSION_TIMEOUT)
return await async_aiohttp_proxy_web( return await async_aiohttp_proxy_web(
self.hass, request, stream_coro) self.hass, request, stream_coro)
@ -83,7 +143,7 @@ class AmcrestCam(Camera):
# streaming via ffmpeg # streaming via ffmpeg
from haffmpeg.camera import CameraMjpeg from haffmpeg.camera import CameraMjpeg
streaming_url = self._camera.rtsp_url(typeno=self._resolution) streaming_url = self._api.rtsp_url(typeno=self._resolution)
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
await stream.open_camera( await stream.open_camera(
streaming_url, extra_cmd=self._ffmpeg_arguments) streaming_url, extra_cmd=self._ffmpeg_arguments)
@ -103,6 +163,19 @@ class AmcrestCam(Camera):
"""Return the name of this camera.""" """Return the name of this camera."""
return self._name return self._name
@property
def device_state_attributes(self):
"""Return the Amcrest-specific camera state attributes."""
attr = {}
if self._audio_enabled is not None:
attr['audio'] = _BOOL_TO_STATE.get(self._audio_enabled)
if self._motion_recording_enabled is not None:
attr['motion_recording'] = _BOOL_TO_STATE.get(
self._motion_recording_enabled)
if self._color_bw is not None:
attr[_ATTR_COLOR_BW] = self._color_bw
return attr
@property @property
def supported_features(self): def supported_features(self):
"""Return supported features.""" """Return supported features."""
@ -120,6 +193,11 @@ class AmcrestCam(Camera):
"""Return the camera brand.""" """Return the camera brand."""
return 'Amcrest' return 'Amcrest'
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
return self._motion_detection_enabled
@property @property
def model(self): def model(self):
"""Return the camera model.""" """Return the camera model."""
@ -128,7 +206,7 @@ class AmcrestCam(Camera):
@property @property
def stream_source(self): def stream_source(self):
"""Return the source of the stream.""" """Return the source of the stream."""
return self._camera.rtsp_url(typeno=self._resolution) return self._api.rtsp_url(typeno=self._resolution)
@property @property
def is_on(self): def is_on(self):
@ -137,6 +215,21 @@ class AmcrestCam(Camera):
# Other Entity method overrides # Other Entity method overrides
async def async_added_to_hass(self):
"""Subscribe to signals and add camera to list."""
for service, params in CAMERA_SERVICES.items():
self._unsub_dispatcher.append(async_dispatcher_connect(
self.hass,
service_signal(service, self.entity_id),
getattr(self, params[1])))
self.hass.data[DATA_AMCREST]['cameras'].append(self.entity_id)
async def async_will_remove_from_hass(self):
"""Remove camera from list and disconnect from signals."""
self.hass.data[DATA_AMCREST]['cameras'].remove(self.entity_id)
for unsub_dispatcher in self._unsub_dispatcher:
unsub_dispatcher()
def update(self): def update(self):
"""Update entity status.""" """Update entity status."""
from amcrest import AmcrestError from amcrest import AmcrestError
@ -144,15 +237,21 @@ class AmcrestCam(Camera):
_LOGGER.debug('Pulling data from %s camera', self.name) _LOGGER.debug('Pulling data from %s camera', self.name)
if self._model is None: if self._model is None:
try: try:
self._model = self._camera.device_type.split('=')[-1].strip() self._model = self._api.device_type.split('=')[-1].strip()
except AmcrestError as error: except AmcrestError as error:
_LOGGER.error( _LOGGER.error(
'Could not get %s camera model due to error: %s', 'Could not get %s camera model due to error: %s',
self.name, error) self.name, error)
self._model = '' self._model = ''
try: try:
self.is_streaming = self._camera.video_enabled self.is_streaming = self._api.video_enabled
self._is_recording = self._camera.record_mode == 'Manual' self._is_recording = self._api.record_mode == 'Manual'
self._motion_detection_enabled = (
self._api.is_motion_detector_on())
self._audio_enabled = self._api.audio_enabled
self._motion_recording_enabled = (
self._api.is_record_on_motion_detection())
self._color_bw = _CBW[self._api.day_night_color]
except AmcrestError as error: except AmcrestError as error:
_LOGGER.error( _LOGGER.error(
'Could not get %s camera attributes due to error: %s', 'Could not get %s camera attributes due to error: %s',
@ -168,14 +267,71 @@ class AmcrestCam(Camera):
"""Turn on camera.""" """Turn on camera."""
self._enable_video_stream(True) self._enable_video_stream(True)
# Utility methods def enable_motion_detection(self):
"""Enable motion detection in the camera."""
self._enable_motion_detection(True)
def disable_motion_detection(self):
"""Disable motion detection in camera."""
self._enable_motion_detection(False)
# Additional Amcrest Camera service methods
async def async_enable_recording(self):
"""Call the job and enable recording."""
await self.hass.async_add_executor_job(self._enable_recording, True)
async def async_disable_recording(self):
"""Call the job and disable recording."""
await self.hass.async_add_executor_job(self._enable_recording, False)
async def async_enable_audio(self):
"""Call the job and enable audio."""
await self.hass.async_add_executor_job(self._enable_audio, True)
async def async_disable_audio(self):
"""Call the job and disable audio."""
await self.hass.async_add_executor_job(self._enable_audio, False)
async def async_enable_motion_recording(self):
"""Call the job and enable motion recording."""
await self.hass.async_add_executor_job(self._enable_motion_recording,
True)
async def async_disable_motion_recording(self):
"""Call the job and disable motion recording."""
await self.hass.async_add_executor_job(self._enable_motion_recording,
False)
async def async_goto_preset(self, preset):
"""Call the job and move camera to preset position."""
await self.hass.async_add_executor_job(self._goto_preset, preset)
async def async_set_color_bw(self, color_bw):
"""Call the job and set camera color mode."""
await self.hass.async_add_executor_job(self._set_color_bw, color_bw)
async def async_start_tour(self):
"""Call the job and start camera tour."""
await self.hass.async_add_executor_job(self._start_tour, True)
async def async_stop_tour(self):
"""Call the job and stop camera tour."""
await self.hass.async_add_executor_job(self._start_tour, False)
# Methods to send commands to Amcrest camera and handle errors
def _enable_video_stream(self, enable): def _enable_video_stream(self, enable):
"""Enable or disable camera video stream.""" """Enable or disable camera video stream."""
from amcrest import AmcrestError from amcrest import AmcrestError
# Given the way the camera's state is determined by
# is_streaming and is_recording, we can't leave
# recording on if video stream is being turned off.
if self.is_recording and not enable:
self._enable_recording(False)
try: try:
self._camera.video_enabled = enable self._api.video_enabled = enable
except AmcrestError as error: except AmcrestError as error:
_LOGGER.error( _LOGGER.error(
'Could not %s %s camera video stream due to error: %s', 'Could not %s %s camera video stream due to error: %s',
@ -183,3 +339,103 @@ class AmcrestCam(Camera):
else: else:
self.is_streaming = enable self.is_streaming = enable
self.schedule_update_ha_state() self.schedule_update_ha_state()
def _enable_recording(self, enable):
"""Turn recording on or off."""
from amcrest import AmcrestError
# Given the way the camera's state is determined by
# is_streaming and is_recording, we can't leave
# video stream off if recording is being turned on.
if not self.is_streaming and enable:
self._enable_video_stream(True)
rec_mode = {'Automatic': 0, 'Manual': 1}
try:
self._api.record_mode = rec_mode[
'Manual' if enable else 'Automatic']
except AmcrestError as error:
_LOGGER.error(
'Could not %s %s camera recording due to error: %s',
'enable' if enable else 'disable', self.name, error)
else:
self._is_recording = enable
self.schedule_update_ha_state()
def _enable_motion_detection(self, enable):
"""Enable or disable motion detection."""
from amcrest import AmcrestError
try:
self._api.motion_detection = str(enable).lower()
except AmcrestError as error:
_LOGGER.error(
'Could not %s %s camera motion detection due to error: %s',
'enable' if enable else 'disable', self.name, error)
else:
self._motion_detection_enabled = enable
self.schedule_update_ha_state()
def _enable_audio(self, enable):
"""Enable or disable audio stream."""
from amcrest import AmcrestError
try:
self._api.audio_enabled = enable
except AmcrestError as error:
_LOGGER.error(
'Could not %s %s camera audio stream due to error: %s',
'enable' if enable else 'disable', self.name, error)
else:
self._audio_enabled = enable
self.schedule_update_ha_state()
def _enable_motion_recording(self, enable):
"""Enable or disable motion recording."""
from amcrest import AmcrestError
try:
self._api.motion_recording = str(enable).lower()
except AmcrestError as error:
_LOGGER.error(
'Could not %s %s camera motion recording due to error: %s',
'enable' if enable else 'disable', self.name, error)
else:
self._motion_recording_enabled = enable
self.schedule_update_ha_state()
def _goto_preset(self, preset):
"""Move camera position and zoom to preset."""
from amcrest import AmcrestError
try:
self._api.go_to_preset(
action='start', preset_point_number=preset)
except AmcrestError as error:
_LOGGER.error(
'Could not move %s camera to preset %i due to error: %s',
self.name, preset, error)
def _set_color_bw(self, cbw):
"""Set camera color mode."""
from amcrest import AmcrestError
try:
self._api.day_night_color = _CBW.index(cbw)
except AmcrestError as error:
_LOGGER.error(
'Could not set %s camera color mode to %s due to error: %s',
self.name, cbw, error)
else:
self._color_bw = cbw
self.schedule_update_ha_state()
def _start_tour(self, start):
"""Start camera tour."""
from amcrest import AmcrestError
try:
self._api.tour(start=start)
except AmcrestError as error:
_LOGGER.error(
'Could not %s %s camera tour due to error: %s',
'start' if start else 'stop', self.name, error)

View File

@ -0,0 +1,7 @@
"""Constants for amcrest component."""
DOMAIN = 'amcrest'
DATA_AMCREST = DOMAIN
BINARY_SENSOR_SCAN_INTERVAL_SECS = 5
CAMERA_WEB_SESSION_TIMEOUT = 10
SENSOR_SCAN_INTERVAL_SECS = 10

View File

@ -0,0 +1,10 @@
"""Helpers for amcrest component."""
from .const import DOMAIN
def service_signal(service, entity_id=None):
"""Encode service and entity_id into signal."""
signal = '{}_{}'.format(DOMAIN, service)
if entity_id:
signal += '_{}'.format(entity_id.replace('.', '_'))
return signal

View File

@ -3,7 +3,7 @@
"name": "Amcrest", "name": "Amcrest",
"documentation": "https://www.home-assistant.io/components/amcrest", "documentation": "https://www.home-assistant.io/components/amcrest",
"requirements": [ "requirements": [
"amcrest==1.3.0" "amcrest==1.4.0"
], ],
"dependencies": [ "dependencies": [
"ffmpeg" "ffmpeg"

View File

@ -5,11 +5,19 @@ import logging
from homeassistant.const import CONF_NAME, CONF_SENSORS from homeassistant.const import CONF_NAME, CONF_SENSORS
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from . import DATA_AMCREST, SENSORS from .const import DATA_AMCREST, SENSOR_SCAN_INTERVAL_SECS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS)
# Sensor types are defined like: Name, units, icon
SENSOR_MOTION_DETECTOR = 'motion_detector'
SENSORS = {
SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'],
'sdcard': ['SD Used', '%', 'mdi:sd'],
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
}
async def async_setup_platform( async def async_setup_platform(
@ -18,30 +26,26 @@ async def async_setup_platform(
if discovery_info is None: if discovery_info is None:
return return
device_name = discovery_info[CONF_NAME] name = discovery_info[CONF_NAME]
sensors = discovery_info[CONF_SENSORS] device = hass.data[DATA_AMCREST]['devices'][name]
amcrest = hass.data[DATA_AMCREST][device_name] async_add_entities(
[AmcrestSensor(name, device, sensor_type)
amcrest_sensors = [] for sensor_type in discovery_info[CONF_SENSORS]],
for sensor_type in sensors: True)
amcrest_sensors.append(
AmcrestSensor(amcrest.name, amcrest.device, sensor_type))
async_add_entities(amcrest_sensors, True)
class AmcrestSensor(Entity): class AmcrestSensor(Entity):
"""A sensor implementation for Amcrest IP camera.""" """A sensor implementation for Amcrest IP camera."""
def __init__(self, name, camera, sensor_type): def __init__(self, name, device, sensor_type):
"""Initialize a sensor for Amcrest camera.""" """Initialize a sensor for Amcrest camera."""
self._attrs = {} self._name = '{} {}'.format(name, SENSORS[sensor_type][0])
self._camera = camera self._api = device.api
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._name = '{0}_{1}'.format(
name, SENSORS.get(self._sensor_type)[0])
self._icon = 'mdi:{}'.format(SENSORS.get(self._sensor_type)[2])
self._state = None self._state = None
self._attrs = {}
self._unit_of_measurement = SENSORS[sensor_type][1]
self._icon = SENSORS[sensor_type][2]
@property @property
def name(self): def name(self):
@ -66,22 +70,30 @@ class AmcrestSensor(Entity):
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the units of measurement.""" """Return the units of measurement."""
return SENSORS.get(self._sensor_type)[1] return self._unit_of_measurement
def update(self): def update(self):
"""Get the latest data and updates the state.""" """Get the latest data and updates the state."""
_LOGGER.debug("Pulling data from %s sensor.", self._name) _LOGGER.debug("Pulling data from %s sensor.", self._name)
if self._sensor_type == 'motion_detector': if self._sensor_type == 'motion_detector':
self._state = self._camera.is_motion_detected self._state = self._api.is_motion_detected
self._attrs['Record Mode'] = self._camera.record_mode self._attrs['Record Mode'] = self._api.record_mode
elif self._sensor_type == 'ptz_preset': elif self._sensor_type == 'ptz_preset':
self._state = self._camera.ptz_presets_count self._state = self._api.ptz_presets_count
elif self._sensor_type == 'sdcard': elif self._sensor_type == 'sdcard':
sd_used = self._camera.storage_used storage = self._api.storage_all
sd_total = self._camera.storage_total try:
self._attrs['Total'] = '{0} {1}'.format(*sd_total) self._attrs['Total'] = '{:.2f} {}'.format(*storage['total'])
self._attrs['Used'] = '{0} {1}'.format(*sd_used) except ValueError:
self._state = self._camera.storage_used_percent self._attrs['Total'] = '{} {}'.format(*storage['total'])
try:
self._attrs['Used'] = '{:.2f} {}'.format(*storage['used'])
except ValueError:
self._attrs['Used'] = '{} {}'.format(*storage['used'])
try:
self._state = '{:.2f}'.format(storage['used_percent'])
except ValueError:
self._state = storage['used_percent']

View File

@ -0,0 +1,75 @@
enable_recording:
description: Enable continuous recording to camera storage.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
disable_recording:
description: Disable continuous recording to camera storage.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
enable_audio:
description: Enable audio stream.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
disable_audio:
description: Disable audio stream.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
enable_motion_recording:
description: Enable recording a clip to camera storage when motion is detected.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
disable_motion_recording:
description: Disable recording a clip to camera storage when motion is detected.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
goto_preset:
description: Move camera to PTZ preset.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
preset:
description: Preset number, starting from 1.
example: 1
set_color_bw:
description: Set camera color mode.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
color_bw:
description: Color mode, one of 'auto', 'color' or 'bw'.
example: auto
start_tour:
description: Start camera's PTZ tour function.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'
stop_tour:
description: Stop camera's PTZ tour function.
fields:
entity_id:
description: "Name(s) of the cameras, or 'all' for all cameras."
example: 'camera.house_front'

View File

@ -1,13 +1,19 @@
"""Support for toggling Amcrest IP camera settings.""" """Support for toggling Amcrest IP camera settings."""
import logging import logging
from homeassistant.const import CONF_NAME, CONF_SWITCHES, STATE_OFF, STATE_ON from homeassistant.const import CONF_NAME, CONF_SWITCHES
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from . import DATA_AMCREST, SWITCHES from .const import DATA_AMCREST
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Switch types are defined like: Name, icon
SWITCHES = {
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
'motion_recording': ['Motion Recording', 'mdi:record-rec']
}
async def async_setup_platform( async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None): hass, config, async_add_entities, discovery_info=None):
@ -16,67 +22,58 @@ async def async_setup_platform(
return return
name = discovery_info[CONF_NAME] name = discovery_info[CONF_NAME]
switches = discovery_info[CONF_SWITCHES] device = hass.data[DATA_AMCREST]['devices'][name]
camera = hass.data[DATA_AMCREST][name].device async_add_entities(
[AmcrestSwitch(name, device, setting)
all_switches = [] for setting in discovery_info[CONF_SWITCHES]],
True)
for setting in switches:
all_switches.append(AmcrestSwitch(setting, camera, name))
async_add_entities(all_switches, True)
class AmcrestSwitch(ToggleEntity): class AmcrestSwitch(ToggleEntity):
"""Representation of an Amcrest IP camera switch.""" """Representation of an Amcrest IP camera switch."""
def __init__(self, setting, camera, name): def __init__(self, name, device, setting):
"""Initialize the Amcrest switch.""" """Initialize the Amcrest switch."""
self._name = '{} {}'.format(name, SWITCHES[setting][0])
self._api = device.api
self._setting = setting self._setting = setting
self._camera = camera self._state = False
self._name = '{} {}'.format(SWITCHES[setting][0], name)
self._icon = SWITCHES[setting][1] self._icon = SWITCHES[setting][1]
self._state = None
@property @property
def name(self): def name(self):
"""Return the name of the switch if any.""" """Return the name of the switch if any."""
return self._name return self._name
@property
def state(self):
"""Return the state of the switch."""
return self._state
@property @property
def is_on(self): def is_on(self):
"""Return true if switch is on.""" """Return true if switch is on."""
return self._state == STATE_ON return self._state
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn setting on.""" """Turn setting on."""
if self._setting == 'motion_detection': if self._setting == 'motion_detection':
self._camera.motion_detection = 'true' self._api.motion_detection = 'true'
elif self._setting == 'motion_recording': elif self._setting == 'motion_recording':
self._camera.motion_recording = 'true' self._api.motion_recording = 'true'
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn setting off.""" """Turn setting off."""
if self._setting == 'motion_detection': if self._setting == 'motion_detection':
self._camera.motion_detection = 'false' self._api.motion_detection = 'false'
elif self._setting == 'motion_recording': elif self._setting == 'motion_recording':
self._camera.motion_recording = 'false' self._api.motion_recording = 'false'
def update(self): def update(self):
"""Update setting state.""" """Update setting state."""
_LOGGER.debug("Polling state for setting: %s ", self._name) _LOGGER.debug("Polling state for setting: %s ", self._name)
if self._setting == 'motion_detection': if self._setting == 'motion_detection':
detection = self._camera.is_motion_detector_on() detection = self._api.is_motion_detector_on()
elif self._setting == 'motion_recording': elif self._setting == 'motion_recording':
detection = self._camera.is_record_on_motion_detection() detection = self._api.is_record_on_motion_detection()
self._state = STATE_ON if detection else STATE_OFF self._state = detection
@property @property
def icon(self): def icon(self):

View File

@ -21,11 +21,11 @@
}, },
"totp": { "totp": {
"error": { "error": {
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor int\u00e9ntalo de nuevo. Si recibes este error de forma consistente, por favor aseg\u00farate de que el reloj de tu Home Assistant es correcto." "invalid_code": "C\u00f3digo inv\u00e1lido, int\u00e9ntalo de nuevo. Si recibes este error de forma consistente, aseg\u00farate de que el reloj de tu sistema Home Assistant es correcto."
}, },
"step": { "step": {
"init": { "init": {
"description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanea el c\u00f3digo QR con tu aplicaci\u00f3n de autenticaci\u00f3n. Si no tienes una, te recomendamos [Autenticador de Google] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \nDespu\u00e9s de escanear el c\u00f3digo, introduce el c\u00f3digo de seis d\u00edgitos de tu aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tienes problemas para escanear el c\u00f3digo QR, realiza una configuraci\u00f3n manual con el c\u00f3digo ** ` {code} ` **.", "description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanea el c\u00f3digo QR con tu aplicaci\u00f3n de autenticaci\u00f3n. Si no tienes una, te recomendamos el [Autenticador de Google](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code} \n \nDespu\u00e9s de escanear el c\u00f3digo, introduce el c\u00f3digo de seis d\u00edgitos de tu aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tienes problemas para escanear el c\u00f3digo QR, realiza una configuraci\u00f3n manual con el c\u00f3digo **`{code}`**.",
"title": "Configure la autenticaci\u00f3n de dos factores utilizando TOTP" "title": "Configure la autenticaci\u00f3n de dos factores utilizando TOTP"
} }
}, },

View File

@ -97,8 +97,7 @@ class AuthProvidersView(HomeAssistantView):
async def get(self, request): async def get(self, request):
"""Get available auth providers.""" """Get available auth providers."""
hass = request.app['hass'] hass = request.app['hass']
if not hass.components.onboarding.async_is_user_onboarded():
if not hass.components.onboarding.async_is_onboarded():
return self.json_message( return self.json_message(
message='Onboarding not finished', message='Onboarding not finished',
status_code=400, status_code=400,

View File

@ -31,6 +31,6 @@ async def async_trigger(hass, config, action, automation_info):
'from_state': from_s, 'from_state': from_s,
'to_state': to_s, 'to_state': to_s,
}, },
}, context=to_s.context)) }, context=(to_s.context if to_s else None)))
return async_track_template(hass, value_template, template_listener) return async_track_template(hass, value_template, template_listener)

View File

@ -1,7 +1,7 @@
"""Constants for the Axis component.""" """Constants for the Axis component."""
import logging import logging
LOGGER = logging.getLogger('homeassistant.components.axis') LOGGER = logging.getLogger(__package__)
DOMAIN = 'axis' DOMAIN = 'axis'

View File

@ -0,0 +1 @@
"""The Bizkaibus bus tracker component."""

View File

@ -0,0 +1,8 @@
{
"domain": "bizkaibus",
"name": "Bizkaibus",
"documentation": "https://www.home-assistant.io/components/bizkaibus",
"dependencies": [],
"codeowners": ["@UgaitzEtxebarria"],
"requirements": ["bizkaibus==0.1.1"]
}

View File

@ -0,0 +1,88 @@
"""Support for Bizkaibus, Biscay (Basque Country, Spain) Bus service."""
import logging
import voluptuous as vol
from bizkaibus.bizkaibus import BizkaibusData
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
ATTR_DUE_IN = 'Due in'
CONF_STOP_ID = 'stopid'
CONF_ROUTE = 'route'
DEFAULT_NAME = 'Next bus'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_STOP_ID): cv.string,
vol.Required(CONF_ROUTE): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Bizkaibus public transport sensor."""
name = config.get(CONF_NAME)
stop = config[CONF_STOP_ID]
route = config[CONF_ROUTE]
data = Bizkaibus(stop, route)
add_entities([BizkaibusSensor(data, stop, route, name)], True)
class BizkaibusSensor(Entity):
"""The class for handling the data."""
def __init__(self, data, stop, route, name):
"""Initialize the sensor."""
self.data = data
self.stop = stop
self.route = route
self._name = name
self._state = None
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement of the sensor."""
return 'minutes'
def update(self):
"""Get the latest data from the webservice."""
self.data.update()
try:
self._state = self.data.info[0][ATTR_DUE_IN]
except TypeError:
pass
class Bizkaibus:
"""The class for handling the data retrieval."""
def __init__(self, stop, route):
"""Initialize the data object."""
self.stop = stop
self.route = route
self.info = None
def update(self):
"""Retrieve the information from API."""
bridge = BizkaibusData(self.stop, self.route)
bridge.getNextBus()
self.info = bridge.info

View File

@ -3,7 +3,7 @@
"name": "Bluesound", "name": "Bluesound",
"documentation": "https://www.home-assistant.io/components/bluesound", "documentation": "https://www.home-assistant.io/components/bluesound",
"requirements": [ "requirements": [
"xmltodict==0.11.0" "xmltodict==0.12.0"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [] "codeowners": []

View File

@ -3,7 +3,7 @@
"name": "Bom", "name": "Bom",
"documentation": "https://www.home-assistant.io/components/bom", "documentation": "https://www.home-assistant.io/components/bom",
"requirements": [ "requirements": [
"bomradarloop==0.1.2" "bomradarloop==0.1.3"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [] "codeowners": []

View File

@ -8,7 +8,7 @@ import voluptuous as vol
from homeassistant.components.calendar import ( from homeassistant.components.calendar import (
PLATFORM_SCHEMA, CalendarEventDevice, get_date) PLATFORM_SCHEMA, CalendarEventDevice, get_date)
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME) CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle, dt from homeassistant.util import Throttle, dt
@ -36,7 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_SEARCH): cv.string, vol.Required(CONF_SEARCH): cv.string,
}) })
])) ])),
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean
}) })
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
@ -50,7 +51,8 @@ def setup_platform(hass, config, add_entities, disc_info=None):
username = config.get(CONF_USERNAME) username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD) password = config.get(CONF_PASSWORD)
client = caldav.DAVClient(url, None, username, password) client = caldav.DAVClient(url, None, username, password,
ssl_verify_cert=config.get(CONF_VERIFY_SSL))
calendars = client.principal().calendars() calendars = client.principal().calendars()

View File

@ -24,6 +24,7 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, dispatcher_send) async_dispatcher_connect, dispatcher_send)
from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.typing import ConfigType, HomeAssistantType
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.util.logging import async_create_catching_coro
from . import DOMAIN as CAST_DOMAIN from . import DOMAIN as CAST_DOMAIN
@ -522,8 +523,8 @@ class CastDevice(MediaPlayerDevice):
if _is_matching_dynamic_group(self._cast_info, discover): if _is_matching_dynamic_group(self._cast_info, discover):
_LOGGER.debug("Discovered matching dynamic group: %s", _LOGGER.debug("Discovered matching dynamic group: %s",
discover) discover)
self.hass.async_create_task( self.hass.async_create_task(async_create_catching_coro(
self.async_set_dynamic_group(discover)) self.async_set_dynamic_group(discover)))
return return
if self._cast_info.uuid != discover.uuid: if self._cast_info.uuid != discover.uuid:
@ -536,7 +537,8 @@ class CastDevice(MediaPlayerDevice):
self._cast_info.host, self._cast_info.port) self._cast_info.host, self._cast_info.port)
return return
_LOGGER.debug("Discovered chromecast with same UUID: %s", discover) _LOGGER.debug("Discovered chromecast with same UUID: %s", discover)
self.hass.async_create_task(self.async_set_cast_info(discover)) self.hass.async_create_task(async_create_catching_coro(
self.async_set_cast_info(discover)))
def async_cast_removed(discover: ChromecastInfo): def async_cast_removed(discover: ChromecastInfo):
"""Handle removal of Chromecast.""" """Handle removal of Chromecast."""
@ -546,13 +548,15 @@ class CastDevice(MediaPlayerDevice):
if (self._dynamic_group_cast_info is not None and if (self._dynamic_group_cast_info is not None and
self._dynamic_group_cast_info.uuid == discover.uuid): self._dynamic_group_cast_info.uuid == discover.uuid):
_LOGGER.debug("Removed matching dynamic group: %s", discover) _LOGGER.debug("Removed matching dynamic group: %s", discover)
self.hass.async_create_task(self.async_del_dynamic_group()) self.hass.async_create_task(async_create_catching_coro(
self.async_del_dynamic_group()))
return return
if self._cast_info.uuid != discover.uuid: if self._cast_info.uuid != discover.uuid:
# Removed is not our device. # Removed is not our device.
return return
_LOGGER.debug("Removed chromecast with same UUID: %s", discover) _LOGGER.debug("Removed chromecast with same UUID: %s", discover)
self.hass.async_create_task(self.async_del_cast_info(discover)) self.hass.async_create_task(async_create_catching_coro(
self.async_del_cast_info(discover)))
async def async_stop(event): async def async_stop(event):
"""Disconnect socket on Home Assistant stop.""" """Disconnect socket on Home Assistant stop."""
@ -565,14 +569,15 @@ class CastDevice(MediaPlayerDevice):
self.hass, SIGNAL_CAST_REMOVED, self.hass, SIGNAL_CAST_REMOVED,
async_cast_removed) async_cast_removed)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop)
self.hass.async_create_task(self.async_set_cast_info(self._cast_info)) self.hass.async_create_task(async_create_catching_coro(
self.async_set_cast_info(self._cast_info)))
for info in self.hass.data[KNOWN_CHROMECAST_INFO_KEY]: for info in self.hass.data[KNOWN_CHROMECAST_INFO_KEY]:
if _is_matching_dynamic_group(self._cast_info, info): if _is_matching_dynamic_group(self._cast_info, info):
_LOGGER.debug("[%s %s (%s:%s)] Found dynamic group: %s", _LOGGER.debug("[%s %s (%s:%s)] Found dynamic group: %s",
self.entity_id, self._cast_info.friendly_name, self.entity_id, self._cast_info.friendly_name,
self._cast_info.host, self._cast_info.port, info) self._cast_info.host, self._cast_info.port, info)
self.hass.async_create_task( self.hass.async_create_task(async_create_catching_coro(
self.async_set_dynamic_group(info)) self.async_set_dynamic_group(info)))
break break
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
@ -1046,6 +1051,11 @@ class CastDevice(MediaPlayerDevice):
return images[0].url if images and images[0].url else None return images[0].url if images and images[0].url else None
@property
def media_image_remotely_accessible(self) -> bool:
"""If the image url is remotely accessible."""
return True
@property @property
def media_title(self): def media_title(self):
"""Title of current playing media.""" """Title of current playing media."""

View File

@ -3,7 +3,9 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME,\
CONF_MAXIMUM, CONF_MINIMUM
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
@ -12,6 +14,8 @@ _LOGGER = logging.getLogger(__name__)
ATTR_INITIAL = 'initial' ATTR_INITIAL = 'initial'
ATTR_STEP = 'step' ATTR_STEP = 'step'
ATTR_MINIMUM = 'minimum'
ATTR_MAXIMUM = 'maximum'
CONF_INITIAL = 'initial' CONF_INITIAL = 'initial'
CONF_RESTORE = 'restore' CONF_RESTORE = 'restore'
@ -26,11 +30,19 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
SERVICE_DECREMENT = 'decrement' SERVICE_DECREMENT = 'decrement'
SERVICE_INCREMENT = 'increment' SERVICE_INCREMENT = 'increment'
SERVICE_RESET = 'reset' SERVICE_RESET = 'reset'
SERVICE_CONFIGURE = 'configure'
SERVICE_SCHEMA = vol.Schema({ SERVICE_SCHEMA_SIMPLE = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
}) })
SERVICE_SCHEMA_CONFIGURE = vol.Schema({
ATTR_ENTITY_ID: cv.comp_entity_ids,
vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)),
vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)),
vol.Optional(ATTR_STEP): cv.positive_int,
})
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: cv.schema_with_slug_keys( DOMAIN: cv.schema_with_slug_keys(
vol.Any({ vol.Any({
@ -38,6 +50,10 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL):
cv.positive_int, cv.positive_int,
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MAXIMUM, default=None):
vol.Any(None, vol.Coerce(int)),
vol.Optional(CONF_MINIMUM, default=None):
vol.Any(None, vol.Coerce(int)),
vol.Optional(CONF_RESTORE, default=True): cv.boolean, vol.Optional(CONF_RESTORE, default=True): cv.boolean,
vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int,
}, None) }, None)
@ -60,21 +76,27 @@ async def async_setup(hass, config):
restore = cfg.get(CONF_RESTORE) restore = cfg.get(CONF_RESTORE)
step = cfg.get(CONF_STEP) step = cfg.get(CONF_STEP)
icon = cfg.get(CONF_ICON) icon = cfg.get(CONF_ICON)
minimum = cfg.get(CONF_MINIMUM)
maximum = cfg.get(CONF_MAXIMUM)
entities.append(Counter(object_id, name, initial, restore, step, icon)) entities.append(Counter(object_id, name, initial, minimum, maximum,
restore, step, icon))
if not entities: if not entities:
return False return False
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_INCREMENT, SERVICE_SCHEMA, SERVICE_INCREMENT, SERVICE_SCHEMA_SIMPLE,
'async_increment') 'async_increment')
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_DECREMENT, SERVICE_SCHEMA, SERVICE_DECREMENT, SERVICE_SCHEMA_SIMPLE,
'async_decrement') 'async_decrement')
component.async_register_entity_service( component.async_register_entity_service(
SERVICE_RESET, SERVICE_SCHEMA, SERVICE_RESET, SERVICE_SCHEMA_SIMPLE,
'async_reset') 'async_reset')
component.async_register_entity_service(
SERVICE_CONFIGURE, SERVICE_SCHEMA_CONFIGURE,
'async_configure')
await component.async_add_entities(entities) await component.async_add_entities(entities)
return True return True
@ -83,13 +105,16 @@ async def async_setup(hass, config):
class Counter(RestoreEntity): class Counter(RestoreEntity):
"""Representation of a counter.""" """Representation of a counter."""
def __init__(self, object_id, name, initial, restore, step, icon): def __init__(self, object_id, name, initial, minimum, maximum,
restore, step, icon):
"""Initialize a counter.""" """Initialize a counter."""
self.entity_id = ENTITY_ID_FORMAT.format(object_id) self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self._name = name self._name = name
self._restore = restore self._restore = restore
self._step = step self._step = step
self._state = self._initial = initial self._state = self._initial = initial
self._min = minimum
self._max = maximum
self._icon = icon self._icon = icon
@property @property
@ -115,10 +140,24 @@ class Counter(RestoreEntity):
@property @property
def state_attributes(self): def state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { ret = {
ATTR_INITIAL: self._initial, ATTR_INITIAL: self._initial,
ATTR_STEP: self._step, ATTR_STEP: self._step,
} }
if self._min is not None:
ret[CONF_MINIMUM] = self._min
if self._max is not None:
ret[CONF_MAXIMUM] = self._max
return ret
def compute_next_state(self, state):
"""Keep the state within the range of min/max values."""
if self._min is not None:
state = max(self._min, state)
if self._max is not None:
state = min(self._max, state)
return state
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Call when entity about to be added to Home Assistant.""" """Call when entity about to be added to Home Assistant."""
@ -128,19 +167,31 @@ class Counter(RestoreEntity):
if self._restore: if self._restore:
state = await self.async_get_last_state() state = await self.async_get_last_state()
if state is not None: if state is not None:
self._state = int(state.state) self._state = self.compute_next_state(int(state.state))
async def async_decrement(self): async def async_decrement(self):
"""Decrement the counter.""" """Decrement the counter."""
self._state -= self._step self._state = self.compute_next_state(self._state - self._step)
await self.async_update_ha_state() await self.async_update_ha_state()
async def async_increment(self): async def async_increment(self):
"""Increment a counter.""" """Increment a counter."""
self._state += self._step self._state = self.compute_next_state(self._state + self._step)
await self.async_update_ha_state() await self.async_update_ha_state()
async def async_reset(self): async def async_reset(self):
"""Reset a counter.""" """Reset a counter."""
self._state = self._initial self._state = self.compute_next_state(self._initial)
await self.async_update_ha_state()
async def async_configure(self, **kwargs):
"""Change the counter's settings with a service."""
if CONF_MINIMUM in kwargs:
self._min = kwargs[CONF_MINIMUM]
if CONF_MAXIMUM in kwargs:
self._max = kwargs[CONF_MAXIMUM]
if CONF_STEP in kwargs:
self._step = kwargs[CONF_STEP]
self._state = self.compute_next_state(self._state)
await self.async_update_ha_state() await self.async_update_ha_state()

View File

@ -18,3 +18,18 @@ reset:
entity_id: entity_id:
description: Entity id of the counter to reset. description: Entity id of the counter to reset.
example: 'counter.count0' example: 'counter.count0'
configure:
description: Change counter parameters
fields:
entity_id:
description: Entity id of the counter to change.
example: 'counter.count0'
minimum:
description: New minimum value for the counter or None to remove minimum
example: 0
maximum:
description: New maximum value for the counter or None to remove maximum
example: 100
step:
description: New value for step
example: 2

View File

@ -6,9 +6,10 @@ import voluptuous as vol
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_OPERATION_MODE, ATTR_AWAY_MODE, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE,
ATTR_SWING_MODE, STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ATTR_OPERATION_MODE, ATTR_SWING_MODE, STATE_AUTO, STATE_COOL, STATE_DRY,
STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE,
SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE,
SUPPORT_TARGET_TEMPERATURE) SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import ( from homeassistant.const import (
ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, STATE_OFF, TEMP_CELSIUS) ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, STATE_OFF, TEMP_CELSIUS)
@ -44,6 +45,7 @@ DAIKIN_TO_HA_STATE = {
} }
HA_ATTR_TO_DAIKIN = { HA_ATTR_TO_DAIKIN = {
ATTR_AWAY_MODE: 'en_hol',
ATTR_OPERATION_MODE: 'mode', ATTR_OPERATION_MODE: 'mode',
ATTR_FAN_MODE: 'f_rate', ATTR_FAN_MODE: 'f_rate',
ATTR_SWING_MODE: 'f_dir', ATTR_SWING_MODE: 'f_dir',
@ -93,8 +95,9 @@ class DaikinClimate(ClimateDevice):
), ),
} }
self._supported_features = SUPPORT_TARGET_TEMPERATURE \ self._supported_features = (SUPPORT_AWAY_MODE | SUPPORT_ON_OFF
| SUPPORT_OPERATION_MODE | SUPPORT_OPERATION_MODE
| SUPPORT_TARGET_TEMPERATURE)
if self._api.device.support_fan_mode: if self._api.device.support_fan_mode:
self._supported_features |= SUPPORT_FAN_MODE self._supported_features |= SUPPORT_FAN_MODE
@ -266,3 +269,36 @@ class DaikinClimate(ClimateDevice):
def device_info(self): def device_info(self):
"""Return a device description for device registry.""" """Return a device description for device registry."""
return self._api.device_info return self._api.device_info
@property
def is_on(self):
"""Return true if on."""
return self._api.device.represent(
HA_ATTR_TO_DAIKIN[ATTR_OPERATION_MODE]
)[1] != HA_STATE_TO_DAIKIN[STATE_OFF]
async def async_turn_on(self):
"""Turn device on."""
await self._api.device.set({})
async def async_turn_off(self):
"""Turn device off."""
await self._api.device.set({
HA_ATTR_TO_DAIKIN[ATTR_OPERATION_MODE]:
HA_STATE_TO_DAIKIN[STATE_OFF]
})
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._api.device.represent(
HA_ATTR_TO_DAIKIN[ATTR_AWAY_MODE]
)[1] != HA_STATE_TO_DAIKIN[STATE_OFF]
async def async_turn_away_mode_on(self):
"""Turn away mode on."""
await self._api.device.set({HA_ATTR_TO_DAIKIN[ATTR_AWAY_MODE]: '1'})
async def async_turn_away_mode_off(self):
"""Turn away mode off."""
await self._api.device.set({HA_ATTR_TO_DAIKIN[ATTR_AWAY_MODE]: '0'})

View File

@ -85,5 +85,9 @@ class DanfossAir:
= self._client.command(ReadCommand.boost) = self._client.command(ReadCommand.boost)
self._data[ReadCommand.battery_percent] \ self._data[ReadCommand.battery_percent] \
= self._client.command(ReadCommand.battery_percent) = self._client.command(ReadCommand.battery_percent)
self._data[ReadCommand.bypass] \
= self._client.command(ReadCommand.bypass)
self._data[ReadCommand.automatic_bypass] \
= self._client.command(ReadCommand.automatic_bypass)
_LOGGER.debug("Done fetching data from Danfoss Air CCM module") _LOGGER.debug("Done fetching data from Danfoss Air CCM module")

View File

@ -3,7 +3,7 @@
"name": "Danfoss air", "name": "Danfoss air",
"documentation": "https://www.home-assistant.io/components/danfoss_air", "documentation": "https://www.home-assistant.io/components/danfoss_air",
"requirements": [ "requirements": [
"pydanfossair==0.0.7" "pydanfossair==0.1.0"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [] "codeowners": []

View File

@ -19,6 +19,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
ReadCommand.boost, ReadCommand.boost,
UpdateCommand.boost_activate, UpdateCommand.boost_activate,
UpdateCommand.boost_deactivate], UpdateCommand.boost_deactivate],
["Danfoss Air Bypass",
ReadCommand.bypass,
UpdateCommand.bypass_activate,
UpdateCommand.bypass_deactivate],
["Danfoss Air Automatic Bypass",
ReadCommand.automatic_bypass,
UpdateCommand.bypass_activate,
UpdateCommand.bypass_deactivate],
] ]
dev = [] dev = []
@ -59,7 +67,7 @@ class DanfossAir(SwitchDevice):
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the switch off.""" """Turn the switch off."""
_LOGGER.debug("Turning of switch with command %s", self._off_command) _LOGGER.debug("Turning off switch with command %s", self._off_command)
self._data.update_state(self._off_command, self._state_command) self._data.update_state(self._off_command, self._state_command)
def update(self): def update(self):

View File

@ -26,7 +26,7 @@
"title": "Definici\u00f3 de la passarel\u00b7la deCONZ" "title": "Definici\u00f3 de la passarel\u00b7la deCONZ"
}, },
"link": { "link": {
"description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ\n2. Prem el bot\u00f3 \"Desbloquejar passarel\u00b7la\"", "description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ -> Passarel\u00b7la -> Avan\u00e7at\n2. Prem el bot\u00f3 \"Autenticar applicaci\u00f3\"",
"title": "Vincular amb deCONZ" "title": "Vincular amb deCONZ"
}, },
"options": { "options": {

View File

@ -12,7 +12,7 @@
"init": { "init": {
"data": { "data": {
"host": "Hostitel", "host": "Hostitel",
"port": "Port (v\u00fdchoz\u00ed hodnota: '80')" "port": "Port"
}, },
"title": "Definujte br\u00e1nu deCONZ" "title": "Definujte br\u00e1nu deCONZ"
}, },

View File

@ -3,7 +3,7 @@
"abort": { "abort": {
"already_configured": "El puente ya esta configurado", "already_configured": "El puente ya esta configurado",
"no_bridges": "No se han descubierto puentes deCONZ", "no_bridges": "No se han descubierto puentes deCONZ",
"one_instance_only": "El componente s\u00f3lo soporta una instancia deCONZ", "one_instance_only": "El componente solo admite una instancia de deCONZ",
"updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host"
}, },
"error": { "error": {
@ -26,7 +26,7 @@
"title": "Definir pasarela deCONZ" "title": "Definir pasarela deCONZ"
}, },
"link": { "link": {
"description": "Desbloquee su pasarela deCONZ para registrarse con Home Assistant. \n\n 1. Ir a la configuraci\u00f3n del sistema deCONZ \n 2. Presione el bot\u00f3n \"Desbloquear Gateway\"", "description": "Desbloquea tu gateway de deCONZ para registrarte con Home Assistant.\n\n1. Dir\u00edgete a deCONZ Settings -> Gateway -> Advanced\n2. Pulsa el bot\u00f3n \"Authenticate app\"",
"title": "Enlazar con deCONZ" "title": "Enlazar con deCONZ"
}, },
"options": { "options": {

View File

@ -3,7 +3,8 @@
"abort": { "abort": {
"already_configured": "Broen er allerede konfigurert", "already_configured": "Broen er allerede konfigurert",
"no_bridges": "Ingen deCONZ broer oppdaget", "no_bridges": "Ingen deCONZ broer oppdaget",
"one_instance_only": "Komponenten st\u00f8tter bare \u00e9n deCONZ forekomst" "one_instance_only": "Komponenten st\u00f8tter bare \u00e9n deCONZ forekomst",
"updated_instance": "Oppdatert deCONZ forekomst med ny vertsadresse"
}, },
"error": { "error": {
"no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel" "no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel"
@ -25,7 +26,7 @@
"title": "Definer deCONZ-gatewayen" "title": "Definer deCONZ-gatewayen"
}, },
"link": { "link": {
"description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen", "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger -> Gateway -> Avansert \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen",
"title": "Koble til deCONZ" "title": "Koble til deCONZ"
}, },
"options": { "options": {

View File

@ -3,7 +3,8 @@
"abort": { "abort": {
"already_configured": "Mostek jest ju\u017c skonfigurowany", "already_configured": "Mostek jest ju\u017c skonfigurowany",
"no_bridges": "Nie odkryto mostk\u00f3w deCONZ", "no_bridges": "Nie odkryto mostk\u00f3w deCONZ",
"one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 deCONZ" "one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 deCONZ",
"updated_instance": "Zaktualizowano instancj\u0119 deCONZ o nowy adres hosta"
}, },
"error": { "error": {
"no_key": "Nie mo\u017cna uzyska\u0107 klucza API" "no_key": "Nie mo\u017cna uzyska\u0107 klucza API"

View File

@ -4,7 +4,7 @@
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430",
"no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b",
"one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ", "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ",
"updated_instance": "deCONZ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d \u0441 \u043d\u043e\u0432\u044b\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0445\u043e\u0441\u0442\u0430" "updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 deCONZ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d"
}, },
"error": { "error": {
"no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API" "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API"

View File

@ -3,7 +3,8 @@
"abort": { "abort": {
"already_configured": "Most je \u017ee nastavljen", "already_configured": "Most je \u017ee nastavljen",
"no_bridges": "Ni odkritih mostov deCONZ", "no_bridges": "Ni odkritih mostov deCONZ",
"one_instance_only": "Komponenta podpira le en primerek deCONZ" "one_instance_only": "Komponenta podpira le en primerek deCONZ",
"updated_instance": "Posodobljen deCONZ z novim naslovom gostitelja"
}, },
"error": { "error": {
"no_key": "Klju\u010da API ni mogo\u010de dobiti" "no_key": "Klju\u010da API ni mogo\u010de dobiti"

View File

@ -9,6 +9,12 @@
"no_key": "Det gick inte att ta emot en API-nyckel" "no_key": "Det gick inte att ta emot en API-nyckel"
}, },
"step": { "step": {
"hassio_confirm": {
"data": {
"allow_clip_sensor": "Till\u00e5t import av virtuella sensorer"
},
"title": "deCONZ Zigbee gateway via Hass.io till\u00e4gg"
},
"init": { "init": {
"data": { "data": {
"host": "V\u00e4rd", "host": "V\u00e4rd",

View File

@ -88,9 +88,11 @@ class DeconzCover(DeconzDevice, CoverDevice):
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
position = kwargs[ATTR_POSITION] position = kwargs[ATTR_POSITION]
data = {'on': False} data = {'on': False}
if position > 0: if position > 0:
data['on'] = True data['on'] = True
data['bri'] = int(position / 100 * 255) data['bri'] = int(position / 100 * 255)
await self._device.async_set_state(data) await self._device.async_set_state(data)
async def async_open_cover(self, **kwargs): async def async_open_cover(self, **kwargs):
@ -126,7 +128,9 @@ class DeconzCoverZigbeeSpec(DeconzCover):
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
position = kwargs[ATTR_POSITION] position = kwargs[ATTR_POSITION]
data = {'on': False} data = {'on': False}
if position < 100: if position < 100:
data['on'] = True data['on'] = True
data['bri'] = 255 - int(position / 100 * 255) data['bri'] = 255 - int(position / 100 * 255)
await self._device.async_set_state(data) await self._device.async_set_state(data)

View File

@ -61,8 +61,10 @@ class DeconzDevice(Entity):
if (self._device.uniqueid is None or if (self._device.uniqueid is None or
self._device.uniqueid.count(':') != 7): self._device.uniqueid.count(':') != 7):
return None return None
serial = self._device.uniqueid.split('-', 1)[0] serial = self._device.uniqueid.split('-', 1)[0]
bridgeid = self.gateway.api.config.bridgeid bridgeid = self.gateway.api.config.bridgeid
return { return {
'connections': {(CONNECTION_ZIGBEE, serial)}, 'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)}, 'identifiers': {(DECONZ_DOMAIN, serial)},

View File

@ -165,6 +165,8 @@ class DeconzLight(DeconzDevice, Light):
"""Return the device state attributes.""" """Return the device state attributes."""
attributes = {} attributes = {}
attributes['is_deconz_group'] = self._device.type == 'LightGroup' attributes['is_deconz_group'] = self._device.type == 'LightGroup'
if self._device.type == 'LightGroup': if self._device.type == 'LightGroup':
attributes['all_on'] = self._device.all_on attributes['all_on'] = self._device.all_on
return attributes return attributes

View File

@ -3,7 +3,7 @@
"name": "Deconz", "name": "Deconz",
"documentation": "https://www.home-assistant.io/components/deconz", "documentation": "https://www.home-assistant.io/components/deconz",
"requirements": [ "requirements": [
"pydeconz==54" "pydeconz==58"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [

View File

@ -51,7 +51,7 @@ class AbstractDemoPlayer(MediaPlayerDevice):
# We only implement the methods that we support # We only implement the methods that we support
def __init__(self, name): def __init__(self, name, device_class=None):
"""Initialize the demo device.""" """Initialize the demo device."""
self._name = name self._name = name
self._player_state = STATE_PLAYING self._player_state = STATE_PLAYING
@ -60,6 +60,7 @@ class AbstractDemoPlayer(MediaPlayerDevice):
self._shuffle = False self._shuffle = False
self._sound_mode_list = SOUND_MODE_LIST self._sound_mode_list = SOUND_MODE_LIST
self._sound_mode = DEFAULT_SOUND_MODE self._sound_mode = DEFAULT_SOUND_MODE
self._device_class = device_class
@property @property
def should_poll(self): def should_poll(self):
@ -101,6 +102,11 @@ class AbstractDemoPlayer(MediaPlayerDevice):
"""Return a list of available sound modes.""" """Return a list of available sound modes."""
return self._sound_mode_list return self._sound_mode_list
@property
def device_class(self):
"""Return the device class of the media player."""
return self._device_class
def turn_on(self): def turn_on(self):
"""Turn the media player on.""" """Turn the media player on."""
self._player_state = STATE_PLAYING self._player_state = STATE_PLAYING

View File

@ -1,7 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Dialogflow.", "not_internet_accessible": "Tu instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Dialogflow.",
"one_instance_allowed": "Solo una instancia es necesaria." "one_instance_allowed": "Solo una instancia es necesaria."
}, },
"create_entry": { "create_entry": {
@ -9,7 +9,8 @@
}, },
"step": { "step": {
"user": { "user": {
"description": "\u00bfEst\u00e1 seguro de que desea configurar Dialogflow?" "description": "\u00bfEst\u00e1s seguro de que quieres configurar Dialogflow?",
"title": "Configurar el Webhook de Dialogflow"
} }
}, },
"title": "Dialogflow" "title": "Dialogflow"

View File

@ -3,7 +3,7 @@
"name": "Discord", "name": "Discord",
"documentation": "https://www.home-assistant.io/components/discord", "documentation": "https://www.home-assistant.io/components/discord",
"requirements": [ "requirements": [
"discord.py==0.16.12" "discord.py==1.0.1"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [] "codeowners": []

View File

@ -1,5 +1,6 @@
"""Discord platform for notify component.""" """Discord platform for notify component."""
import logging import logging
import os.path
import voluptuous as vol import voluptuous as vol
@ -33,36 +34,69 @@ class DiscordNotificationService(BaseNotificationService):
self.token = token self.token = token
self.hass = hass self.hass = hass
def file_exists(self, filename):
"""Check if a file exists on disk and is in authorized path."""
if not self.hass.config.is_allowed_path(filename):
return False
return os.path.isfile(filename)
async def async_send_message(self, message, **kwargs): async def async_send_message(self, message, **kwargs):
"""Login to Discord, send message to channel(s) and log out.""" """Login to Discord, send message to channel(s) and log out."""
import discord import discord
discord.VoiceClient.warn_nacl = False discord.VoiceClient.warn_nacl = False
discord_bot = discord.Client(loop=self.hass.loop) discord_bot = discord.Client(loop=self.hass.loop)
images = None
if ATTR_TARGET not in kwargs: if ATTR_TARGET not in kwargs:
_LOGGER.error("No target specified") _LOGGER.error("No target specified")
return None return None
if ATTR_DATA in kwargs:
data = kwargs.get(ATTR_DATA)
if ATTR_IMAGES in data:
images = list()
for image in data.get(ATTR_IMAGES):
image_exists = await self.hass.async_add_executor_job(
self.file_exists,
image)
if image_exists:
images.append(image)
else:
_LOGGER.warning("Image not found: %s", image)
# pylint: disable=unused-variable # pylint: disable=unused-variable
@discord_bot.event @discord_bot.event
async def on_ready(): async def on_ready():
"""Send the messages when the bot is ready.""" """Send the messages when the bot is ready."""
try: try:
data = kwargs.get(ATTR_DATA)
images = None
if data:
images = data.get(ATTR_IMAGES)
for channelid in kwargs[ATTR_TARGET]: for channelid in kwargs[ATTR_TARGET]:
channel = discord.Object(id=channelid) channelid = int(channelid)
await discord_bot.send_message(channel, message) channel = discord_bot.get_channel(channelid)
if channel is None:
_LOGGER.warning(
"Channel not found for id: %s",
channelid)
continue
# Must create new instances of File for each channel.
files = None
if images: if images:
for anum, f_name in enumerate(images): files = list()
await discord_bot.send_file(channel, f_name) for image in images:
files.append(discord.File(image))
await channel.send(message, files=files)
except (discord.errors.HTTPException, except (discord.errors.HTTPException,
discord.errors.NotFound) as error: discord.errors.NotFound) as error:
_LOGGER.warning("Communication error: %s", error) _LOGGER.warning("Communication error: %s", error)
await discord_bot.logout() await discord_bot.logout()
await discord_bot.close() await discord_bot.close()
await discord_bot.start(self.token) # Using reconnect=False prevents multiple ready events to be fired.
await discord_bot.start(self.token, reconnect=False)

View File

@ -105,16 +105,19 @@ OPTIONAL_SERVICE_HANDLERS = {
SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'), SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'),
} }
DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS)
DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS)
CONF_IGNORE = 'ignore' CONF_IGNORE = 'ignore'
CONF_ENABLE = 'enable' CONF_ENABLE = 'enable'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
vol.Optional(DOMAIN): vol.Schema({ vol.Optional(DOMAIN): vol.Schema({
vol.Optional(CONF_IGNORE, default=[]): vol.Optional(CONF_IGNORE, default=[]):
vol.All(cv.ensure_list, [ vol.All(cv.ensure_list, [vol.In(DEFAULT_ENABLED)]),
vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))]),
vol.Optional(CONF_ENABLE, default=[]): vol.Optional(CONF_ENABLE, default=[]):
vol.All(cv.ensure_list, [vol.In(OPTIONAL_SERVICE_HANDLERS)]) vol.All(cv.ensure_list, [
vol.In(DEFAULT_DISABLED + DEFAULT_ENABLED)]),
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -140,6 +143,14 @@ async def async_setup(hass, config):
ignored_platforms = [] ignored_platforms = []
enabled_platforms = [] enabled_platforms = []
for platform in enabled_platforms:
if platform in DEFAULT_ENABLED:
logger.warning(
"Please remove %s from your discovery.enable configuration "
"as it is now enabled by default",
platform,
)
async def new_service_found(service, info): async def new_service_found(service, info):
"""Handle a new service if one is found.""" """Handle a new service if one is found."""
if service in ignored_platforms: if service in ignored_platforms:

View File

@ -3,7 +3,7 @@
"name": "Dnsip", "name": "Dnsip",
"documentation": "https://www.home-assistant.io/components/dnsip", "documentation": "https://www.home-assistant.io/components/dnsip",
"requirements": [ "requirements": [
"aiodns==1.1.1" "aiodns==2.0.0"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [] "codeowners": []

View File

@ -1,26 +1,26 @@
"""Get your own public IP address or that of any host.""" """Get your own public IP address or that of any host."""
import logging
from datetime import timedelta from datetime import timedelta
import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_NAME = 'name'
CONF_HOSTNAME = 'hostname' CONF_HOSTNAME = 'hostname'
CONF_IPV6 = 'ipv6'
CONF_RESOLVER = 'resolver' CONF_RESOLVER = 'resolver'
CONF_RESOLVER_IPV6 = 'resolver_ipv6' CONF_RESOLVER_IPV6 = 'resolver_ipv6'
CONF_IPV6 = 'ipv6'
DEFAULT_NAME = 'myip'
DEFAULT_HOSTNAME = 'myip.opendns.com' DEFAULT_HOSTNAME = 'myip.opendns.com'
DEFAULT_IPV6 = False
DEFAULT_NAME = 'myip'
DEFAULT_RESOLVER = '208.67.222.222' DEFAULT_RESOLVER = '208.67.222.222'
DEFAULT_RESOLVER_IPV6 = '2620:0:ccc::2' DEFAULT_RESOLVER_IPV6 = '2620:0:ccc::2'
DEFAULT_IPV6 = False
SCAN_INTERVAL = timedelta(seconds=120) SCAN_INTERVAL = timedelta(seconds=120)
@ -33,8 +33,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
async def async_setup_platform(hass, config, async_add_devices, async def async_setup_platform(
discovery_info=None): hass, config, async_add_devices, discovery_info=None):
"""Set up the DNS IP sensor.""" """Set up the DNS IP sensor."""
hostname = config.get(CONF_HOSTNAME) hostname = config.get(CONF_HOSTNAME)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
@ -57,8 +57,9 @@ class WanIpSensor(Entity):
"""Implementation of a DNS IP sensor.""" """Implementation of a DNS IP sensor."""
def __init__(self, hass, name, hostname, resolver, ipv6): def __init__(self, hass, name, hostname, resolver, ipv6):
"""Initialize the sensor.""" """Initialize the DNS IP sensor."""
import aiodns import aiodns
self.hass = hass self.hass = hass
self._name = name self._name = name
self.hostname = hostname self.hostname = hostname
@ -80,9 +81,10 @@ class WanIpSensor(Entity):
async def async_update(self): async def async_update(self):
"""Get the current DNS IP address for hostname.""" """Get the current DNS IP address for hostname."""
from aiodns.error import DNSError from aiodns.error import DNSError
try: try:
response = await self.resolver.query(self.hostname, response = await self.resolver.query(
self.querytype) self.hostname, self.querytype)
except DNSError as err: except DNSError as err:
_LOGGER.warning("Exception while resolving host: %s", err) _LOGGER.warning("Exception while resolving host: %s", err)
response = None response = None

View File

@ -16,6 +16,7 @@ CONF_RETRY = 'retry'
DEFAULT_TIMEOUT = 5 DEFAULT_TIMEOUT = 5
DEFAULT_RETRY = 10 DEFAULT_RETRY = 10
DYSON_DEVICES = 'dyson_devices' DYSON_DEVICES = 'dyson_devices'
DYSON_PLATFORMS = ['sensor', 'fan', 'vacuum', 'climate', 'air_quality']
DOMAIN = 'dyson' DOMAIN = 'dyson'
@ -91,9 +92,7 @@ def setup(hass, config):
# Start fan/sensors components # Start fan/sensors components
if hass.data[DYSON_DEVICES]: if hass.data[DYSON_DEVICES]:
_LOGGER.debug("Starting sensor/fan components") _LOGGER.debug("Starting sensor/fan components")
discovery.load_platform(hass, "sensor", DOMAIN, {}, config) for platform in DYSON_PLATFORMS:
discovery.load_platform(hass, "fan", DOMAIN, {}, config) discovery.load_platform(hass, platform, DOMAIN, {}, config)
discovery.load_platform(hass, "vacuum", DOMAIN, {}, config)
discovery.load_platform(hass, "climate", DOMAIN, {}, config)
return True return True

View File

@ -0,0 +1,126 @@
"""Support for Dyson Pure Cool Air Quality Sensors."""
import logging
from homeassistant.components.air_quality import AirQualityEntity, DOMAIN
from . import DYSON_DEVICES
ATTRIBUTION = 'Dyson purifier air quality sensor'
_LOGGER = logging.getLogger(__name__)
DYSON_AIQ_DEVICES = 'dyson_aiq_devices'
ATTR_VOC = 'volatile_organic_compounds'
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Dyson Sensors."""
from libpurecool.dyson_pure_cool import DysonPureCool
if discovery_info is None:
return
hass.data.setdefault(DYSON_AIQ_DEVICES, [])
# Get Dyson Devices from parent component
device_ids = [device.unique_id for device in hass.data[DYSON_AIQ_DEVICES]]
for device in hass.data[DYSON_DEVICES]:
if isinstance(device, DysonPureCool) and \
device.serial not in device_ids:
hass.data[DYSON_AIQ_DEVICES].append(DysonAirSensor(device))
add_entities(hass.data[DYSON_AIQ_DEVICES])
class DysonAirSensor(AirQualityEntity):
"""Representation of a generic Dyson air quality sensor."""
def __init__(self, device):
"""Create a new generic air quality Dyson sensor."""
self._device = device
self._old_value = None
self._name = device.name
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self.hass.async_add_executor_job(
self._device.add_message_listener, self.on_message)
def on_message(self, message):
"""Handle new messages which are received from the fan."""
from libpurecool.dyson_pure_state_v2 import \
DysonEnvironmentalSensorV2State
_LOGGER.debug('%s: Message received for %s device: %s',
DOMAIN, self.name, message)
if (self._old_value is None or
self._old_value != self._device.environmental_state) and \
isinstance(message, DysonEnvironmentalSensorV2State):
self._old_value = self._device.environmental_state
self.schedule_update_ha_state()
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the Dyson sensor."""
return self._name
@property
def attribution(self):
"""Return the attribution."""
return ATTRIBUTION
@property
def air_quality_index(self):
"""Return the Air Quality Index (AQI)."""
return max(self.particulate_matter_2_5,
self.particulate_matter_10,
self.nitrogen_dioxide,
self.volatile_organic_compounds)
@property
def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level."""
if self._device.environmental_state:
return int(self._device.environmental_state.particulate_matter_25)
return None
@property
def particulate_matter_10(self):
"""Return the particulate matter 10 level."""
if self._device.environmental_state:
return int(self._device.environmental_state.particulate_matter_10)
return None
@property
def nitrogen_dioxide(self):
"""Return the NO2 (nitrogen dioxide) level."""
if self._device.environmental_state:
return int(self._device.environmental_state.nitrogen_dioxide)
return None
@property
def volatile_organic_compounds(self):
"""Return the VOC (Volatile Organic Compounds) level."""
if self._device.environmental_state:
return int(self._device.
environmental_state.volatile_organic_compounds)
return None
@property
def unique_id(self):
"""Return the sensor's unique id."""
return self._device.serial
@property
def device_state_attributes(self):
"""Return the device state attributes."""
data = {}
voc = self.volatile_organic_compounds
if voc is not None:
data[ATTR_VOC] = voc
return data

View File

@ -474,7 +474,8 @@ class DysonPureCoolDevice(FanEntity):
FanSpeed.FAN_SPEED_6.value: SPEED_MEDIUM, FanSpeed.FAN_SPEED_6.value: SPEED_MEDIUM,
FanSpeed.FAN_SPEED_7.value: SPEED_MEDIUM, FanSpeed.FAN_SPEED_7.value: SPEED_MEDIUM,
FanSpeed.FAN_SPEED_8.value: SPEED_HIGH, FanSpeed.FAN_SPEED_8.value: SPEED_HIGH,
FanSpeed.FAN_SPEED_9.value: SPEED_HIGH} FanSpeed.FAN_SPEED_9.value: SPEED_HIGH,
FanSpeed.FAN_SPEED_10.value: SPEED_HIGH}
return speed_map[self._device.state.speed] return speed_map[self._device.state.speed]

View File

@ -3,7 +3,6 @@ import logging
from homeassistant.const import STATE_OFF, TEMP_CELSIUS from homeassistant.const import STATE_OFF, TEMP_CELSIUS
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from . import DYSON_DEVICES from . import DYSON_DEVICES
SENSOR_UNITS = { SENSOR_UNITS = {
@ -21,21 +20,33 @@ SENSOR_ICONS = {
'temperature': 'mdi:thermometer', 'temperature': 'mdi:thermometer',
} }
DYSON_SENSOR_DEVICES = 'dyson_sensor_devices'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Dyson Sensors.""" """Set up the Dyson Sensors."""
_LOGGER.debug("Creating new Dyson fans")
devices = []
unit = hass.config.units.temperature_unit
# Get Dyson Devices from parent component
from libpurecool.dyson_pure_cool import DysonPureCool
from libpurecool.dyson_pure_cool_link import DysonPureCoolLink from libpurecool.dyson_pure_cool_link import DysonPureCoolLink
from libpurecool.dyson_pure_cool import DysonPureCool
for device in [d for d in hass.data[DYSON_DEVICES] if discovery_info is None:
if isinstance(d, DysonPureCoolLink) and return
not isinstance(d, DysonPureCool)]:
hass.data.setdefault(DYSON_SENSOR_DEVICES, [])
unit = hass.config.units.temperature_unit
devices = hass.data[DYSON_SENSOR_DEVICES]
# Get Dyson Devices from parent component
device_ids = [device.unique_id for device in
hass.data[DYSON_SENSOR_DEVICES]]
for device in hass.data[DYSON_DEVICES]:
if isinstance(device, DysonPureCool):
if '{}-{}'.format(device.serial, 'temperature') not in device_ids:
devices.append(DysonTemperatureSensor(device, unit))
if '{}-{}'.format(device.serial, 'humidity') not in device_ids:
devices.append(DysonHumiditySensor(device))
elif isinstance(device, DysonPureCoolLink):
devices.append(DysonFilterLifeSensor(device)) devices.append(DysonFilterLifeSensor(device))
devices.append(DysonDustSensor(device)) devices.append(DysonDustSensor(device))
devices.append(DysonHumiditySensor(device)) devices.append(DysonHumiditySensor(device))
@ -56,7 +67,7 @@ class DysonSensor(Entity):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Call when entity is added to hass.""" """Call when entity is added to hass."""
self.hass.async_add_job( self.hass.async_add_executor_job(
self._device.add_message_listener, self.on_message) self._device.add_message_listener, self.on_message)
def on_message(self, message): def on_message(self, message):
@ -88,6 +99,11 @@ class DysonSensor(Entity):
"""Return the icon for this sensor.""" """Return the icon for this sensor."""
return SENSOR_ICONS[self._sensor_type] return SENSOR_ICONS[self._sensor_type]
@property
def unique_id(self):
"""Return the sensor's unique id."""
return '{}-{}'.format(self._device.serial, self._sensor_type)
class DysonFilterLifeSensor(DysonSensor): class DysonFilterLifeSensor(DysonSensor):
"""Representation of Dyson Filter Life sensor (in hours).""" """Representation of Dyson Filter Life sensor (in hours)."""

View File

@ -0,0 +1,6 @@
{
"state": {
"day": "Den",
"night": "Noc"
}
}

View File

@ -3,7 +3,7 @@
"name": "Econet", "name": "Econet",
"documentation": "https://www.home-assistant.io/components/econet", "documentation": "https://www.home-assistant.io/components/econet",
"requirements": [ "requirements": [
"pyeconet==0.0.10" "pyeconet==0.0.11"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [] "codeowners": []

View File

@ -5,7 +5,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import CoreState, EventOrigin from homeassistant.core import CoreState, EventOrigin
LOGGER = logging.getLogger('.') LOGGER = logging.getLogger(__package__)
EVENT_ROKU_COMMAND = 'roku_command' EVENT_ROKU_COMMAND = 'roku_command'

View File

@ -4,13 +4,13 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_DEVICE from homeassistant.const import CONF_DEVICE
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'enocean' DOMAIN = 'enocean'
DATA_ENOCEAN = 'enocean'
ENOCEAN_DONGLE = None
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -18,14 +18,15 @@ CONFIG_SCHEMA = vol.Schema({
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
SIGNAL_RECEIVE_MESSAGE = 'enocean.receive_message'
SIGNAL_SEND_MESSAGE = 'enocean.send_message'
def setup(hass, config): def setup(hass, config):
"""Set up the EnOcean component.""" """Set up the EnOcean component."""
global ENOCEAN_DONGLE
serial_dev = config[DOMAIN].get(CONF_DEVICE) serial_dev = config[DOMAIN].get(CONF_DEVICE)
dongle = EnOceanDongle(hass, serial_dev)
ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev) hass.data[DATA_ENOCEAN] = dongle
return True return True
@ -39,87 +40,53 @@ class EnOceanDongle:
self.__communicator = SerialCommunicator( self.__communicator = SerialCommunicator(
port=ser, callback=self.callback) port=ser, callback=self.callback)
self.__communicator.start() self.__communicator.start()
self.__devices = [] self.hass = hass
self.hass.helpers.dispatcher.dispatcher_connect(
SIGNAL_SEND_MESSAGE, self._send_message_callback)
def register_device(self, dev): def _send_message_callback(self, command):
"""Register another device.""" """Send a command through the EnOcean dongle."""
self.__devices.append(dev)
def send_command(self, command):
"""Send a command from the EnOcean dongle."""
self.__communicator.send(command) self.__communicator.send(command)
# pylint: disable=no-self-use def callback(self, packet):
def _combine_hex(self, data):
"""Combine list of integer values to one big integer."""
output = 0x00
for i, j in enumerate(reversed(data)):
output |= (j << i * 8)
return output
def callback(self, temp):
"""Handle EnOcean device's callback. """Handle EnOcean device's callback.
This is the callback function called by python-enocan whenever there This is the callback function called by python-enocan whenever there
is an incoming packet. is an incoming packet.
""" """
from enocean.protocol.packet import RadioPacket from enocean.protocol.packet import RadioPacket
if isinstance(temp, RadioPacket): if isinstance(packet, RadioPacket):
_LOGGER.debug("Received radio packet: %s", temp) _LOGGER.debug("Received radio packet: %s", packet)
rxtype = None self.hass.helpers.dispatcher.dispatcher_send(
value = None SIGNAL_RECEIVE_MESSAGE, packet)
channel = 0
if temp.data[6] == 0x30:
rxtype = "wallswitch"
value = 1
elif temp.data[6] == 0x20:
rxtype = "wallswitch"
value = 0
elif temp.data[4] == 0x0c:
rxtype = "power"
value = temp.data[3] + (temp.data[2] << 8)
elif temp.data[2] & 0x60 == 0x60:
rxtype = "switch_status"
channel = temp.data[2] & 0x1F
if temp.data[3] == 0xe4:
value = 1
elif temp.data[3] == 0x80:
value = 0
elif temp.data[0] == 0xa5 and temp.data[1] == 0x02:
rxtype = "dimmerstatus"
value = temp.data[2]
for device in self.__devices:
if rxtype == "wallswitch" and device.stype == "listener":
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value, temp.data[1])
if rxtype == "power" and device.stype == "powersensor":
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value)
if rxtype == "power" and device.stype == "switch":
if temp.sender_int == self._combine_hex(device.dev_id):
if value > 10:
device.value_changed(1)
if rxtype == "switch_status" and device.stype == "switch" and \
channel == device.channel:
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value)
if rxtype == "dimmerstatus" and device.stype == "dimmer":
if temp.sender_int == self._combine_hex(device.dev_id):
device.value_changed(value)
class EnOceanDevice(): class EnOceanDevice(Entity):
"""Parent class for all devices associated with the EnOcean component.""" """Parent class for all devices associated with the EnOcean component."""
def __init__(self): def __init__(self, dev_id, dev_name="EnOcean device"):
"""Initialize the device.""" """Initialize the device."""
ENOCEAN_DONGLE.register_device(self) self.dev_id = dev_id
self.stype = "" self.dev_name = dev_name
self.sensorid = [0x00, 0x00, 0x00, 0x00]
async def async_added_to_hass(self):
"""Register callbacks."""
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_RECEIVE_MESSAGE, self._message_received_callback)
def _message_received_callback(self, packet):
"""Handle incoming packets."""
from enocean.utils import combine_hex
if packet.sender_int == combine_hex(self.dev_id):
self.value_changed(packet)
def value_changed(self, packet):
"""Update the internal state of the device when a packet arrives."""
# pylint: disable=no-self-use # pylint: disable=no-self-use
def send_command(self, data, optional, packet_type): def send_command(self, data, optional, packet_type):
"""Send a command via the EnOcean dongle.""" """Send a command via the EnOcean dongle."""
from enocean.protocol.packet import Packet from enocean.protocol.packet import Packet
packet = Packet(packet_type, data=data, optional=optional) packet = Packet(packet_type, data=data, optional=optional)
ENOCEAN_DONGLE.send_command(packet) self.hass.helpers.dispatcher.dispatcher_send(
SIGNAL_SEND_MESSAGE, packet)

View File

@ -3,16 +3,17 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
from homeassistant.components import enocean from homeassistant.components import enocean
from homeassistant.const import ( from homeassistant.components.binary_sensor import (
CONF_NAME, CONF_ID, CONF_DEVICE_CLASS) DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.const import CONF_DEVICE_CLASS, CONF_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'EnOcean binary sensor' DEFAULT_NAME = 'EnOcean binary sensor'
DEPENDENCIES = ['enocean']
EVENT_BUTTON_PRESSED = 'button_pressed'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
@ -24,61 +25,80 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Binary Sensor platform for EnOcean.""" """Set up the Binary Sensor platform for EnOcean."""
dev_id = config.get(CONF_ID) dev_id = config.get(CONF_ID)
devname = config.get(CONF_NAME) dev_name = config.get(CONF_NAME)
device_class = config.get(CONF_DEVICE_CLASS) device_class = config.get(CONF_DEVICE_CLASS)
add_entities([EnOceanBinarySensor(dev_id, devname, device_class)]) add_entities([EnOceanBinarySensor(dev_id, dev_name, device_class)])
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice): class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
"""Representation of EnOcean binary sensors such as wall switches.""" """Representation of EnOcean binary sensors such as wall switches.
def __init__(self, dev_id, devname, device_class): Supported EEPs (EnOcean Equipment Profiles):
- F6-02-01 (Light and Blind Control - Application Style 2)
- F6-02-02 (Light and Blind Control - Application Style 1)
"""
def __init__(self, dev_id, dev_name, device_class):
"""Initialize the EnOcean binary sensor.""" """Initialize the EnOcean binary sensor."""
enocean.EnOceanDevice.__init__(self) super().__init__(dev_id, dev_name)
self.stype = 'listener' self._device_class = device_class
self.dev_id = dev_id
self.which = -1 self.which = -1
self.onoff = -1 self.onoff = -1
self.devname = devname
self._device_class = device_class
@property @property
def name(self): def name(self):
"""Return the default name for the binary sensor.""" """Return the default name for the binary sensor."""
return self.devname return self.dev_name
@property @property
def device_class(self): def device_class(self):
"""Return the class of this sensor.""" """Return the class of this sensor."""
return self._device_class return self._device_class
def value_changed(self, value, value2): def value_changed(self, packet):
"""Fire an event with the data that have changed. """Fire an event with the data that have changed.
This method is called when there is an incoming packet associated This method is called when there is an incoming packet associated
with this platform. with this platform.
Example packet data:
- 2nd button pressed
['0xf6', '0x10', '0x00', '0x2d', '0xcf', '0x45', '0x30']
- button released
['0xf6', '0x00', '0x00', '0x2d', '0xcf', '0x45', '0x20']
""" """
# Energy Bow
pushed = None
if packet.data[6] == 0x30:
pushed = 1
elif packet.data[6] == 0x20:
pushed = 0
self.schedule_update_ha_state() self.schedule_update_ha_state()
if value2 == 0x70:
action = packet.data[1]
if action == 0x70:
self.which = 0 self.which = 0
self.onoff = 0 self.onoff = 0
elif value2 == 0x50: elif action == 0x50:
self.which = 0 self.which = 0
self.onoff = 1 self.onoff = 1
elif value2 == 0x30: elif action == 0x30:
self.which = 1 self.which = 1
self.onoff = 0 self.onoff = 0
elif value2 == 0x10: elif action == 0x10:
self.which = 1 self.which = 1
self.onoff = 1 self.onoff = 1
elif value2 == 0x37: elif action == 0x37:
self.which = 10 self.which = 10
self.onoff = 0 self.onoff = 0
elif value2 == 0x15: elif action == 0x15:
self.which = 10 self.which = 10
self.onoff = 1 self.onoff = 1
self.hass.bus.fire('button_pressed', {'id': self.dev_id, self.hass.bus.fire(EVENT_BUTTON_PRESSED,
'pushed': value, {'id': self.dev_id,
'pushed': pushed,
'which': self.which, 'which': self.which,
'onoff': self.onoff}) 'onoff': self.onoff})

View File

@ -4,10 +4,10 @@ import math
import voluptuous as vol import voluptuous as vol
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME, CONF_ID)
from homeassistant.components import enocean from homeassistant.components import enocean
from homeassistant.components.light import (
ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light)
from homeassistant.const import CONF_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -28,29 +28,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the EnOcean light platform.""" """Set up the EnOcean light platform."""
sender_id = config.get(CONF_SENDER_ID) sender_id = config.get(CONF_SENDER_ID)
devname = config.get(CONF_NAME) dev_name = config.get(CONF_NAME)
dev_id = config.get(CONF_ID) dev_id = config.get(CONF_ID)
add_entities([EnOceanLight(sender_id, devname, dev_id)]) add_entities([EnOceanLight(sender_id, dev_id, dev_name)])
class EnOceanLight(enocean.EnOceanDevice, Light): class EnOceanLight(enocean.EnOceanDevice, Light):
"""Representation of an EnOcean light source.""" """Representation of an EnOcean light source."""
def __init__(self, sender_id, devname, dev_id): def __init__(self, sender_id, dev_id, dev_name):
"""Initialize the EnOcean light source.""" """Initialize the EnOcean light source."""
enocean.EnOceanDevice.__init__(self) super().__init__(dev_id, dev_name)
self._on_state = False self._on_state = False
self._brightness = 50 self._brightness = 50
self._sender_id = sender_id self._sender_id = sender_id
self.dev_id = dev_id
self._devname = devname
self.stype = 'dimmer'
@property @property
def name(self): def name(self):
"""Return the name of the device if any.""" """Return the name of the device if any."""
return self._devname return self.dev_name
@property @property
def brightness(self): def brightness(self):
@ -94,8 +91,14 @@ class EnOceanLight(enocean.EnOceanDevice, Light):
self.send_command(command, [], 0x01) self.send_command(command, [], 0x01)
self._on_state = False self._on_state = False
def value_changed(self, val): def value_changed(self, packet):
"""Update the internal state of this device.""" """Update the internal state of this device.
Dimmer devices like Eltako FUD61 send telegram in different RORGs.
We only care about the 4BS (0xA5).
"""
if packet.data[0] == 0xa5 and packet.data[1] == 0x02:
val = packet.data[2]
self._brightness = math.floor(val / 100.0 * 256.0) self._brightness = math.floor(val / 100.0 * 256.0)
self._on_state = bool(val != 0) self._on_state = bool(val != 0)
self.schedule_update_ha_state() self.schedule_update_ha_state()

View File

@ -3,8 +3,8 @@
"name": "Enocean", "name": "Enocean",
"documentation": "https://www.home-assistant.io/components/enocean", "documentation": "https://www.home-assistant.io/components/enocean",
"requirements": [ "requirements": [
"enocean==0.40" "enocean==0.50"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [] "codeowners": ["@bdurrer"]
} }

View File

@ -3,58 +3,201 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, CONF_ID, POWER_WATT)
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.components import enocean from homeassistant.components import enocean
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_ID, CONF_NAME, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, POWER_WATT)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_MAX_TEMP = 'max_temp'
CONF_MIN_TEMP = 'min_temp'
CONF_RANGE_FROM = 'range_from'
CONF_RANGE_TO = 'range_to'
DEFAULT_NAME = 'EnOcean sensor' DEFAULT_NAME = 'EnOcean sensor'
DEVICE_CLASS_POWER = 'powersensor'
SENSOR_TYPES = {
DEVICE_CLASS_HUMIDITY: {
'name': 'Humidity',
'unit': '%',
'icon': 'mdi:water-percent',
'class': DEVICE_CLASS_HUMIDITY,
},
DEVICE_CLASS_POWER: {
'name': 'Power',
'unit': POWER_WATT,
'icon': 'mdi:power-plug',
'class': DEVICE_CLASS_POWER,
},
DEVICE_CLASS_TEMPERATURE: {
'name': 'Temperature',
'unit': TEMP_CELSIUS,
'icon': 'mdi:thermometer',
'class': DEVICE_CLASS_TEMPERATURE,
},
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_POWER): cv.string,
vol.Optional(CONF_MAX_TEMP, default=40): vol.Coerce(int),
vol.Optional(CONF_MIN_TEMP, default=0): vol.Coerce(int),
vol.Optional(CONF_RANGE_FROM, default=255): cv.positive_int,
vol.Optional(CONF_RANGE_TO, default=0): cv.positive_int,
}) })
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up an EnOcean sensor device.""" """Set up an EnOcean sensor device."""
dev_id = config.get(CONF_ID) dev_id = config.get(CONF_ID)
devname = config.get(CONF_NAME) dev_name = config.get(CONF_NAME)
dev_class = config.get(CONF_DEVICE_CLASS)
add_entities([EnOceanSensor(dev_id, devname)]) if dev_class == DEVICE_CLASS_TEMPERATURE:
temp_min = config.get(CONF_MIN_TEMP)
temp_max = config.get(CONF_MAX_TEMP)
range_from = config.get(CONF_RANGE_FROM)
range_to = config.get(CONF_RANGE_TO)
add_entities([EnOceanTemperatureSensor(
dev_id, dev_name, temp_min, temp_max, range_from, range_to)])
elif dev_class == DEVICE_CLASS_HUMIDITY:
add_entities([EnOceanHumiditySensor(dev_id, dev_name)])
elif dev_class == DEVICE_CLASS_POWER:
add_entities([EnOceanPowerSensor(dev_id, dev_name)])
class EnOceanSensor(enocean.EnOceanDevice, Entity): class EnOceanSensor(enocean.EnOceanDevice):
"""Representation of an EnOcean sensor device such as a power meter.""" """Representation of an EnOcean sensor device such as a power meter."""
def __init__(self, dev_id, devname): def __init__(self, dev_id, dev_name, sensor_type):
"""Initialize the EnOcean sensor device.""" """Initialize the EnOcean sensor device."""
enocean.EnOceanDevice.__init__(self) super().__init__(dev_id, dev_name)
self.stype = "powersensor" self._sensor_type = sensor_type
self.power = None self._device_class = SENSOR_TYPES[self._sensor_type]['class']
self.dev_id = dev_id self._dev_name = '{} {}'.format(
self.which = -1 SENSOR_TYPES[self._sensor_type]['name'], dev_name)
self.onoff = -1 self._unit_of_measurement = SENSOR_TYPES[self._sensor_type]['unit']
self.devname = devname self._icon = SENSOR_TYPES[self._sensor_type]['icon']
self._state = None
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
return 'Power %s' % self.devname return self._dev_name
def value_changed(self, value): @property
"""Update the internal state of the device.""" def icon(self):
self.power = value """Icon to use in the frontend."""
self.schedule_update_ha_state() return self._icon
@property
def device_class(self):
"""Return the device class of the sensor."""
return self._device_class
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
return self.power return self._state
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
return POWER_WATT return self._unit_of_measurement
def value_changed(self, packet):
"""Update the internal state of the sensor."""
class EnOceanPowerSensor(EnOceanSensor):
"""Representation of an EnOcean power sensor.
EEPs (EnOcean Equipment Profiles):
- A5-12-01 (Automated Meter Reading, Electricity)
"""
def __init__(self, dev_id, dev_name):
"""Initialize the EnOcean power sensor device."""
super().__init__(dev_id, dev_name, DEVICE_CLASS_POWER)
def value_changed(self, packet):
"""Update the internal state of the sensor."""
if packet.rorg != 0xA5:
return
packet.parse_eep(0x12, 0x01)
if packet.parsed['DT']['raw_value'] == 1:
# this packet reports the current value
raw_val = packet.parsed['MR']['raw_value']
divisor = packet.parsed['DIV']['raw_value']
self._state = raw_val / (10 ** divisor)
self.schedule_update_ha_state()
class EnOceanTemperatureSensor(EnOceanSensor):
"""Representation of an EnOcean temperature sensor device.
EEPs (EnOcean Equipment Profiles):
- A5-02-01 to A5-02-1B All 8 Bit Temperature Sensors of A5-02
- A5-10-01 to A5-10-14 (Room Operating Panels)
- A5-04-01 (Temp. and Humidity Sensor, Range 0°C to +40°C and 0% to 100%)
- A5-04-02 (Temp. and Humidity Sensor, Range -20°C to +60°C and 0% to 100%)
- A5-10-10 (Temp. and Humidity Sensor and Set Point)
- A5-10-12 (Temp. and Humidity Sensor, Set Point and Occupancy Control)
- 10 Bit Temp. Sensors are not supported (A5-02-20, A5-02-30)
For the following EEPs the scales must be set to "0 to 250":
- A5-04-01
- A5-04-02
- A5-10-10 to A5-10-14
"""
def __init__(self, dev_id, dev_name, scale_min, scale_max,
range_from, range_to):
"""Initialize the EnOcean temperature sensor device."""
super().__init__(dev_id, dev_name, DEVICE_CLASS_TEMPERATURE)
self._scale_min = scale_min
self._scale_max = scale_max
self.range_from = range_from
self.range_to = range_to
def value_changed(self, packet):
"""Update the internal state of the sensor."""
if packet.data[0] != 0xa5:
return
temp_scale = self._scale_max - self._scale_min
temp_range = self.range_to - self.range_from
raw_val = packet.data[3]
temperature = temp_scale / temp_range * (raw_val - self.range_from)
temperature += self._scale_min
self._state = round(temperature, 1)
self.schedule_update_ha_state()
class EnOceanHumiditySensor(EnOceanSensor):
"""Representation of an EnOcean humidity sensor device.
EEPs (EnOcean Equipment Profiles):
- A5-04-01 (Temp. and Humidity Sensor, Range 0°C to +40°C and 0% to 100%)
- A5-04-02 (Temp. and Humidity Sensor, Range -20°C to +60°C and 0% to 100%)
- A5-10-10 to A5-10-14 (Room Operating Panels)
"""
def __init__(self, dev_id, dev_name):
"""Initialize the EnOcean humidity sensor device."""
super().__init__(dev_id, dev_name, DEVICE_CLASS_HUMIDITY)
def value_changed(self, packet):
"""Update the internal state of the sensor."""
if packet.rorg != 0xA5:
return
humidity = packet.data[2] * 100 / 250
self._state = round(humidity, 1)
self.schedule_update_ha_state()

View File

@ -3,16 +3,16 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, CONF_ID)
from homeassistant.components import enocean from homeassistant.components import enocean
from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.switch import PLATFORM_SCHEMA
from homeassistant.const import CONF_ID, CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'EnOcean Switch'
CONF_CHANNEL = 'channel' CONF_CHANNEL = 'channel'
DEFAULT_NAME = 'EnOcean Switch'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
@ -23,26 +23,23 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the EnOcean switch platform.""" """Set up the EnOcean switch platform."""
dev_id = config.get(CONF_ID)
devname = config.get(CONF_NAME)
channel = config.get(CONF_CHANNEL) channel = config.get(CONF_CHANNEL)
dev_id = config.get(CONF_ID)
dev_name = config.get(CONF_NAME)
add_entities([EnOceanSwitch(dev_id, devname, channel)]) add_entities([EnOceanSwitch(dev_id, dev_name, channel)])
class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity): class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity):
"""Representation of an EnOcean switch device.""" """Representation of an EnOcean switch device."""
def __init__(self, dev_id, devname, channel): def __init__(self, dev_id, dev_name, channel):
"""Initialize the EnOcean switch device.""" """Initialize the EnOcean switch device."""
enocean.EnOceanDevice.__init__(self) super().__init__(dev_id, dev_name)
self.dev_id = dev_id
self._devname = devname
self._light = None self._light = None
self._on_state = False self._on_state = False
self._on_state2 = False self._on_state2 = False
self.channel = channel self.channel = channel
self.stype = "switch"
@property @property
def is_on(self): def is_on(self):
@ -52,7 +49,7 @@ class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity):
@property @property
def name(self): def name(self):
"""Return the device name.""" """Return the device name."""
return self._devname return self.dev_name
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn on the switch.""" """Turn on the switch."""
@ -74,7 +71,24 @@ class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity):
packet_type=0x01) packet_type=0x01)
self._on_state = False self._on_state = False
def value_changed(self, val): def value_changed(self, packet):
"""Update the internal state of the switch.""" """Update the internal state of the switch."""
self._on_state = val if packet.data[0] == 0xa5:
# power meter telegram, turn on if > 10 watts
packet.parse_eep(0x12, 0x01)
if packet.parsed['DT']['raw_value'] == 1:
raw_val = packet.parsed['MR']['raw_value']
divisor = packet.parsed['DIV']['raw_value']
watts = raw_val / (10 ** divisor)
if watts > 1:
self._on_state = True
self.schedule_update_ha_state()
elif packet.data[0] == 0xd2:
# actuator status telegram
packet.parse_eep(0x01, 0x01)
if packet.parsed['CMD']['raw_value'] == 4:
channel = packet.parsed['IO']['raw_value']
output = packet.parsed['OV']['raw_value']
if channel == self.channel:
self._on_state = output > 0
self.schedule_update_ha_state() self.schedule_update_ha_state()

View File

@ -4,6 +4,6 @@
"documentation": "https://www.home-assistant.io/components/epsonworkforce", "documentation": "https://www.home-assistant.io/components/epsonworkforce",
"dependencies": [], "dependencies": [],
"codeowners": ["@ThaStealth"], "codeowners": ["@ThaStealth"],
"requirements": ["epsonprinter==0.0.8"] "requirements": ["epsonprinter==0.0.9"]
} }

View File

@ -10,15 +10,16 @@ from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['epsonprinter==0.0.8'] REQUIREMENTS = ['epsonprinter==0.0.9']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MONITORED_CONDITIONS = { MONITORED_CONDITIONS = {
'black': ['Inklevel Black', '%', 'mdi:water'], 'black': ['Ink level Black', '%', 'mdi:water'],
'magenta': ['Inklevel Magenta', '%', 'mdi:water'], 'photoblack': ['Ink level Photoblack', '%', 'mdi:water'],
'cyan': ['Inklevel Cyan', '%', 'mdi:water'], 'magenta': ['Ink level Magenta', '%', 'mdi:water'],
'yellow': ['Inklevel Yellow', '%', 'mdi:water'], 'cyan': ['Ink level Cyan', '%', 'mdi:water'],
'clean': ['Inklevel Cleaning', '%', 'mdi:water'], 'yellow': ['Ink level Yellow', '%', 'mdi:water'],
'clean': ['Cleaning level', '%', 'mdi:water'],
} }
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,

View File

@ -13,7 +13,7 @@
"data": { "data": {
"password": "Contrasenya" "password": "Contrasenya"
}, },
"description": "Introdueix la contrasenya que has posat en la teva configuraci\u00f3.", "description": "Introdueix la contrasenya que has posat en la teva configuraci\u00f3 com a {name}.",
"title": "Introdueix la contrasenya" "title": "Introdueix la contrasenya"
}, },
"discovery_confirm": { "discovery_confirm": {

View File

@ -0,0 +1,13 @@
{
"config": {
"step": {
"authenticate": {
"description": "Zadejte heslo, kter\u00e9 jste nastavili ve va\u0161\u00ed konfiguraci pro {name} ."
},
"discovery_confirm": {
"description": "Chcete do domovsk\u00e9ho asistenta p\u0159idat uzel ESPHome `{name}`?",
"title": "Nalezen uzel ESPHome"
}
}
}
}

View File

@ -4,14 +4,16 @@
"already_configured": "ESP ya est\u00e1 configurado" "already_configured": "ESP ya est\u00e1 configurado"
}, },
"error": { "error": {
"invalid_password": "\u00a1Contrase\u00f1a incorrecta!" "connection_error": "No se puede conectar a ESP. Aseg\u00farate de que tu archivo YAML contenga una l\u00ednea 'api:'.",
"invalid_password": "\u00a1Contrase\u00f1a incorrecta!",
"resolve_error": "No se puede resolver la direcci\u00f3n de ESP. Si el error persiste, configura una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
}, },
"step": { "step": {
"authenticate": { "authenticate": {
"data": { "data": {
"password": "Contrase\u00f1a" "password": "Contrase\u00f1a"
}, },
"description": "Escribe la contrase\u00f1a que hayas establecido en tu configuraci\u00f3n.", "description": "Escribe la contrase\u00f1a que hayas puesto en la configuraci\u00f3n para {name}.",
"title": "Escribe la contrase\u00f1a" "title": "Escribe la contrase\u00f1a"
}, },
"discovery_confirm": { "discovery_confirm": {
@ -23,6 +25,7 @@
"host": "Host", "host": "Host",
"port": "Puerto" "port": "Puerto"
}, },
"description": "Introduce la configuraci\u00f3n de la conexi\u00f3n de tu nodo [ESPHome](https://esphomelib.com/).",
"title": "ESPHome" "title": "ESPHome"
} }
}, },

View File

@ -0,0 +1 @@
"""The Essent component."""

View File

@ -0,0 +1,8 @@
{
"domain": "essent",
"name": "Essent",
"documentation": "https://www.home-assistant.io/components/essent",
"requirements": ["PyEssent==0.10"],
"dependencies": [],
"codeowners": ["@TheLastProject"]
}

View File

@ -0,0 +1,112 @@
"""Support for Essent API."""
from datetime import timedelta
from pyessent import PyEssent
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, ENERGY_KILO_WATT_HOUR)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
SCAN_INTERVAL = timedelta(hours=1)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Essent platform."""
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
essent = EssentBase(username, password)
meters = []
for meter in essent.retrieve_meters():
data = essent.retrieve_meter_data(meter)
for tariff in data['values']['LVR'].keys():
meters.append(EssentMeter(
essent,
meter,
data['type'],
tariff,
data['values']['LVR'][tariff]['unit']))
add_devices(meters, True)
class EssentBase():
"""Essent Base."""
def __init__(self, username, password):
"""Initialize the Essent API."""
self._username = username
self._password = password
self._meters = []
self._meter_data = {}
self.update()
def retrieve_meters(self):
"""Retrieve the list of meters."""
return self._meters
def retrieve_meter_data(self, meter):
"""Retrieve the data for this meter."""
return self._meter_data[meter]
@Throttle(timedelta(minutes=30))
def update(self):
"""Retrieve the latest meter data from Essent."""
essent = PyEssent(self._username, self._password)
self._meters = essent.get_EANs()
for meter in self._meters:
self._meter_data[meter] = essent.read_meter(
meter, only_last_meter_reading=True)
class EssentMeter(Entity):
"""Representation of Essent measurements."""
def __init__(self, essent_base, meter, meter_type, tariff, unit):
"""Initialize the sensor."""
self._state = None
self._essent_base = essent_base
self._meter = meter
self._type = meter_type
self._tariff = tariff
self._unit = unit
@property
def name(self):
"""Return the name of the sensor."""
return "Essent {} ({})".format(self._type, self._tariff)
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
if self._unit.lower() == 'kwh':
return ENERGY_KILO_WATT_HOUR
return self._unit
def update(self):
"""Fetch the energy usage."""
# Ensure our data isn't too old
self._essent_base.update()
# Retrieve our meter
data = self._essent_base.retrieve_meter_data(self._meter)
# Set our value
self._state = next(
iter(data['values']['LVR'][self._tariff]['records'].values()))

View File

@ -5,26 +5,31 @@
# 0-12 Heating zones (a.k.a. Zone), and # 0-12 Heating zones (a.k.a. Zone), and
# 0-1 DHW controller, (a.k.a. Boiler) # 0-1 DHW controller, (a.k.a. Boiler)
# The TCS & Zones are implemented as Climate devices, Boiler as a WaterHeater # The TCS & Zones are implemented as Climate devices, Boiler as a WaterHeater
from datetime import timedelta from datetime import datetime, timedelta
import logging import logging
import requests.exceptions import requests.exceptions
import voluptuous as vol import voluptuous as vol
import evohomeclient2
from homeassistant.const import ( from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, CONF_PASSWORD,
EVENT_HOMEASSISTANT_START) EVENT_HOMEASSISTANT_START,
HTTP_SERVICE_UNAVAILABLE, HTTP_TOO_MANY_REQUESTS,
PRECISION_HALVES, TEMP_CELSIUS)
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.entity import Entity
from .const import (
DOMAIN, DATA_EVOHOME, DISPATCHER_EVOHOME, GWS, TCS)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'evohome'
DATA_EVOHOME = 'data_' + DOMAIN
DISPATCHER_EVOHOME = 'dispatcher_' + DOMAIN
CONF_LOCATION_IDX = 'location_idx' CONF_LOCATION_IDX = 'location_idx'
SCAN_INTERVAL_DEFAULT = timedelta(seconds=300) SCAN_INTERVAL_DEFAULT = timedelta(seconds=300)
SCAN_INTERVAL_MINIMUM = timedelta(seconds=180) SCAN_INTERVAL_MINIMUM = timedelta(seconds=180)
@ -43,10 +48,6 @@ CONF_SECRETS = [
CONF_USERNAME, CONF_PASSWORD, CONF_USERNAME, CONF_PASSWORD,
] ]
# These are used to help prevent E501 (line too long) violations.
GWS = 'gateways'
TCS = 'temperatureControlSystems'
# bit masks for dispatcher packets # bit masks for dispatcher packets
EVO_PARENT = 0x01 EVO_PARENT = 0x01
EVO_CHILD = 0x02 EVO_CHILD = 0x02
@ -66,8 +67,6 @@ def setup(hass, hass_config):
scan_interval = timedelta( scan_interval = timedelta(
minutes=(scan_interval.total_seconds() + 59) // 60) minutes=(scan_interval.total_seconds() + 59) // 60)
import evohomeclient2
try: try:
client = evo_data['client'] = evohomeclient2.EvohomeClient( client = evo_data['client'] = evohomeclient2.EvohomeClient(
evo_data['params'][CONF_USERNAME], evo_data['params'][CONF_USERNAME],
@ -145,3 +144,129 @@ def setup(hass, hass_config):
hass.bus.listen(EVENT_HOMEASSISTANT_START, _first_update) hass.bus.listen(EVENT_HOMEASSISTANT_START, _first_update)
return True return True
class EvoDevice(Entity):
"""Base for any Honeywell evohome device.
Such devices include the Controller, (up to 12) Heating Zones and
(optionally) a DHW controller.
"""
def __init__(self, evo_data, client, obj_ref):
"""Initialize the evohome entity."""
self._client = client
self._obj = obj_ref
self._name = None
self._icon = None
self._type = None
self._supported_features = None
self._operation_list = None
self._params = evo_data['params']
self._timers = evo_data['timers']
self._status = {}
self._available = False # should become True after first update()
@callback
def _connect(self, packet):
if packet['to'] & self._type and packet['signal'] == 'refresh':
self.async_schedule_update_ha_state(force_refresh=True)
def _handle_exception(self, err):
try:
raise err
except evohomeclient2.AuthenticationError:
_LOGGER.error(
"Failed to (re)authenticate with the vendor's server. "
"This may be a temporary error. Message is: %s",
err
)
except requests.exceptions.ConnectionError:
# this appears to be common with Honeywell's servers
_LOGGER.warning(
"Unable to connect with the vendor's server. "
"Check your network and the vendor's status page."
)
except requests.exceptions.HTTPError:
if err.response.status_code == HTTP_SERVICE_UNAVAILABLE:
_LOGGER.warning(
"Vendor says their server is currently unavailable. "
"This may be temporary; check the vendor's status page."
)
elif err.response.status_code == HTTP_TOO_MANY_REQUESTS:
_LOGGER.warning(
"The vendor's API rate limit has been exceeded. "
"So will cease polling, and will resume after %s seconds.",
(self._params[CONF_SCAN_INTERVAL] * 3).total_seconds()
)
self._timers['statusUpdated'] = datetime.now() + \
self._params[CONF_SCAN_INTERVAL] * 3
else:
raise # we don't expect/handle any other HTTPErrors
# These properties, methods are from the Entity class
async def async_added_to_hass(self):
"""Run when entity about to be added."""
async_dispatcher_connect(self.hass, DISPATCHER_EVOHOME, self._connect)
@property
def should_poll(self) -> bool:
"""Most evohome devices push their state to HA.
Only the Controller should be polled.
"""
return False
@property
def name(self) -> str:
"""Return the name to use in the frontend UI."""
return self._name
@property
def device_state_attributes(self):
"""Return the device state attributes of the evohome device.
This is state data that is not available otherwise, due to the
restrictions placed upon ClimateDevice properties, etc. by HA.
"""
return {'status': self._status}
@property
def icon(self):
"""Return the icon to use in the frontend UI."""
return self._icon
@property
def available(self) -> bool:
"""Return True if the device is currently available."""
return self._available
@property
def supported_features(self):
"""Get the list of supported features of the device."""
return self._supported_features
# These properties are common to ClimateDevice, WaterHeaterDevice classes
@property
def precision(self):
"""Return the temperature precision to use in the frontend UI."""
return PRECISION_HALVES
@property
def temperature_unit(self):
"""Return the temperature unit to use in the frontend UI."""
return TEMP_CELSIUS
@property
def operation_list(self):
"""Return the list of available operations."""
return self._operation_list

Some files were not shown because too many files have changed in this diff Show More