mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
commit
584bfbaa76
@ -57,7 +57,7 @@ commands:
|
||||
<<# parameters.all >>pip install -q --progress-bar off -r requirements_all.txt -c homeassistant/package_constraints.txt<</ parameters.all>>
|
||||
<<# parameters.test >>pip install -q --progress-bar off -r requirements_test.txt -c homeassistant/package_constraints.txt<</ parameters.test>>
|
||||
<<# parameters.test_all >>pip install -q --progress-bar off -r requirements_test_all.txt -c homeassistant/package_constraints.txt<</ parameters.test_all>>
|
||||
no_output_timeout: 15m
|
||||
no_output_timeout: 15m
|
||||
- save_cache:
|
||||
paths:
|
||||
- ./venv
|
||||
@ -90,7 +90,7 @@ jobs:
|
||||
name: run static check
|
||||
command: |
|
||||
. venv/bin/activate
|
||||
flake8
|
||||
flake8 homeassistant tests script
|
||||
|
||||
- run:
|
||||
name: run static type check
|
||||
|
@ -13,3 +13,4 @@ coverage:
|
||||
url: "secret:TgWDUM4Jw0w7wMJxuxNF/yhSOHglIo1fGwInJnRLEVPy2P2aLimkoK1mtKCowH5TFw+baUXVXT3eAqefbdvIuM8BjRR4aRji95C6CYyD0QHy4N8i7nn1SQkWDPpS8IthYTg07rUDF7s5guurkKv2RrgoCdnnqjAMSzHoExMOF7xUmblMdhBTWJgBpWEhASJy85w/xxjlsE1xoTkzeJu9Q67pTXtRcn+5kb5/vIzPSYg="
|
||||
comment:
|
||||
require_changes: yes
|
||||
branches: master
|
||||
|
14
.coveragerc
14
.coveragerc
@ -22,6 +22,7 @@ omit =
|
||||
homeassistant/components/alarmdotcom/alarm_control_panel.py
|
||||
homeassistant/components/alpha_vantage/sensor.py
|
||||
homeassistant/components/amazon_polly/tts.py
|
||||
homeassistant/components/ambiclimate/climate.py
|
||||
homeassistant/components/ambient_station/*
|
||||
homeassistant/components/amcrest/*
|
||||
homeassistant/components/ampio/*
|
||||
@ -52,6 +53,7 @@ omit =
|
||||
homeassistant/components/bbox/sensor.py
|
||||
homeassistant/components/bh1750/sensor.py
|
||||
homeassistant/components/bitcoin/sensor.py
|
||||
homeassistant/components/bizkaibus/sensor.py
|
||||
homeassistant/components/blink/*
|
||||
homeassistant/components/blinksticklight/light.py
|
||||
homeassistant/components/blinkt/light.py
|
||||
@ -173,6 +175,7 @@ omit =
|
||||
homeassistant/components/esphome/light.py
|
||||
homeassistant/components/esphome/sensor.py
|
||||
homeassistant/components/esphome/switch.py
|
||||
homeassistant/components/essent/sensor.py
|
||||
homeassistant/components/etherscan/sensor.py
|
||||
homeassistant/components/eufy/*
|
||||
homeassistant/components/everlights/light.py
|
||||
@ -275,9 +278,11 @@ omit =
|
||||
homeassistant/components/imap_email_content/sensor.py
|
||||
homeassistant/components/influxdb/sensor.py
|
||||
homeassistant/components/insteon/*
|
||||
homeassistant/components/incomfort/*
|
||||
homeassistant/components/ios/*
|
||||
homeassistant/components/iota/*
|
||||
homeassistant/components/iperf3/*
|
||||
homeassistant/components/iqvia/*
|
||||
homeassistant/components/irish_rail_transport/sensor.py
|
||||
homeassistant/components/iss/binary_sensor.py
|
||||
homeassistant/components/isy994/*
|
||||
@ -344,6 +349,7 @@ omit =
|
||||
homeassistant/components/message_bird/notify.py
|
||||
homeassistant/components/met/weather.py
|
||||
homeassistant/components/meteo_france/*
|
||||
homeassistant/components/meteoalarm/*
|
||||
homeassistant/components/metoffice/sensor.py
|
||||
homeassistant/components/metoffice/weather.py
|
||||
homeassistant/components/microsoft/tts.py
|
||||
@ -418,6 +424,7 @@ omit =
|
||||
homeassistant/components/openweathermap/sensor.py
|
||||
homeassistant/components/openweathermap/weather.py
|
||||
homeassistant/components/opple/light.py
|
||||
homeassistant/components/orangepi_gpio/*
|
||||
homeassistant/components/orvibo/switch.py
|
||||
homeassistant/components/osramlightify/light.py
|
||||
homeassistant/components/otp/sensor.py
|
||||
@ -440,7 +447,6 @@ omit =
|
||||
homeassistant/components/plum_lightpad/*
|
||||
homeassistant/components/pocketcasts/sensor.py
|
||||
homeassistant/components/point/*
|
||||
homeassistant/components/pollen/sensor.py
|
||||
homeassistant/components/postnl/sensor.py
|
||||
homeassistant/components/prezzibenzina/sensor.py
|
||||
homeassistant/components/proliphix/climate.py
|
||||
@ -449,6 +455,7 @@ omit =
|
||||
homeassistant/components/proxy/camera.py
|
||||
homeassistant/components/ps4/__init__.py
|
||||
homeassistant/components/ps4/media_player.py
|
||||
homeassistant/components/ptvsd/*
|
||||
homeassistant/components/pulseaudio_loopback/switch.py
|
||||
homeassistant/components/pushbullet/notify.py
|
||||
homeassistant/components/pushbullet/sensor.py
|
||||
@ -534,9 +541,7 @@ omit =
|
||||
homeassistant/components/smappee/*
|
||||
homeassistant/components/smtp/notify.py
|
||||
homeassistant/components/snapcast/media_player.py
|
||||
homeassistant/components/snmp/device_tracker.py
|
||||
homeassistant/components/snmp/sensor.py
|
||||
homeassistant/components/snmp/switch.py
|
||||
homeassistant/components/snmp/*
|
||||
homeassistant/components/sochain/sensor.py
|
||||
homeassistant/components/socialblade/sensor.py
|
||||
homeassistant/components/solaredge/sensor.py
|
||||
@ -561,6 +566,7 @@ omit =
|
||||
homeassistant/components/swiss_public_transport/sensor.py
|
||||
homeassistant/components/swisscom/device_tracker.py
|
||||
homeassistant/components/switchbot/switch.py
|
||||
homeassistant/components/switcher_kis/switch.py
|
||||
homeassistant/components/switchmate/switch.py
|
||||
homeassistant/components/syncthru/sensor.py
|
||||
homeassistant/components/synology/camera.py
|
||||
|
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -7,7 +7,7 @@
|
||||
|
||||
**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):
|
||||
```yaml
|
||||
@ -18,21 +18,18 @@
|
||||
- [ ] The code change is tested and works locally.
|
||||
- [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] 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:
|
||||
- [ ] 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:
|
||||
- [ ] [_The manifest file_][manifest-docs] has all fields filled out correctly ([example][ex-manifest]).
|
||||
- [ ] New dependencies have been added to `requirements` in the manifest ([example][ex-requir]).
|
||||
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
|
||||
- [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
|
||||
- [ ] New files were added to `.coveragerc`.
|
||||
- [ ] [_The manifest file_][manifest-docs] has all fields filled out correctly. Update and include derived files by running `python3 -m script.hassfest`.
|
||||
- [ ] New or updated dependencies have been added to `requirements_all.txt` by running `python3 -m script.gen_requirements_all`.
|
||||
- [ ] Untested files have been added to `.coveragerc`.
|
||||
|
||||
If the code does not interact with devices:
|
||||
- [ ] 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
|
||||
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mobile_app/manifest.json#L5
|
||||
[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_
|
||||
[dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
|
||||
[manifest-docs]: https://developers.home-assistant.io/docs/en/creating_integration_manifest.html
|
||||
|
14
.github/main.workflow
vendored
14
.github/main.workflow
vendored
@ -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
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
config/*
|
||||
config2/*
|
||||
|
||||
tests/testing_config/deps
|
||||
tests/testing_config/home-assistant.log
|
||||
@ -84,7 +85,7 @@ Scripts/
|
||||
# vimmy stuff
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
tags
|
||||
ctags.tmp
|
||||
|
||||
# vagrant stuff
|
||||
|
33
.travis.yml
Normal file
33
.travis.yml
Normal 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
|
23
CODEOWNERS
23
CODEOWNERS
@ -21,6 +21,7 @@ homeassistant/components/airvisual/* @bachya
|
||||
homeassistant/components/alarm_control_panel/* @colinodell
|
||||
homeassistant/components/alpha_vantage/* @fabaff
|
||||
homeassistant/components/amazon_polly/* @robbiet480
|
||||
homeassistant/components/ambiclimate/* @danielhiversen
|
||||
homeassistant/components/ambient_station/* @bachya
|
||||
homeassistant/components/api/* @home-assistant/core
|
||||
homeassistant/components/arduino/* @fabaff
|
||||
@ -32,6 +33,7 @@ homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/aws/* @awarecan @robbiet480
|
||||
homeassistant/components/axis/* @kane610
|
||||
homeassistant/components/bitcoin/* @fabaff
|
||||
homeassistant/components/bizkaibus/* @UgaitzEtxebarria
|
||||
homeassistant/components/blink/* @fronzbot
|
||||
homeassistant/components/bmw_connected_drive/* @ChristianKuehnel
|
||||
homeassistant/components/braviatv/* @robbiet480
|
||||
@ -66,10 +68,13 @@ homeassistant/components/egardia/* @jeroenterheerdt
|
||||
homeassistant/components/eight_sleep/* @mezz64
|
||||
homeassistant/components/emby/* @mezz64
|
||||
homeassistant/components/enigma2/* @fbradyirl
|
||||
homeassistant/components/enocean/* @bdurrer
|
||||
homeassistant/components/ephember/* @ttroy50
|
||||
homeassistant/components/epsonworkforce/* @ThaStealth
|
||||
homeassistant/components/eq3btsmart/* @rytilahti
|
||||
homeassistant/components/esphome/* @OttoWinter
|
||||
homeassistant/components/essent/* @TheLastProject
|
||||
homeassistant/components/evohome/* @zxdavb
|
||||
homeassistant/components/file/* @fabaff
|
||||
homeassistant/components/filter/* @dgomes
|
||||
homeassistant/components/fitbit/* @robbiet480
|
||||
@ -80,6 +85,7 @@ homeassistant/components/foursquare/* @robbiet480
|
||||
homeassistant/components/freebox/* @snoof85
|
||||
homeassistant/components/frontend/* @home-assistant/core
|
||||
homeassistant/components/gearbest/* @HerrHofrat
|
||||
homeassistant/components/geniushub/* @zxdavb
|
||||
homeassistant/components/gitter/* @fabaff
|
||||
homeassistant/components/glances/* @fabaff
|
||||
homeassistant/components/gntp/* @robbiet480
|
||||
@ -99,6 +105,7 @@ homeassistant/components/history_graph/* @andrey-git
|
||||
homeassistant/components/hive/* @Rendili @KJonline
|
||||
homeassistant/components/homeassistant/* @home-assistant/core
|
||||
homeassistant/components/homekit/* @cdce8p
|
||||
homeassistant/components/homekit_controller/* @Jc2k
|
||||
homeassistant/components/homematic/* @pvizeli @danielperna84
|
||||
homeassistant/components/html5/* @robbiet480
|
||||
homeassistant/components/http/* @home-assistant/core
|
||||
@ -106,6 +113,7 @@ homeassistant/components/huawei_lte/* @scop
|
||||
homeassistant/components/huawei_router/* @abmantis
|
||||
homeassistant/components/hue/* @balloob
|
||||
homeassistant/components/ign_sismologia/* @exxamalte
|
||||
homeassistant/components/incomfort/* @zxdavb
|
||||
homeassistant/components/influxdb/* @fabaff
|
||||
homeassistant/components/input_boolean/* @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/ios/* @robbiet480
|
||||
homeassistant/components/ipma/* @dgomes
|
||||
homeassistant/components/iqvia/* @bachya
|
||||
homeassistant/components/irish_rail_transport/* @ttroy50
|
||||
homeassistant/components/jewish_calendar/* @tsvi
|
||||
homeassistant/components/knx/* @Julius2342
|
||||
@ -137,6 +146,7 @@ homeassistant/components/matrix/* @tinloaf
|
||||
homeassistant/components/mediaroom/* @dgomes
|
||||
homeassistant/components/melissa/* @kennedyshead
|
||||
homeassistant/components/met/* @danielhiversen
|
||||
homeassistant/components/meteoalarm/* @rolfberkenbosch
|
||||
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/mill/* @danielhiversen
|
||||
homeassistant/components/min_max/* @fabaff
|
||||
@ -150,24 +160,28 @@ homeassistant/components/nello/* @pschmitt
|
||||
homeassistant/components/ness_alarm/* @nickw444
|
||||
homeassistant/components/nest/* @awarecan
|
||||
homeassistant/components/netdata/* @fabaff
|
||||
homeassistant/components/nextbus/* @vividboarder
|
||||
homeassistant/components/nissan_leaf/* @filcole
|
||||
homeassistant/components/nmbs/* @thibmaek
|
||||
homeassistant/components/no_ip/* @fabaff
|
||||
homeassistant/components/notify/* @flowolf
|
||||
homeassistant/components/notify/* @home-assistant/core
|
||||
homeassistant/components/nsw_fuel_station/* @nickw444
|
||||
homeassistant/components/nuki/* @pschmitt
|
||||
homeassistant/components/ohmconnect/* @robbiet480
|
||||
homeassistant/components/onboarding/* @home-assistant/core
|
||||
homeassistant/components/openuv/* @bachya
|
||||
homeassistant/components/openweathermap/* @fabaff
|
||||
homeassistant/components/orangepi_gpio/* @pascallj
|
||||
homeassistant/components/owlet/* @oblogic7
|
||||
homeassistant/components/panel_custom/* @home-assistant/core
|
||||
homeassistant/components/panel_iframe/* @home-assistant/core
|
||||
homeassistant/components/persistent_notification/* @home-assistant/core
|
||||
homeassistant/components/philips_js/* @elupus
|
||||
homeassistant/components/pi_hole/* @fabaff
|
||||
homeassistant/components/plant/* @ChristianKuehnel
|
||||
homeassistant/components/point/* @fredrike
|
||||
homeassistant/components/pollen/* @bachya
|
||||
homeassistant/components/ps4/* @ktnrg45
|
||||
homeassistant/components/ptvsd/* @swamp-ig
|
||||
homeassistant/components/push/* @dgomes
|
||||
homeassistant/components/pvoutput/* @fabaff
|
||||
homeassistant/components/qnap/* @colinodell
|
||||
@ -204,7 +218,9 @@ homeassistant/components/supla/* @mwegrzynek
|
||||
homeassistant/components/swiss_hydrological_data/* @fabaff
|
||||
homeassistant/components/swiss_public_transport/* @fabaff
|
||||
homeassistant/components/switchbot/* @danielhiversen
|
||||
homeassistant/components/switcher_kis/* @tomerfi
|
||||
homeassistant/components/switchmate/* @danielhiversen
|
||||
homeassistant/components/syncthru/* @nielstron
|
||||
homeassistant/components/synology_srm/* @aerialls
|
||||
homeassistant/components/syslog/* @fabaff
|
||||
homeassistant/components/sytadin/* @gautric
|
||||
@ -235,6 +251,7 @@ homeassistant/components/uptimerobot/* @ludeeus
|
||||
homeassistant/components/utility_meter/* @dgomes
|
||||
homeassistant/components/velux/* @Julius2342
|
||||
homeassistant/components/version/* @fabaff
|
||||
homeassistant/components/vizio/* @raman325
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
homeassistant/components/weather/* @fabaff
|
||||
homeassistant/components/weblink/* @home-assistant/core
|
||||
@ -245,7 +262,7 @@ homeassistant/components/xfinity/* @cisasteelersfan
|
||||
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
|
||||
homeassistant/components/xiaomi_miio/* @rytilahti @syssi
|
||||
homeassistant/components/xiaomi_tv/* @fattdev
|
||||
homeassistant/components/xmpp/* @fabaff
|
||||
homeassistant/components/xmpp/* @fabaff @flowolf
|
||||
homeassistant/components/yamaha_musiccast/* @jalmeroth
|
||||
homeassistant/components/yeelight/* @rytilahti @zewelor
|
||||
homeassistant/components/yeelightsunflower/* @lindsaymarkward
|
||||
|
186
azure-pipelines.yml
Normal file
186
azure-pipelines.yml
Normal 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
|
@ -7,8 +7,9 @@ import platform
|
||||
import subprocess
|
||||
import sys
|
||||
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.const import (
|
||||
@ -18,6 +19,9 @@ from homeassistant.const import (
|
||||
RESTART_EXIT_CODE,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant import core
|
||||
|
||||
|
||||
def set_loop() -> None:
|
||||
"""Attempt to use uvloop."""
|
||||
@ -86,10 +90,12 @@ def ensure_config_path(config_dir: str) -> None:
|
||||
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."""
|
||||
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:
|
||||
print('Error getting configuration path')
|
||||
@ -261,6 +267,7 @@ def cmdline() -> List[str]:
|
||||
async def setup_and_run_hass(config_dir: str,
|
||||
args: argparse.Namespace) -> int:
|
||||
"""Set up HASS and run."""
|
||||
# pylint: disable=redefined-outer-name
|
||||
from homeassistant import bootstrap, core
|
||||
|
||||
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,
|
||||
log_file=args.log_file, log_no_color=args.log_no_color)
|
||||
else:
|
||||
config_file = ensure_config_file(config_dir)
|
||||
config_file = await ensure_config_file(hass, config_dir)
|
||||
print('Config directory:', config_dir)
|
||||
await bootstrap.async_from_config_file(
|
||||
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:
|
||||
try_to_restart()
|
||||
|
||||
return exit_code # type: ignore # mypy cannot yet infer it
|
||||
return exit_code # type: ignore
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
||||
|
||||
REQUIREMENTS = ['pyotp==2.2.6']
|
||||
REQUIREMENTS = ['pyotp==2.2.7']
|
||||
|
||||
CONF_MESSAGE = 'message'
|
||||
|
||||
|
@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
|
||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||
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({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
@ -11,6 +11,7 @@ from .models import PermissionLookup
|
||||
from .types import PolicyType
|
||||
from .entities import ENTITY_POLICY_SCHEMA, compile_entities
|
||||
from .merge import merge_policies # noqa
|
||||
from .util import test_all
|
||||
|
||||
|
||||
POLICY_SCHEMA = vol.Schema({
|
||||
@ -29,6 +30,10 @@ class AbstractPermissions:
|
||||
"""Return a function that can test entity access."""
|
||||
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:
|
||||
"""Check if we can access entity."""
|
||||
entity_func = self._cached_entity_func
|
||||
@ -48,6 +53,10 @@ class PolicyPermissions(AbstractPermissions):
|
||||
self._policy = policy
|
||||
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]:
|
||||
"""Return a function that can test entity access."""
|
||||
return compile_entities(self._policy.get(CAT_ENTITIES),
|
||||
@ -65,6 +74,10 @@ class _OwnerPermissions(AbstractPermissions):
|
||||
|
||||
# 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]:
|
||||
"""Return a function that can test entity access."""
|
||||
return lambda entity_id, key: True
|
||||
|
@ -3,6 +3,7 @@ from functools import wraps
|
||||
|
||||
from typing import Callable, Dict, List, Optional, Union, cast # noqa: F401
|
||||
|
||||
from .const import SUBCAT_ALL
|
||||
from .models import PermissionLookup
|
||||
from .types import CategoryType, SubCategoryDict, ValueType
|
||||
|
||||
@ -96,3 +97,16 @@ def _gen_dict_test_func(
|
||||
return schema.get(key)
|
||||
|
||||
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)
|
||||
|
@ -26,6 +26,7 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
# hass.data key for logging information.
|
||||
DATA_LOGGING = 'logging'
|
||||
|
||||
DEBUGGER_INTEGRATIONS = {'ptvsd', }
|
||||
CORE_INTEGRATIONS = ('homeassistant', 'persistent_notification')
|
||||
LOGGING_INTEGRATIONS = {'logger', 'system_log'}
|
||||
STAGE_1_INTEGRATIONS = {
|
||||
@ -306,6 +307,15 @@ async def _async_set_up_integrations(
|
||||
"""Set up all the integrations."""
|
||||
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
|
||||
# and integrations that need faster initialization.
|
||||
resolved_domains_task = asyncio.gather(*[
|
||||
@ -339,7 +349,7 @@ async def _async_set_up_integrations(
|
||||
stage_2_domains = domains - logging_domains - stage_1_domains
|
||||
|
||||
if logging_domains:
|
||||
_LOGGER.debug("Setting up %s", logging_domains)
|
||||
_LOGGER.info("Setting up %s", logging_domains)
|
||||
|
||||
await asyncio.gather(*[
|
||||
async_setup_component(hass, domain, config)
|
||||
|
@ -31,9 +31,11 @@ CONF_ADS_TYPE = 'adstype'
|
||||
CONF_ADS_VALUE = 'value'
|
||||
CONF_ADS_VAR = 'adsvar'
|
||||
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
|
||||
CONF_ADS_VAR_POSITION = 'adsvar_position'
|
||||
|
||||
STATE_KEY_STATE = 'state'
|
||||
STATE_KEY_BRIGHTNESS = 'brightness'
|
||||
STATE_KEY_POSITION = 'position'
|
||||
|
||||
DOMAIN = 'ads'
|
||||
|
||||
|
165
homeassistant/components/ads/cover.py
Normal file
165
homeassistant/components/ads/cover.py
Normal 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
|
@ -449,9 +449,9 @@ class _AlexaPowerController(_AlexaInterface):
|
||||
if name != 'powerState':
|
||||
raise _UnsupportedProperty(name)
|
||||
|
||||
if self.entity.state == STATE_ON:
|
||||
return 'ON'
|
||||
return 'OFF'
|
||||
if self.entity.state == STATE_OFF:
|
||||
return 'OFF'
|
||||
return 'ON'
|
||||
|
||||
|
||||
class _AlexaLockController(_AlexaInterface):
|
||||
|
23
homeassistant/components/ambiclimate/.translations/ca.json
Normal file
23
homeassistant/components/ambiclimate/.translations/ca.json
Normal 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"
|
||||
}
|
||||
}
|
15
homeassistant/components/ambiclimate/.translations/cs.json
Normal file
15
homeassistant/components/ambiclimate/.translations/cs.json
Normal 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"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/de.json
Normal file
23
homeassistant/components/ambiclimate/.translations/de.json
Normal 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"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/en.json
Normal file
23
homeassistant/components/ambiclimate/.translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/ko.json
Normal file
23
homeassistant/components/ambiclimate/.translations/ko.json
Normal 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"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/lb.json
Normal file
23
homeassistant/components/ambiclimate/.translations/lb.json
Normal 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"
|
||||
}
|
||||
}
|
23
homeassistant/components/ambiclimate/.translations/ru.json
Normal file
23
homeassistant/components/ambiclimate/.translations/ru.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
44
homeassistant/components/ambiclimate/__init__.py
Normal file
44
homeassistant/components/ambiclimate/__init__.py
Normal 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
|
230
homeassistant/components/ambiclimate/climate.py
Normal file
230
homeassistant/components/ambiclimate/climate.py
Normal 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()
|
153
homeassistant/components/ambiclimate/config_flow.py
Normal file
153
homeassistant/components/ambiclimate/config_flow.py
Normal 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!"
|
14
homeassistant/components/ambiclimate/const.py
Normal file
14
homeassistant/components/ambiclimate/const.py
Normal 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'
|
12
homeassistant/components/ambiclimate/manifest.json
Normal file
12
homeassistant/components/ambiclimate/manifest.json
Normal 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"
|
||||
]
|
||||
}
|
36
homeassistant/components/ambiclimate/services.yaml
Normal file
36
homeassistant/components/ambiclimate/services.yaml
Normal 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
|
23
homeassistant/components/ambiclimate/strings.json
Normal file
23
homeassistant/components/ambiclimate/strings.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
@ -5,16 +5,30 @@ from datetime import timedelta
|
||||
import aiohttp
|
||||
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 (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL,
|
||||
HTTP_BASIC_AUTHENTICATION)
|
||||
ATTR_ENTITY_ID, CONF_AUTHENTICATION, CONF_BINARY_SENSORS, CONF_HOST,
|
||||
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS,
|
||||
CONF_SWITCHES, CONF_USERNAME, ENTITY_MATCH_ALL, HTTP_BASIC_AUTHENTICATION)
|
||||
from homeassistant.exceptions import Unauthorized, UnknownUser
|
||||
from homeassistant.helpers import discovery
|
||||
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__)
|
||||
|
||||
CONF_AUTHENTICATION = 'authentication'
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
@ -22,12 +36,7 @@ CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'snapshot'
|
||||
DEFAULT_ARGUMENTS = '-pred 1'
|
||||
TIMEOUT = 10
|
||||
|
||||
DATA_AMCREST = 'amcrest'
|
||||
DOMAIN = 'amcrest'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
@ -43,70 +52,60 @@ AUTHENTICATION_LIST = {
|
||||
'basic': 'basic'
|
||||
}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
BINARY_SENSORS = {
|
||||
'motion_detected': 'Motion Detected'
|
||||
}
|
||||
|
||||
# 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:
|
||||
def _deprecated_sensor_values(sensors):
|
||||
if SENSOR_MOTION_DETECTOR in sensors:
|
||||
_LOGGER.warning(
|
||||
'sensors option %s is deprecated. '
|
||||
'Please remove from your configuration and '
|
||||
'use binary_sensors option motion_detected instead.',
|
||||
SENSOR_MOTION_DETECTOR)
|
||||
return value
|
||||
"The 'sensors' option value '%s' is deprecated, "
|
||||
"please remove it from your configuration and use "
|
||||
"the 'binary_sensors' option with value 'motion_detected' "
|
||||
"instead.", SENSOR_MOTION_DETECTOR)
|
||||
return sensors
|
||||
|
||||
|
||||
def _has_unique_names(value):
|
||||
names = [camera[CONF_NAME] for camera in value]
|
||||
def _deprecated_switches(config):
|
||||
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)
|
||||
return value
|
||||
return devices
|
||||
|
||||
|
||||
AMCREST_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS):
|
||||
cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_BINARY_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)], _deprecated_sensors),
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
})
|
||||
AMCREST_SCHEMA = vol.All(
|
||||
vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS):
|
||||
cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_BINARY_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)],
|
||||
_deprecated_sensor_values),
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
}),
|
||||
_deprecated_switches
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
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."""
|
||||
from amcrest import AmcrestCamera, AmcrestError
|
||||
|
||||
hass.data.setdefault(DATA_AMCREST, {})
|
||||
amcrest_cams = config[DOMAIN]
|
||||
hass.data.setdefault(DATA_AMCREST, {'devices': {}, 'cameras': []})
|
||||
devices = config[DOMAIN]
|
||||
|
||||
for device in amcrest_cams:
|
||||
for device in devices:
|
||||
name = device[CONF_NAME]
|
||||
username = device[CONF_USERNAME]
|
||||
password = device[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
camera = AmcrestCamera(device[CONF_HOST],
|
||||
device[CONF_PORT],
|
||||
username,
|
||||
password).camera
|
||||
api = AmcrestCamera(device[CONF_HOST],
|
||||
device[CONF_PORT],
|
||||
username,
|
||||
password).camera
|
||||
# pylint: disable=pointless-statement
|
||||
camera.current_time
|
||||
# Test camera communications.
|
||||
api.current_time
|
||||
|
||||
except AmcrestError as 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)
|
||||
sensors = device.get(CONF_SENSORS)
|
||||
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
|
||||
# only valid for mjpeg streaming
|
||||
@ -157,47 +157,97 @@ def setup(hass, config):
|
||||
else:
|
||||
authentication = None
|
||||
|
||||
hass.data[DATA_AMCREST][name] = AmcrestDevice(
|
||||
camera, name, authentication, ffmpeg_arguments, stream_source,
|
||||
hass.data[DATA_AMCREST]['devices'][name] = AmcrestDevice(
|
||||
api, authentication, ffmpeg_arguments, stream_source,
|
||||
resolution)
|
||||
|
||||
discovery.load_platform(
|
||||
hass, 'camera', DOMAIN, {
|
||||
hass, CAMERA, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
}, config)
|
||||
|
||||
if binary_sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {
|
||||
hass, BINARY_SENSOR, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_BINARY_SENSORS: binary_sensors
|
||||
}, config)
|
||||
|
||||
if sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
hass, SENSOR, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config)
|
||||
|
||||
if switches:
|
||||
discovery.load_platform(
|
||||
hass, 'switch', DOMAIN, {
|
||||
hass, SWITCH, DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_SWITCHES: switches
|
||||
}, 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:
|
||||
"""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):
|
||||
"""Initialize the entity."""
|
||||
self.device = camera
|
||||
self.name = name
|
||||
self.api = api
|
||||
self.authentication = authentication
|
||||
self.ffmpeg_arguments = ffmpeg_arguments
|
||||
self.stream_source = stream_source
|
||||
|
@ -5,38 +5,39 @@ import logging
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, DEVICE_CLASS_MOTION)
|
||||
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__)
|
||||
|
||||
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):
|
||||
"""Set up a binary sensor for an Amcrest IP Camera."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
binary_sensors = discovery_info[CONF_BINARY_SENSORS]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
amcrest_binary_sensors = []
|
||||
for sensor_type in binary_sensors:
|
||||
amcrest_binary_sensors.append(
|
||||
AmcrestBinarySensor(amcrest.name, amcrest.device, sensor_type))
|
||||
|
||||
async_add_devices(amcrest_binary_sensors, True)
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
async_add_entities(
|
||||
[AmcrestBinarySensor(name, device, sensor_type)
|
||||
for sensor_type in discovery_info[CONF_BINARY_SENSORS]],
|
||||
True)
|
||||
|
||||
|
||||
class AmcrestBinarySensor(BinarySensorDevice):
|
||||
"""Binary sensor for Amcrest camera."""
|
||||
|
||||
def __init__(self, name, camera, sensor_type):
|
||||
def __init__(self, name, device, sensor_type):
|
||||
"""Initialize entity."""
|
||||
self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type])
|
||||
self._camera = camera
|
||||
self._api = device.api
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
|
||||
@ -62,7 +63,7 @@ class AmcrestBinarySensor(BinarySensorDevice):
|
||||
_LOGGER.debug('Pulling data from %s binary sensor', self._name)
|
||||
|
||||
try:
|
||||
self._state = self._camera.is_motion_detected
|
||||
self._state = self._api.is_motion_detected
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not update %s binary sensor due to error: %s',
|
||||
|
@ -2,18 +2,72 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
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.const import CONF_NAME
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, STATE_ON, STATE_OFF)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_stream, async_aiohttp_proxy_web,
|
||||
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__)
|
||||
|
||||
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,
|
||||
discovery_info=None):
|
||||
@ -21,28 +75,33 @@ async def async_setup_platform(hass, config, async_add_entities,
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
async_add_entities([AmcrestCam(hass, amcrest)], True)
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
async_add_entities([
|
||||
AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True)
|
||||
|
||||
|
||||
class AmcrestCam(Camera):
|
||||
"""An implementation of an Amcrest IP camera."""
|
||||
|
||||
def __init__(self, hass, amcrest):
|
||||
def __init__(self, name, device, ffmpeg):
|
||||
"""Initialize an Amcrest camera."""
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._name = amcrest.name
|
||||
self._camera = amcrest.device
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = amcrest.ffmpeg_arguments
|
||||
self._stream_source = amcrest.stream_source
|
||||
self._resolution = amcrest.resolution
|
||||
self._token = self._auth = amcrest.authentication
|
||||
super().__init__()
|
||||
self._name = name
|
||||
self._api = device.api
|
||||
self._ffmpeg = ffmpeg
|
||||
self._ffmpeg_arguments = device.ffmpeg_arguments
|
||||
self._stream_source = device.stream_source
|
||||
self._resolution = device.resolution
|
||||
self._token = self._auth = device.authentication
|
||||
self._is_recording = False
|
||||
self._motion_detection_enabled = None
|
||||
self._model = None
|
||||
self._audio_enabled = None
|
||||
self._motion_recording_enabled = None
|
||||
self._color_bw = None
|
||||
self._snapshot_lock = asyncio.Lock()
|
||||
self._unsub_dispatcher = []
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
@ -56,7 +115,7 @@ class AmcrestCam(Camera):
|
||||
try:
|
||||
# Send the request to snap a picture and return raw jpg data
|
||||
response = await self.hass.async_add_executor_job(
|
||||
self._camera.snapshot, self._resolution)
|
||||
self._api.snapshot, self._resolution)
|
||||
return response.data
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
@ -67,15 +126,16 @@ class AmcrestCam(Camera):
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Return an MJPEG stream."""
|
||||
# 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)
|
||||
|
||||
if self._stream_source == STREAM_SOURCE_LIST['mjpeg']:
|
||||
if self._stream_source == 'mjpeg':
|
||||
# stream an MJPEG image stream directly from the camera
|
||||
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(
|
||||
streaming_url, auth=self._token, timeout=TIMEOUT)
|
||||
streaming_url, auth=self._token,
|
||||
timeout=CAMERA_WEB_SESSION_TIMEOUT)
|
||||
|
||||
return await async_aiohttp_proxy_web(
|
||||
self.hass, request, stream_coro)
|
||||
@ -83,7 +143,7 @@ class AmcrestCam(Camera):
|
||||
# streaming via ffmpeg
|
||||
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)
|
||||
await stream.open_camera(
|
||||
streaming_url, extra_cmd=self._ffmpeg_arguments)
|
||||
@ -103,6 +163,19 @@ class AmcrestCam(Camera):
|
||||
"""Return the name of this camera."""
|
||||
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
|
||||
def supported_features(self):
|
||||
"""Return supported features."""
|
||||
@ -120,6 +193,11 @@ class AmcrestCam(Camera):
|
||||
"""Return the camera brand."""
|
||||
return 'Amcrest'
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Return the camera motion detection status."""
|
||||
return self._motion_detection_enabled
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return the camera model."""
|
||||
@ -128,7 +206,7 @@ class AmcrestCam(Camera):
|
||||
@property
|
||||
def stream_source(self):
|
||||
"""Return the source of the stream."""
|
||||
return self._camera.rtsp_url(typeno=self._resolution)
|
||||
return self._api.rtsp_url(typeno=self._resolution)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@ -137,6 +215,21 @@ class AmcrestCam(Camera):
|
||||
|
||||
# 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):
|
||||
"""Update entity status."""
|
||||
from amcrest import AmcrestError
|
||||
@ -144,15 +237,21 @@ class AmcrestCam(Camera):
|
||||
_LOGGER.debug('Pulling data from %s camera', self.name)
|
||||
if self._model is None:
|
||||
try:
|
||||
self._model = self._camera.device_type.split('=')[-1].strip()
|
||||
self._model = self._api.device_type.split('=')[-1].strip()
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not get %s camera model due to error: %s',
|
||||
self.name, error)
|
||||
self._model = ''
|
||||
try:
|
||||
self.is_streaming = self._camera.video_enabled
|
||||
self._is_recording = self._camera.record_mode == 'Manual'
|
||||
self.is_streaming = self._api.video_enabled
|
||||
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:
|
||||
_LOGGER.error(
|
||||
'Could not get %s camera attributes due to error: %s',
|
||||
@ -168,14 +267,71 @@ class AmcrestCam(Camera):
|
||||
"""Turn on camera."""
|
||||
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):
|
||||
"""Enable or disable camera video stream."""
|
||||
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:
|
||||
self._camera.video_enabled = enable
|
||||
self._api.video_enabled = enable
|
||||
except AmcrestError as error:
|
||||
_LOGGER.error(
|
||||
'Could not %s %s camera video stream due to error: %s',
|
||||
@ -183,3 +339,103 @@ class AmcrestCam(Camera):
|
||||
else:
|
||||
self.is_streaming = enable
|
||||
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)
|
||||
|
7
homeassistant/components/amcrest/const.py
Normal file
7
homeassistant/components/amcrest/const.py
Normal 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
|
10
homeassistant/components/amcrest/helpers.py
Normal file
10
homeassistant/components/amcrest/helpers.py
Normal 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
|
@ -3,7 +3,7 @@
|
||||
"name": "Amcrest",
|
||||
"documentation": "https://www.home-assistant.io/components/amcrest",
|
||||
"requirements": [
|
||||
"amcrest==1.3.0"
|
||||
"amcrest==1.4.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"ffmpeg"
|
||||
|
@ -5,11 +5,19 @@ import logging
|
||||
from homeassistant.const import CONF_NAME, CONF_SENSORS
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import DATA_AMCREST, SENSORS
|
||||
from .const import DATA_AMCREST, SENSOR_SCAN_INTERVAL_SECS
|
||||
|
||||
_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(
|
||||
@ -18,30 +26,26 @@ async def async_setup_platform(
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
device_name = discovery_info[CONF_NAME]
|
||||
sensors = discovery_info[CONF_SENSORS]
|
||||
amcrest = hass.data[DATA_AMCREST][device_name]
|
||||
|
||||
amcrest_sensors = []
|
||||
for sensor_type in sensors:
|
||||
amcrest_sensors.append(
|
||||
AmcrestSensor(amcrest.name, amcrest.device, sensor_type))
|
||||
|
||||
async_add_entities(amcrest_sensors, True)
|
||||
name = discovery_info[CONF_NAME]
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
async_add_entities(
|
||||
[AmcrestSensor(name, device, sensor_type)
|
||||
for sensor_type in discovery_info[CONF_SENSORS]],
|
||||
True)
|
||||
|
||||
|
||||
class AmcrestSensor(Entity):
|
||||
"""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."""
|
||||
self._attrs = {}
|
||||
self._camera = camera
|
||||
self._name = '{} {}'.format(name, SENSORS[sensor_type][0])
|
||||
self._api = device.api
|
||||
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._attrs = {}
|
||||
self._unit_of_measurement = SENSORS[sensor_type][1]
|
||||
self._icon = SENSORS[sensor_type][2]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -66,22 +70,30 @@ class AmcrestSensor(Entity):
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the units of measurement."""
|
||||
return SENSORS.get(self._sensor_type)[1]
|
||||
return self._unit_of_measurement
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
_LOGGER.debug("Pulling data from %s sensor.", self._name)
|
||||
|
||||
if self._sensor_type == 'motion_detector':
|
||||
self._state = self._camera.is_motion_detected
|
||||
self._attrs['Record Mode'] = self._camera.record_mode
|
||||
self._state = self._api.is_motion_detected
|
||||
self._attrs['Record Mode'] = self._api.record_mode
|
||||
|
||||
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':
|
||||
sd_used = self._camera.storage_used
|
||||
sd_total = self._camera.storage_total
|
||||
self._attrs['Total'] = '{0} {1}'.format(*sd_total)
|
||||
self._attrs['Used'] = '{0} {1}'.format(*sd_used)
|
||||
self._state = self._camera.storage_used_percent
|
||||
storage = self._api.storage_all
|
||||
try:
|
||||
self._attrs['Total'] = '{:.2f} {}'.format(*storage['total'])
|
||||
except ValueError:
|
||||
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']
|
||||
|
75
homeassistant/components/amcrest/services.yaml
Normal file
75
homeassistant/components/amcrest/services.yaml
Normal 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'
|
@ -1,13 +1,19 @@
|
||||
"""Support for toggling Amcrest IP camera settings."""
|
||||
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 . import DATA_AMCREST, SWITCHES
|
||||
from .const import DATA_AMCREST
|
||||
|
||||
_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(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
@ -16,67 +22,58 @@ async def async_setup_platform(
|
||||
return
|
||||
|
||||
name = discovery_info[CONF_NAME]
|
||||
switches = discovery_info[CONF_SWITCHES]
|
||||
camera = hass.data[DATA_AMCREST][name].device
|
||||
|
||||
all_switches = []
|
||||
|
||||
for setting in switches:
|
||||
all_switches.append(AmcrestSwitch(setting, camera, name))
|
||||
|
||||
async_add_entities(all_switches, True)
|
||||
device = hass.data[DATA_AMCREST]['devices'][name]
|
||||
async_add_entities(
|
||||
[AmcrestSwitch(name, device, setting)
|
||||
for setting in discovery_info[CONF_SWITCHES]],
|
||||
True)
|
||||
|
||||
|
||||
class AmcrestSwitch(ToggleEntity):
|
||||
"""Representation of an Amcrest IP camera switch."""
|
||||
|
||||
def __init__(self, setting, camera, name):
|
||||
def __init__(self, name, device, setting):
|
||||
"""Initialize the Amcrest switch."""
|
||||
self._name = '{} {}'.format(name, SWITCHES[setting][0])
|
||||
self._api = device.api
|
||||
self._setting = setting
|
||||
self._camera = camera
|
||||
self._name = '{} {}'.format(SWITCHES[setting][0], name)
|
||||
self._state = False
|
||||
self._icon = SWITCHES[setting][1]
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the switch if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the switch."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self._state == STATE_ON
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn setting on."""
|
||||
if self._setting == 'motion_detection':
|
||||
self._camera.motion_detection = 'true'
|
||||
self._api.motion_detection = 'true'
|
||||
elif self._setting == 'motion_recording':
|
||||
self._camera.motion_recording = 'true'
|
||||
self._api.motion_recording = 'true'
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn setting off."""
|
||||
if self._setting == 'motion_detection':
|
||||
self._camera.motion_detection = 'false'
|
||||
self._api.motion_detection = 'false'
|
||||
elif self._setting == 'motion_recording':
|
||||
self._camera.motion_recording = 'false'
|
||||
self._api.motion_recording = 'false'
|
||||
|
||||
def update(self):
|
||||
"""Update setting state."""
|
||||
_LOGGER.debug("Polling state for setting: %s ", self._name)
|
||||
|
||||
if self._setting == 'motion_detection':
|
||||
detection = self._camera.is_motion_detector_on()
|
||||
detection = self._api.is_motion_detector_on()
|
||||
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
|
||||
def icon(self):
|
||||
|
@ -21,11 +21,11 @@
|
||||
},
|
||||
"totp": {
|
||||
"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": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
|
@ -97,8 +97,7 @@ class AuthProvidersView(HomeAssistantView):
|
||||
async def get(self, request):
|
||||
"""Get available auth providers."""
|
||||
hass = request.app['hass']
|
||||
|
||||
if not hass.components.onboarding.async_is_onboarded():
|
||||
if not hass.components.onboarding.async_is_user_onboarded():
|
||||
return self.json_message(
|
||||
message='Onboarding not finished',
|
||||
status_code=400,
|
||||
|
@ -31,6 +31,6 @@ async def async_trigger(hass, config, action, automation_info):
|
||||
'from_state': from_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)
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Constants for the Axis component."""
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger('homeassistant.components.axis')
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = 'axis'
|
||||
|
||||
|
1
homeassistant/components/bizkaibus/__init__.py
Normal file
1
homeassistant/components/bizkaibus/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""The Bizkaibus bus tracker component."""
|
8
homeassistant/components/bizkaibus/manifest.json
Normal file
8
homeassistant/components/bizkaibus/manifest.json
Normal 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"]
|
||||
}
|
88
homeassistant/components/bizkaibus/sensor.py
Executable file
88
homeassistant/components/bizkaibus/sensor.py
Executable 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
|
@ -3,7 +3,7 @@
|
||||
"name": "Bluesound",
|
||||
"documentation": "https://www.home-assistant.io/components/bluesound",
|
||||
"requirements": [
|
||||
"xmltodict==0.11.0"
|
||||
"xmltodict==0.12.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Bom",
|
||||
"documentation": "https://www.home-assistant.io/components/bom",
|
||||
"requirements": [
|
||||
"bomradarloop==0.1.2"
|
||||
"bomradarloop==0.1.3"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
@ -8,7 +8,7 @@ import voluptuous as vol
|
||||
from homeassistant.components.calendar import (
|
||||
PLATFORM_SCHEMA, CalendarEventDevice, get_date)
|
||||
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
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
@ -36,7 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_SEARCH): cv.string,
|
||||
})
|
||||
]))
|
||||
])),
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean
|
||||
})
|
||||
|
||||
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)
|
||||
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()
|
||||
|
||||
|
@ -24,6 +24,7 @@ from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, dispatcher_send)
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.logging import async_create_catching_coro
|
||||
|
||||
from . import DOMAIN as CAST_DOMAIN
|
||||
|
||||
@ -522,8 +523,8 @@ class CastDevice(MediaPlayerDevice):
|
||||
if _is_matching_dynamic_group(self._cast_info, discover):
|
||||
_LOGGER.debug("Discovered matching dynamic group: %s",
|
||||
discover)
|
||||
self.hass.async_create_task(
|
||||
self.async_set_dynamic_group(discover))
|
||||
self.hass.async_create_task(async_create_catching_coro(
|
||||
self.async_set_dynamic_group(discover)))
|
||||
return
|
||||
|
||||
if self._cast_info.uuid != discover.uuid:
|
||||
@ -536,7 +537,8 @@ class CastDevice(MediaPlayerDevice):
|
||||
self._cast_info.host, self._cast_info.port)
|
||||
return
|
||||
_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):
|
||||
"""Handle removal of Chromecast."""
|
||||
@ -546,13 +548,15 @@ class CastDevice(MediaPlayerDevice):
|
||||
if (self._dynamic_group_cast_info is not None and
|
||||
self._dynamic_group_cast_info.uuid == discover.uuid):
|
||||
_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
|
||||
if self._cast_info.uuid != discover.uuid:
|
||||
# Removed is not our device.
|
||||
return
|
||||
_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):
|
||||
"""Disconnect socket on Home Assistant stop."""
|
||||
@ -565,14 +569,15 @@ class CastDevice(MediaPlayerDevice):
|
||||
self.hass, SIGNAL_CAST_REMOVED,
|
||||
async_cast_removed)
|
||||
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]:
|
||||
if _is_matching_dynamic_group(self._cast_info, info):
|
||||
_LOGGER.debug("[%s %s (%s:%s)] Found dynamic group: %s",
|
||||
self.entity_id, self._cast_info.friendly_name,
|
||||
self._cast_info.host, self._cast_info.port, info)
|
||||
self.hass.async_create_task(
|
||||
self.async_set_dynamic_group(info))
|
||||
self.hass.async_create_task(async_create_catching_coro(
|
||||
self.async_set_dynamic_group(info)))
|
||||
break
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def media_image_remotely_accessible(self) -> bool:
|
||||
"""If the image url is remotely accessible."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
|
@ -3,7 +3,9 @@ import logging
|
||||
|
||||
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
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@ -12,6 +14,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_INITIAL = 'initial'
|
||||
ATTR_STEP = 'step'
|
||||
ATTR_MINIMUM = 'minimum'
|
||||
ATTR_MAXIMUM = 'maximum'
|
||||
|
||||
CONF_INITIAL = 'initial'
|
||||
CONF_RESTORE = 'restore'
|
||||
@ -26,11 +30,19 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
SERVICE_DECREMENT = 'decrement'
|
||||
SERVICE_INCREMENT = 'increment'
|
||||
SERVICE_RESET = 'reset'
|
||||
SERVICE_CONFIGURE = 'configure'
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
SERVICE_SCHEMA_SIMPLE = vol.Schema({
|
||||
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({
|
||||
DOMAIN: cv.schema_with_slug_keys(
|
||||
vol.Any({
|
||||
@ -38,6 +50,10 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL):
|
||||
cv.positive_int,
|
||||
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_STEP, default=DEFAULT_STEP): cv.positive_int,
|
||||
}, None)
|
||||
@ -60,21 +76,27 @@ async def async_setup(hass, config):
|
||||
restore = cfg.get(CONF_RESTORE)
|
||||
step = cfg.get(CONF_STEP)
|
||||
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:
|
||||
return False
|
||||
|
||||
component.async_register_entity_service(
|
||||
SERVICE_INCREMENT, SERVICE_SCHEMA,
|
||||
SERVICE_INCREMENT, SERVICE_SCHEMA_SIMPLE,
|
||||
'async_increment')
|
||||
component.async_register_entity_service(
|
||||
SERVICE_DECREMENT, SERVICE_SCHEMA,
|
||||
SERVICE_DECREMENT, SERVICE_SCHEMA_SIMPLE,
|
||||
'async_decrement')
|
||||
component.async_register_entity_service(
|
||||
SERVICE_RESET, SERVICE_SCHEMA,
|
||||
SERVICE_RESET, SERVICE_SCHEMA_SIMPLE,
|
||||
'async_reset')
|
||||
component.async_register_entity_service(
|
||||
SERVICE_CONFIGURE, SERVICE_SCHEMA_CONFIGURE,
|
||||
'async_configure')
|
||||
|
||||
await component.async_add_entities(entities)
|
||||
return True
|
||||
@ -83,13 +105,16 @@ async def async_setup(hass, config):
|
||||
class Counter(RestoreEntity):
|
||||
"""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."""
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
self._restore = restore
|
||||
self._step = step
|
||||
self._state = self._initial = initial
|
||||
self._min = minimum
|
||||
self._max = maximum
|
||||
self._icon = icon
|
||||
|
||||
@property
|
||||
@ -115,10 +140,24 @@ class Counter(RestoreEntity):
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ret = {
|
||||
ATTR_INITIAL: self._initial,
|
||||
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):
|
||||
"""Call when entity about to be added to Home Assistant."""
|
||||
@ -128,19 +167,31 @@ class Counter(RestoreEntity):
|
||||
if self._restore:
|
||||
state = await self.async_get_last_state()
|
||||
if state is not None:
|
||||
self._state = int(state.state)
|
||||
self._state = self.compute_next_state(int(state.state))
|
||||
|
||||
async def async_decrement(self):
|
||||
"""Decrement the counter."""
|
||||
self._state -= self._step
|
||||
self._state = self.compute_next_state(self._state - self._step)
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_increment(self):
|
||||
"""Increment a counter."""
|
||||
self._state += self._step
|
||||
self._state = self.compute_next_state(self._state + self._step)
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_reset(self):
|
||||
"""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()
|
||||
|
@ -17,4 +17,19 @@ reset:
|
||||
fields:
|
||||
entity_id:
|
||||
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
|
||||
|
@ -6,9 +6,10 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_OPERATION_MODE,
|
||||
ATTR_SWING_MODE, STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY,
|
||||
STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE,
|
||||
ATTR_AWAY_MODE, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE,
|
||||
ATTR_OPERATION_MODE, ATTR_SWING_MODE, STATE_AUTO, STATE_COOL, STATE_DRY,
|
||||
STATE_FAN_ONLY, STATE_HEAT, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE,
|
||||
SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, STATE_OFF, TEMP_CELSIUS)
|
||||
@ -44,6 +45,7 @@ DAIKIN_TO_HA_STATE = {
|
||||
}
|
||||
|
||||
HA_ATTR_TO_DAIKIN = {
|
||||
ATTR_AWAY_MODE: 'en_hol',
|
||||
ATTR_OPERATION_MODE: 'mode',
|
||||
ATTR_FAN_MODE: 'f_rate',
|
||||
ATTR_SWING_MODE: 'f_dir',
|
||||
@ -93,8 +95,9 @@ class DaikinClimate(ClimateDevice):
|
||||
),
|
||||
}
|
||||
|
||||
self._supported_features = SUPPORT_TARGET_TEMPERATURE \
|
||||
| SUPPORT_OPERATION_MODE
|
||||
self._supported_features = (SUPPORT_AWAY_MODE | SUPPORT_ON_OFF
|
||||
| SUPPORT_OPERATION_MODE
|
||||
| SUPPORT_TARGET_TEMPERATURE)
|
||||
|
||||
if self._api.device.support_fan_mode:
|
||||
self._supported_features |= SUPPORT_FAN_MODE
|
||||
@ -266,3 +269,36 @@ class DaikinClimate(ClimateDevice):
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
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'})
|
||||
|
@ -85,5 +85,9 @@ class DanfossAir:
|
||||
= self._client.command(ReadCommand.boost)
|
||||
self._data[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")
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Danfoss air",
|
||||
"documentation": "https://www.home-assistant.io/components/danfoss_air",
|
||||
"requirements": [
|
||||
"pydanfossair==0.0.7"
|
||||
"pydanfossair==0.1.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
@ -19,6 +19,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
ReadCommand.boost,
|
||||
UpdateCommand.boost_activate,
|
||||
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 = []
|
||||
@ -59,7 +67,7 @@ class DanfossAir(SwitchDevice):
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""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)
|
||||
|
||||
def update(self):
|
||||
|
@ -26,7 +26,7 @@
|
||||
"title": "Definici\u00f3 de la passarel\u00b7la deCONZ"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"options": {
|
||||
|
@ -12,7 +12,7 @@
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "Hostitel",
|
||||
"port": "Port (v\u00fdchoz\u00ed hodnota: '80')"
|
||||
"port": "Port"
|
||||
},
|
||||
"title": "Definujte br\u00e1nu deCONZ"
|
||||
},
|
||||
|
@ -3,7 +3,7 @@
|
||||
"abort": {
|
||||
"already_configured": "El puente ya esta configurado",
|
||||
"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"
|
||||
},
|
||||
"error": {
|
||||
@ -26,7 +26,7 @@
|
||||
"title": "Definir pasarela deCONZ"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"options": {
|
||||
|
@ -3,7 +3,8 @@
|
||||
"abort": {
|
||||
"already_configured": "Broen er allerede konfigurert",
|
||||
"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": {
|
||||
"no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel"
|
||||
@ -25,7 +26,7 @@
|
||||
"title": "Definer deCONZ-gatewayen"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"options": {
|
||||
|
@ -3,7 +3,8 @@
|
||||
"abort": {
|
||||
"already_configured": "Mostek jest ju\u017c skonfigurowany",
|
||||
"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": {
|
||||
"no_key": "Nie mo\u017cna uzyska\u0107 klucza API"
|
||||
|
@ -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",
|
||||
"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",
|
||||
"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": {
|
||||
"no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API"
|
||||
|
@ -3,7 +3,8 @@
|
||||
"abort": {
|
||||
"already_configured": "Most je \u017ee nastavljen",
|
||||
"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": {
|
||||
"no_key": "Klju\u010da API ni mogo\u010de dobiti"
|
||||
|
@ -9,6 +9,12 @@
|
||||
"no_key": "Det gick inte att ta emot en API-nyckel"
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"data": {
|
||||
"allow_clip_sensor": "Till\u00e5t import av virtuella sensorer"
|
||||
},
|
||||
"title": "deCONZ Zigbee gateway via Hass.io till\u00e4gg"
|
||||
},
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "V\u00e4rd",
|
||||
|
@ -88,9 +88,11 @@ class DeconzCover(DeconzDevice, CoverDevice):
|
||||
"""Move the cover to a specific position."""
|
||||
position = kwargs[ATTR_POSITION]
|
||||
data = {'on': False}
|
||||
|
||||
if position > 0:
|
||||
data['on'] = True
|
||||
data['bri'] = int(position / 100 * 255)
|
||||
|
||||
await self._device.async_set_state(data)
|
||||
|
||||
async def async_open_cover(self, **kwargs):
|
||||
@ -126,7 +128,9 @@ class DeconzCoverZigbeeSpec(DeconzCover):
|
||||
"""Move the cover to a specific position."""
|
||||
position = kwargs[ATTR_POSITION]
|
||||
data = {'on': False}
|
||||
|
||||
if position < 100:
|
||||
data['on'] = True
|
||||
data['bri'] = 255 - int(position / 100 * 255)
|
||||
|
||||
await self._device.async_set_state(data)
|
||||
|
@ -61,8 +61,10 @@ class DeconzDevice(Entity):
|
||||
if (self._device.uniqueid is None or
|
||||
self._device.uniqueid.count(':') != 7):
|
||||
return None
|
||||
|
||||
serial = self._device.uniqueid.split('-', 1)[0]
|
||||
bridgeid = self.gateway.api.config.bridgeid
|
||||
|
||||
return {
|
||||
'connections': {(CONNECTION_ZIGBEE, serial)},
|
||||
'identifiers': {(DECONZ_DOMAIN, serial)},
|
||||
|
@ -165,6 +165,8 @@ class DeconzLight(DeconzDevice, Light):
|
||||
"""Return the device state attributes."""
|
||||
attributes = {}
|
||||
attributes['is_deconz_group'] = self._device.type == 'LightGroup'
|
||||
|
||||
if self._device.type == 'LightGroup':
|
||||
attributes['all_on'] = self._device.all_on
|
||||
|
||||
return attributes
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Deconz",
|
||||
"documentation": "https://www.home-assistant.io/components/deconz",
|
||||
"requirements": [
|
||||
"pydeconz==54"
|
||||
"pydeconz==58"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
|
@ -51,7 +51,7 @@ class AbstractDemoPlayer(MediaPlayerDevice):
|
||||
|
||||
# We only implement the methods that we support
|
||||
|
||||
def __init__(self, name):
|
||||
def __init__(self, name, device_class=None):
|
||||
"""Initialize the demo device."""
|
||||
self._name = name
|
||||
self._player_state = STATE_PLAYING
|
||||
@ -60,6 +60,7 @@ class AbstractDemoPlayer(MediaPlayerDevice):
|
||||
self._shuffle = False
|
||||
self._sound_mode_list = SOUND_MODE_LIST
|
||||
self._sound_mode = DEFAULT_SOUND_MODE
|
||||
self._device_class = device_class
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -101,6 +102,11 @@ class AbstractDemoPlayer(MediaPlayerDevice):
|
||||
"""Return a list of available sound modes."""
|
||||
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):
|
||||
"""Turn the media player on."""
|
||||
self._player_state = STATE_PLAYING
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"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."
|
||||
},
|
||||
"create_entry": {
|
||||
@ -9,7 +9,8 @@
|
||||
},
|
||||
"step": {
|
||||
"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"
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Discord",
|
||||
"documentation": "https://www.home-assistant.io/components/discord",
|
||||
"requirements": [
|
||||
"discord.py==0.16.12"
|
||||
"discord.py==1.0.1"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Discord platform for notify component."""
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -33,36 +34,69 @@ class DiscordNotificationService(BaseNotificationService):
|
||||
self.token = token
|
||||
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):
|
||||
"""Login to Discord, send message to channel(s) and log out."""
|
||||
import discord
|
||||
|
||||
discord.VoiceClient.warn_nacl = False
|
||||
discord_bot = discord.Client(loop=self.hass.loop)
|
||||
images = None
|
||||
|
||||
if ATTR_TARGET not in kwargs:
|
||||
_LOGGER.error("No target specified")
|
||||
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
|
||||
@discord_bot.event
|
||||
async def on_ready():
|
||||
"""Send the messages when the bot is ready."""
|
||||
try:
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
images = None
|
||||
if data:
|
||||
images = data.get(ATTR_IMAGES)
|
||||
for channelid in kwargs[ATTR_TARGET]:
|
||||
channel = discord.Object(id=channelid)
|
||||
await discord_bot.send_message(channel, message)
|
||||
channelid = int(channelid)
|
||||
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:
|
||||
for anum, f_name in enumerate(images):
|
||||
await discord_bot.send_file(channel, f_name)
|
||||
files = list()
|
||||
for image in images:
|
||||
files.append(discord.File(image))
|
||||
|
||||
await channel.send(message, files=files)
|
||||
except (discord.errors.HTTPException,
|
||||
discord.errors.NotFound) as error:
|
||||
_LOGGER.warning("Communication error: %s", error)
|
||||
await discord_bot.logout()
|
||||
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)
|
||||
|
@ -105,16 +105,19 @@ OPTIONAL_SERVICE_HANDLERS = {
|
||||
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_ENABLE = 'enable'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(DOMAIN): vol.Schema({
|
||||
vol.Optional(CONF_IGNORE, default=[]):
|
||||
vol.All(cv.ensure_list, [
|
||||
vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))]),
|
||||
vol.All(cv.ensure_list, [vol.In(DEFAULT_ENABLED)]),
|
||||
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)
|
||||
|
||||
@ -140,6 +143,14 @@ async def async_setup(hass, config):
|
||||
ignored_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):
|
||||
"""Handle a new service if one is found."""
|
||||
if service in ignored_platforms:
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Dnsip",
|
||||
"documentation": "https://www.home-assistant.io/components/dnsip",
|
||||
"requirements": [
|
||||
"aiodns==1.1.1"
|
||||
"aiodns==2.0.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
@ -1,26 +1,26 @@
|
||||
"""Get your own public IP address or that of any host."""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NAME = 'name'
|
||||
CONF_HOSTNAME = 'hostname'
|
||||
CONF_IPV6 = 'ipv6'
|
||||
CONF_RESOLVER = 'resolver'
|
||||
CONF_RESOLVER_IPV6 = 'resolver_ipv6'
|
||||
CONF_IPV6 = 'ipv6'
|
||||
|
||||
DEFAULT_NAME = 'myip'
|
||||
DEFAULT_HOSTNAME = 'myip.opendns.com'
|
||||
DEFAULT_IPV6 = False
|
||||
DEFAULT_NAME = 'myip'
|
||||
DEFAULT_RESOLVER = '208.67.222.222'
|
||||
DEFAULT_RESOLVER_IPV6 = '2620:0:ccc::2'
|
||||
DEFAULT_IPV6 = False
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=120)
|
||||
|
||||
@ -33,8 +33,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the DNS IP sensor."""
|
||||
hostname = config.get(CONF_HOSTNAME)
|
||||
name = config.get(CONF_NAME)
|
||||
@ -57,8 +57,9 @@ class WanIpSensor(Entity):
|
||||
"""Implementation of a DNS IP sensor."""
|
||||
|
||||
def __init__(self, hass, name, hostname, resolver, ipv6):
|
||||
"""Initialize the sensor."""
|
||||
"""Initialize the DNS IP sensor."""
|
||||
import aiodns
|
||||
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self.hostname = hostname
|
||||
@ -80,9 +81,10 @@ class WanIpSensor(Entity):
|
||||
async def async_update(self):
|
||||
"""Get the current DNS IP address for hostname."""
|
||||
from aiodns.error import DNSError
|
||||
|
||||
try:
|
||||
response = await self.resolver.query(self.hostname,
|
||||
self.querytype)
|
||||
response = await self.resolver.query(
|
||||
self.hostname, self.querytype)
|
||||
except DNSError as err:
|
||||
_LOGGER.warning("Exception while resolving host: %s", err)
|
||||
response = None
|
||||
|
@ -16,6 +16,7 @@ CONF_RETRY = 'retry'
|
||||
DEFAULT_TIMEOUT = 5
|
||||
DEFAULT_RETRY = 10
|
||||
DYSON_DEVICES = 'dyson_devices'
|
||||
DYSON_PLATFORMS = ['sensor', 'fan', 'vacuum', 'climate', 'air_quality']
|
||||
|
||||
DOMAIN = 'dyson'
|
||||
|
||||
@ -91,9 +92,7 @@ def setup(hass, config):
|
||||
# Start fan/sensors components
|
||||
if hass.data[DYSON_DEVICES]:
|
||||
_LOGGER.debug("Starting sensor/fan components")
|
||||
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, "fan", DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, "vacuum", DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, "climate", DOMAIN, {}, config)
|
||||
for platform in DYSON_PLATFORMS:
|
||||
discovery.load_platform(hass, platform, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
126
homeassistant/components/dyson/air_quality.py
Normal file
126
homeassistant/components/dyson/air_quality.py
Normal 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
|
@ -474,7 +474,8 @@ class DysonPureCoolDevice(FanEntity):
|
||||
FanSpeed.FAN_SPEED_6.value: SPEED_MEDIUM,
|
||||
FanSpeed.FAN_SPEED_7.value: SPEED_MEDIUM,
|
||||
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]
|
||||
|
||||
|
@ -3,7 +3,6 @@ import logging
|
||||
|
||||
from homeassistant.const import STATE_OFF, TEMP_CELSIUS
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import DYSON_DEVICES
|
||||
|
||||
SENSOR_UNITS = {
|
||||
@ -21,26 +20,38 @@ SENSOR_ICONS = {
|
||||
'temperature': 'mdi:thermometer',
|
||||
}
|
||||
|
||||
DYSON_SENSOR_DEVICES = 'dyson_sensor_devices'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""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 import DysonPureCool
|
||||
|
||||
for device in [d for d in hass.data[DYSON_DEVICES]
|
||||
if isinstance(d, DysonPureCoolLink) and
|
||||
not isinstance(d, DysonPureCool)]:
|
||||
devices.append(DysonFilterLifeSensor(device))
|
||||
devices.append(DysonDustSensor(device))
|
||||
devices.append(DysonHumiditySensor(device))
|
||||
devices.append(DysonTemperatureSensor(device, unit))
|
||||
devices.append(DysonAirQualitySensor(device))
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
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(DysonDustSensor(device))
|
||||
devices.append(DysonHumiditySensor(device))
|
||||
devices.append(DysonTemperatureSensor(device, unit))
|
||||
devices.append(DysonAirQualitySensor(device))
|
||||
add_entities(devices)
|
||||
|
||||
|
||||
@ -56,7 +67,7 @@ class DysonSensor(Entity):
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""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)
|
||||
|
||||
def on_message(self, message):
|
||||
@ -88,6 +99,11 @@ class DysonSensor(Entity):
|
||||
"""Return the icon for this sensor."""
|
||||
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):
|
||||
"""Representation of Dyson Filter Life sensor (in hours)."""
|
||||
|
6
homeassistant/components/ebusd/.translations/cs.json
Normal file
6
homeassistant/components/ebusd/.translations/cs.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"state": {
|
||||
"day": "Den",
|
||||
"night": "Noc"
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
"name": "Econet",
|
||||
"documentation": "https://www.home-assistant.io/components/econet",
|
||||
"requirements": [
|
||||
"pyeconet==0.0.10"
|
||||
"pyeconet==0.0.11"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
|
@ -5,7 +5,7 @@ from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.core import CoreState, EventOrigin
|
||||
|
||||
LOGGER = logging.getLogger('.')
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
EVENT_ROKU_COMMAND = 'roku_command'
|
||||
|
||||
|
@ -4,13 +4,13 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_DEVICE
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'enocean'
|
||||
|
||||
ENOCEAN_DONGLE = None
|
||||
DATA_ENOCEAN = 'enocean'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@ -18,14 +18,15 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SIGNAL_RECEIVE_MESSAGE = 'enocean.receive_message'
|
||||
SIGNAL_SEND_MESSAGE = 'enocean.send_message'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the EnOcean component."""
|
||||
global ENOCEAN_DONGLE
|
||||
|
||||
serial_dev = config[DOMAIN].get(CONF_DEVICE)
|
||||
|
||||
ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev)
|
||||
dongle = EnOceanDongle(hass, serial_dev)
|
||||
hass.data[DATA_ENOCEAN] = dongle
|
||||
|
||||
return True
|
||||
|
||||
@ -39,87 +40,53 @@ class EnOceanDongle:
|
||||
self.__communicator = SerialCommunicator(
|
||||
port=ser, callback=self.callback)
|
||||
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):
|
||||
"""Register another device."""
|
||||
self.__devices.append(dev)
|
||||
|
||||
def send_command(self, command):
|
||||
"""Send a command from the EnOcean dongle."""
|
||||
def _send_message_callback(self, command):
|
||||
"""Send a command through the EnOcean dongle."""
|
||||
self.__communicator.send(command)
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
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):
|
||||
def callback(self, packet):
|
||||
"""Handle EnOcean device's callback.
|
||||
|
||||
This is the callback function called by python-enocan whenever there
|
||||
is an incoming packet.
|
||||
"""
|
||||
from enocean.protocol.packet import RadioPacket
|
||||
if isinstance(temp, RadioPacket):
|
||||
_LOGGER.debug("Received radio packet: %s", temp)
|
||||
rxtype = None
|
||||
value = None
|
||||
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)
|
||||
if isinstance(packet, RadioPacket):
|
||||
_LOGGER.debug("Received radio packet: %s", packet)
|
||||
self.hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_RECEIVE_MESSAGE, packet)
|
||||
|
||||
|
||||
class EnOceanDevice():
|
||||
class EnOceanDevice(Entity):
|
||||
"""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."""
|
||||
ENOCEAN_DONGLE.register_device(self)
|
||||
self.stype = ""
|
||||
self.sensorid = [0x00, 0x00, 0x00, 0x00]
|
||||
self.dev_id = dev_id
|
||||
self.dev_name = dev_name
|
||||
|
||||
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
|
||||
def send_command(self, data, optional, packet_type):
|
||||
"""Send a command via the EnOcean dongle."""
|
||||
from enocean.protocol.packet import Packet
|
||||
packet = Packet(packet_type, data=data, optional=optional)
|
||||
ENOCEAN_DONGLE.send_command(packet)
|
||||
self.hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_SEND_MESSAGE, packet)
|
||||
|
@ -3,16 +3,17 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.components import enocean
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_ID, CONF_DEVICE_CLASS)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_ID, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'EnOcean binary sensor'
|
||||
DEPENDENCIES = ['enocean']
|
||||
EVENT_BUTTON_PRESSED = 'button_pressed'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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):
|
||||
"""Set up the Binary Sensor platform for EnOcean."""
|
||||
dev_id = config.get(CONF_ID)
|
||||
devname = config.get(CONF_NAME)
|
||||
dev_name = config.get(CONF_NAME)
|
||||
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):
|
||||
"""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."""
|
||||
enocean.EnOceanDevice.__init__(self)
|
||||
self.stype = 'listener'
|
||||
self.dev_id = dev_id
|
||||
super().__init__(dev_id, dev_name)
|
||||
self._device_class = device_class
|
||||
self.which = -1
|
||||
self.onoff = -1
|
||||
self.devname = devname
|
||||
self._device_class = device_class
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the default name for the binary sensor."""
|
||||
return self.devname
|
||||
return self.dev_name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._device_class
|
||||
|
||||
def value_changed(self, value, value2):
|
||||
def value_changed(self, packet):
|
||||
"""Fire an event with the data that have changed.
|
||||
|
||||
This method is called when there is an incoming packet associated
|
||||
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()
|
||||
if value2 == 0x70:
|
||||
|
||||
action = packet.data[1]
|
||||
if action == 0x70:
|
||||
self.which = 0
|
||||
self.onoff = 0
|
||||
elif value2 == 0x50:
|
||||
elif action == 0x50:
|
||||
self.which = 0
|
||||
self.onoff = 1
|
||||
elif value2 == 0x30:
|
||||
elif action == 0x30:
|
||||
self.which = 1
|
||||
self.onoff = 0
|
||||
elif value2 == 0x10:
|
||||
elif action == 0x10:
|
||||
self.which = 1
|
||||
self.onoff = 1
|
||||
elif value2 == 0x37:
|
||||
elif action == 0x37:
|
||||
self.which = 10
|
||||
self.onoff = 0
|
||||
elif value2 == 0x15:
|
||||
elif action == 0x15:
|
||||
self.which = 10
|
||||
self.onoff = 1
|
||||
self.hass.bus.fire('button_pressed', {'id': self.dev_id,
|
||||
'pushed': value,
|
||||
'which': self.which,
|
||||
'onoff': self.onoff})
|
||||
self.hass.bus.fire(EVENT_BUTTON_PRESSED,
|
||||
{'id': self.dev_id,
|
||||
'pushed': pushed,
|
||||
'which': self.which,
|
||||
'onoff': self.onoff})
|
||||
|
@ -4,10 +4,10 @@ import math
|
||||
|
||||
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.light import (
|
||||
ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light)
|
||||
from homeassistant.const import CONF_ID, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -28,29 +28,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the EnOcean light platform."""
|
||||
sender_id = config.get(CONF_SENDER_ID)
|
||||
devname = config.get(CONF_NAME)
|
||||
dev_name = config.get(CONF_NAME)
|
||||
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):
|
||||
"""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."""
|
||||
enocean.EnOceanDevice.__init__(self)
|
||||
super().__init__(dev_id, dev_name)
|
||||
self._on_state = False
|
||||
self._brightness = 50
|
||||
self._sender_id = sender_id
|
||||
self.dev_id = dev_id
|
||||
self._devname = devname
|
||||
self.stype = 'dimmer'
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._devname
|
||||
return self.dev_name
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
@ -94,8 +91,14 @@ class EnOceanLight(enocean.EnOceanDevice, Light):
|
||||
self.send_command(command, [], 0x01)
|
||||
self._on_state = False
|
||||
|
||||
def value_changed(self, val):
|
||||
"""Update the internal state of this device."""
|
||||
self._brightness = math.floor(val / 100.0 * 256.0)
|
||||
self._on_state = bool(val != 0)
|
||||
self.schedule_update_ha_state()
|
||||
def value_changed(self, packet):
|
||||
"""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._on_state = bool(val != 0)
|
||||
self.schedule_update_ha_state()
|
||||
|
@ -3,8 +3,8 @@
|
||||
"name": "Enocean",
|
||||
"documentation": "https://www.home-assistant.io/components/enocean",
|
||||
"requirements": [
|
||||
"enocean==0.40"
|
||||
"enocean==0.50"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
"codeowners": ["@bdurrer"]
|
||||
}
|
||||
|
@ -3,58 +3,201 @@ import logging
|
||||
|
||||
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.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__)
|
||||
|
||||
CONF_MAX_TEMP = 'max_temp'
|
||||
CONF_MIN_TEMP = 'min_temp'
|
||||
CONF_RANGE_FROM = 'range_from'
|
||||
CONF_RANGE_TO = 'range_to'
|
||||
|
||||
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({
|
||||
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
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):
|
||||
"""Set up an EnOcean sensor device."""
|
||||
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):
|
||||
"""Representation of an EnOcean sensor device such as a power meter."""
|
||||
class EnOceanSensor(enocean.EnOceanDevice):
|
||||
"""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."""
|
||||
enocean.EnOceanDevice.__init__(self)
|
||||
self.stype = "powersensor"
|
||||
self.power = None
|
||||
self.dev_id = dev_id
|
||||
self.which = -1
|
||||
self.onoff = -1
|
||||
self.devname = devname
|
||||
super().__init__(dev_id, dev_name)
|
||||
self._sensor_type = sensor_type
|
||||
self._device_class = SENSOR_TYPES[self._sensor_type]['class']
|
||||
self._dev_name = '{} {}'.format(
|
||||
SENSOR_TYPES[self._sensor_type]['name'], dev_name)
|
||||
self._unit_of_measurement = SENSOR_TYPES[self._sensor_type]['unit']
|
||||
self._icon = SENSOR_TYPES[self._sensor_type]['icon']
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return 'Power %s' % self.devname
|
||||
return self._dev_name
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Update the internal state of the device."""
|
||||
self.power = value
|
||||
self.schedule_update_ha_state()
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of the sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self.power
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""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()
|
||||
|
@ -3,16 +3,16 @@ import logging
|
||||
|
||||
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.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
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'EnOcean Switch'
|
||||
CONF_CHANNEL = 'channel'
|
||||
DEFAULT_NAME = 'EnOcean Switch'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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):
|
||||
"""Set up the EnOcean switch platform."""
|
||||
dev_id = config.get(CONF_ID)
|
||||
devname = config.get(CONF_NAME)
|
||||
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):
|
||||
"""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."""
|
||||
enocean.EnOceanDevice.__init__(self)
|
||||
self.dev_id = dev_id
|
||||
self._devname = devname
|
||||
super().__init__(dev_id, dev_name)
|
||||
self._light = None
|
||||
self._on_state = False
|
||||
self._on_state2 = False
|
||||
self.channel = channel
|
||||
self.stype = "switch"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@ -52,7 +49,7 @@ class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the device name."""
|
||||
return self._devname
|
||||
return self.dev_name
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn on the switch."""
|
||||
@ -74,7 +71,24 @@ class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity):
|
||||
packet_type=0x01)
|
||||
self._on_state = False
|
||||
|
||||
def value_changed(self, val):
|
||||
def value_changed(self, packet):
|
||||
"""Update the internal state of the switch."""
|
||||
self._on_state = val
|
||||
self.schedule_update_ha_state()
|
||||
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()
|
||||
|
@ -4,6 +4,6 @@
|
||||
"documentation": "https://www.home-assistant.io/components/epsonworkforce",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@ThaStealth"],
|
||||
"requirements": ["epsonprinter==0.0.8"]
|
||||
"requirements": ["epsonprinter==0.0.9"]
|
||||
}
|
||||
|
||||
|
@ -10,15 +10,16 @@ from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['epsonprinter==0.0.8']
|
||||
REQUIREMENTS = ['epsonprinter==0.0.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
MONITORED_CONDITIONS = {
|
||||
'black': ['Inklevel Black', '%', 'mdi:water'],
|
||||
'magenta': ['Inklevel Magenta', '%', 'mdi:water'],
|
||||
'cyan': ['Inklevel Cyan', '%', 'mdi:water'],
|
||||
'yellow': ['Inklevel Yellow', '%', 'mdi:water'],
|
||||
'clean': ['Inklevel Cleaning', '%', 'mdi:water'],
|
||||
'black': ['Ink level Black', '%', 'mdi:water'],
|
||||
'photoblack': ['Ink level Photoblack', '%', 'mdi:water'],
|
||||
'magenta': ['Ink level Magenta', '%', 'mdi:water'],
|
||||
'cyan': ['Ink level Cyan', '%', 'mdi:water'],
|
||||
'yellow': ['Ink level Yellow', '%', 'mdi:water'],
|
||||
'clean': ['Cleaning level', '%', 'mdi:water'],
|
||||
}
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
|
@ -13,7 +13,7 @@
|
||||
"data": {
|
||||
"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"
|
||||
},
|
||||
"discovery_confirm": {
|
||||
|
13
homeassistant/components/esphome/.translations/cs.json
Normal file
13
homeassistant/components/esphome/.translations/cs.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,14 +4,16 @@
|
||||
"already_configured": "ESP ya est\u00e1 configurado"
|
||||
},
|
||||
"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": {
|
||||
"authenticate": {
|
||||
"data": {
|
||||
"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"
|
||||
},
|
||||
"discovery_confirm": {
|
||||
@ -23,6 +25,7 @@
|
||||
"host": "Host",
|
||||
"port": "Puerto"
|
||||
},
|
||||
"description": "Introduce la configuraci\u00f3n de la conexi\u00f3n de tu nodo [ESPHome](https://esphomelib.com/).",
|
||||
"title": "ESPHome"
|
||||
}
|
||||
},
|
||||
|
1
homeassistant/components/essent/__init__.py
Normal file
1
homeassistant/components/essent/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""The Essent component."""
|
8
homeassistant/components/essent/manifest.json
Normal file
8
homeassistant/components/essent/manifest.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "essent",
|
||||
"name": "Essent",
|
||||
"documentation": "https://www.home-assistant.io/components/essent",
|
||||
"requirements": ["PyEssent==0.10"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@TheLastProject"]
|
||||
}
|
112
homeassistant/components/essent/sensor.py
Normal file
112
homeassistant/components/essent/sensor.py
Normal 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()))
|
@ -5,26 +5,31 @@
|
||||
# 0-12 Heating zones (a.k.a. Zone), and
|
||||
# 0-1 DHW controller, (a.k.a. Boiler)
|
||||
# The TCS & Zones are implemented as Climate devices, Boiler as a WaterHeater
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
import requests.exceptions
|
||||
import voluptuous as vol
|
||||
|
||||
import evohomeclient2
|
||||
|
||||
from homeassistant.const import (
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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__)
|
||||
|
||||
DOMAIN = 'evohome'
|
||||
DATA_EVOHOME = 'data_' + DOMAIN
|
||||
DISPATCHER_EVOHOME = 'dispatcher_' + DOMAIN
|
||||
|
||||
CONF_LOCATION_IDX = 'location_idx'
|
||||
SCAN_INTERVAL_DEFAULT = timedelta(seconds=300)
|
||||
SCAN_INTERVAL_MINIMUM = timedelta(seconds=180)
|
||||
@ -43,10 +48,6 @@ CONF_SECRETS = [
|
||||
CONF_USERNAME, CONF_PASSWORD,
|
||||
]
|
||||
|
||||
# These are used to help prevent E501 (line too long) violations.
|
||||
GWS = 'gateways'
|
||||
TCS = 'temperatureControlSystems'
|
||||
|
||||
# bit masks for dispatcher packets
|
||||
EVO_PARENT = 0x01
|
||||
EVO_CHILD = 0x02
|
||||
@ -66,8 +67,6 @@ def setup(hass, hass_config):
|
||||
scan_interval = timedelta(
|
||||
minutes=(scan_interval.total_seconds() + 59) // 60)
|
||||
|
||||
import evohomeclient2
|
||||
|
||||
try:
|
||||
client = evo_data['client'] = evohomeclient2.EvohomeClient(
|
||||
evo_data['params'][CONF_USERNAME],
|
||||
@ -145,3 +144,129 @@ def setup(hass, hass_config):
|
||||
hass.bus.listen(EVENT_HOMEASSISTANT_START, _first_update)
|
||||
|
||||
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
Loading…
x
Reference in New Issue
Block a user