mirror of
https://github.com/home-assistant/core.git
synced 2025-08-10 08:05:06 +02:00
Merge remote-tracking branch 'upstream/dev' into samsungtv_configflow
This commit is contained in:
47
.coveragerc
47
.coveragerc
@@ -5,7 +5,6 @@ omit =
|
||||
homeassistant/__main__.py
|
||||
homeassistant/helpers/signal.py
|
||||
homeassistant/helpers/typing.py
|
||||
homeassistant/monkey_patch.py
|
||||
homeassistant/scripts/*.py
|
||||
homeassistant/util/async.py
|
||||
|
||||
@@ -62,6 +61,7 @@ omit =
|
||||
homeassistant/components/asterisk_cdr/mailbox.py
|
||||
homeassistant/components/asterisk_mbox/*
|
||||
homeassistant/components/asuswrt/device_tracker.py
|
||||
homeassistant/components/aten_pe/*
|
||||
homeassistant/components/atome/*
|
||||
homeassistant/components/august/*
|
||||
homeassistant/components/aurora_abb_powerone/sensor.py
|
||||
@@ -83,9 +83,9 @@ omit =
|
||||
homeassistant/components/blinkt/light.py
|
||||
homeassistant/components/blockchain/sensor.py
|
||||
homeassistant/components/bloomsky/*
|
||||
homeassistant/components/bluesound/media_player.py
|
||||
homeassistant/components/bluesound/*
|
||||
homeassistant/components/bluetooth_le_tracker/device_tracker.py
|
||||
homeassistant/components/bluetooth_tracker/device_tracker.py
|
||||
homeassistant/components/bluetooth_tracker/*
|
||||
homeassistant/components/bme280/sensor.py
|
||||
homeassistant/components/bme680/sensor.py
|
||||
homeassistant/components/bmw_connected_drive/*
|
||||
@@ -93,6 +93,7 @@ omit =
|
||||
homeassistant/components/bom/sensor.py
|
||||
homeassistant/components/bom/weather.py
|
||||
homeassistant/components/braviatv/media_player.py
|
||||
homeassistant/components/broadlink/remote.py
|
||||
homeassistant/components/broadlink/sensor.py
|
||||
homeassistant/components/broadlink/switch.py
|
||||
homeassistant/components/brottsplatskartan/sensor.py
|
||||
@@ -109,7 +110,7 @@ omit =
|
||||
homeassistant/components/cast/*
|
||||
homeassistant/components/cert_expiry/sensor.py
|
||||
homeassistant/components/cert_expiry/helper.py
|
||||
homeassistant/components/channels/media_player.py
|
||||
homeassistant/components/channels/*
|
||||
homeassistant/components/cisco_ios/device_tracker.py
|
||||
homeassistant/components/cisco_mobility_express/device_tracker.py
|
||||
homeassistant/components/cisco_webex_teams/notify.py
|
||||
@@ -163,6 +164,7 @@ omit =
|
||||
homeassistant/components/doorbird/*
|
||||
homeassistant/components/dovado/*
|
||||
homeassistant/components/downloader/*
|
||||
homeassistant/components/dsmr_reader/*
|
||||
homeassistant/components/dte_energy_bridge/sensor.py
|
||||
homeassistant/components/dublin_bus_transport/sensor.py
|
||||
homeassistant/components/duke_energy/sensor.py
|
||||
@@ -178,7 +180,7 @@ omit =
|
||||
homeassistant/components/ecobee/notify.py
|
||||
homeassistant/components/ecobee/sensor.py
|
||||
homeassistant/components/ecobee/weather.py
|
||||
homeassistant/components/econet/water_heater.py
|
||||
homeassistant/components/econet/*
|
||||
homeassistant/components/ecovacs/*
|
||||
homeassistant/components/eddystone_temperature/sensor.py
|
||||
homeassistant/components/edimax/switch.py
|
||||
@@ -199,6 +201,7 @@ omit =
|
||||
homeassistant/components/envirophat/sensor.py
|
||||
homeassistant/components/envisalink/*
|
||||
homeassistant/components/ephember/climate.py
|
||||
homeassistant/components/epson/const.py
|
||||
homeassistant/components/epson/media_player.py
|
||||
homeassistant/components/epsonworkforce/sensor.py
|
||||
homeassistant/components/eq3btsmart/climate.py
|
||||
@@ -229,6 +232,7 @@ omit =
|
||||
homeassistant/components/flexit/climate.py
|
||||
homeassistant/components/flic/binary_sensor.py
|
||||
homeassistant/components/flock/notify.py
|
||||
homeassistant/components/flume/*
|
||||
homeassistant/components/flunearyou/sensor.py
|
||||
homeassistant/components/flux_led/light.py
|
||||
homeassistant/components/folder/sensor.py
|
||||
@@ -254,6 +258,9 @@ omit =
|
||||
homeassistant/components/geniushub/*
|
||||
homeassistant/components/gearbest/sensor.py
|
||||
homeassistant/components/geizhals/sensor.py
|
||||
homeassistant/components/gios/__init__.py
|
||||
homeassistant/components/gios/air_quality.py
|
||||
homeassistant/components/gios/consts.py
|
||||
homeassistant/components/github/sensor.py
|
||||
homeassistant/components/gitlab_ci/sensor.py
|
||||
homeassistant/components/gitter/sensor.py
|
||||
@@ -282,7 +289,7 @@ omit =
|
||||
homeassistant/components/hangouts/hangouts_bot.py
|
||||
homeassistant/components/hangouts/hangups_utils.py
|
||||
homeassistant/components/harman_kardon_avr/media_player.py
|
||||
homeassistant/components/harmony/remote.py
|
||||
homeassistant/components/harmony/*
|
||||
homeassistant/components/haveibeenpwned/sensor.py
|
||||
homeassistant/components/hdmi_cec/*
|
||||
homeassistant/components/heatmiser/climate.py
|
||||
@@ -314,7 +321,9 @@ omit =
|
||||
homeassistant/components/iaqualink/light.py
|
||||
homeassistant/components/iaqualink/sensor.py
|
||||
homeassistant/components/iaqualink/switch.py
|
||||
homeassistant/components/icloud/__init__.py
|
||||
homeassistant/components/icloud/device_tracker.py
|
||||
homeassistant/components/icloud/sensor.py
|
||||
homeassistant/components/izone/climate.py
|
||||
homeassistant/components/izone/discovery.py
|
||||
homeassistant/components/izone/__init__.py
|
||||
@@ -327,6 +336,7 @@ omit =
|
||||
homeassistant/components/influxdb/sensor.py
|
||||
homeassistant/components/insteon/*
|
||||
homeassistant/components/incomfort/*
|
||||
homeassistant/components/intesishome/*
|
||||
homeassistant/components/ios/*
|
||||
homeassistant/components/iota/*
|
||||
homeassistant/components/iperf3/*
|
||||
@@ -410,6 +420,7 @@ omit =
|
||||
homeassistant/components/miflora/sensor.py
|
||||
homeassistant/components/mikrotik/*
|
||||
homeassistant/components/mill/climate.py
|
||||
homeassistant/components/mill/const.py
|
||||
homeassistant/components/minio/*
|
||||
homeassistant/components/mitemp_bt/sensor.py
|
||||
homeassistant/components/mjpeg/camera.py
|
||||
@@ -526,6 +537,7 @@ omit =
|
||||
homeassistant/components/proliphix/climate.py
|
||||
homeassistant/components/prometheus/*
|
||||
homeassistant/components/prowl/notify.py
|
||||
homeassistant/components/proxmoxve/*
|
||||
homeassistant/components/proxy/camera.py
|
||||
homeassistant/components/ptvsd/*
|
||||
homeassistant/components/pulseaudio_loopback/switch.py
|
||||
@@ -603,6 +615,8 @@ omit =
|
||||
homeassistant/components/shodan/sensor.py
|
||||
homeassistant/components/sht31/sensor.py
|
||||
homeassistant/components/sigfox/sensor.py
|
||||
homeassistant/components/signal_messenger/__init__.py
|
||||
homeassistant/components/signal_messenger/notify.py
|
||||
homeassistant/components/simplepush/notify.py
|
||||
homeassistant/components/simplisafe/__init__.py
|
||||
homeassistant/components/simplisafe/alarm_control_panel.py
|
||||
@@ -634,7 +648,7 @@ omit =
|
||||
homeassistant/components/somfy/*
|
||||
homeassistant/components/somfy_mylink/*
|
||||
homeassistant/components/sonarr/sensor.py
|
||||
homeassistant/components/songpal/media_player.py
|
||||
homeassistant/components/songpal/*
|
||||
homeassistant/components/sonos/*
|
||||
homeassistant/components/sony_projector/switch.py
|
||||
homeassistant/components/spc/*
|
||||
@@ -642,7 +656,8 @@ omit =
|
||||
homeassistant/components/spider/*
|
||||
homeassistant/components/spotcrime/sensor.py
|
||||
homeassistant/components/spotify/media_player.py
|
||||
homeassistant/components/squeezebox/media_player.py
|
||||
homeassistant/components/squeezebox/*
|
||||
homeassistant/components/starline/*
|
||||
homeassistant/components/starlingbank/sensor.py
|
||||
homeassistant/components/steam_online/sensor.py
|
||||
homeassistant/components/stiebel_eltron/*
|
||||
@@ -676,7 +691,14 @@ omit =
|
||||
homeassistant/components/telnet/switch.py
|
||||
homeassistant/components/temper/sensor.py
|
||||
homeassistant/components/tensorflow/image_processing.py
|
||||
homeassistant/components/tesla/*
|
||||
homeassistant/components/tesla/__init__.py
|
||||
homeassistant/components/tesla/binary_sensor.py
|
||||
homeassistant/components/tesla/climate.py
|
||||
homeassistant/components/tesla/const.py
|
||||
homeassistant/components/tesla/device_tracker.py
|
||||
homeassistant/components/tesla/lock.py
|
||||
homeassistant/components/tesla/sensor.py
|
||||
homeassistant/components/tesla/switch.py
|
||||
homeassistant/components/tfiac/climate.py
|
||||
homeassistant/components/thermoworks_smoke/sensor.py
|
||||
homeassistant/components/thethingsnetwork/*
|
||||
@@ -688,6 +710,7 @@ omit =
|
||||
homeassistant/components/tile/device_tracker.py
|
||||
homeassistant/components/time_date/sensor.py
|
||||
homeassistant/components/todoist/calendar.py
|
||||
homeassistant/components/todoist/const.py
|
||||
homeassistant/components/tof/sensor.py
|
||||
homeassistant/components/tomato/device_tracker.py
|
||||
homeassistant/components/toon/*
|
||||
@@ -695,7 +718,6 @@ omit =
|
||||
homeassistant/components/totalconnect/*
|
||||
homeassistant/components/touchline/climate.py
|
||||
homeassistant/components/tplink/device_tracker.py
|
||||
homeassistant/components/tplink/light.py
|
||||
homeassistant/components/tplink/switch.py
|
||||
homeassistant/components/tplink_lte/*
|
||||
homeassistant/components/traccar/device_tracker.py
|
||||
@@ -736,12 +758,13 @@ omit =
|
||||
homeassistant/components/velbus/climate.py
|
||||
homeassistant/components/velbus/const.py
|
||||
homeassistant/components/velbus/cover.py
|
||||
homeassistant/components/velbus/light.py
|
||||
homeassistant/components/velbus/sensor.py
|
||||
homeassistant/components/velbus/switch.py
|
||||
homeassistant/components/velux/*
|
||||
homeassistant/components/venstar/climate.py
|
||||
homeassistant/components/vera/*
|
||||
homeassistant/components/verisure/*
|
||||
homeassistant/components/versasense/*
|
||||
homeassistant/components/vesync/__init__.py
|
||||
homeassistant/components/vesync/common.py
|
||||
homeassistant/components/vesync/const.py
|
||||
@@ -763,7 +786,6 @@ omit =
|
||||
homeassistant/components/waze_travel_time/sensor.py
|
||||
homeassistant/components/webostv/*
|
||||
homeassistant/components/wemo/*
|
||||
homeassistant/components/wemo/fan.py
|
||||
homeassistant/components/whois/sensor.py
|
||||
homeassistant/components/wink/*
|
||||
homeassistant/components/wirelesstag/*
|
||||
@@ -783,7 +805,6 @@ omit =
|
||||
homeassistant/components/xmpp/notify.py
|
||||
homeassistant/components/xs1/*
|
||||
homeassistant/components/yale_smart_alarm/alarm_control_panel.py
|
||||
homeassistant/components/yamaha/media_player.py
|
||||
homeassistant/components/yamaha_musiccast/media_player.py
|
||||
homeassistant/components/yandex_transport/*
|
||||
homeassistant/components/yeelight/*
|
||||
|
@@ -1,4 +1,3 @@
|
||||
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
|
||||
{
|
||||
"name": "Home Assistant Dev",
|
||||
"context": "..",
|
||||
|
@@ -18,14 +18,31 @@ repos:
|
||||
- --safe
|
||||
- --quiet
|
||||
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 3.7.9
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-docstrings==1.5.0
|
||||
- pydocstyle==4.0.1
|
||||
- pydocstyle==5.0.1
|
||||
files: ^(homeassistant|script|tests)/.+\.py$
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.6.2
|
||||
hooks:
|
||||
- id: bandit
|
||||
args:
|
||||
- --quiet
|
||||
- --format=custom
|
||||
- --configfile=tests/bandit.yaml
|
||||
files: ^(homeassistant|script|tests)/.+\.py$
|
||||
- repo: https://github.com/pre-commit/mirrors-isort
|
||||
rev: v4.3.21
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.4.0
|
||||
hooks:
|
||||
- id: check-json
|
||||
# Using a local "system" mypy instead of the mypy hook, because its
|
||||
# results depend on what is installed. And the mypy hook runs in a
|
||||
# virtualenv of its own, meaning we'd need to install and maintain
|
||||
|
@@ -20,5 +20,22 @@ repos:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-docstrings==1.5.0
|
||||
- pydocstyle==4.0.1
|
||||
- pydocstyle==5.0.1
|
||||
files: ^(homeassistant|script|tests)/.+\.py$
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.6.2
|
||||
hooks:
|
||||
- id: bandit
|
||||
args:
|
||||
- --quiet
|
||||
- --format=custom
|
||||
- --configfile=tests/bandit.yaml
|
||||
files: ^(homeassistant|script|tests)/.+\.py$
|
||||
- repo: https://github.com/pre-commit/mirrors-isort
|
||||
rev: v4.3.21
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.4.0
|
||||
hooks:
|
||||
- id: check-json
|
||||
|
@@ -4,7 +4,7 @@ build:
|
||||
image: latest
|
||||
|
||||
python:
|
||||
version: 3.6
|
||||
version: 3.7
|
||||
setup_py_install: true
|
||||
|
||||
requirements_file: requirements_docs.txt
|
||||
|
14
.travis.yml
14
.travis.yml
@@ -1,9 +1,7 @@
|
||||
sudo: false
|
||||
dist: xenial
|
||||
dist: bionic
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: "ppa:jonathonf/ffmpeg-4"
|
||||
packages:
|
||||
- libudev-dev
|
||||
- libavformat-dev
|
||||
@@ -16,15 +14,13 @@ addons:
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: "3.6.1"
|
||||
- python: "3.7.0"
|
||||
env: TOXENV=lint
|
||||
- python: "3.6.1"
|
||||
- python: "3.7.0"
|
||||
env: TOXENV=pylint PYLINT_ARGS=--jobs=0 TRAVIS_WAIT=30
|
||||
- python: "3.6.1"
|
||||
- python: "3.7.0"
|
||||
env: TOXENV=typing
|
||||
- python: "3.6.1"
|
||||
env: TOXENV=py36
|
||||
- python: "3.7"
|
||||
- python: "3.7.0"
|
||||
env: TOXENV=py37
|
||||
|
||||
cache:
|
||||
|
31
CODEOWNERS
31
CODEOWNERS
@@ -17,7 +17,6 @@ homeassistant/components/abode/* @shred86
|
||||
homeassistant/components/adguard/* @frenck
|
||||
homeassistant/components/airly/* @bieniu
|
||||
homeassistant/components/airvisual/* @bachya
|
||||
homeassistant/components/alarm_control_panel/* @colinodell
|
||||
homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy
|
||||
homeassistant/components/almond/* @gcampax @balloob
|
||||
homeassistant/components/alpha_vantage/* @fabaff
|
||||
@@ -33,6 +32,7 @@ homeassistant/components/arcam_fmj/* @elupus
|
||||
homeassistant/components/arduino/* @fabaff
|
||||
homeassistant/components/arest/* @fabaff
|
||||
homeassistant/components/asuswrt/* @kennedyshead
|
||||
homeassistant/components/aten_pe/* @mtdcr
|
||||
homeassistant/components/atome/* @baqs
|
||||
homeassistant/components/aurora_abb_powerone/* @davet2001
|
||||
homeassistant/components/auth/* @home-assistant/core
|
||||
@@ -50,7 +50,7 @@ homeassistant/components/bizkaibus/* @UgaitzEtxebarria
|
||||
homeassistant/components/blink/* @fronzbot
|
||||
homeassistant/components/bmw_connected_drive/* @gerard33
|
||||
homeassistant/components/braviatv/* @robbiet480
|
||||
homeassistant/components/broadlink/* @danielhiversen
|
||||
homeassistant/components/broadlink/* @danielhiversen @felipediel
|
||||
homeassistant/components/brunt/* @eavanvalkenburg
|
||||
homeassistant/components/bt_smarthub/* @jxwolstenholme
|
||||
homeassistant/components/buienradar/* @mjj4791 @ties
|
||||
@@ -79,15 +79,19 @@ homeassistant/components/device_automation/* @home-assistant/core
|
||||
homeassistant/components/digital_ocean/* @fabaff
|
||||
homeassistant/components/discogs/* @thibmaek
|
||||
homeassistant/components/doorbird/* @oblogic7
|
||||
homeassistant/components/dsmr_reader/* @depl0y
|
||||
homeassistant/components/dweet/* @fabaff
|
||||
homeassistant/components/ecobee/* @marthoc
|
||||
homeassistant/components/ecovacs/* @OverloadUT
|
||||
homeassistant/components/egardia/* @jeroenterheerdt
|
||||
homeassistant/components/eight_sleep/* @mezz64
|
||||
homeassistant/components/elgato/* @frenck
|
||||
homeassistant/components/elv/* @majuss
|
||||
homeassistant/components/emby/* @mezz64
|
||||
homeassistant/components/emulated_hue/* @NobleKangaroo
|
||||
homeassistant/components/enigma2/* @fbradyirl
|
||||
homeassistant/components/enocean/* @bdurrer
|
||||
homeassistant/components/entur_public_transport/* @hfurubotten
|
||||
homeassistant/components/environment_canada/* @michaeldavie
|
||||
homeassistant/components/ephember/* @ttroy50
|
||||
homeassistant/components/epsonworkforce/* @ThaStealth
|
||||
@@ -95,11 +99,13 @@ homeassistant/components/eq3btsmart/* @rytilahti
|
||||
homeassistant/components/esphome/* @OttoWinter
|
||||
homeassistant/components/essent/* @TheLastProject
|
||||
homeassistant/components/evohome/* @zxdavb
|
||||
homeassistant/components/fastdotcom/* @rohankapoorcom
|
||||
homeassistant/components/file/* @fabaff
|
||||
homeassistant/components/filter/* @dgomes
|
||||
homeassistant/components/fitbit/* @robbiet480
|
||||
homeassistant/components/fixer/* @fabaff
|
||||
homeassistant/components/flock/* @fabaff
|
||||
homeassistant/components/flume/* @ChrisMandich
|
||||
homeassistant/components/flunearyou/* @bachya
|
||||
homeassistant/components/fortigate/* @kifeo
|
||||
homeassistant/components/fortios/* @kimfrellsen
|
||||
@@ -112,6 +118,8 @@ homeassistant/components/gearbest/* @HerrHofrat
|
||||
homeassistant/components/geniushub/* @zxdavb
|
||||
homeassistant/components/geo_rss_events/* @exxamalte
|
||||
homeassistant/components/geonetnz_quakes/* @exxamalte
|
||||
homeassistant/components/geonetnz_volcano/* @exxamalte
|
||||
homeassistant/components/gios/* @bieniu
|
||||
homeassistant/components/gitter/* @fabaff
|
||||
homeassistant/components/glances/* @fabaff @engrbm87
|
||||
homeassistant/components/gntp/* @robbiet480
|
||||
@@ -125,6 +133,7 @@ homeassistant/components/growatt_server/* @indykoning
|
||||
homeassistant/components/gtfs/* @robbiet480
|
||||
homeassistant/components/harmony/* @ehendrix23
|
||||
homeassistant/components/hassio/* @home-assistant/hass-io
|
||||
homeassistant/components/heatmiser/* @andylockran
|
||||
homeassistant/components/heos/* @andrewsayre
|
||||
homeassistant/components/here_travel_time/* @eifinger
|
||||
homeassistant/components/hikvision/* @mezz64
|
||||
@@ -144,6 +153,7 @@ homeassistant/components/huawei_lte/* @scop
|
||||
homeassistant/components/huawei_router/* @abmantis
|
||||
homeassistant/components/hue/* @balloob
|
||||
homeassistant/components/iaqualink/* @flz
|
||||
homeassistant/components/icloud/* @Quentame
|
||||
homeassistant/components/ign_sismologia/* @exxamalte
|
||||
homeassistant/components/incomfort/* @zxdavb
|
||||
homeassistant/components/influxdb/* @fabaff
|
||||
@@ -153,7 +163,10 @@ homeassistant/components/input_number/* @home-assistant/core
|
||||
homeassistant/components/input_select/* @home-assistant/core
|
||||
homeassistant/components/input_text/* @home-assistant/core
|
||||
homeassistant/components/integration/* @dgomes
|
||||
homeassistant/components/intent/* @home-assistant/core
|
||||
homeassistant/components/intesishome/* @jnimmo
|
||||
homeassistant/components/ios/* @robbiet480
|
||||
homeassistant/components/iperf3/* @rohankapoorcom
|
||||
homeassistant/components/ipma/* @dgomes
|
||||
homeassistant/components/iqvia/* @bachya
|
||||
homeassistant/components/irish_rail_transport/* @ttroy50
|
||||
@@ -174,11 +187,13 @@ homeassistant/components/life360/* @pnbruckner
|
||||
homeassistant/components/linky/* @Quentame
|
||||
homeassistant/components/linux_battery/* @fabaff
|
||||
homeassistant/components/liveboxplaytv/* @pschmitt
|
||||
homeassistant/components/local_ip/* @issacg
|
||||
homeassistant/components/logger/* @home-assistant/core
|
||||
homeassistant/components/logi_circle/* @evanjd
|
||||
homeassistant/components/lovelace/* @home-assistant/frontend
|
||||
homeassistant/components/luci/* @fbradyirl @mzdrale
|
||||
homeassistant/components/luftdaten/* @fabaff
|
||||
homeassistant/components/lupusec/* @majuss
|
||||
homeassistant/components/lutron/* @JonGilmore
|
||||
homeassistant/components/mastodon/* @fabaff
|
||||
homeassistant/components/matrix/* @tinloaf
|
||||
@@ -193,6 +208,7 @@ homeassistant/components/mill/* @danielhiversen
|
||||
homeassistant/components/min_max/* @fabaff
|
||||
homeassistant/components/minio/* @tkislan
|
||||
homeassistant/components/mobile_app/* @robbiet480
|
||||
homeassistant/components/modbus/* @adamchengtkc
|
||||
homeassistant/components/monoprice/* @etsinko
|
||||
homeassistant/components/moon/* @fabaff
|
||||
homeassistant/components/mpd/* @fabaff
|
||||
@@ -206,6 +222,7 @@ homeassistant/components/ness_alarm/* @nickw444
|
||||
homeassistant/components/nest/* @awarecan
|
||||
homeassistant/components/netdata/* @fabaff
|
||||
homeassistant/components/nextbus/* @vividboarder
|
||||
homeassistant/components/nilu/* @hfurubotten
|
||||
homeassistant/components/nissan_leaf/* @filcole
|
||||
homeassistant/components/nmbs/* @thibmaek
|
||||
homeassistant/components/no_ip/* @fabaff
|
||||
@@ -220,6 +237,7 @@ homeassistant/components/obihai/* @dshokouhi
|
||||
homeassistant/components/ohmconnect/* @robbiet480
|
||||
homeassistant/components/ombi/* @larssont
|
||||
homeassistant/components/onboarding/* @home-assistant/core
|
||||
homeassistant/components/onewire/* @garbled1
|
||||
homeassistant/components/opentherm_gw/* @mvn23
|
||||
homeassistant/components/openuv/* @bachya
|
||||
homeassistant/components/openweathermap/* @fabaff
|
||||
@@ -237,6 +255,7 @@ homeassistant/components/plant/* @ChristianKuehnel
|
||||
homeassistant/components/plex/* @jjlawren
|
||||
homeassistant/components/plugwise/* @laetificat @CoMPaTech @bouwew
|
||||
homeassistant/components/point/* @fredrike
|
||||
homeassistant/components/proxmoxve/* @k4ds3
|
||||
homeassistant/components/ps4/* @ktnrg45
|
||||
homeassistant/components/ptvsd/* @swamp-ig
|
||||
homeassistant/components/push/* @dgomes
|
||||
@@ -266,6 +285,7 @@ homeassistant/components/seventeentrack/* @bachya
|
||||
homeassistant/components/shell_command/* @home-assistant/core
|
||||
homeassistant/components/shiftr/* @fabaff
|
||||
homeassistant/components/shodan/* @fabaff
|
||||
homeassistant/components/signal_messenger/* @bbernhard
|
||||
homeassistant/components/simplisafe/* @bachya
|
||||
homeassistant/components/sinch/* @bendikrb
|
||||
homeassistant/components/slide/* @ualex73
|
||||
@@ -281,8 +301,10 @@ homeassistant/components/soma/* @ratsept
|
||||
homeassistant/components/somfy/* @tetienne
|
||||
homeassistant/components/songpal/* @rytilahti
|
||||
homeassistant/components/spaceapi/* @fabaff
|
||||
homeassistant/components/speedtestdotnet/* @rohankapoorcom
|
||||
homeassistant/components/spider/* @peternijssen
|
||||
homeassistant/components/sql/* @dgomes
|
||||
homeassistant/components/starline/* @anonym-tsk
|
||||
homeassistant/components/statistics/* @fabaff
|
||||
homeassistant/components/stiebel_eltron/* @fucm
|
||||
homeassistant/components/stream/* @hunterjm
|
||||
@@ -298,11 +320,12 @@ homeassistant/components/switchmate/* @danielhiversen
|
||||
homeassistant/components/syncthru/* @nielstron
|
||||
homeassistant/components/synology_srm/* @aerialls
|
||||
homeassistant/components/syslog/* @fabaff
|
||||
homeassistant/components/tado/* @michaelarnauts
|
||||
homeassistant/components/tahoma/* @philklei
|
||||
homeassistant/components/tautulli/* @ludeeus
|
||||
homeassistant/components/tellduslive/* @fredrike
|
||||
homeassistant/components/template/* @PhracturedBlue
|
||||
homeassistant/components/tesla/* @zabuldon
|
||||
homeassistant/components/tesla/* @zabuldon @alandtse
|
||||
homeassistant/components/tfiac/* @fredrike @mellado
|
||||
homeassistant/components/thethingsnetwork/* @fabaff
|
||||
homeassistant/components/threshold/* @fabaff
|
||||
@@ -331,6 +354,7 @@ homeassistant/components/usgs_earthquakes_feed/* @exxamalte
|
||||
homeassistant/components/utility_meter/* @dgomes
|
||||
homeassistant/components/velbus/* @cereal2nd
|
||||
homeassistant/components/velux/* @Julius2342
|
||||
homeassistant/components/versasense/* @flamm3blemuff1n
|
||||
homeassistant/components/version/* @fabaff
|
||||
homeassistant/components/vesync/* @markperdue @webdjoe
|
||||
homeassistant/components/vicare/* @oischinger
|
||||
@@ -345,6 +369,7 @@ homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/wemo/* @sqldiablo
|
||||
homeassistant/components/withings/* @vangorra
|
||||
homeassistant/components/wled/* @frenck
|
||||
homeassistant/components/workday/* @fabaff
|
||||
homeassistant/components/worldclock/* @fabaff
|
||||
homeassistant/components/wwlln/* @bachya
|
||||
homeassistant/components/xbox_live/* @MartinHjelmare
|
||||
|
@@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot
|
||||
|
||||
The process is straight-forward.
|
||||
|
||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0)
|
||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0 and 1)
|
||||
- Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant).
|
||||
- Write the code for your device, notification service, sensor, or IoT thing.
|
||||
- Ensure tests work.
|
||||
@@ -12,3 +12,7 @@ The process is straight-forward.
|
||||
|
||||
Still interested? Then you should take a peek at the [developer documentation](https://developers.home-assistant.io/) to get more details.
|
||||
|
||||
## Feature suggestions
|
||||
|
||||
If you want to suggest a new feature for Home Assistant (e.g., new integrations), please open a thread in our [Community Forum: Feature Requests](https://community.home-assistant.io/c/feature-requests).
|
||||
We use [GitHub for tracking issues](https://github.com/home-assistant/home-assistant/issues), not for tracking feature requests.
|
@@ -1,14 +1,7 @@
|
||||
Home Assistant |Chat Status|
|
||||
=================================================================================
|
||||
|
||||
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
|
||||
|
||||
To get started:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
python3 -m pip install homeassistant
|
||||
hass --open-ui
|
||||
Open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server.
|
||||
|
||||
Check out `home-assistant.io <https://home-assistant.io>`__ for `a
|
||||
demo <https://home-assistant.io/demo/>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
|
||||
|
@@ -14,8 +14,6 @@ pr:
|
||||
|
||||
resources:
|
||||
containers:
|
||||
- container: 36
|
||||
image: homeassistant/ci-azure:3.6
|
||||
- container: 37
|
||||
image: homeassistant/ci-azure:3.7
|
||||
repositories:
|
||||
@@ -25,7 +23,7 @@ resources:
|
||||
endpoint: 'home-assistant'
|
||||
variables:
|
||||
- name: PythonMain
|
||||
value: '36'
|
||||
value: '37'
|
||||
- group: codecov
|
||||
|
||||
stages:
|
||||
@@ -50,6 +48,18 @@ stages:
|
||||
. venv/bin/activate
|
||||
pre-commit run flake8 --all-files
|
||||
displayName: 'Run flake8'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run bandit --all-files
|
||||
displayName: 'Run bandit'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run isort --all-files --show-diff-on-failure
|
||||
displayName: 'Run isort'
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run check-json --all-files
|
||||
displayName: 'Run check-json'
|
||||
- job: 'Validate'
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
@@ -87,7 +97,7 @@ stages:
|
||||
pre-commit install-hooks --config .pre-commit-config-all.yaml
|
||||
- script: |
|
||||
. venv/bin/activate
|
||||
pre-commit run black --all-files
|
||||
pre-commit run black --all-files --show-diff-on-failure
|
||||
displayName: 'Check Black formatting'
|
||||
|
||||
- stage: 'Tests'
|
||||
@@ -100,8 +110,6 @@ stages:
|
||||
strategy:
|
||||
maxParallel: 3
|
||||
matrix:
|
||||
Python36:
|
||||
python.container: '36'
|
||||
Python37:
|
||||
python.container: '37'
|
||||
container: $[ variables['python.container'] ]
|
||||
@@ -158,7 +166,7 @@ stages:
|
||||
python -m venv venv
|
||||
|
||||
. venv/bin/activate
|
||||
pip install -U pip setuptools
|
||||
pip install -U pip setuptools wheel
|
||||
pip install -r requirements_all.txt -c homeassistant/package_constraints.txt
|
||||
pip install -r requirements_test.txt -c homeassistant/package_constraints.txt
|
||||
- script: |
|
||||
|
@@ -8,7 +8,6 @@ Loosely based on https://github.com/astropy/astropy/pull/347
|
||||
import os
|
||||
import warnings
|
||||
|
||||
|
||||
__licence__ = 'BSD (3 clause)'
|
||||
|
||||
|
||||
|
@@ -17,11 +17,11 @@
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import sys
|
||||
import os
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
|
||||
from homeassistant.const import __version__, __short_version__
|
||||
from homeassistant.const import __short_version__, __version__
|
||||
|
||||
PROJECT_NAME = 'Home Assistant'
|
||||
PROJECT_PACKAGE_NAME = 'homeassistant'
|
||||
|
@@ -1,26 +1,23 @@
|
||||
"""Start Home Assistant."""
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from typing import List, Dict, Any, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Dict, List
|
||||
|
||||
from homeassistant import monkey_patch
|
||||
from homeassistant.const import __version__, REQUIRED_PYTHON_VER, RESTART_EXIT_CODE
|
||||
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant import core
|
||||
|
||||
|
||||
def set_loop() -> None:
|
||||
"""Attempt to use uvloop."""
|
||||
import asyncio
|
||||
"""Attempt to use different loop."""
|
||||
from asyncio.events import BaseDefaultEventLoopPolicy
|
||||
|
||||
policy = None
|
||||
|
||||
if sys.platform == "win32":
|
||||
if hasattr(asyncio, "WindowsProactorEventLoopPolicy"):
|
||||
# pylint: disable=no-member
|
||||
@@ -33,15 +30,7 @@ def set_loop() -> None:
|
||||
_loop_factory = asyncio.ProactorEventLoop
|
||||
|
||||
policy = ProactorPolicy()
|
||||
else:
|
||||
try:
|
||||
import uvloop
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
policy = uvloop.EventLoopPolicy()
|
||||
|
||||
if policy is not None:
|
||||
asyncio.set_event_loop_policy(policy)
|
||||
|
||||
|
||||
@@ -89,11 +78,7 @@ def ensure_config_path(config_dir: str) -> None:
|
||||
try:
|
||||
os.mkdir(lib_dir)
|
||||
except OSError:
|
||||
print(
|
||||
("Fatal Error: Unable to create library " "directory {} ").format(
|
||||
lib_dir
|
||||
)
|
||||
)
|
||||
print("Fatal Error: Unable to create library directory {}".format(lib_dir))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -158,7 +143,7 @@ def get_arguments() -> argparse.Namespace:
|
||||
"--log-file",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Log file to write to. If not set, CONFIG/home-assistant.log " "is used",
|
||||
help="Log file to write to. If not set, CONFIG/home-assistant.log is used",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-no-color", action="store_true", help="Disable color logs"
|
||||
@@ -272,7 +257,6 @@ 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()
|
||||
@@ -356,11 +340,6 @@ def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
validate_python()
|
||||
|
||||
monkey_patch_needed = sys.version_info[:3] < (3, 6, 3)
|
||||
if monkey_patch_needed and os.environ.get("HASS_NO_MONKEY") != "1":
|
||||
monkey_patch.disable_c_asyncio()
|
||||
monkey_patch.patch_weakref_tasks()
|
||||
|
||||
set_loop()
|
||||
|
||||
# Run a simple daemon runner process on Windows to handle restarts
|
||||
@@ -394,13 +373,11 @@ def main() -> int:
|
||||
if args.pid_file:
|
||||
write_pid(args.pid_file)
|
||||
|
||||
from homeassistant.util.async_ import asyncio_run
|
||||
|
||||
exit_code = asyncio_run(setup_and_run_hass(config_dir, args))
|
||||
exit_code = asyncio.run(setup_and_run_hass(config_dir, args))
|
||||
if exit_code == RESTART_EXIT_CODE and not args.runner:
|
||||
try_to_restart()
|
||||
|
||||
return exit_code # type: ignore
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@@ -1,21 +1,21 @@
|
||||
"""Provide an authentication layer for Home Assistant."""
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Tuple, cast
|
||||
|
||||
import jwt
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import auth_store, models
|
||||
from .const import GROUP_ID_ADMIN
|
||||
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
|
||||
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
|
||||
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
|
||||
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
|
||||
|
||||
EVENT_USER_ADDED = "user_added"
|
||||
EVENT_USER_REMOVED = "user_removed"
|
||||
|
@@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import models
|
||||
from .const import GROUP_ID_ADMIN, GROUP_ID_USER, GROUP_ID_READ_ONLY
|
||||
from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY, GROUP_ID_USER
|
||||
from .permissions import PermissionLookup, system_policies
|
||||
from .permissions.types import PolicyType
|
||||
|
||||
|
@@ -7,7 +7,7 @@ from typing import Any, Dict, Optional
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant import requirements, data_entry_flow
|
||||
from homeassistant import data_entry_flow, requirements
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -42,7 +42,7 @@ class MultiFactorAuthModule:
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def id(self) -> str: # pylint: disable=invalid-name
|
||||
def id(self) -> str:
|
||||
"""Return id of the auth module.
|
||||
|
||||
Default is same as type
|
||||
|
@@ -7,9 +7,9 @@ import voluptuous as vol
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import (
|
||||
MultiFactorAuthModule,
|
||||
MULTI_FACTOR_AUTH_MODULES,
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
|
||||
MULTI_FACTOR_AUTH_MODULES,
|
||||
MultiFactorAuthModule,
|
||||
SetupFlow,
|
||||
)
|
||||
|
||||
|
@@ -3,9 +3,9 @@
|
||||
Sending HOTP through notify service
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Dict, Optional, List
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
@@ -16,9 +16,9 @@ from homeassistant.exceptions import ServiceNotFound
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from . import (
|
||||
MultiFactorAuthModule,
|
||||
MULTI_FACTOR_AUTH_MODULES,
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
|
||||
MULTI_FACTOR_AUTH_MODULES,
|
||||
MultiFactorAuthModule,
|
||||
SetupFlow,
|
||||
)
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Time-based One Time Password auth module."""
|
||||
import asyncio
|
||||
import logging
|
||||
from io import BytesIO
|
||||
import logging
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -10,9 +10,9 @@ from homeassistant.auth.models import User
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import (
|
||||
MultiFactorAuthModule,
|
||||
MULTI_FACTOR_AUTH_MODULES,
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
|
||||
MULTI_FACTOR_AUTH_MODULES,
|
||||
MultiFactorAuthModule,
|
||||
SetupFlow,
|
||||
)
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
"""Auth models."""
|
||||
from datetime import datetime, timedelta
|
||||
import secrets
|
||||
from typing import Dict, List, NamedTuple, Optional
|
||||
import uuid
|
||||
|
||||
@@ -9,7 +10,6 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import permissions as perm_mdl
|
||||
from .const import GROUP_ID_ADMIN
|
||||
from .util import generate_secret
|
||||
|
||||
TOKEN_TYPE_NORMAL = "normal"
|
||||
TOKEN_TYPE_SYSTEM = "system"
|
||||
@@ -96,8 +96,8 @@ class RefreshToken:
|
||||
)
|
||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
||||
created_at = attr.ib(type=datetime, factory=dt_util.utcnow)
|
||||
token = attr.ib(type=str, factory=lambda: generate_secret(64))
|
||||
jwt_key = attr.ib(type=str, factory=lambda: generate_secret(64))
|
||||
token = attr.ib(type=str, factory=lambda: secrets.token_hex(64))
|
||||
jwt_key = attr.ib(type=str, factory=lambda: secrets.token_hex(64))
|
||||
|
||||
last_used_at = attr.ib(type=Optional[datetime], default=None)
|
||||
last_used_ip = attr.ib(type=Optional[str], default=None)
|
||||
|
@@ -5,13 +5,12 @@ from typing import Any, Callable, Optional
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import CAT_ENTITIES
|
||||
from .models import PermissionLookup
|
||||
from .types import PolicyType
|
||||
from .entities import ENTITY_POLICY_SCHEMA, compile_entities
|
||||
from .merge import merge_policies # noqa: F401
|
||||
from .models import PermissionLookup
|
||||
from .types import PolicyType
|
||||
from .util import test_all
|
||||
|
||||
|
||||
POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -58,15 +57,12 @@ class PolicyPermissions(AbstractPermissions):
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Equals check."""
|
||||
# pylint: disable=protected-access
|
||||
return isinstance(other, PolicyPermissions) and other._policy == self._policy
|
||||
|
||||
|
||||
class _OwnerPermissions(AbstractPermissions):
|
||||
"""Owner permissions."""
|
||||
|
||||
# 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
|
||||
|
@@ -4,11 +4,10 @@ from typing import Callable, Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT
|
||||
from .const import POLICY_CONTROL, POLICY_EDIT, POLICY_READ, SUBCAT_ALL
|
||||
from .models import PermissionLookup
|
||||
from .types import CategoryType, SubCategoryDict, ValueType
|
||||
|
||||
from .util import SubCatLookupType, lookup_all, compile_policy
|
||||
from .util import SubCatLookupType, compile_policy, lookup_all
|
||||
|
||||
SINGLE_ENTITY_SCHEMA = vol.Any(
|
||||
True,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Merging of policies."""
|
||||
from typing import cast, Dict, List, Set
|
||||
from typing import Dict, List, Set, cast
|
||||
|
||||
from .types import PolicyType, CategoryType
|
||||
from .types import CategoryType, PolicyType
|
||||
|
||||
|
||||
def merge_policies(policies: List[PolicyType]) -> PolicyType:
|
||||
|
@@ -1,5 +1,5 @@
|
||||
"""System policies."""
|
||||
from .const import CAT_ENTITIES, SUBCAT_ALL, POLICY_READ
|
||||
from .const import CAT_ENTITIES, POLICY_READ, SUBCAT_ALL
|
||||
|
||||
ADMIN_POLICY = {CAT_ENTITIES: True}
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
"""Helpers to deal with permissions."""
|
||||
from functools import wraps
|
||||
|
||||
from typing import Callable, Dict, List, Optional, cast
|
||||
|
||||
from .const import SUBCAT_ALL
|
||||
|
@@ -8,8 +8,8 @@ import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant import data_entry_flow, requirements
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
@@ -48,7 +48,7 @@ class AuthProvider:
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def id(self) -> Optional[str]: # pylint: disable=invalid-name
|
||||
def id(self) -> Optional[str]:
|
||||
"""Return id of the auth provider.
|
||||
|
||||
Optional, can be None.
|
||||
|
@@ -1,20 +1,18 @@
|
||||
"""Auth provider that validates credentials via an external command."""
|
||||
|
||||
from typing import Any, Dict, Optional, cast
|
||||
|
||||
import asyncio.subprocess
|
||||
import collections
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, Optional, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
from ..models import Credentials, UserMeta
|
||||
|
||||
|
||||
CONF_COMMAND = "command"
|
||||
CONF_ARGS = "args"
|
||||
CONF_META = "meta"
|
||||
@@ -78,7 +76,7 @@ class CommandLineAuthProvider(AuthProvider):
|
||||
|
||||
if process.returncode != 0:
|
||||
_LOGGER.error(
|
||||
"User %r failed to authenticate, command exited " "with code %d.",
|
||||
"User %r failed to authenticate, command exited with code %d.",
|
||||
username,
|
||||
process.returncode,
|
||||
)
|
||||
|
@@ -3,21 +3,18 @@ import asyncio
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
|
||||
from typing import Any, Dict, List, Optional, Set, cast
|
||||
|
||||
import bcrypt
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
|
||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
from ..models import Credentials, UserMeta
|
||||
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = "auth_provider.homeassistant"
|
||||
|
||||
|
@@ -5,13 +5,12 @@ from typing import Any, Dict, Optional, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
from ..models import Credentials, UserMeta
|
||||
|
||||
|
||||
USER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("username"): str,
|
||||
|
@@ -12,9 +12,9 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
from .. import AuthManager
|
||||
from ..models import Credentials, UserMeta, User
|
||||
from ..models import Credentials, User, UserMeta
|
||||
|
||||
AUTH_PROVIDER_TYPE = "legacy_api_password"
|
||||
CONF_API_PASSWORD = "api_password"
|
||||
|
@@ -3,15 +3,16 @@
|
||||
It shows list of users if access from trusted network.
|
||||
Abort login flow if not access from trusted network.
|
||||
"""
|
||||
from ipaddress import ip_network, IPv4Address, IPv6Address, IPv4Network, IPv6Network
|
||||
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_network
|
||||
from typing import Any, Dict, List, Optional, Union, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
from ..models import Credentials, UserMeta
|
||||
|
||||
IPAddress = Union[IPv4Address, IPv6Address]
|
||||
|
@@ -1,13 +0,0 @@
|
||||
"""Auth utils."""
|
||||
import binascii
|
||||
import os
|
||||
|
||||
|
||||
def generate_secret(entropy: int = 32) -> str:
|
||||
"""Generate a secret.
|
||||
|
||||
Backport of secrets.token_hex from Python 3.6
|
||||
|
||||
Event loop friendly.
|
||||
"""
|
||||
return binascii.hexlify(os.urandom(entropy)).decode("ascii")
|
@@ -1,22 +1,26 @@
|
||||
"""Provide methods to bootstrap a Home Assistant instance."""
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
from time import time
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Optional, Dict, Set
|
||||
from typing import Any, Dict, Optional, Set
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import core, config as conf_util, config_entries, loader
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant import config as conf_util, config_entries, core, loader
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_CLOSE,
|
||||
REQUIRED_NEXT_PYTHON_DATE,
|
||||
REQUIRED_NEXT_PYTHON_VER,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.package import async_get_user_site, is_virtual_env
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -62,7 +66,7 @@ async def async_from_config_dict(
|
||||
hass.config.skip_pip = skip_pip
|
||||
if skip_pip:
|
||||
_LOGGER.warning(
|
||||
"Skipping pip installation of required modules. " "This may cause issues"
|
||||
"Skipping pip installation of required modules. This may cause issues"
|
||||
)
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
@@ -95,11 +99,14 @@ async def async_from_config_dict(
|
||||
stop = time()
|
||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop - start)
|
||||
|
||||
if sys.version_info[:3] < (3, 7, 0):
|
||||
if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER:
|
||||
msg = (
|
||||
"Python 3.6 support is deprecated and will "
|
||||
"be removed in the first release after December 15, 2019. Please "
|
||||
"upgrade Python to 3.7.0 or higher."
|
||||
"Support for the running Python version "
|
||||
f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will "
|
||||
f"be removed in the first release after {REQUIRED_NEXT_PYTHON_DATE}. "
|
||||
"Please upgrade Python to "
|
||||
f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER)} or "
|
||||
"higher."
|
||||
)
|
||||
_LOGGER.warning(msg)
|
||||
hass.components.persistent_notification.async_create(
|
||||
@@ -161,7 +168,7 @@ def async_enable_logging(
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
fmt = "%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s"
|
||||
fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s"
|
||||
datefmt = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
if not log_no_color:
|
||||
|
@@ -11,7 +11,6 @@ import logging
|
||||
|
||||
from homeassistant.core import split_entity_id
|
||||
|
||||
|
||||
# mypy: allow-untyped-defs
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@@ -12,7 +12,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Adgangskode",
|
||||
"username": "Email adresse"
|
||||
"username": "Email-adresse"
|
||||
},
|
||||
"title": "Udfyld dine Abode-loginoplysninger"
|
||||
}
|
||||
|
@@ -5,8 +5,8 @@
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a Abode.",
|
||||
"identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.",
|
||||
"invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435."
|
||||
"identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.",
|
||||
"invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
|
@@ -20,14 +20,14 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
DOMAIN,
|
||||
DEFAULT_CACHEDB,
|
||||
DOMAIN,
|
||||
SIGNAL_CAPTURE_IMAGE,
|
||||
SIGNAL_TRIGGER_QUICK_ACTION,
|
||||
)
|
||||
|
@@ -2,6 +2,10 @@
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel.const import (
|
||||
SUPPORT_ALARM_ARM_AWAY,
|
||||
SUPPORT_ALARM_ARM_HOME,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
@@ -51,6 +55,11 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel):
|
||||
state = None
|
||||
return state
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._device.set_standby()
|
||||
|
@@ -10,7 +10,7 @@ from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import DOMAIN, DEFAULT_CACHEDB # pylint: disable=W0611
|
||||
from .const import DEFAULT_CACHEDB, DOMAIN # pylint: disable=unused-import
|
||||
|
||||
CONF_POLLING = "polling"
|
||||
|
||||
|
@@ -72,19 +72,19 @@ class AbodeSensor(AbodeDevice):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._sensor_type == "temp":
|
||||
if self._sensor_type == CONST.TEMP_STATUS_KEY:
|
||||
return self._device.temp
|
||||
if self._sensor_type == "humidity":
|
||||
if self._sensor_type == CONST.HUMI_STATUS_KEY:
|
||||
return self._device.humidity
|
||||
if self._sensor_type == "lux":
|
||||
if self._sensor_type == CONST.LUX_STATUS_KEY:
|
||||
return self._device.lux
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the units of measurement."""
|
||||
if self._sensor_type == "temp":
|
||||
if self._sensor_type == CONST.TEMP_STATUS_KEY:
|
||||
return self._device.temp_unit
|
||||
if self._sensor_type == "humidity":
|
||||
if self._sensor_type == CONST.HUMI_STATUS_KEY:
|
||||
return self._device.humidity_unit
|
||||
if self._sensor_type == "lux":
|
||||
if self._sensor_type == CONST.LUX_STATUS_KEY:
|
||||
return self._device.lux_unit
|
||||
|
@@ -1,17 +1,17 @@
|
||||
"""Use serial protocol of Acer projector to obtain state of the projector."""
|
||||
import logging
|
||||
import re
|
||||
import serial
|
||||
|
||||
import serial
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
|
||||
from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
STATE_UNKNOWN,
|
||||
CONF_NAME,
|
||||
CONF_FILENAME,
|
||||
CONF_NAME,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
@@ -1,18 +1,19 @@
|
||||
"""Support for Actiontec MI424WR (Verizon FIOS) routers."""
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import re
|
||||
import telnetlib
|
||||
from collections import namedtuple
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN,
|
||||
PLATFORM_SCHEMA,
|
||||
DeviceScanner,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -1,16 +1,18 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"adguard_home_addon_outdated": "Denne integration kr\u00e6ver AdGuard Home {minimal_version} eller h\u00f8jere, du har {current_version}. Opdater venligst din Hass.io AdGuard Home-tilf\u00f8jelse.",
|
||||
"adguard_home_outdated": "Denne integration kr\u00e6ver AdGuard Home {minimal_version} eller h\u00f8jere, du har {current_version}.",
|
||||
"existing_instance_updated": "Opdaterede eksisterende konfiguration.",
|
||||
"single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af AdGuard Home."
|
||||
"single_instance_allowed": "Kun en enkelt konfiguration af AdGuard Home er tilladt."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Forbindelse mislykkedes."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home, der leveres af Hass.io add-on: {addon}?",
|
||||
"title": "AdGuard Home via Hass.io add-on"
|
||||
"description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home leveret af Hass.io-tilf\u00f8jelsen: {addon}?",
|
||||
"title": "AdGuard Home via Hass.io-tilf\u00f8jelse"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
@@ -21,8 +23,8 @@
|
||||
"username": "Brugernavn",
|
||||
"verify_ssl": "AdGuard Home bruger et korrekt certifikat"
|
||||
},
|
||||
"description": "Konfigurer din AdGuard Home instans for at tillade overv\u00e5gning og kontrol.",
|
||||
"title": "Link AdGuard Home."
|
||||
"description": "Konfigurer din AdGuard Home-instans for at tillade overv\u00e5gning og kontrol.",
|
||||
"title": "Forbind din AdGuard Home."
|
||||
}
|
||||
},
|
||||
"title": "AdGuard Home"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"adguard_home_addon_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}. Aggiorna il componente aggiuntivo Hass.io AdGuard Home.",
|
||||
"adguard_home_addon_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}. Aggiorna il componente aggiuntivo AdGuard Home di Hass.io.",
|
||||
"adguard_home_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}.",
|
||||
"existing_instance_updated": "Configurazione esistente aggiornata.",
|
||||
"single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home."
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Vuoi configurare Home Assistant per connettersi alla AdGuard Home fornita dal componente aggiuntivo di Hass.io: {addon} ?",
|
||||
"description": "Vuoi configurare Home Assistant per connettersi alla AdGuard Home fornita dal componente aggiuntivo di Hass.io: {addon}?",
|
||||
"title": "AdGuard Home tramite il componente aggiuntivo di Hass.io"
|
||||
},
|
||||
"user": {
|
||||
|
@@ -178,7 +178,7 @@ class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor):
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(
|
||||
adguard,
|
||||
"Searches Safe Search Enforced",
|
||||
"AdGuard Safe Searches Enforced",
|
||||
"mdi:shield-search",
|
||||
"enforced_safesearch",
|
||||
"requests",
|
||||
|
@@ -1,14 +1,13 @@
|
||||
"""Support for Automation Device Specification (ADS)."""
|
||||
import threading
|
||||
import struct
|
||||
import logging
|
||||
import ctypes
|
||||
from collections import namedtuple
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
import ctypes
|
||||
import logging
|
||||
import struct
|
||||
import threading
|
||||
|
||||
import async_timeout
|
||||
|
||||
import pyads
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
|
@@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import CONF_ADS_VAR, DATA_ADS, AdsEntity, STATE_KEY_STATE
|
||||
from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -4,25 +4,25 @@ 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,
|
||||
PLATFORM_SCHEMA,
|
||||
SUPPORT_CLOSE,
|
||||
SUPPORT_OPEN,
|
||||
SUPPORT_SET_POSITION,
|
||||
SUPPORT_STOP,
|
||||
CoverDevice,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
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,
|
||||
STATE_KEY_STATE,
|
||||
AdsEntity,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@@ -16,9 +16,9 @@ from . import (
|
||||
CONF_ADS_VAR,
|
||||
CONF_ADS_VAR_BRIGHTNESS,
|
||||
DATA_ADS,
|
||||
AdsEntity,
|
||||
STATE_KEY_BRIGHTNESS,
|
||||
STATE_KEY_STATE,
|
||||
AdsEntity,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@@ -8,7 +8,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, AdsEntity, STATE_KEY_STATE
|
||||
from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, STATE_KEY_STATE, AdsEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -3,11 +3,11 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
|
||||
from homeassistant.const import CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import CONF_ADS_VAR, DATA_ADS, AdsEntity, STATE_KEY_STATE
|
||||
from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -2,6 +2,7 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pyaftership.tracker import Tracking
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
@@ -11,6 +12,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -56,8 +58,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the AfterShip sensor platform."""
|
||||
from pyaftership.tracker import Tracking
|
||||
|
||||
apikey = config[CONF_API_KEY]
|
||||
name = config[CONF_NAME]
|
||||
|
||||
|
@@ -2,12 +2,12 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||
PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA_BASE,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"error": {
|
||||
"auth": "API-n\u00f8glen er ikke korrekt.",
|
||||
"name_exists": "Navnet findes allerede.",
|
||||
"wrong_location": "Ingen Airly m\u00e5lestationer i dette omr\u00e5de."
|
||||
"wrong_location": "Ingen Airly-m\u00e5lestationer i dette omr\u00e5de."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
@@ -13,7 +13,7 @@
|
||||
"longitude": "L\u00e6ngdegrad",
|
||||
"name": "Integrationens navn"
|
||||
},
|
||||
"description": "Konfigurer Airly luftkvalitet integration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register",
|
||||
"description": "Konfigurer Airly luftkvalitetsintegration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register",
|
||||
"title": "Airly"
|
||||
}
|
||||
},
|
||||
|
@@ -10,7 +10,6 @@ import async_timeout
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import Config, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@@ -48,9 +47,6 @@ async def async_setup_entry(hass, config_entry):
|
||||
|
||||
await airly.async_update()
|
||||
|
||||
if not airly.data:
|
||||
raise ConfigEntryNotReady()
|
||||
|
||||
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly
|
||||
|
||||
hass.async_create_task(
|
||||
|
@@ -1,11 +1,11 @@
|
||||
"""Support for the Airly air_quality service."""
|
||||
from homeassistant.components.air_quality import (
|
||||
AirQualityEntity,
|
||||
ATTR_AQI,
|
||||
ATTR_PM_10,
|
||||
ATTR_PM_2_5,
|
||||
ATTR_PM_10,
|
||||
AirQualityEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
|
||||
from .const import (
|
||||
ATTR_API_ADVICE,
|
||||
@@ -35,10 +35,13 @@ LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit"
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up Airly air_quality entity based on a config entry."""
|
||||
name = config_entry.data[CONF_NAME]
|
||||
latitude = config_entry.data[CONF_LATITUDE]
|
||||
longitude = config_entry.data[CONF_LONGITUDE]
|
||||
unique_id = f"{latitude}-{longitude}"
|
||||
|
||||
data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]
|
||||
|
||||
async_add_entities([AirlyAirQuality(data, name)], True)
|
||||
async_add_entities([AirlyAirQuality(data, name, unique_id)], True)
|
||||
|
||||
|
||||
def round_state(func):
|
||||
@@ -56,11 +59,12 @@ def round_state(func):
|
||||
class AirlyAirQuality(AirQualityEntity):
|
||||
"""Define an Airly air quality."""
|
||||
|
||||
def __init__(self, airly, name):
|
||||
def __init__(self, airly, name, unique_id):
|
||||
"""Initialize."""
|
||||
self.airly = airly
|
||||
self.data = airly.data
|
||||
self._name = name
|
||||
self._unique_id = unique_id
|
||||
self._pm_2_5 = None
|
||||
self._pm_10 = None
|
||||
self._aqi = None
|
||||
@@ -108,12 +112,12 @@ class AirlyAirQuality(AirQualityEntity):
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique_id for this entity."""
|
||||
return f"{self.airly.latitude}-{self.airly.longitude}"
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return bool(self.airly.data)
|
||||
return bool(self.data)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
@@ -132,7 +136,6 @@ class AirlyAirQuality(AirQualityEntity):
|
||||
|
||||
if self.airly.data:
|
||||
self.data = self.airly.data
|
||||
|
||||
self._pm_10 = self.data[ATTR_API_PM10]
|
||||
self._pm_2_5 = self.data[ATTR_API_PM25]
|
||||
self._aqi = self.data[ATTR_API_CAQI]
|
||||
self._pm_10 = self.data[ATTR_API_PM10]
|
||||
self._pm_2_5 = self.data[ATTR_API_PM25]
|
||||
self._aqi = self.data[ATTR_API_CAQI]
|
||||
|
@@ -1,14 +1,14 @@
|
||||
"""Adds config flow for Airly."""
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
from airly import Airly
|
||||
from airly.exceptions import AirlyError
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import DEFAULT_NAME, DOMAIN, NO_AIRLY_SENSORS
|
||||
|
||||
|
@@ -2,6 +2,8 @@
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DEVICE_CLASS,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_NAME,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
@@ -60,12 +62,16 @@ SENSOR_TYPES = {
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up Airly sensor entities based on a config entry."""
|
||||
name = config_entry.data[CONF_NAME]
|
||||
latitude = config_entry.data[CONF_LATITUDE]
|
||||
longitude = config_entry.data[CONF_LONGITUDE]
|
||||
|
||||
data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]
|
||||
|
||||
sensors = []
|
||||
for sensor in SENSOR_TYPES:
|
||||
sensors.append(AirlySensor(data, name, sensor))
|
||||
unique_id = f"{latitude}-{longitude}-{sensor.lower()}"
|
||||
sensors.append(AirlySensor(data, name, sensor, unique_id))
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
|
||||
|
||||
@@ -84,11 +90,12 @@ def round_state(func):
|
||||
class AirlySensor(Entity):
|
||||
"""Define an Airly sensor."""
|
||||
|
||||
def __init__(self, airly, name, kind):
|
||||
def __init__(self, airly, name, kind, unique_id):
|
||||
"""Initialize."""
|
||||
self.airly = airly
|
||||
self.data = airly.data
|
||||
self._name = name
|
||||
self._unique_id = unique_id
|
||||
self.kind = kind
|
||||
self._device_class = None
|
||||
self._state = None
|
||||
@@ -130,7 +137,7 @@ class AirlySensor(Entity):
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique_id for this entity."""
|
||||
return f"{self.airly.latitude}-{self.airly.longitude}-{self.kind.lower()}"
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
@@ -140,7 +147,7 @@ class AirlySensor(Entity):
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return bool(self.airly.data)
|
||||
return bool(self.data)
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the sensor."""
|
||||
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u043d\u043e\u0449\u0435\u043d \u0440\u0435\u0436\u0438\u043c",
|
||||
"disarm": "\u0414\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0439 {entity_name}",
|
||||
"trigger": "\u0417\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d\u0435 {entity_name}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430",
|
||||
"armed_home": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 - \u0432\u043a\u044a\u0449\u0438",
|
||||
"armed_night": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 - \u043d\u043e\u0449",
|
||||
"disarmed": "{entity_name} \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0430",
|
||||
"triggered": "{entity_name} \u0437\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d\u0430"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "Activa {entity_name} nocturn",
|
||||
"disarm": "Desactiva {entity_name}",
|
||||
"trigger": "Dispara {entity_name}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} activada en mode a fora",
|
||||
"armed_home": "{entity_name} activada en mode a casa",
|
||||
"armed_night": "{entity_name} activada en mode nocturn",
|
||||
"disarmed": "{entity_name} desactivada",
|
||||
"triggered": "{entity_name} disparat/ada"
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +1,18 @@
|
||||
{
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"arm_away": "Tilkobl {entity_name} ude",
|
||||
"arm_home": "Tilkobl {entity_name} hjemme",
|
||||
"arm_night": "Tilkobl {entity_name} nat",
|
||||
"disarm": "Frakobl {entity_name}",
|
||||
"trigger": "Udl\u00f8s {entity_name}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} tilkoblet ude",
|
||||
"armed_home": "{entity_name} tilkoblet hjemme",
|
||||
"armed_night": "{entity_name} tilkoblet nat",
|
||||
"disarmed": "{entity_name} frakoblet",
|
||||
"triggered": "{entity_name} udl\u00f8st"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "Arm {entity_name} night",
|
||||
"disarm": "Disarm {entity_name}",
|
||||
"trigger": "Trigger {entity_name}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} armed away",
|
||||
"armed_home": "{entity_name} armed home",
|
||||
"armed_night": "{entity_name} armed night",
|
||||
"disarmed": "{entity_name} disarmed",
|
||||
"triggered": "{entity_name} triggered"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "Armar {entity_name} por la noche",
|
||||
"disarm": "Desarmar {entity_name}",
|
||||
"trigger": "Lanzar {entity_name}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} armado fuera",
|
||||
"armed_home": "{entity_name} armado en casa",
|
||||
"armed_night": "{entity_name} armado modo noche",
|
||||
"disarmed": "{entity_name} desarmado",
|
||||
"triggered": "{entity_name} activado"
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,11 +1,18 @@
|
||||
{
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"arm_away": "Armer {entity_name} mode sortie",
|
||||
"arm_home": "Armer {entity_name} mode \u00e0 la maison",
|
||||
"arm_night": "Armer {entity_name} mode nuit",
|
||||
"arm_away": "Armer {entity_name} en mode \"sortie\"",
|
||||
"arm_home": "Armer {entity_name} en mode \"maison\"",
|
||||
"arm_night": "Armer {entity_name} en mode \"nuit\"",
|
||||
"disarm": "D\u00e9sarmer {entity_name}",
|
||||
"trigger": "D\u00e9clencheur {entity_name}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "Armer {entity_name} en mode \"sortie\"",
|
||||
"armed_home": "Armer {entity_name} en mode \"maison\"",
|
||||
"armed_night": "Armer {entity_name} en mode \"nuit\"",
|
||||
"disarmed": "{entity_name} d\u00e9sarm\u00e9",
|
||||
"triggered": "{entity_name} d\u00e9clench\u00e9"
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"arm_away": "{entity_name} \u00e9les\u00edt\u00e9se t\u00e1voz\u00f3 m\u00f3dban",
|
||||
"arm_home": "{entity_name} \u00e9les\u00edt\u00e9se otthon marad\u00f3 m\u00f3dban",
|
||||
"arm_night": "{entity_name} \u00e9les\u00edt\u00e9se \u00e9jszakai m\u00f3dban",
|
||||
"disarm": "{entity_name} hat\u00e1stalan\u00edt\u00e1sa",
|
||||
"trigger": "{entity_name} riaszt\u00e1si esem\u00e9ny ind\u00edt\u00e1sa"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} t\u00e1voz\u00f3 m\u00f3dban lett \u00e9les\u00edtve",
|
||||
"armed_home": "{entity_name} otthon marad\u00f3 m\u00f3dban lett \u00e9les\u00edtve",
|
||||
"armed_night": "{entity_name} \u00e9jszakai m\u00f3dban lett \u00e9les\u00edtve",
|
||||
"disarmed": "{entity_name} hat\u00e1stalan\u00edtva lett",
|
||||
"triggered": "{entity_name} riaszt\u00e1sba ker\u00fclt"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "Armare {entity_name} notte",
|
||||
"disarm": "Disarmare {entity_name}",
|
||||
"trigger": "Attivazione {entity_name}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} armata modalit\u00e0 fuori casa",
|
||||
"armed_home": "{entity_name} armata modalit\u00e0 a casa",
|
||||
"armed_night": "{entity_name} armata modalit\u00e0 notte",
|
||||
"disarmed": "{entity_name} disarmato",
|
||||
"triggered": "{entity_name} attivato"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "{entity_name} \uc57c\uac04\uacbd\ube44",
|
||||
"disarm": "{entity_name} \uacbd\ube44\ud574\uc81c",
|
||||
"trigger": "{entity_name} \ud2b8\ub9ac\uac70"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c",
|
||||
"armed_home": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c",
|
||||
"armed_night": "{entity_name} \uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c",
|
||||
"disarmed": "{entity_name} \uc774(\uac00) \ud574\uc81c\ub420 \ub54c",
|
||||
"triggered": "{entity_name} \uc774(\uac00) \ud2b8\ub9ac\uac70\ub420 \ub54c"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "{entity_name} fir Nuecht uschalten",
|
||||
"disarm": "{entity_name} entsch\u00e4rfen",
|
||||
"trigger": "{entity_name} ausl\u00e9isen"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} ugeschalt fir Ennerwee",
|
||||
"armed_home": "{entity_name} ugeschalt fir Doheem",
|
||||
"armed_night": "{entity_name} ugeschalt fir Nuecht",
|
||||
"disarmed": "{entity_name} entsch\u00e4rft",
|
||||
"triggered": "{entity_name} ausgel\u00e9ist"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "Inschakelen {entity_name} nacht",
|
||||
"disarm": "Uitschakelen {entity_name}",
|
||||
"trigger": "Trigger {entity_name}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} afwezig ingeschakeld",
|
||||
"armed_home": "{entity_name} thuis ingeschakeld",
|
||||
"armed_night": "{entity_name} nachtstand ingeschakeld",
|
||||
"disarmed": "{entity_name} uitgeschakeld",
|
||||
"triggered": "{entity_name} geactiveerd"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "Aktiver {entity_name} natt",
|
||||
"disarm": "Deaktiver {entity_name}",
|
||||
"trigger": "Utl\u00f8ser {entity_name}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} borte sikkring ",
|
||||
"armed_home": "{entity_name} hjemme sikkring",
|
||||
"armed_night": "{entity_name} natt sikkring",
|
||||
"disarmed": "{entity_name} deaktivert",
|
||||
"triggered": "{entity_name} utl\u00f8st"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "uzbr\u00f3j (noc) {entity_name}",
|
||||
"disarm": "rozbr\u00f3j {entity_name}",
|
||||
"trigger": "wyzw\u00f3l {entity_name}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} zostanie uzbrojony (poza domem)",
|
||||
"armed_home": "{entity_name} zostanie uzbrojony (w domu)",
|
||||
"armed_night": "{entity_name} zostanie uzbrojony (noc)",
|
||||
"disarmed": "{entity_name} zostanie rozbrojony",
|
||||
"triggered": "{entity_name} zostanie wyzwolony"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "Armar {entity_name} noite",
|
||||
"disarm": "Desarmar {entity_name}",
|
||||
"trigger": "Disparar {entidade_nome}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} armado modo longe",
|
||||
"armed_home": "{entidade_nome} armadado modo casa ",
|
||||
"armed_night": "{entity_name} armadado para noite",
|
||||
"disarmed": "{entity_name} desarmado",
|
||||
"triggered": "{entity_name} acionado"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}",
|
||||
"disarm": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0445\u0440\u0430\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}",
|
||||
"trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}",
|
||||
"armed_home": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}",
|
||||
"armed_night": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}",
|
||||
"disarmed": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}",
|
||||
"triggered": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "Vklju\u010di {entity_name} no\u010d",
|
||||
"disarm": "Razoro\u017ei {entity_name}",
|
||||
"trigger": "Spro\u017ei {entity_name}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} oboro\u017een - zdoma",
|
||||
"armed_home": "{entity_name} oboro\u017een - dom",
|
||||
"armed_night": "{entity_name} oboro\u017een - no\u010d",
|
||||
"disarmed": "{entity_name} razoro\u017een",
|
||||
"triggered": "{entity_name} spro\u017een"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "\u8a2d\u5b9a {entity_name} \u591c\u9593\u6a21\u5f0f",
|
||||
"disarm": "\u89e3\u9664 {entity_name}",
|
||||
"trigger": "\u89f8\u767c {entity_name}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} \u8a2d\u5b9a\u5916\u51fa",
|
||||
"armed_home": "{entity_name} \u8a2d\u5b9a\u5728\u5bb6",
|
||||
"armed_night": "{entity_name} \u8a2d\u5b9a\u591c\u9593",
|
||||
"disarmed": "{entity_name} \u5df2\u89e3\u9664",
|
||||
"triggered": "{entity_name} \u5df2\u89f8\u767c"
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
"""Component to interface with an alarm control panel."""
|
||||
from abc import abstractmethod
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
@@ -7,22 +8,30 @@ import voluptuous as vol
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE,
|
||||
ATTR_CODE_FORMAT,
|
||||
SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_ALARM_DISARM,
|
||||
SERVICE_ALARM_ARM_HOME,
|
||||
SERVICE_ALARM_ARM_AWAY,
|
||||
SERVICE_ALARM_ARM_NIGHT,
|
||||
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
|
||||
)
|
||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||
ENTITY_SERVICE_SCHEMA,
|
||||
PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA_BASE,
|
||||
SERVICE_ALARM_ARM_HOME,
|
||||
SERVICE_ALARM_ARM_NIGHT,
|
||||
SERVICE_ALARM_DISARM,
|
||||
SERVICE_ALARM_TRIGGER,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||
PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA_BASE,
|
||||
make_entity_service_schema,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from .const import (
|
||||
SUPPORT_ALARM_ARM_AWAY,
|
||||
SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
|
||||
SUPPORT_ALARM_ARM_HOME,
|
||||
SUPPORT_ALARM_ARM_NIGHT,
|
||||
SUPPORT_ALARM_TRIGGER,
|
||||
)
|
||||
|
||||
DOMAIN = "alarm_control_panel"
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
ATTR_CHANGED_BY = "changed_by"
|
||||
@@ -32,9 +41,7 @@ ATTR_CODE_ARM_REQUIRED = "code_arm_required"
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
ALARM_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend(
|
||||
{vol.Optional(ATTR_CODE): cv.string}
|
||||
)
|
||||
ALARM_SERVICE_SCHEMA = make_entity_service_schema({vol.Optional(ATTR_CODE): cv.string})
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
@@ -49,21 +56,34 @@ async def async_setup(hass, config):
|
||||
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, "async_alarm_disarm"
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, "async_alarm_arm_home"
|
||||
SERVICE_ALARM_ARM_HOME,
|
||||
ALARM_SERVICE_SCHEMA,
|
||||
"async_alarm_arm_home",
|
||||
[SUPPORT_ALARM_ARM_HOME],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, "async_alarm_arm_away"
|
||||
SERVICE_ALARM_ARM_AWAY,
|
||||
ALARM_SERVICE_SCHEMA,
|
||||
"async_alarm_arm_away",
|
||||
[SUPPORT_ALARM_ARM_AWAY],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, "async_alarm_arm_night"
|
||||
SERVICE_ALARM_ARM_NIGHT,
|
||||
ALARM_SERVICE_SCHEMA,
|
||||
"async_alarm_arm_night",
|
||||
[SUPPORT_ALARM_ARM_NIGHT],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
|
||||
ALARM_SERVICE_SCHEMA,
|
||||
"async_alarm_arm_custom_bypass",
|
||||
[SUPPORT_ALARM_ARM_CUSTOM_BYPASS],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA, "async_alarm_trigger"
|
||||
SERVICE_ALARM_TRIGGER,
|
||||
ALARM_SERVICE_SCHEMA,
|
||||
"async_alarm_trigger",
|
||||
[SUPPORT_ALARM_TRIGGER],
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -79,7 +99,6 @@ async def async_unload_entry(hass, entry):
|
||||
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class AlarmControlPanel(Entity):
|
||||
"""An abstract class for alarm control devices."""
|
||||
|
||||
@@ -164,6 +183,11 @@ class AlarmControlPanel(Entity):
|
||||
"""
|
||||
return self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
|
7
homeassistant/components/alarm_control_panel/const.py
Normal file
7
homeassistant/components/alarm_control_panel/const.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Provides the constants needed for component."""
|
||||
|
||||
SUPPORT_ALARM_ARM_HOME = 1
|
||||
SUPPORT_ALARM_ARM_AWAY = 2
|
||||
SUPPORT_ALARM_ARM_NIGHT = 4
|
||||
SUPPORT_ALARM_TRIGGER = 8
|
||||
SUPPORT_ALARM_ARM_CUSTOM_BYPASS = 16
|
@@ -1,5 +1,6 @@
|
||||
"""Provides device automations for Alarm control panel."""
|
||||
from typing import Optional, List
|
||||
from typing import List, Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
@@ -16,10 +17,17 @@ from homeassistant.const import (
|
||||
SERVICE_ALARM_DISARM,
|
||||
SERVICE_ALARM_TRIGGER,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, Context
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.helpers import entity_registry
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import ATTR_CODE_ARM_REQUIRED, DOMAIN
|
||||
from .const import (
|
||||
SUPPORT_ALARM_ARM_AWAY,
|
||||
SUPPORT_ALARM_ARM_HOME,
|
||||
SUPPORT_ALARM_ARM_NIGHT,
|
||||
SUPPORT_ALARM_TRIGGER,
|
||||
)
|
||||
|
||||
ACTION_TYPES = {"arm_away", "arm_home", "arm_night", "disarm", "trigger"}
|
||||
|
||||
@@ -42,31 +50,42 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
|
||||
if entry.domain != DOMAIN:
|
||||
continue
|
||||
|
||||
state = hass.states.get(entry.entity_id)
|
||||
|
||||
# We need a state or else we can't populate the HVAC and preset modes.
|
||||
if state is None:
|
||||
continue
|
||||
|
||||
supported_features = state.attributes["supported_features"]
|
||||
|
||||
# Add actions for each entity that belongs to this integration
|
||||
actions.append(
|
||||
{
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "arm_away",
|
||||
}
|
||||
)
|
||||
actions.append(
|
||||
{
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "arm_home",
|
||||
}
|
||||
)
|
||||
actions.append(
|
||||
{
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "arm_night",
|
||||
}
|
||||
)
|
||||
if supported_features & SUPPORT_ALARM_ARM_AWAY:
|
||||
actions.append(
|
||||
{
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "arm_away",
|
||||
}
|
||||
)
|
||||
if supported_features & SUPPORT_ALARM_ARM_HOME:
|
||||
actions.append(
|
||||
{
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "arm_home",
|
||||
}
|
||||
)
|
||||
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
|
||||
actions.append(
|
||||
{
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "arm_night",
|
||||
}
|
||||
)
|
||||
actions.append(
|
||||
{
|
||||
CONF_DEVICE_ID: device_id,
|
||||
@@ -75,14 +94,15 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
|
||||
CONF_TYPE: "disarm",
|
||||
}
|
||||
)
|
||||
actions.append(
|
||||
{
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "trigger",
|
||||
}
|
||||
)
|
||||
if supported_features & SUPPORT_ALARM_TRIGGER:
|
||||
actions.append(
|
||||
{
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "trigger",
|
||||
}
|
||||
)
|
||||
|
||||
return actions
|
||||
|
||||
|
151
homeassistant/components/alarm_control_panel/device_trigger.py
Normal file
151
homeassistant/components/alarm_control_panel/device_trigger.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""Provides device automations for Alarm control panel."""
|
||||
from typing import List
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel.const import (
|
||||
SUPPORT_ALARM_ARM_AWAY,
|
||||
SUPPORT_ALARM_ARM_HOME,
|
||||
SUPPORT_ALARM_ARM_NIGHT,
|
||||
)
|
||||
from homeassistant.components.automation import AutomationActionType, state
|
||||
from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
CONF_DOMAIN,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_PLATFORM,
|
||||
CONF_TYPE,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
TRIGGER_TYPES = {
|
||||
"triggered",
|
||||
"disarmed",
|
||||
"armed_home",
|
||||
"armed_away",
|
||||
"armed_night",
|
||||
}
|
||||
|
||||
TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
|
||||
"""List device triggers for Alarm control panel devices."""
|
||||
registry = await entity_registry.async_get_registry(hass)
|
||||
triggers = []
|
||||
|
||||
# Get all the integrations entities for this device
|
||||
for entry in entity_registry.async_entries_for_device(registry, device_id):
|
||||
if entry.domain != DOMAIN:
|
||||
continue
|
||||
|
||||
entity_state = hass.states.get(entry.entity_id)
|
||||
|
||||
# We need a state or else we can't populate the HVAC and preset modes.
|
||||
if entity_state is None:
|
||||
continue
|
||||
|
||||
supported_features = entity_state.attributes["supported_features"]
|
||||
|
||||
# Add triggers for each entity that belongs to this integration
|
||||
triggers += [
|
||||
{
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "disarmed",
|
||||
},
|
||||
{
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "triggered",
|
||||
},
|
||||
]
|
||||
if supported_features & SUPPORT_ALARM_ARM_HOME:
|
||||
triggers.append(
|
||||
{
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "armed_home",
|
||||
}
|
||||
)
|
||||
if supported_features & SUPPORT_ALARM_ARM_AWAY:
|
||||
triggers.append(
|
||||
{
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "armed_away",
|
||||
}
|
||||
)
|
||||
if supported_features & SUPPORT_ALARM_ARM_NIGHT:
|
||||
triggers.append(
|
||||
{
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DEVICE_ID: device_id,
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
CONF_TYPE: "armed_night",
|
||||
}
|
||||
)
|
||||
|
||||
return triggers
|
||||
|
||||
|
||||
async def async_attach_trigger(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: AutomationActionType,
|
||||
automation_info: dict,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
config = TRIGGER_SCHEMA(config)
|
||||
|
||||
if config[CONF_TYPE] == "triggered":
|
||||
from_state = STATE_ALARM_PENDING
|
||||
to_state = STATE_ALARM_TRIGGERED
|
||||
elif config[CONF_TYPE] == "disarmed":
|
||||
from_state = STATE_ALARM_TRIGGERED
|
||||
to_state = STATE_ALARM_DISARMED
|
||||
elif config[CONF_TYPE] == "armed_home":
|
||||
from_state = STATE_ALARM_PENDING
|
||||
to_state = STATE_ALARM_ARMED_HOME
|
||||
elif config[CONF_TYPE] == "armed_away":
|
||||
from_state = STATE_ALARM_PENDING
|
||||
to_state = STATE_ALARM_ARMED_AWAY
|
||||
elif config[CONF_TYPE] == "armed_night":
|
||||
from_state = STATE_ALARM_PENDING
|
||||
to_state = STATE_ALARM_ARMED_NIGHT
|
||||
|
||||
state_config = {
|
||||
state.CONF_PLATFORM: "state",
|
||||
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
|
||||
state.CONF_FROM: from_state,
|
||||
state.CONF_TO: to_state,
|
||||
}
|
||||
state_config = state.TRIGGER_SCHEMA(state_config)
|
||||
return await state.async_attach_trigger(
|
||||
hass, state_config, action, automation_info, platform_type="device"
|
||||
)
|
@@ -4,7 +4,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/alarm_control_panel",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@colinodell"
|
||||
]
|
||||
"codeowners": []
|
||||
}
|
||||
|
@@ -59,85 +59,3 @@ alarm_trigger:
|
||||
code:
|
||||
description: An optional code to trigger the alarm control panel with.
|
||||
example: 1234
|
||||
|
||||
envisalink_alarm_keypress:
|
||||
description: Send custom keypresses to the alarm.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel to trigger.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
keypress:
|
||||
description: 'String to send to the alarm panel (1-6 characters).'
|
||||
example: '*71'
|
||||
|
||||
alarmdecoder_alarm_toggle_chime:
|
||||
description: Send the alarm the toggle chime command.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel to trigger.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: A required code to toggle the alarm control panel chime with.
|
||||
example: 1234
|
||||
|
||||
ifttt_push_alarm_state:
|
||||
description: Update the alarm state to the specified value.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel which state has to be updated.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
state:
|
||||
description: The state to which the alarm control panel has to be set.
|
||||
example: 'armed_night'
|
||||
|
||||
elkm1_alarm_arm_vacation:
|
||||
description: Arm the ElkM1 in vacation mode.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm.
|
||||
example: 'alarm_control_panel.main'
|
||||
code:
|
||||
description: An code to arm the alarm control panel.
|
||||
example: 1234
|
||||
|
||||
elkm1_alarm_arm_home_instant:
|
||||
description: Arm the ElkM1 in home instant mode.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm.
|
||||
example: 'alarm_control_panel.main'
|
||||
code:
|
||||
description: An code to arm the alarm control panel.
|
||||
example: 1234
|
||||
|
||||
elkm1_alarm_arm_night_instant:
|
||||
description: Arm the ElkM1 in night instant mode.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm.
|
||||
example: 'alarm_control_panel.main'
|
||||
code:
|
||||
description: An code to arm the alarm control panel.
|
||||
example: 1234
|
||||
|
||||
elkm1_alarm_display_message:
|
||||
description: Display a message on all of the ElkM1 keypads for an area.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to display messages on.
|
||||
example: 'alarm_control_panel.main'
|
||||
clear:
|
||||
description: 0=clear message, 1=clear message with * key, 2=Display until timeout; default 2
|
||||
example: 1
|
||||
beep:
|
||||
description: 0=no beep, 1=beep; default 0
|
||||
example: 1
|
||||
timeout:
|
||||
description: Time to display message, 0=forever, max 65535, default 0
|
||||
example: 4242
|
||||
line1:
|
||||
description: Up to 16 characters of text (truncated if too long). Default blank.
|
||||
example: The answer to life,
|
||||
line2:
|
||||
description: Up to 16 characters of text (truncated if too long). Default blank.
|
||||
example: the universe, and everything.
|
||||
|
@@ -6,6 +6,13 @@
|
||||
"arm_night": "Arm {entity_name} night",
|
||||
"disarm": "Disarm {entity_name}",
|
||||
"trigger": "Trigger {entity_name}"
|
||||
},
|
||||
"trigger_type": {
|
||||
"triggered": "{entity_name} triggered",
|
||||
"disarmed": "{entity_name} disarmed",
|
||||
"armed_home": "{entity_name} armed home",
|
||||
"armed_away": "{entity_name} armed away",
|
||||
"armed_night": "{entity_name} armed night"
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,14 +1,17 @@
|
||||
"""Support for AlarmDecoder devices."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
from alarmdecoder import AlarmDecoder
|
||||
from alarmdecoder.devices import SerialDevice, SocketDevice, USBDevice
|
||||
from alarmdecoder.util import NoDeviceError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
|
||||
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -109,9 +112,6 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up for the AlarmDecoder devices."""
|
||||
from alarmdecoder import AlarmDecoder
|
||||
from alarmdecoder.devices import SocketDevice, SerialDevice, USBDevice
|
||||
|
||||
conf = config.get(DOMAIN)
|
||||
|
||||
restart = False
|
||||
@@ -134,8 +134,6 @@ def setup(hass, config):
|
||||
|
||||
def open_connection(now=None):
|
||||
"""Open a connection to AlarmDecoder."""
|
||||
from alarmdecoder.util import NoDeviceError
|
||||
|
||||
nonlocal restart
|
||||
try:
|
||||
controller.open(baud)
|
||||
|
@@ -3,7 +3,15 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
FORMAT_NUMBER,
|
||||
AlarmControlPanel,
|
||||
)
|
||||
from homeassistant.components.alarm_control_panel.const import (
|
||||
SUPPORT_ALARM_ARM_AWAY,
|
||||
SUPPORT_ALARM_ARM_HOME,
|
||||
SUPPORT_ALARM_ARM_NIGHT,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
@@ -13,11 +21,11 @@ from homeassistant.const import (
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import DATA_AD, DOMAIN as DOMAIN_ALARMDECODER, SIGNAL_PANEL_MESSAGE
|
||||
from . import DATA_AD, DOMAIN, SIGNAL_PANEL_MESSAGE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_ALARM_TOGGLE_CHIME = "alarmdecoder_alarm_toggle_chime"
|
||||
SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime"
|
||||
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string})
|
||||
|
||||
SERVICE_ALARM_KEYPRESS = "alarm_keypress"
|
||||
@@ -36,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
device.alarm_toggle_chime(code)
|
||||
|
||||
hass.services.register(
|
||||
alarm.DOMAIN,
|
||||
DOMAIN,
|
||||
SERVICE_ALARM_TOGGLE_CHIME,
|
||||
alarm_toggle_chime_handler,
|
||||
schema=ALARM_TOGGLE_CHIME_SCHEMA,
|
||||
@@ -48,14 +56,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
device.alarm_keypress(keypress)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN_ALARMDECODER,
|
||||
DOMAIN,
|
||||
SERVICE_ALARM_KEYPRESS,
|
||||
alarm_keypress_handler,
|
||||
schema=ALARM_KEYPRESS_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
class AlarmDecoderAlarmPanel(AlarmControlPanel):
|
||||
"""Representation of an AlarmDecoder-based alarm panel."""
|
||||
|
||||
def __init__(self):
|
||||
@@ -115,13 +123,18 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
return alarm.FORMAT_NUMBER
|
||||
return FORMAT_NUMBER
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
|
@@ -2,9 +2,7 @@
|
||||
"domain": "alarmdecoder",
|
||||
"name": "Alarmdecoder",
|
||||
"documentation": "https://www.home-assistant.io/integrations/alarmdecoder",
|
||||
"requirements": [
|
||||
"alarmdecoder==1.13.2"
|
||||
],
|
||||
"requirements": ["alarmdecoder==1.13.9"],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
|
@@ -7,3 +7,13 @@ alarm_keypress:
|
||||
keypress:
|
||||
description: 'String to send to the alarm panel.'
|
||||
example: '*71'
|
||||
|
||||
alarm_toggle_chime:
|
||||
description: Send the alarm the toggle chime command.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the alarm control panel to trigger.
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: A required code to toggle the alarm control panel chime with.
|
||||
example: 1234
|
||||
|
@@ -7,6 +7,10 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.components.alarm_control_panel.const import (
|
||||
SUPPORT_ALARM_ARM_AWAY,
|
||||
SUPPORT_ALARM_ARM_HOME,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_CODE,
|
||||
CONF_NAME,
|
||||
@@ -95,6 +99,11 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return None
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
|
@@ -1,30 +1,30 @@
|
||||
"""Support for repeating alerts when conditions are met."""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_DATA,
|
||||
ATTR_MESSAGE,
|
||||
ATTR_TITLE,
|
||||
ATTR_DATA,
|
||||
DOMAIN as DOMAIN_NOTIFY,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_ENTITY_ID,
|
||||
STATE_IDLE,
|
||||
CONF_NAME,
|
||||
CONF_STATE,
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE,
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_IDLE,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.helpers import service, event
|
||||
from homeassistant.helpers import event, service
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.util.dt import now
|
||||
|
||||
|
@@ -3,25 +3,24 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import entityfilter
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers import config_validation as cv, entityfilter
|
||||
|
||||
from . import flash_briefings, intent, smart_home_http
|
||||
from .const import (
|
||||
CONF_AUDIO,
|
||||
CONF_CLIENT_ID,
|
||||
CONF_CLIENT_SECRET,
|
||||
CONF_DESCRIPTION,
|
||||
CONF_DISPLAY_CATEGORIES,
|
||||
CONF_DISPLAY_URL,
|
||||
CONF_ENDPOINT,
|
||||
CONF_ENTITY_CONFIG,
|
||||
CONF_FILTER,
|
||||
CONF_TEXT,
|
||||
CONF_TITLE,
|
||||
CONF_UID,
|
||||
DOMAIN,
|
||||
CONF_FILTER,
|
||||
CONF_ENTITY_CONFIG,
|
||||
CONF_DESCRIPTION,
|
||||
CONF_DISPLAY_CATEGORIES,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@@ -1,8 +1,9 @@
|
||||
"""Support for Alexa skill auth."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import json
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
@@ -50,7 +51,7 @@ class Auth:
|
||||
"client_secret": self.client_secret,
|
||||
}
|
||||
_LOGGER.debug(
|
||||
"Calling LWA to get the access token (first time), " "with: %s",
|
||||
"Calling LWA to get the access token (first time), with: %s",
|
||||
json.dumps(lwa_params),
|
||||
)
|
||||
|
||||
|
@@ -1,6 +1,10 @@
|
||||
"""Alexa capabilities."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components import cover, fan, image_processing, input_number, light
|
||||
from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER
|
||||
import homeassistant.components.climate.const as climate
|
||||
import homeassistant.components.media_player.const as media_player
|
||||
from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_TEMPERATURE,
|
||||
@@ -18,23 +22,26 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
STATE_UNLOCKED,
|
||||
)
|
||||
import homeassistant.components.climate.const as climate
|
||||
import homeassistant.components.media_player.const as media_player
|
||||
from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER
|
||||
from homeassistant.components import light, fan, cover
|
||||
import homeassistant.util.color as color_util
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import (
|
||||
Catalog,
|
||||
API_TEMP_UNITS,
|
||||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
DATE_FORMAT,
|
||||
PERCENTAGE_FAN_MAP,
|
||||
RANGE_FAN_MAP,
|
||||
Inputs,
|
||||
)
|
||||
from .errors import UnsupportedProperty
|
||||
from .resources import (
|
||||
AlexaCapabilityResource,
|
||||
AlexaGlobalCatalog,
|
||||
AlexaModeResource,
|
||||
AlexaPresetResource,
|
||||
AlexaSemantics,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -105,12 +112,41 @@ class AlexaCapability:
|
||||
|
||||
@staticmethod
|
||||
def capability_resources():
|
||||
"""Applicable to ToggleController, RangeController, and ModeController interfaces."""
|
||||
"""Return the capability object.
|
||||
|
||||
Applicable to ToggleController, RangeController, and ModeController interfaces.
|
||||
"""
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def configuration():
|
||||
"""Return the Configuration object."""
|
||||
"""Return the configuration object.
|
||||
|
||||
Applicable to the ThermostatController, SecurityControlPanel, ModeController, RangeController,
|
||||
and EventDetectionSensor.
|
||||
"""
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def configurations():
|
||||
"""Return the configurations object.
|
||||
|
||||
The plural configurations object is different that the singular configuration object.
|
||||
Applicable to EqualizerController interface.
|
||||
"""
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def inputs():
|
||||
"""Applicable only to media players."""
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def semantics():
|
||||
"""Return the semantics object.
|
||||
|
||||
Applicable to ToggleController, RangeController, and ModeController interfaces.
|
||||
"""
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
@@ -122,6 +158,10 @@ class AlexaCapability:
|
||||
"""Serialize according to the Discovery API."""
|
||||
result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"}
|
||||
|
||||
instance = self.instance
|
||||
if instance is not None:
|
||||
result["instance"] = instance
|
||||
|
||||
properties_supported = self.properties_supported()
|
||||
if properties_supported:
|
||||
result["properties"] = {
|
||||
@@ -130,22 +170,19 @@ class AlexaCapability:
|
||||
"retrievable": self.properties_retrievable(),
|
||||
}
|
||||
|
||||
# pylint: disable=assignment-from-none
|
||||
proactively_reported = self.capability_proactively_reported()
|
||||
if proactively_reported is not None:
|
||||
result["proactivelyReported"] = proactively_reported
|
||||
|
||||
# pylint: disable=assignment-from-none
|
||||
non_controllable = self.properties_non_controllable()
|
||||
if non_controllable is not None:
|
||||
result["properties"]["nonControllable"] = non_controllable
|
||||
|
||||
# pylint: disable=assignment-from-none
|
||||
supports_deactivation = self.supports_deactivation()
|
||||
if supports_deactivation is not None:
|
||||
result["supportsDeactivation"] = supports_deactivation
|
||||
|
||||
capability_resources = self.serialize_capability_resources()
|
||||
capability_resources = self.capability_resources()
|
||||
if capability_resources:
|
||||
result["capabilityResources"] = capability_resources
|
||||
|
||||
@@ -153,15 +190,23 @@ class AlexaCapability:
|
||||
if configuration:
|
||||
result["configuration"] = configuration
|
||||
|
||||
# pylint: disable=assignment-from-none
|
||||
instance = self.instance
|
||||
if instance is not None:
|
||||
result["instance"] = instance
|
||||
# The plural configurations object is different than the singular configuration object above.
|
||||
configurations = self.configurations()
|
||||
if configurations:
|
||||
result["configurations"] = configurations
|
||||
|
||||
semantics = self.semantics()
|
||||
if semantics:
|
||||
result["semantics"] = semantics
|
||||
|
||||
supported_operations = self.supported_operations()
|
||||
if supported_operations:
|
||||
result["supportedOperations"] = supported_operations
|
||||
|
||||
inputs = self.inputs()
|
||||
if inputs:
|
||||
result["inputs"] = inputs
|
||||
|
||||
return result
|
||||
|
||||
def serialize_properties(self):
|
||||
@@ -184,35 +229,19 @@ class AlexaCapability:
|
||||
|
||||
yield result
|
||||
|
||||
def serialize_capability_resources(self):
|
||||
"""Return capabilityResources friendlyNames serialized for an API response."""
|
||||
resources = self.capability_resources()
|
||||
if resources:
|
||||
return {"friendlyNames": self.serialize_friendly_names(resources)}
|
||||
|
||||
return None
|
||||
class Alexa(AlexaCapability):
|
||||
"""Implements Alexa Interface.
|
||||
|
||||
@staticmethod
|
||||
def serialize_friendly_names(resources):
|
||||
"""Return capabilityResources, ModeResources, or presetResources friendlyNames serialized for an API response."""
|
||||
friendly_names = []
|
||||
for resource in resources:
|
||||
if resource["type"] == Catalog.LABEL_ASSET:
|
||||
friendly_names.append(
|
||||
{
|
||||
"@type": Catalog.LABEL_ASSET,
|
||||
"value": {"assetId": resource["value"]},
|
||||
}
|
||||
)
|
||||
else:
|
||||
friendly_names.append(
|
||||
{
|
||||
"@type": Catalog.LABEL_TEXT,
|
||||
"value": {"text": resource["value"], "locale": "en-US"},
|
||||
}
|
||||
)
|
||||
Although endpoints implement this interface implicitly,
|
||||
The API suggests you should explicitly include this interface.
|
||||
|
||||
return friendly_names
|
||||
https://developer.amazon.com/docs/device-apis/alexa-interface.html
|
||||
"""
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return "Alexa"
|
||||
|
||||
|
||||
class AlexaEndpointHealth(AlexaCapability):
|
||||
@@ -236,7 +265,7 @@ class AlexaEndpointHealth(AlexaCapability):
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return False
|
||||
return True
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
@@ -529,6 +558,23 @@ class AlexaInputController(AlexaCapability):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return "Alexa.InputController"
|
||||
|
||||
def inputs(self):
|
||||
"""Return the list of valid supported inputs."""
|
||||
source_list = self.entity.attributes.get(
|
||||
media_player.ATTR_INPUT_SOURCE_LIST, []
|
||||
)
|
||||
input_list = []
|
||||
for source in source_list:
|
||||
formatted_source = (
|
||||
source.lower().replace("-", "").replace("_", "").replace(" ", "")
|
||||
)
|
||||
if formatted_source in Inputs.VALID_SOURCE_NAME_MAP.keys():
|
||||
input_list.append(
|
||||
{"name": Inputs.VALID_SOURCE_NAME_MAP[formatted_source]}
|
||||
)
|
||||
|
||||
return input_list
|
||||
|
||||
|
||||
class AlexaTemperatureSensor(AlexaCapability):
|
||||
"""Implements Alexa.TemperatureSensor.
|
||||
@@ -752,10 +798,11 @@ class AlexaThermostatController(AlexaCapability):
|
||||
supported_modes.append(thermostat_mode)
|
||||
|
||||
preset_modes = self.entity.attributes.get(climate.ATTR_PRESET_MODES)
|
||||
for mode in preset_modes:
|
||||
thermostat_mode = API_THERMOSTAT_PRESETS.get(mode)
|
||||
if thermostat_mode:
|
||||
supported_modes.append(thermostat_mode)
|
||||
if preset_modes:
|
||||
for mode in preset_modes:
|
||||
thermostat_mode = API_THERMOSTAT_PRESETS.get(mode)
|
||||
if thermostat_mode:
|
||||
supported_modes.append(thermostat_mode)
|
||||
|
||||
# Return False for supportsScheduling until supported with event listener in handler.
|
||||
configuration = {"supportsScheduling": False}
|
||||
@@ -862,6 +909,8 @@ class AlexaModeController(AlexaCapability):
|
||||
def __init__(self, entity, instance, non_controllable=False):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entity, instance)
|
||||
self._resource = None
|
||||
self._semantics = None
|
||||
self.properties_non_controllable = lambda: non_controllable
|
||||
|
||||
def name(self):
|
||||
@@ -878,73 +927,102 @@ class AlexaModeController(AlexaCapability):
|
||||
|
||||
def properties_retrievable(self):
|
||||
"""Return True if properties can be retrieved."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != "mode":
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
# Fan Direction
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
||||
return self.entity.attributes.get(fan.ATTR_DIRECTION)
|
||||
mode = self.entity.attributes.get(fan.ATTR_DIRECTION, None)
|
||||
if mode in (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE, STATE_UNKNOWN):
|
||||
return f"{fan.ATTR_DIRECTION}.{mode}"
|
||||
|
||||
# Cover Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
# Return state instead of position when using ModeController.
|
||||
mode = self.entity.state
|
||||
if mode in (
|
||||
cover.STATE_OPEN,
|
||||
cover.STATE_OPENING,
|
||||
cover.STATE_CLOSED,
|
||||
cover.STATE_CLOSING,
|
||||
STATE_UNKNOWN,
|
||||
):
|
||||
return f"{cover.ATTR_POSITION}.{mode}"
|
||||
|
||||
return None
|
||||
|
||||
def configuration(self):
|
||||
"""Return configuration with modeResources."""
|
||||
return self.serialize_mode_resources()
|
||||
if isinstance(self._resource, AlexaCapabilityResource):
|
||||
return self._resource.serialize_configuration()
|
||||
|
||||
return None
|
||||
|
||||
def capability_resources(self):
|
||||
"""Return capabilityResources object."""
|
||||
capability_resources = []
|
||||
|
||||
# Fan Direction Resource
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
||||
capability_resources = [
|
||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION}
|
||||
]
|
||||
self._resource = AlexaModeResource(
|
||||
[AlexaGlobalCatalog.SETTING_DIRECTION], False
|
||||
)
|
||||
self._resource.add_mode(
|
||||
f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}", [fan.DIRECTION_FORWARD]
|
||||
)
|
||||
self._resource.add_mode(
|
||||
f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}", [fan.DIRECTION_REVERSE]
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
return capability_resources
|
||||
# Cover Position Resources
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
self._resource = AlexaModeResource(
|
||||
["Position", AlexaGlobalCatalog.SETTING_OPENING], False
|
||||
)
|
||||
self._resource.add_mode(
|
||||
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
|
||||
[AlexaGlobalCatalog.VALUE_OPEN],
|
||||
)
|
||||
self._resource.add_mode(
|
||||
f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}",
|
||||
[AlexaGlobalCatalog.VALUE_CLOSE],
|
||||
)
|
||||
self._resource.add_mode(f"{cover.ATTR_POSITION}.custom", ["Custom"])
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
def mode_resources(self):
|
||||
"""Return modeResources object."""
|
||||
mode_resources = None
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
||||
mode_resources = {
|
||||
"ordered": False,
|
||||
"resources": [
|
||||
{
|
||||
"value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}",
|
||||
"friendly_names": [
|
||||
{"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_FORWARD}
|
||||
],
|
||||
},
|
||||
{
|
||||
"value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}",
|
||||
"friendly_names": [
|
||||
{"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_REVERSE}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
return None
|
||||
|
||||
return mode_resources
|
||||
def semantics(self):
|
||||
"""Build and return semantics object."""
|
||||
|
||||
def serialize_mode_resources(self):
|
||||
"""Return ModeResources, friendlyNames serialized for an API response."""
|
||||
mode_resources = []
|
||||
resources = self.mode_resources()
|
||||
ordered = resources["ordered"]
|
||||
for resource in resources["resources"]:
|
||||
mode_value = resource["value"]
|
||||
friendly_names = resource["friendly_names"]
|
||||
result = {
|
||||
"value": mode_value,
|
||||
"modeResources": {
|
||||
"friendlyNames": self.serialize_friendly_names(friendly_names)
|
||||
},
|
||||
}
|
||||
mode_resources.append(result)
|
||||
# Cover Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
self._semantics = AlexaSemantics()
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_CLOSE, AlexaSemantics.ACTION_LOWER],
|
||||
"SetMode",
|
||||
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"},
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_OPEN, AlexaSemantics.ACTION_RAISE],
|
||||
"SetMode",
|
||||
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"},
|
||||
)
|
||||
self._semantics.add_states_to_value(
|
||||
[AlexaSemantics.STATES_CLOSED],
|
||||
f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}",
|
||||
)
|
||||
self._semantics.add_states_to_value(
|
||||
[AlexaSemantics.STATES_OPEN],
|
||||
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
|
||||
)
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
return {"ordered": ordered, "supportedModes": mode_resources}
|
||||
return None
|
||||
|
||||
|
||||
class AlexaRangeController(AlexaCapability):
|
||||
@@ -956,6 +1034,8 @@ class AlexaRangeController(AlexaCapability):
|
||||
def __init__(self, entity, instance, non_controllable=False):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entity, instance)
|
||||
self._resource = None
|
||||
self._semantics = None
|
||||
self.properties_non_controllable = lambda: non_controllable
|
||||
|
||||
def name(self):
|
||||
@@ -979,88 +1059,137 @@ class AlexaRangeController(AlexaCapability):
|
||||
if name != "rangeValue":
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
# Fan Speed
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
speed = self.entity.attributes.get(fan.ATTR_SPEED)
|
||||
return RANGE_FAN_MAP.get(speed, 0)
|
||||
|
||||
# Cover Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION)
|
||||
|
||||
# Cover Tilt Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION)
|
||||
|
||||
# Input Number Value
|
||||
if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
return float(self.entity.state)
|
||||
|
||||
return None
|
||||
|
||||
def configuration(self):
|
||||
"""Return configuration with presetResources."""
|
||||
return self.serialize_preset_resources()
|
||||
if isinstance(self._resource, AlexaCapabilityResource):
|
||||
return self._resource.serialize_configuration()
|
||||
|
||||
return None
|
||||
|
||||
def capability_resources(self):
|
||||
"""Return capabilityResources object."""
|
||||
capability_resources = []
|
||||
|
||||
# Fan Speed Resources
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
return [{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_FANSPEED}]
|
||||
|
||||
return capability_resources
|
||||
|
||||
def preset_resources(self):
|
||||
"""Return presetResources object."""
|
||||
preset_resources = []
|
||||
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
preset_resources = {
|
||||
"minimumValue": 1,
|
||||
"maximumValue": 3,
|
||||
"precision": 1,
|
||||
"presets": [
|
||||
{
|
||||
"rangeValue": 1,
|
||||
"names": [
|
||||
{
|
||||
"type": Catalog.LABEL_ASSET,
|
||||
"value": Catalog.VALUE_MINIMUM,
|
||||
},
|
||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_LOW},
|
||||
],
|
||||
},
|
||||
{
|
||||
"rangeValue": 2,
|
||||
"names": [
|
||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_MEDIUM}
|
||||
],
|
||||
},
|
||||
{
|
||||
"rangeValue": 3,
|
||||
"names": [
|
||||
{
|
||||
"type": Catalog.LABEL_ASSET,
|
||||
"value": Catalog.VALUE_MAXIMUM,
|
||||
},
|
||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_HIGH},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return preset_resources
|
||||
|
||||
def serialize_preset_resources(self):
|
||||
"""Return PresetResources, friendlyNames serialized for an API response."""
|
||||
preset_resources = []
|
||||
resources = self.preset_resources()
|
||||
for preset in resources["presets"]:
|
||||
preset_resources.append(
|
||||
{
|
||||
"rangeValue": preset["rangeValue"],
|
||||
"presetResources": {
|
||||
"friendlyNames": self.serialize_friendly_names(preset["names"])
|
||||
},
|
||||
}
|
||||
self._resource = AlexaPresetResource(
|
||||
labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED],
|
||||
min_value=1,
|
||||
max_value=3,
|
||||
precision=1,
|
||||
)
|
||||
self._resource.add_preset(
|
||||
value=1,
|
||||
labels=[AlexaGlobalCatalog.VALUE_LOW, AlexaGlobalCatalog.VALUE_MINIMUM],
|
||||
)
|
||||
self._resource.add_preset(value=2, labels=[AlexaGlobalCatalog.VALUE_MEDIUM])
|
||||
self._resource.add_preset(
|
||||
value=3,
|
||||
labels=[
|
||||
AlexaGlobalCatalog.VALUE_HIGH,
|
||||
AlexaGlobalCatalog.VALUE_MAXIMUM,
|
||||
],
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
return {
|
||||
"supportedRange": {
|
||||
"minimumValue": resources["minimumValue"],
|
||||
"maximumValue": resources["maximumValue"],
|
||||
"precision": resources["precision"],
|
||||
},
|
||||
"presets": preset_resources,
|
||||
}
|
||||
# Cover Position Resources
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
self._resource = AlexaPresetResource(
|
||||
["Position", AlexaGlobalCatalog.SETTING_OPENING],
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
precision=1,
|
||||
unit=AlexaGlobalCatalog.UNIT_PERCENT,
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Cover Tilt Position Resources
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
self._resource = AlexaPresetResource(
|
||||
["Tilt Position", AlexaGlobalCatalog.SETTING_OPENING],
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
precision=1,
|
||||
unit=AlexaGlobalCatalog.UNIT_PERCENT,
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Input Number Value
|
||||
if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
min_value = float(self.entity.attributes[input_number.ATTR_MIN])
|
||||
max_value = float(self.entity.attributes[input_number.ATTR_MAX])
|
||||
precision = float(self.entity.attributes.get(input_number.ATTR_STEP, 1))
|
||||
unit = self.entity.attributes.get(input_number.ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
self._resource = AlexaPresetResource(
|
||||
["Value"],
|
||||
min_value=min_value,
|
||||
max_value=max_value,
|
||||
precision=precision,
|
||||
unit=unit,
|
||||
)
|
||||
self._resource.add_preset(
|
||||
value=min_value, labels=[AlexaGlobalCatalog.VALUE_MINIMUM]
|
||||
)
|
||||
self._resource.add_preset(
|
||||
value=max_value, labels=[AlexaGlobalCatalog.VALUE_MAXIMUM]
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
return None
|
||||
|
||||
def semantics(self):
|
||||
"""Build and return semantics object."""
|
||||
|
||||
# Cover Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
self._semantics = AlexaSemantics()
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_LOWER], "SetRangeValue", {"rangeValue": 0}
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_RAISE], "SetRangeValue", {"rangeValue": 100}
|
||||
)
|
||||
self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0)
|
||||
self._semantics.add_states_to_range(
|
||||
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
|
||||
)
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
# Cover Tilt Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
self._semantics = AlexaSemantics()
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0}
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_OPEN], "SetRangeValue", {"rangeValue": 100}
|
||||
)
|
||||
self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0)
|
||||
self._semantics.add_states_to_range(
|
||||
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
|
||||
)
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class AlexaToggleController(AlexaCapability):
|
||||
@@ -1072,6 +1201,8 @@ class AlexaToggleController(AlexaCapability):
|
||||
def __init__(self, entity, instance, non_controllable=False):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entity, instance)
|
||||
self._resource = None
|
||||
self._semantics = None
|
||||
self.properties_non_controllable = lambda: non_controllable
|
||||
|
||||
def name(self):
|
||||
@@ -1095,6 +1226,7 @@ class AlexaToggleController(AlexaCapability):
|
||||
if name != "toggleState":
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
# Fan Oscillating
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||
is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING))
|
||||
return "ON" if is_on else "OFF"
|
||||
@@ -1103,16 +1235,15 @@ class AlexaToggleController(AlexaCapability):
|
||||
|
||||
def capability_resources(self):
|
||||
"""Return capabilityResources object."""
|
||||
capability_resources = []
|
||||
|
||||
# Fan Oscillating Resource
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||
capability_resources = [
|
||||
{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_OSCILLATE},
|
||||
{"type": Catalog.LABEL_TEXT, "value": "Rotate"},
|
||||
{"type": Catalog.LABEL_TEXT, "value": "Rotation"},
|
||||
]
|
||||
self._resource = AlexaCapabilityResource(
|
||||
[AlexaGlobalCatalog.SETTING_OSCILLATE, "Rotate", "Rotation"]
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
return capability_resources
|
||||
return None
|
||||
|
||||
|
||||
class AlexaChannelController(AlexaCapability):
|
||||
@@ -1186,3 +1317,112 @@ class AlexaSeekController(AlexaCapability):
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return "Alexa.SeekController"
|
||||
|
||||
|
||||
class AlexaEventDetectionSensor(AlexaCapability):
|
||||
"""Implements Alexa.EventDetectionSensor.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-eventdetectionsensor.html
|
||||
"""
|
||||
|
||||
def __init__(self, hass, entity):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(entity)
|
||||
self.hass = hass
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return "Alexa.EventDetectionSensor"
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports."""
|
||||
return [{"name": "humanPresenceDetectionState"}]
|
||||
|
||||
def properties_proactively_reported(self):
|
||||
"""Return True if properties asynchronously reported."""
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != "humanPresenceDetectionState":
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
human_presence = "NOT_DETECTED"
|
||||
state = self.entity.state
|
||||
|
||||
# Return None for unavailable and unknown states.
|
||||
# Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport.
|
||||
if state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None):
|
||||
return None
|
||||
|
||||
if self.entity.domain == image_processing.DOMAIN:
|
||||
if int(state):
|
||||
human_presence = "DETECTED"
|
||||
elif state == STATE_ON:
|
||||
human_presence = "DETECTED"
|
||||
|
||||
return {"value": human_presence}
|
||||
|
||||
def configuration(self):
|
||||
"""Return supported detection types."""
|
||||
return {
|
||||
"detectionMethods": ["AUDIO", "VIDEO"],
|
||||
"detectionModes": {
|
||||
"humanPresence": {
|
||||
"featureAvailability": "ENABLED",
|
||||
"supportsNotDetected": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class AlexaEqualizerController(AlexaCapability):
|
||||
"""Implements Alexa.EqualizerController.
|
||||
|
||||
https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-equalizercontroller.html
|
||||
"""
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
return "Alexa.EqualizerController"
|
||||
|
||||
def properties_supported(self):
|
||||
"""Return what properties this entity supports.
|
||||
|
||||
Either bands, mode or both can be specified. Only mode is supported at this time.
|
||||
"""
|
||||
return [{"name": "mode"}]
|
||||
|
||||
def get_property(self, name):
|
||||
"""Read and return a property."""
|
||||
if name != "mode":
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
sound_mode = self.entity.attributes.get(media_player.ATTR_SOUND_MODE)
|
||||
if sound_mode and sound_mode.upper() in (
|
||||
"MOVIE",
|
||||
"MUSIC",
|
||||
"NIGHT",
|
||||
"SPORT",
|
||||
"TV",
|
||||
):
|
||||
return sound_mode.upper()
|
||||
|
||||
return None
|
||||
|
||||
def configurations(self):
|
||||
"""Return the sound modes supported in the configurations object.
|
||||
|
||||
Valid Values for modes are: MOVIE, MUSIC, NIGHT, SPORT, TV.
|
||||
"""
|
||||
configurations = None
|
||||
sound_mode_list = self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST)
|
||||
if sound_mode_list:
|
||||
supported_sound_modes = []
|
||||
for sound_mode in sound_mode_list:
|
||||
if sound_mode.upper() in ("MOVIE", "MUSIC", "NIGHT", "SPORT", "TV"):
|
||||
supported_sound_modes.append({"name": sound_mode.upper()})
|
||||
|
||||
configurations = {"modes": {"supported": supported_sound_modes}}
|
||||
|
||||
return configurations
|
||||
|
@@ -1,9 +1,9 @@
|
||||
"""Constants for the Alexa integration."""
|
||||
from collections import OrderedDict
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.components import fan
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
|
||||
DOMAIN = "alexa"
|
||||
|
||||
@@ -117,158 +117,90 @@ class Cause:
|
||||
VOICE_INTERACTION = "VOICE_INTERACTION"
|
||||
|
||||
|
||||
class Catalog:
|
||||
"""The Global Alexa catalog.
|
||||
class Inputs:
|
||||
"""Valid names for the InputController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog
|
||||
|
||||
You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units.
|
||||
This catalog is localized into all the languages that Alexa supports.
|
||||
|
||||
You can reference the following catalog of pre-defined friendly names.
|
||||
Each item in the following list is an asset identifier followed by its supported friendly names.
|
||||
The first friendly name for each identifier is the one displayed in the Alexa mobile app.
|
||||
https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#input
|
||||
"""
|
||||
|
||||
LABEL_ASSET = "asset"
|
||||
LABEL_TEXT = "text"
|
||||
VALID_SOURCE_NAME_MAP = {
|
||||
"aux": "AUX 1",
|
||||
"aux1": "AUX 1",
|
||||
"aux2": "AUX 2",
|
||||
"aux3": "AUX 3",
|
||||
"aux4": "AUX 4",
|
||||
"aux5": "AUX 5",
|
||||
"aux6": "AUX 6",
|
||||
"aux7": "AUX 7",
|
||||
"bluray": "BLURAY",
|
||||
"cable": "CABLE",
|
||||
"cd": "CD",
|
||||
"coax": "COAX 1",
|
||||
"coax1": "COAX 1",
|
||||
"coax2": "COAX 2",
|
||||
"composite": "COMPOSITE 1",
|
||||
"composite1": "COMPOSITE 1",
|
||||
"dvd": "DVD",
|
||||
"game": "GAME",
|
||||
"gameconsole": "GAME",
|
||||
"hdradio": "HD RADIO",
|
||||
"hdmi": "HDMI 1",
|
||||
"hdmi1": "HDMI 1",
|
||||
"hdmi2": "HDMI 2",
|
||||
"hdmi3": "HDMI 3",
|
||||
"hdmi4": "HDMI 4",
|
||||
"hdmi5": "HDMI 5",
|
||||
"hdmi6": "HDMI 6",
|
||||
"hdmi7": "HDMI 7",
|
||||
"hdmi8": "HDMI 8",
|
||||
"hdmi9": "HDMI 9",
|
||||
"hdmi10": "HDMI 10",
|
||||
"hdmiarc": "HDMI ARC",
|
||||
"input": "INPUT 1",
|
||||
"input1": "INPUT 1",
|
||||
"input2": "INPUT 2",
|
||||
"input3": "INPUT 3",
|
||||
"input4": "INPUT 4",
|
||||
"input5": "INPUT 5",
|
||||
"input6": "INPUT 6",
|
||||
"input7": "INPUT 7",
|
||||
"input8": "INPUT 8",
|
||||
"input9": "INPUT 9",
|
||||
"input10": "INPUT 10",
|
||||
"ipod": "IPOD",
|
||||
"line": "LINE 1",
|
||||
"line1": "LINE 1",
|
||||
"line2": "LINE 2",
|
||||
"line3": "LINE 3",
|
||||
"line4": "LINE 4",
|
||||
"line5": "LINE 5",
|
||||
"line6": "LINE 6",
|
||||
"line7": "LINE 7",
|
||||
"mediaplayer": "MEDIA PLAYER",
|
||||
"optical": "OPTICAL 1",
|
||||
"optical1": "OPTICAL 1",
|
||||
"optical2": "OPTICAL 2",
|
||||
"phono": "PHONO",
|
||||
"playstation": "PLAYSTATION",
|
||||
"playstation3": "PLAYSTATION 3",
|
||||
"playstation4": "PLAYSTATION 4",
|
||||
"satellite": "SATELLITE",
|
||||
"satellitetv": "SATELLITE",
|
||||
"smartcast": "SMARTCAST",
|
||||
"tuner": "TUNER",
|
||||
"tv": "TV",
|
||||
"usbdac": "USB DAC",
|
||||
"video": "VIDEO 1",
|
||||
"video1": "VIDEO 1",
|
||||
"video2": "VIDEO 2",
|
||||
"video3": "VIDEO 3",
|
||||
"xbox": "XBOX",
|
||||
}
|
||||
|
||||
# Shower
|
||||
DEVICENAME_SHOWER = "Alexa.DeviceName.Shower"
|
||||
|
||||
# Washer, Washing Machine
|
||||
DEVICENAME_WASHER = "Alexa.DeviceName.Washer"
|
||||
|
||||
# Router, Internet Router, Network Router, Wifi Router, Net Router
|
||||
DEVICENAME_ROUTER = "Alexa.DeviceName.Router"
|
||||
|
||||
# Fan, Blower
|
||||
DEVICENAME_FAN = "Alexa.DeviceName.Fan"
|
||||
|
||||
# Air Purifier, Air Cleaner,Clean Air Machine
|
||||
DEVICENAME_AIRPURIFIER = "Alexa.DeviceName.AirPurifier"
|
||||
|
||||
# Space Heater, Portable Heater
|
||||
DEVICENAME_SPACEHEATER = "Alexa.DeviceName.SpaceHeater"
|
||||
|
||||
# Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet
|
||||
SHOWER_RAINHEAD = "Alexa.Shower.RainHead"
|
||||
|
||||
# Handheld Shower, Shower Wand, Hand Shower
|
||||
SHOWER_HANDHELD = "Alexa.Shower.HandHeld"
|
||||
|
||||
# Water Temperature, Water Temp, Water Heat
|
||||
SETTING_WATERTEMPERATURE = "Alexa.Setting.WaterTemperature"
|
||||
|
||||
# Temperature, Temp
|
||||
SETTING_TEMPERATURE = "Alexa.Setting.Temperature"
|
||||
|
||||
# Wash Cycle, Wash Preset, Wash setting
|
||||
SETTING_WASHCYCLE = "Alexa.Setting.WashCycle"
|
||||
|
||||
# 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi
|
||||
SETTING_2GGUESTWIFI = "Alexa.Setting.2GGuestWiFi"
|
||||
|
||||
# 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi
|
||||
SETTING_5GGUESTWIFI = "Alexa.Setting.5GGuestWiFi"
|
||||
|
||||
# Guest Wi-fi, Guest Network, Guest Net
|
||||
SETTING_GUESTWIFI = "Alexa.Setting.GuestWiFi"
|
||||
|
||||
# Auto, Automatic, Automatic Mode, Auto Mode
|
||||
SETTING_AUTO = "Alexa.Setting.Auto"
|
||||
|
||||
# #Night, Night Mode
|
||||
SETTING_NIGHT = "Alexa.Setting.Night"
|
||||
|
||||
# Quiet, Quiet Mode, Noiseless, Silent
|
||||
SETTING_QUIET = "Alexa.Setting.Quiet"
|
||||
|
||||
# Oscillate, Swivel, Oscillation, Spin, Back and forth
|
||||
SETTING_OSCILLATE = "Alexa.Setting.Oscillate"
|
||||
|
||||
# Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity
|
||||
SETTING_FANSPEED = "Alexa.Setting.FanSpeed"
|
||||
|
||||
# Preset, Setting
|
||||
SETTING_PRESET = "Alexa.Setting.Preset"
|
||||
|
||||
# Mode
|
||||
SETTING_MODE = "Alexa.Setting.Mode"
|
||||
|
||||
# Direction
|
||||
SETTING_DIRECTION = "Alexa.Setting.Direction"
|
||||
|
||||
# Delicates, Delicate
|
||||
VALUE_DELICATE = "Alexa.Value.Delicate"
|
||||
|
||||
# Quick Wash, Fast Wash, Wash Quickly, Speed Wash
|
||||
VALUE_QUICKWASH = "Alexa.Value.QuickWash"
|
||||
|
||||
# Maximum, Max
|
||||
VALUE_MAXIMUM = "Alexa.Value.Maximum"
|
||||
|
||||
# Minimum, Min
|
||||
VALUE_MINIMUM = "Alexa.Value.Minimum"
|
||||
|
||||
# High
|
||||
VALUE_HIGH = "Alexa.Value.High"
|
||||
|
||||
# Low
|
||||
VALUE_LOW = "Alexa.Value.Low"
|
||||
|
||||
# Medium, Mid
|
||||
VALUE_MEDIUM = "Alexa.Value.Medium"
|
||||
|
||||
|
||||
class Unit:
|
||||
"""Alexa Units of Measure.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#units-of-measure
|
||||
"""
|
||||
|
||||
ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees"
|
||||
|
||||
ANGLE_RADIANS = "Alexa.Unit.Angle.Radians"
|
||||
|
||||
DISTANCE_FEET = "Alexa.Unit.Distance.Feet"
|
||||
|
||||
DISTANCE_INCHES = "Alexa.Unit.Distance.Inches"
|
||||
|
||||
DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers"
|
||||
|
||||
DISTANCE_METERS = "Alexa.Unit.Distance.Meters"
|
||||
|
||||
DISTANCE_MILES = "Alexa.Unit.Distance.Miles"
|
||||
|
||||
DISTANCE_YARDS = "Alexa.Unit.Distance.Yards"
|
||||
|
||||
MASS_GRAMS = "Alexa.Unit.Mass.Grams"
|
||||
|
||||
MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms"
|
||||
|
||||
PERCENT = "Alexa.Unit.Percent"
|
||||
|
||||
TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius"
|
||||
|
||||
TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees"
|
||||
|
||||
TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit"
|
||||
|
||||
TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin"
|
||||
|
||||
VOLUME_CUBICFEET = "Alexa.Unit.Volume.CubicFeet"
|
||||
|
||||
VOLUME_CUBICMETERS = "Alexa.Unit.Volume.CubicMeters"
|
||||
|
||||
VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons"
|
||||
|
||||
VOLUME_LITERS = "Alexa.Unit.Volume.Liters"
|
||||
|
||||
VOLUME_PINTS = "Alexa.Unit.Volume.Pints"
|
||||
|
||||
VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts"
|
||||
|
||||
WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces"
|
||||
|
||||
WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds"
|
||||
VALID_SOUND_MODE_MAP = {
|
||||
"movie": "MOVIE",
|
||||
"music": "MUSIC",
|
||||
"night": "NIGHT",
|
||||
"sport": "SPORT",
|
||||
"tv": "TV",
|
||||
}
|
||||
|
@@ -1,7 +1,26 @@
|
||||
"""Alexa entity adapters."""
|
||||
from typing import List
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import (
|
||||
alarm_control_panel,
|
||||
alert,
|
||||
automation,
|
||||
binary_sensor,
|
||||
cover,
|
||||
fan,
|
||||
group,
|
||||
image_processing,
|
||||
input_boolean,
|
||||
input_number,
|
||||
light,
|
||||
lock,
|
||||
media_player,
|
||||
scene,
|
||||
script,
|
||||
sensor,
|
||||
switch,
|
||||
)
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
@@ -11,28 +30,11 @@ from homeassistant.const import (
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.components import (
|
||||
alarm_control_panel,
|
||||
alert,
|
||||
automation,
|
||||
binary_sensor,
|
||||
cover,
|
||||
fan,
|
||||
group,
|
||||
input_boolean,
|
||||
light,
|
||||
lock,
|
||||
media_player,
|
||||
scene,
|
||||
script,
|
||||
sensor,
|
||||
switch,
|
||||
)
|
||||
|
||||
from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES
|
||||
from .capabilities import (
|
||||
Alexa,
|
||||
AlexaBrightnessController,
|
||||
AlexaChannelController,
|
||||
AlexaColorController,
|
||||
@@ -40,6 +42,8 @@ from .capabilities import (
|
||||
AlexaContactSensor,
|
||||
AlexaDoorbellEventSource,
|
||||
AlexaEndpointHealth,
|
||||
AlexaEqualizerController,
|
||||
AlexaEventDetectionSensor,
|
||||
AlexaInputController,
|
||||
AlexaLockController,
|
||||
AlexaModeController,
|
||||
@@ -59,6 +63,7 @@ from .capabilities import (
|
||||
AlexaThermostatController,
|
||||
AlexaToggleController,
|
||||
)
|
||||
from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES
|
||||
|
||||
ENTITY_ADAPTERS = Registry()
|
||||
|
||||
@@ -80,6 +85,9 @@ class DisplayCategory:
|
||||
# Indicates media devices with video or photo capabilities.
|
||||
CAMERA = "CAMERA"
|
||||
|
||||
# Indicates a non-mobile computer, such as a desktop computer.
|
||||
COMPUTER = "COMPUTER"
|
||||
|
||||
# Indicates an endpoint that detects and reports contact.
|
||||
CONTACT_SENSOR = "CONTACT_SENSOR"
|
||||
|
||||
@@ -89,27 +97,60 @@ class DisplayCategory:
|
||||
# Indicates a doorbell.
|
||||
DOORBELL = "DOORBELL"
|
||||
|
||||
# Indicates a window covering on the outside of a structure.
|
||||
EXTERIOR_BLIND = "EXTERIOR_BLIND"
|
||||
|
||||
# Indicates a fan.
|
||||
FAN = "FAN"
|
||||
|
||||
# Indicates a game console, such as Microsoft Xbox or Nintendo Switch
|
||||
GAME_CONSOLE = "GAME_CONSOLE"
|
||||
|
||||
# Indicates a garage door. Garage doors must implement the ModeController interface to open and close the door.
|
||||
GARAGE_DOOR = "GARAGE_DOOR"
|
||||
|
||||
# Indicates a window covering on the inside of a structure.
|
||||
INTERIOR_BLIND = "INTERIOR_BLIND"
|
||||
|
||||
# Indicates a laptop or other mobile computer.
|
||||
LAPTOP = "LAPTOP"
|
||||
|
||||
# Indicates light sources or fixtures.
|
||||
LIGHT = "LIGHT"
|
||||
|
||||
# Indicates a microwave oven.
|
||||
MICROWAVE = "MICROWAVE"
|
||||
|
||||
# Indicates a mobile phone.
|
||||
MOBILE_PHONE = "MOBILE_PHONE"
|
||||
|
||||
# Indicates an endpoint that detects and reports motion.
|
||||
MOTION_SENSOR = "MOTION_SENSOR"
|
||||
|
||||
# Indicates a network-connected music system.
|
||||
MUSIC_SYSTEM = "MUSIC_SYSTEM"
|
||||
|
||||
# An endpoint that cannot be described in on of the other categories.
|
||||
OTHER = "OTHER"
|
||||
|
||||
# Indicates a network router.
|
||||
NETWORK_HARDWARE = "NETWORK_HARDWARE"
|
||||
|
||||
# Indicates an oven cooking appliance.
|
||||
OVEN = "OVEN"
|
||||
|
||||
# Indicates a non-mobile phone, such as landline or an IP phone.
|
||||
PHONE = "PHONE"
|
||||
|
||||
# Describes a combination of devices set to a specific state, when the
|
||||
# order of the state change is not important. For example a bedtime scene
|
||||
# might include turning off lights and lowering the thermostat, but the
|
||||
# order is unimportant. Applies to Scenes
|
||||
SCENE_TRIGGER = "SCENE_TRIGGER"
|
||||
|
||||
# Indicates a projector screen.
|
||||
SCREEN = "SCREEN"
|
||||
|
||||
# Indicates a security panel.
|
||||
SECURITY_PANEL = "SECURITY_PANEL"
|
||||
|
||||
@@ -123,10 +164,16 @@ class DisplayCategory:
|
||||
# Indicates the endpoint is a speaker or speaker system.
|
||||
SPEAKER = "SPEAKER"
|
||||
|
||||
# Indicates a streaming device such as Apple TV, Chromecast, or Roku.
|
||||
STREAMING_DEVICE = "STREAMING_DEVICE"
|
||||
|
||||
# Indicates in-wall switches wired to the electrical system. Can control a
|
||||
# variety of devices.
|
||||
SWITCH = "SWITCH"
|
||||
|
||||
# Indicates a tablet computer.
|
||||
TABLET = "TABLET"
|
||||
|
||||
# Indicates endpoints that report the temperature only.
|
||||
TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR"
|
||||
|
||||
@@ -137,6 +184,9 @@ class DisplayCategory:
|
||||
# Indicates the endpoint is a television.
|
||||
TV = "TV"
|
||||
|
||||
# Indicates a network-connected wearable device, such as an Apple Watch, Fitbit, or Samsung Gear.
|
||||
WEARABLE = "WEARABLE"
|
||||
|
||||
|
||||
class AlexaEntity:
|
||||
"""An adaptation of an entity, expressed in Alexa's terms.
|
||||
@@ -261,6 +311,7 @@ class GenericCapabilities(AlexaEntity):
|
||||
return [
|
||||
AlexaPowerController(self.entity),
|
||||
AlexaEndpointHealth(self.hass, self.entity),
|
||||
Alexa(self.hass),
|
||||
]
|
||||
|
||||
|
||||
@@ -270,6 +321,10 @@ class SwitchCapabilities(AlexaEntity):
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS)
|
||||
if device_class == switch.DEVICE_CLASS_OUTLET:
|
||||
return [DisplayCategory.SMARTPLUG]
|
||||
|
||||
return [DisplayCategory.SWITCH]
|
||||
|
||||
def interfaces(self):
|
||||
@@ -277,6 +332,7 @@ class SwitchCapabilities(AlexaEntity):
|
||||
return [
|
||||
AlexaPowerController(self.entity),
|
||||
AlexaEndpointHealth(self.hass, self.entity),
|
||||
Alexa(self.hass),
|
||||
]
|
||||
|
||||
|
||||
@@ -299,6 +355,7 @@ class ClimateCapabilities(AlexaEntity):
|
||||
yield AlexaThermostatController(self.hass, self.entity)
|
||||
yield AlexaTemperatureSensor(self.hass, self.entity)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
||||
@@ -307,15 +364,43 @@ class CoverCapabilities(AlexaEntity):
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.DOOR]
|
||||
device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS)
|
||||
if device_class == cover.DEVICE_CLASS_GARAGE:
|
||||
return [DisplayCategory.GARAGE_DOOR]
|
||||
if device_class == cover.DEVICE_CLASS_DOOR:
|
||||
return [DisplayCategory.DOOR]
|
||||
if device_class in (
|
||||
cover.DEVICE_CLASS_BLIND,
|
||||
cover.DEVICE_CLASS_SHADE,
|
||||
cover.DEVICE_CLASS_CURTAIN,
|
||||
):
|
||||
return [DisplayCategory.INTERIOR_BLIND]
|
||||
if device_class in (
|
||||
cover.DEVICE_CLASS_WINDOW,
|
||||
cover.DEVICE_CLASS_AWNING,
|
||||
cover.DEVICE_CLASS_SHUTTER,
|
||||
):
|
||||
return [DisplayCategory.EXTERIOR_BLIND]
|
||||
|
||||
return [DisplayCategory.OTHER]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
yield AlexaPowerController(self.entity)
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & cover.SUPPORT_SET_POSITION:
|
||||
yield AlexaPercentageController(self.entity)
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}"
|
||||
)
|
||||
elif supported & (cover.SUPPORT_CLOSE | cover.SUPPORT_OPEN):
|
||||
yield AlexaModeController(
|
||||
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}"
|
||||
)
|
||||
if supported & cover.SUPPORT_SET_TILT_POSITION:
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}"
|
||||
)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(light.DOMAIN)
|
||||
@@ -337,7 +422,9 @@ class LightCapabilities(AlexaEntity):
|
||||
yield AlexaColorController(self.entity)
|
||||
if supported & light.SUPPORT_COLOR_TEMP:
|
||||
yield AlexaColorTemperatureController(self.entity)
|
||||
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(fan.DOMAIN)
|
||||
@@ -351,6 +438,7 @@ class FanCapabilities(AlexaEntity):
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
yield AlexaPowerController(self.entity)
|
||||
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported & fan.SUPPORT_SET_SPEED:
|
||||
yield AlexaPercentageController(self.entity)
|
||||
@@ -358,7 +446,6 @@ class FanCapabilities(AlexaEntity):
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}"
|
||||
)
|
||||
|
||||
if supported & fan.SUPPORT_OSCILLATE:
|
||||
yield AlexaToggleController(
|
||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
|
||||
@@ -369,6 +456,7 @@ class FanCapabilities(AlexaEntity):
|
||||
)
|
||||
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(lock.DOMAIN)
|
||||
@@ -384,6 +472,7 @@ class LockCapabilities(AlexaEntity):
|
||||
return [
|
||||
AlexaLockController(self.entity),
|
||||
AlexaEndpointHealth(self.hass, self.entity),
|
||||
Alexa(self.hass),
|
||||
]
|
||||
|
||||
|
||||
@@ -401,7 +490,6 @@ class MediaPlayerCapabilities(AlexaEntity):
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield AlexaPowerController(self.entity)
|
||||
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
@@ -435,6 +523,12 @@ class MediaPlayerCapabilities(AlexaEntity):
|
||||
if supported & media_player.const.SUPPORT_PLAY_MEDIA:
|
||||
yield AlexaChannelController(self.entity)
|
||||
|
||||
if supported & media_player.const.SUPPORT_SELECT_SOUND_MODE:
|
||||
yield AlexaEqualizerController(self.entity)
|
||||
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(scene.DOMAIN)
|
||||
class SceneCapabilities(AlexaEntity):
|
||||
@@ -453,7 +547,10 @@ class SceneCapabilities(AlexaEntity):
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
return [AlexaSceneController(self.entity, supports_deactivation=False)]
|
||||
return [
|
||||
AlexaSceneController(self.entity, supports_deactivation=False),
|
||||
Alexa(self.hass),
|
||||
]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(script.DOMAIN)
|
||||
@@ -467,7 +564,10 @@ class ScriptCapabilities(AlexaEntity):
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
can_cancel = bool(self.entity.attributes.get("can_cancel"))
|
||||
return [AlexaSceneController(self.entity, supports_deactivation=can_cancel)]
|
||||
return [
|
||||
AlexaSceneController(self.entity, supports_deactivation=can_cancel),
|
||||
Alexa(self.hass),
|
||||
]
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(sensor.DOMAIN)
|
||||
@@ -486,6 +586,7 @@ class SensorCapabilities(AlexaEntity):
|
||||
if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in (TEMP_FAHRENHEIT, TEMP_CELSIUS):
|
||||
yield AlexaTemperatureSensor(self.hass, self.entity)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(binary_sensor.DOMAIN)
|
||||
@@ -494,6 +595,7 @@ class BinarySensorCapabilities(AlexaEntity):
|
||||
|
||||
TYPE_CONTACT = "contact"
|
||||
TYPE_MOTION = "motion"
|
||||
TYPE_PRESENCE = "presence"
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
@@ -502,6 +604,8 @@ class BinarySensorCapabilities(AlexaEntity):
|
||||
return [DisplayCategory.CONTACT_SENSOR]
|
||||
if sensor_type is self.TYPE_MOTION:
|
||||
return [DisplayCategory.MOTION_SENSOR]
|
||||
if sensor_type is self.TYPE_PRESENCE:
|
||||
return [DisplayCategory.CAMERA]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
@@ -510,22 +614,41 @@ class BinarySensorCapabilities(AlexaEntity):
|
||||
yield AlexaContactSensor(self.hass, self.entity)
|
||||
elif sensor_type is self.TYPE_MOTION:
|
||||
yield AlexaMotionSensor(self.hass, self.entity)
|
||||
elif sensor_type is self.TYPE_PRESENCE:
|
||||
yield AlexaEventDetectionSensor(self.hass, self.entity)
|
||||
|
||||
# yield additional interfaces based on specified display category in config.
|
||||
entity_conf = self.config.entity_config.get(self.entity.entity_id, {})
|
||||
if CONF_DISPLAY_CATEGORIES in entity_conf:
|
||||
if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL:
|
||||
yield AlexaDoorbellEventSource(self.entity)
|
||||
elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.CONTACT_SENSOR:
|
||||
yield AlexaContactSensor(self.hass, self.entity)
|
||||
elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.MOTION_SENSOR:
|
||||
yield AlexaMotionSensor(self.hass, self.entity)
|
||||
elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.CAMERA:
|
||||
yield AlexaEventDetectionSensor(self.hass, self.entity)
|
||||
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
def get_type(self):
|
||||
"""Return the type of binary sensor."""
|
||||
attrs = self.entity.attributes
|
||||
if attrs.get(ATTR_DEVICE_CLASS) in ("door", "garage_door", "opening", "window"):
|
||||
if attrs.get(ATTR_DEVICE_CLASS) in (
|
||||
binary_sensor.DEVICE_CLASS_DOOR,
|
||||
binary_sensor.DEVICE_CLASS_GARAGE_DOOR,
|
||||
binary_sensor.DEVICE_CLASS_OPENING,
|
||||
binary_sensor.DEVICE_CLASS_WINDOW,
|
||||
):
|
||||
return self.TYPE_CONTACT
|
||||
if attrs.get(ATTR_DEVICE_CLASS) == "motion":
|
||||
|
||||
if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_MOTION:
|
||||
return self.TYPE_MOTION
|
||||
|
||||
if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_PRESENCE:
|
||||
return self.TYPE_PRESENCE
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(alarm_control_panel.DOMAIN)
|
||||
class AlarmControlPanelCapabilities(AlexaEntity):
|
||||
@@ -540,3 +663,37 @@ class AlarmControlPanelCapabilities(AlexaEntity):
|
||||
if not self.entity.attributes.get("code_arm_required"):
|
||||
yield AlexaSecurityPanelController(self.hass, self.entity)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(image_processing.DOMAIN)
|
||||
class ImageProcessingCapabilities(AlexaEntity):
|
||||
"""Class to represent image_processing capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.CAMERA]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
yield AlexaEventDetectionSensor(self.hass, self.entity)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(input_number.DOMAIN)
|
||||
class InputNumberCapabilities(AlexaEntity):
|
||||
"""Class to represent input_number capabilities."""
|
||||
|
||||
def default_display_categories(self):
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.OTHER]
|
||||
|
||||
def interfaces(self):
|
||||
"""Yield the supported interfaces."""
|
||||
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}"
|
||||
)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
@@ -3,10 +3,10 @@ import copy
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components import http
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import template
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ATTR_MAIN_TEXT,
|
||||
|
@@ -3,13 +3,19 @@ import logging
|
||||
import math
|
||||
|
||||
from homeassistant import core as ha
|
||||
from homeassistant.components import cover, fan, group, light, media_player
|
||||
from homeassistant.components import (
|
||||
cover,
|
||||
fan,
|
||||
group,
|
||||
input_number,
|
||||
light,
|
||||
media_player,
|
||||
)
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_TEMPERATURE,
|
||||
STATE_ALARM_DISARMED,
|
||||
SERVICE_ALARM_ARM_AWAY,
|
||||
SERVICE_ALARM_ARM_HOME,
|
||||
SERVICE_ALARM_ARM_NIGHT,
|
||||
@@ -21,6 +27,7 @@ from homeassistant.const import (
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
SERVICE_MEDIA_STOP,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
SERVICE_SET_COVER_TILT_POSITION,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
SERVICE_UNLOCK,
|
||||
@@ -28,23 +35,25 @@ from homeassistant.const import (
|
||||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_SET,
|
||||
SERVICE_VOLUME_UP,
|
||||
STATE_ALARM_DISARMED,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
import homeassistant.util.color as color_util
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
|
||||
from .const import (
|
||||
API_TEMP_UNITS,
|
||||
API_THERMOSTAT_MODES_CUSTOM,
|
||||
API_THERMOSTAT_MODES,
|
||||
API_THERMOSTAT_MODES_CUSTOM,
|
||||
API_THERMOSTAT_PRESETS,
|
||||
Cause,
|
||||
PERCENTAGE_FAN_MAP,
|
||||
RANGE_FAN_MAP,
|
||||
SPEED_FAN_MAP,
|
||||
Cause,
|
||||
Inputs,
|
||||
)
|
||||
from .entities import async_get_entities
|
||||
from .errors import (
|
||||
@@ -110,9 +119,7 @@ async def async_api_turn_on(hass, config, directive, context):
|
||||
domain = ha.DOMAIN
|
||||
|
||||
service = SERVICE_TURN_ON
|
||||
if domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_OPEN_COVER
|
||||
elif domain == media_player.DOMAIN:
|
||||
if domain == media_player.DOMAIN:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF
|
||||
if not supported & power_features:
|
||||
@@ -138,9 +145,7 @@ async def async_api_turn_off(hass, config, directive, context):
|
||||
domain = ha.DOMAIN
|
||||
|
||||
service = SERVICE_TURN_OFF
|
||||
if entity.domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
elif domain == media_player.DOMAIN:
|
||||
if domain == media_player.DOMAIN:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF
|
||||
if not supported & power_features:
|
||||
@@ -345,10 +350,6 @@ async def async_api_set_percentage(hass, config, directive, context):
|
||||
speed = "high"
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
|
||||
elif entity.domain == cover.DOMAIN:
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
data[cover.ATTR_POSITION] = percentage
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context
|
||||
)
|
||||
@@ -382,13 +383,6 @@ async def async_api_adjust_percentage(hass, config, directive, context):
|
||||
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
|
||||
elif entity.domain == cover.DOMAIN:
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
|
||||
current = entity.attributes.get(cover.ATTR_POSITION)
|
||||
|
||||
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context
|
||||
)
|
||||
@@ -459,13 +453,20 @@ async def async_api_select_input(hass, config, directive, context):
|
||||
media_input = directive.payload["input"]
|
||||
entity = directive.entity
|
||||
|
||||
# attempt to map the ALL UPPERCASE payload name to a source
|
||||
source_list = entity.attributes[media_player.const.ATTR_INPUT_SOURCE_LIST] or []
|
||||
# Attempt to map the ALL UPPERCASE payload name to a source.
|
||||
# Strips trailing 1 to match single input devices.
|
||||
source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST, [])
|
||||
for source in source_list:
|
||||
# response will always be space separated, so format the source in the
|
||||
# most likely way to find a match
|
||||
formatted_source = source.lower().replace("-", " ").replace("_", " ")
|
||||
if formatted_source in media_input.lower():
|
||||
formatted_source = (
|
||||
source.lower().replace("-", "").replace("_", "").replace(" ", "")
|
||||
)
|
||||
media_input = media_input.lower().replace(" ", "")
|
||||
if (
|
||||
formatted_source in Inputs.VALID_SOURCE_NAME_MAP.keys()
|
||||
and formatted_source == media_input
|
||||
) or (
|
||||
media_input.endswith("1") and formatted_source == media_input.rstrip("1")
|
||||
):
|
||||
media_input = source
|
||||
break
|
||||
else:
|
||||
@@ -950,7 +951,7 @@ async def async_api_disarm(hass, config, directive, context):
|
||||
|
||||
@HANDLERS.register(("Alexa.ModeController", "SetMode"))
|
||||
async def async_api_set_mode(hass, config, directive, context):
|
||||
"""Process a next request."""
|
||||
"""Process a SetMode directive."""
|
||||
entity = directive.entity
|
||||
instance = directive.instance
|
||||
domain = entity.domain
|
||||
@@ -958,45 +959,56 @@ async def async_api_set_mode(hass, config, directive, context):
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
mode = directive.payload["mode"]
|
||||
|
||||
if domain != fan.DOMAIN:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
# Fan Direction
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
||||
mode, direction = mode.split(".")
|
||||
if direction in [fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD]:
|
||||
_, direction = mode.split(".")
|
||||
if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD):
|
||||
service = fan.SERVICE_SET_DIRECTION
|
||||
data[fan.ATTR_DIRECTION] = direction
|
||||
|
||||
# Cover Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
_, position = mode.split(".")
|
||||
|
||||
if position == cover.STATE_CLOSED:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
elif position == cover.STATE_OPEN:
|
||||
service = cover.SERVICE_OPEN_COVER
|
||||
elif position == "custom":
|
||||
service = cover.SERVICE_STOP_COVER
|
||||
|
||||
else:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
await hass.services.async_call(
|
||||
domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
response = directive.response()
|
||||
response.add_context_property(
|
||||
{
|
||||
"namespace": "Alexa.ModeController",
|
||||
"instance": instance,
|
||||
"name": "mode",
|
||||
"value": mode,
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.ModeController", "AdjustMode"))
|
||||
async def async_api_adjust_mode(hass, config, directive, context):
|
||||
"""Process a AdjustMode request.
|
||||
|
||||
Requires modeResources to be ordered.
|
||||
Only modes that are ordered support the adjustMode directive.
|
||||
Requires capabilityResources supportedModes to be ordered.
|
||||
Only supportedModes with ordered=True support the adjustMode directive.
|
||||
"""
|
||||
entity = directive.entity
|
||||
instance = directive.instance
|
||||
domain = entity.domain
|
||||
|
||||
if domain != fan.DOMAIN:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
if instance is None:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
# No modeResources are currently ordered to support this request.
|
||||
|
||||
return directive.response()
|
||||
# Currently no supportedModes are configured with ordered=True to support this request.
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.ToggleController", "TurnOn"))
|
||||
@@ -1008,19 +1020,29 @@ async def async_api_toggle_on(hass, config, directive, context):
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if domain != fan.DOMAIN:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
# Fan Oscillating
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||
service = fan.SERVICE_OSCILLATE
|
||||
data[fan.ATTR_OSCILLATING] = True
|
||||
else:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
await hass.services.async_call(
|
||||
domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
response = directive.response()
|
||||
response.add_context_property(
|
||||
{
|
||||
"namespace": "Alexa.ToggleController",
|
||||
"instance": instance,
|
||||
"name": "toggleState",
|
||||
"value": "ON",
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.ToggleController", "TurnOff"))
|
||||
@@ -1032,19 +1054,29 @@ async def async_api_toggle_off(hass, config, directive, context):
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if domain != fan.DOMAIN:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
# Fan Oscillating
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}":
|
||||
service = fan.SERVICE_OSCILLATE
|
||||
data[fan.ATTR_OSCILLATING] = False
|
||||
else:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
await hass.services.async_call(
|
||||
domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
response = directive.response()
|
||||
response.add_context_property(
|
||||
{
|
||||
"namespace": "Alexa.ToggleController",
|
||||
"instance": instance,
|
||||
"name": "toggleState",
|
||||
"value": "OFF",
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.RangeController", "SetRangeValue"))
|
||||
@@ -1055,15 +1087,12 @@ async def async_api_set_range(hass, config, directive, context):
|
||||
domain = entity.domain
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
range_value = int(directive.payload["rangeValue"])
|
||||
|
||||
if domain != fan.DOMAIN:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
range_value = directive.payload["rangeValue"]
|
||||
|
||||
# Fan Speed
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = SPEED_FAN_MAP.get(range_value, None)
|
||||
speed = SPEED_FAN_MAP.get(int(range_value))
|
||||
|
||||
if not speed:
|
||||
msg = "Entity does not support value"
|
||||
@@ -1074,11 +1103,55 @@ async def async_api_set_range(hass, config, directive, context):
|
||||
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
|
||||
# Cover Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
range_value = int(range_value)
|
||||
if range_value == 0:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
elif range_value == 100:
|
||||
service = cover.SERVICE_OPEN_COVER
|
||||
else:
|
||||
service = cover.SERVICE_SET_COVER_POSITION
|
||||
data[cover.ATTR_POSITION] = range_value
|
||||
|
||||
# Cover Tilt Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
range_value = int(range_value)
|
||||
if range_value == 0:
|
||||
service = cover.SERVICE_CLOSE_COVER_TILT
|
||||
elif range_value == 100:
|
||||
service = cover.SERVICE_OPEN_COVER_TILT
|
||||
else:
|
||||
service = cover.SERVICE_SET_COVER_TILT_POSITION
|
||||
data[cover.ATTR_POSITION] = range_value
|
||||
|
||||
# Input Number Value
|
||||
elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
range_value = float(range_value)
|
||||
service = input_number.SERVICE_SET_VALUE
|
||||
min_value = float(entity.attributes[input_number.ATTR_MIN])
|
||||
max_value = float(entity.attributes[input_number.ATTR_MAX])
|
||||
data[input_number.ATTR_VALUE] = min(max_value, max(min_value, range_value))
|
||||
|
||||
else:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
await hass.services.async_call(
|
||||
domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
response = directive.response()
|
||||
response.add_context_property(
|
||||
{
|
||||
"namespace": "Alexa.RangeController",
|
||||
"instance": instance,
|
||||
"name": "rangeValue",
|
||||
"value": range_value,
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue"))
|
||||
@@ -1089,25 +1162,71 @@ async def async_api_adjust_range(hass, config, directive, context):
|
||||
domain = entity.domain
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
range_delta = int(directive.payload["rangeValueDelta"])
|
||||
range_delta = directive.payload["rangeValueDelta"]
|
||||
response_value = 0
|
||||
|
||||
# Fan Speed
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}":
|
||||
range_delta = int(range_delta)
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
|
||||
# adjust range
|
||||
current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0)
|
||||
speed = SPEED_FAN_MAP.get(max(0, range_delta + current_range), fan.SPEED_OFF)
|
||||
speed = SPEED_FAN_MAP.get(
|
||||
min(3, max(0, range_delta + current_range)), fan.SPEED_OFF
|
||||
)
|
||||
|
||||
if speed == fan.SPEED_OFF:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
data[fan.ATTR_SPEED] = response_value = speed
|
||||
|
||||
# Cover Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
range_delta = int(range_delta)
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
current = entity.attributes.get(cover.ATTR_POSITION)
|
||||
data[cover.ATTR_POSITION] = response_value = min(
|
||||
100, max(0, range_delta + current)
|
||||
)
|
||||
|
||||
# Cover Tilt Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
range_delta = int(range_delta)
|
||||
service = SERVICE_SET_COVER_TILT_POSITION
|
||||
current = entity.attributes.get(cover.ATTR_TILT_POSITION)
|
||||
data[cover.ATTR_TILT_POSITION] = response_value = min(
|
||||
100, max(0, range_delta + current)
|
||||
)
|
||||
|
||||
# Input Number Value
|
||||
elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}":
|
||||
range_delta = float(range_delta)
|
||||
service = input_number.SERVICE_SET_VALUE
|
||||
min_value = float(entity.attributes[input_number.ATTR_MIN])
|
||||
max_value = float(entity.attributes[input_number.ATTR_MAX])
|
||||
current = float(entity.state)
|
||||
data[input_number.ATTR_VALUE] = response_value = min(
|
||||
max_value, max(min_value, range_delta + current)
|
||||
)
|
||||
|
||||
else:
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
||||
await hass.services.async_call(
|
||||
domain, service, data, blocking=False, context=context
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
response = directive.response()
|
||||
response.add_context_property(
|
||||
{
|
||||
"namespace": "Alexa.RangeController",
|
||||
"instance": instance,
|
||||
"name": "rangeValue",
|
||||
"value": response_value,
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.ChannelController", "ChangeChannel"))
|
||||
@@ -1115,21 +1234,25 @@ async def async_api_changechannel(hass, config, directive, context):
|
||||
"""Process a change channel request."""
|
||||
channel = "0"
|
||||
entity = directive.entity
|
||||
payload = directive.payload["channel"]
|
||||
channel_payload = directive.payload["channel"]
|
||||
metadata_payload = directive.payload["channelMetadata"]
|
||||
payload_name = "number"
|
||||
|
||||
if "number" in payload:
|
||||
channel = payload["number"]
|
||||
if "number" in channel_payload:
|
||||
channel = channel_payload["number"]
|
||||
payload_name = "number"
|
||||
elif "callSign" in payload:
|
||||
channel = payload["callSign"]
|
||||
elif "callSign" in channel_payload:
|
||||
channel = channel_payload["callSign"]
|
||||
payload_name = "callSign"
|
||||
elif "affiliateCallSign" in payload:
|
||||
channel = payload["affiliateCallSign"]
|
||||
elif "affiliateCallSign" in channel_payload:
|
||||
channel = channel_payload["affiliateCallSign"]
|
||||
payload_name = "affiliateCallSign"
|
||||
elif "uri" in payload:
|
||||
channel = payload["uri"]
|
||||
elif "uri" in channel_payload:
|
||||
channel = channel_payload["uri"]
|
||||
payload_name = "uri"
|
||||
elif "name" in metadata_payload:
|
||||
channel = metadata_payload["name"]
|
||||
payload_name = "callSign"
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
@@ -1229,3 +1352,43 @@ async def async_api_seek(hass, config, directive, context):
|
||||
return directive.response(
|
||||
name="StateReport", namespace="Alexa.SeekController", payload=payload
|
||||
)
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.EqualizerController", "SetMode"))
|
||||
async def async_api_set_eq_mode(hass, config, directive, context):
|
||||
"""Process a SetMode request for EqualizerController."""
|
||||
mode = directive.payload["mode"]
|
||||
entity = directive.entity
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
|
||||
if sound_mode_list and mode.lower() in sound_mode_list:
|
||||
data[media_player.const.ATTR_SOUND_MODE] = mode.lower()
|
||||
else:
|
||||
msg = "failed to map sound mode {} to a mode on {}".format(
|
||||
mode, entity.entity_id
|
||||
)
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain,
|
||||
media_player.SERVICE_SELECT_SOUND_MODE,
|
||||
data,
|
||||
blocking=False,
|
||||
context=context,
|
||||
)
|
||||
|
||||
return directive.response()
|
||||
|
||||
|
||||
@HANDLERS.register(("Alexa.EqualizerController", "AdjustBands"))
|
||||
@HANDLERS.register(("Alexa.EqualizerController", "ResetBands"))
|
||||
@HANDLERS.register(("Alexa.EqualizerController", "SetBands"))
|
||||
async def async_api_bands_directive(hass, config, directive, context):
|
||||
"""Handle an AdjustBands, ResetBands, SetBands request.
|
||||
|
||||
Only mode directives are currently supported for the EqualizerController.
|
||||
"""
|
||||
# Currently bands directives are not supported.
|
||||
msg = "Entity does not support directive"
|
||||
raise AlexaInvalidDirectiveError(msg)
|
||||
|
387
homeassistant/components/alexa/resources.py
Normal file
387
homeassistant/components/alexa/resources.py
Normal file
@@ -0,0 +1,387 @@
|
||||
"""Alexa Resources and Assets."""
|
||||
|
||||
|
||||
class AlexaGlobalCatalog:
|
||||
"""The Global Alexa catalog.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog
|
||||
|
||||
You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units.
|
||||
This catalog is localized into all the languages that Alexa supports.
|
||||
|
||||
You can reference the following catalog of pre-defined friendly names.
|
||||
Each item in the following list is an asset identifier followed by its supported friendly names.
|
||||
The first friendly name for each identifier is the one displayed in the Alexa mobile app.
|
||||
"""
|
||||
|
||||
# Air Purifier, Air Cleaner,Clean Air Machine
|
||||
DEVICE_NAME_AIR_PURIFIER = "Alexa.DeviceName.AirPurifier"
|
||||
|
||||
# Fan, Blower
|
||||
DEVICE_NAME_FAN = "Alexa.DeviceName.Fan"
|
||||
|
||||
# Router, Internet Router, Network Router, Wifi Router, Net Router
|
||||
DEVICE_NAME_ROUTER = "Alexa.DeviceName.Router"
|
||||
|
||||
# Shade, Blind, Curtain, Roller, Shutter, Drape, Awning, Window shade, Interior blind
|
||||
DEVICE_NAME_SHADE = "Alexa.DeviceName.Shade"
|
||||
|
||||
# Shower
|
||||
DEVICE_NAME_SHOWER = "Alexa.DeviceName.Shower"
|
||||
|
||||
# Space Heater, Portable Heater
|
||||
DEVICE_NAME_SPACE_HEATER = "Alexa.DeviceName.SpaceHeater"
|
||||
|
||||
# Washer, Washing Machine
|
||||
DEVICE_NAME_WASHER = "Alexa.DeviceName.Washer"
|
||||
|
||||
# 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi
|
||||
SETTING_2G_GUEST_WIFI = "Alexa.Setting.2GGuestWiFi"
|
||||
|
||||
# 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi
|
||||
SETTING_5G_GUEST_WIFI = "Alexa.Setting.5GGuestWiFi"
|
||||
|
||||
# Auto, Automatic, Automatic Mode, Auto Mode
|
||||
SETTING_AUTO = "Alexa.Setting.Auto"
|
||||
|
||||
# Direction
|
||||
SETTING_DIRECTION = "Alexa.Setting.Direction"
|
||||
|
||||
# Dry Cycle, Dry Preset, Dry Setting, Dryer Cycle, Dryer Preset, Dryer Setting
|
||||
SETTING_DRY_CYCLE = "Alexa.Setting.DryCycle"
|
||||
|
||||
# Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity
|
||||
SETTING_FAN_SPEED = "Alexa.Setting.FanSpeed"
|
||||
|
||||
# Guest Wi-fi, Guest Network, Guest Net
|
||||
SETTING_GUEST_WIFI = "Alexa.Setting.GuestWiFi"
|
||||
|
||||
# Heat
|
||||
SETTING_HEAT = "Alexa.Setting.Heat"
|
||||
|
||||
# Mode
|
||||
SETTING_MODE = "Alexa.Setting.Mode"
|
||||
|
||||
# Night, Night Mode
|
||||
SETTING_NIGHT = "Alexa.Setting.Night"
|
||||
|
||||
# Opening, Height, Lift, Width
|
||||
SETTING_OPENING = "Alexa.Setting.Opening"
|
||||
|
||||
# Oscillate, Swivel, Oscillation, Spin, Back and forth
|
||||
SETTING_OSCILLATE = "Alexa.Setting.Oscillate"
|
||||
|
||||
# Preset, Setting
|
||||
SETTING_PRESET = "Alexa.Setting.Preset"
|
||||
|
||||
# Quiet, Quiet Mode, Noiseless, Silent
|
||||
SETTING_QUIET = "Alexa.Setting.Quiet"
|
||||
|
||||
# Temperature, Temp
|
||||
SETTING_TEMPERATURE = "Alexa.Setting.Temperature"
|
||||
|
||||
# Wash Cycle, Wash Preset, Wash setting
|
||||
SETTING_WASH_CYCLE = "Alexa.Setting.WashCycle"
|
||||
|
||||
# Water Temperature, Water Temp, Water Heat
|
||||
SETTING_WATER_TEMPERATURE = "Alexa.Setting.WaterTemperature"
|
||||
|
||||
# Handheld Shower, Shower Wand, Hand Shower
|
||||
SHOWER_HAND_HELD = "Alexa.Shower.HandHeld"
|
||||
|
||||
# Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet
|
||||
SHOWER_RAIN_HEAD = "Alexa.Shower.RainHead"
|
||||
|
||||
# Degrees, Degree
|
||||
UNIT_ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees"
|
||||
|
||||
# Radians, Radian
|
||||
UNIT_ANGLE_RADIANS = "Alexa.Unit.Angle.Radians"
|
||||
|
||||
# Feet, Foot
|
||||
UNIT_DISTANCE_FEET = "Alexa.Unit.Distance.Feet"
|
||||
|
||||
# Inches, Inch
|
||||
UNIT_DISTANCE_INCHES = "Alexa.Unit.Distance.Inches"
|
||||
|
||||
# Kilometers
|
||||
UNIT_DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers"
|
||||
|
||||
# Meters, Meter, m
|
||||
UNIT_DISTANCE_METERS = "Alexa.Unit.Distance.Meters"
|
||||
|
||||
# Miles, Mile
|
||||
UNIT_DISTANCE_MILES = "Alexa.Unit.Distance.Miles"
|
||||
|
||||
# Yards, Yard
|
||||
UNIT_DISTANCE_YARDS = "Alexa.Unit.Distance.Yards"
|
||||
|
||||
# Grams, Gram, g
|
||||
UNIT_MASS_GRAMS = "Alexa.Unit.Mass.Grams"
|
||||
|
||||
# Kilograms, Kilogram, kg
|
||||
UNIT_MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms"
|
||||
|
||||
# Percent
|
||||
UNIT_PERCENT = "Alexa.Unit.Percent"
|
||||
|
||||
# Celsius, Degrees Celsius, Degrees, C, Centigrade, Degrees Centigrade
|
||||
UNIT_TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius"
|
||||
|
||||
# Degrees, Degree
|
||||
UNIT_TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees"
|
||||
|
||||
# Fahrenheit, Degrees Fahrenheit, Degrees F, Degrees, F
|
||||
UNIT_TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit"
|
||||
|
||||
# Kelvin, Degrees Kelvin, Degrees K, Degrees, K
|
||||
UNIT_TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin"
|
||||
|
||||
# Cubic Feet, Cubic Foot
|
||||
UNIT_VOLUME_CUBIC_FEET = "Alexa.Unit.Volume.CubicFeet"
|
||||
|
||||
# Cubic Meters, Cubic Meter, Meters Cubed
|
||||
UNIT_VOLUME_CUBIC_METERS = "Alexa.Unit.Volume.CubicMeters"
|
||||
|
||||
# Gallons, Gallon
|
||||
UNIT_VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons"
|
||||
|
||||
# Liters, Liter, L
|
||||
UNIT_VOLUME_LITERS = "Alexa.Unit.Volume.Liters"
|
||||
|
||||
# Pints, Pint
|
||||
UNIT_VOLUME_PINTS = "Alexa.Unit.Volume.Pints"
|
||||
|
||||
# Quarts, Quart
|
||||
UNIT_VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts"
|
||||
|
||||
# Ounces, Ounce, oz
|
||||
UNIT_WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces"
|
||||
|
||||
# Pounds, Pound, lbs
|
||||
UNIT_WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds"
|
||||
|
||||
# Close
|
||||
VALUE_CLOSE = "Alexa.Value.Close"
|
||||
|
||||
# Delicates, Delicate
|
||||
VALUE_DELICATE = "Alexa.Value.Delicate"
|
||||
|
||||
# High
|
||||
VALUE_HIGH = "Alexa.Value.High"
|
||||
|
||||
# Low
|
||||
VALUE_LOW = "Alexa.Value.Low"
|
||||
|
||||
# Maximum, Max
|
||||
VALUE_MAXIMUM = "Alexa.Value.Maximum"
|
||||
|
||||
# Medium, Mid
|
||||
VALUE_MEDIUM = "Alexa.Value.Medium"
|
||||
|
||||
# Minimum, Min
|
||||
VALUE_MINIMUM = "Alexa.Value.Minimum"
|
||||
|
||||
# Open
|
||||
VALUE_OPEN = "Alexa.Value.Open"
|
||||
|
||||
# Quick Wash, Fast Wash, Wash Quickly, Speed Wash
|
||||
VALUE_QUICK_WASH = "Alexa.Value.QuickWash"
|
||||
|
||||
|
||||
class AlexaCapabilityResource:
|
||||
"""Base class for Alexa capabilityResources, ModeResources, and presetResources objects.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources
|
||||
"""
|
||||
|
||||
def __init__(self, labels):
|
||||
"""Initialize an Alexa resource."""
|
||||
self._resource_labels = []
|
||||
for label in labels:
|
||||
self._resource_labels.append(label)
|
||||
|
||||
def serialize_capability_resources(self):
|
||||
"""Return capabilityResources object serialized for an API response."""
|
||||
return self.serialize_labels(self._resource_labels)
|
||||
|
||||
@staticmethod
|
||||
def serialize_configuration():
|
||||
"""Return ModeResources, PresetResources friendlyNames serialized for an API response."""
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def serialize_labels(resources):
|
||||
"""Return resource label objects for friendlyNames serialized for an API response."""
|
||||
labels = []
|
||||
for label in resources:
|
||||
if label in AlexaGlobalCatalog.__dict__.values():
|
||||
label = {"@type": "asset", "value": {"assetId": label}}
|
||||
else:
|
||||
label = {"@type": "text", "value": {"text": label, "locale": "en-US"}}
|
||||
|
||||
labels.append(label)
|
||||
|
||||
return {"friendlyNames": labels}
|
||||
|
||||
|
||||
class AlexaModeResource(AlexaCapabilityResource):
|
||||
"""Implements Alexa ModeResources.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources
|
||||
"""
|
||||
|
||||
def __init__(self, labels, ordered=False):
|
||||
"""Initialize an Alexa modeResource."""
|
||||
super().__init__(labels)
|
||||
self._supported_modes = []
|
||||
self._mode_ordered = ordered
|
||||
|
||||
def add_mode(self, value, labels):
|
||||
"""Add mode to the supportedModes object."""
|
||||
self._supported_modes.append({"value": value, "labels": labels})
|
||||
|
||||
def serialize_configuration(self):
|
||||
"""Return configuration for ModeResources friendlyNames serialized for an API response."""
|
||||
mode_resources = []
|
||||
for mode in self._supported_modes:
|
||||
result = {
|
||||
"value": mode["value"],
|
||||
"modeResources": self.serialize_labels(mode["labels"]),
|
||||
}
|
||||
mode_resources.append(result)
|
||||
|
||||
return {"ordered": self._mode_ordered, "supportedModes": mode_resources}
|
||||
|
||||
|
||||
class AlexaPresetResource(AlexaCapabilityResource):
|
||||
"""Implements Alexa PresetResources.
|
||||
|
||||
Use presetResources with RangeController to provide a set of friendlyNames for each RangeController preset.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources
|
||||
"""
|
||||
|
||||
def __init__(self, labels, min_value, max_value, precision, unit=None):
|
||||
"""Initialize an Alexa presetResource."""
|
||||
super().__init__(labels)
|
||||
self._presets = []
|
||||
self._minimum_value = min_value
|
||||
self._maximum_value = max_value
|
||||
self._precision = precision
|
||||
self._unit_of_measure = None
|
||||
if unit in AlexaGlobalCatalog.__dict__.values():
|
||||
self._unit_of_measure = unit
|
||||
|
||||
def add_preset(self, value, labels):
|
||||
"""Add preset to configuration presets array."""
|
||||
self._presets.append({"value": value, "labels": labels})
|
||||
|
||||
def serialize_configuration(self):
|
||||
"""Return configuration for PresetResources friendlyNames serialized for an API response."""
|
||||
configuration = {
|
||||
"supportedRange": {
|
||||
"minimumValue": self._minimum_value,
|
||||
"maximumValue": self._maximum_value,
|
||||
"precision": self._precision,
|
||||
}
|
||||
}
|
||||
|
||||
if self._unit_of_measure:
|
||||
configuration["unitOfMeasure"] = self._unit_of_measure
|
||||
|
||||
if self._presets:
|
||||
preset_resources = []
|
||||
for preset in self._presets:
|
||||
preset_resources.append(
|
||||
{
|
||||
"rangeValue": preset["value"],
|
||||
"presetResources": self.serialize_labels(preset["labels"]),
|
||||
}
|
||||
)
|
||||
configuration["presets"] = preset_resources
|
||||
|
||||
return configuration
|
||||
|
||||
|
||||
class AlexaSemantics:
|
||||
"""Class for Alexa Semantics Object.
|
||||
|
||||
You can optionally enable additional utterances by using semantics. When you use semantics,
|
||||
you manually map the phrases "open", "close", "raise", and "lower" to directives.
|
||||
|
||||
Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object
|
||||
"""
|
||||
|
||||
MAPPINGS_ACTION = "actionMappings"
|
||||
MAPPINGS_STATE = "stateMappings"
|
||||
|
||||
ACTIONS_TO_DIRECTIVE = "ActionsToDirective"
|
||||
STATES_TO_VALUE = "StatesToValue"
|
||||
STATES_TO_RANGE = "StatesToRange"
|
||||
|
||||
ACTION_CLOSE = "Alexa.Actions.Close"
|
||||
ACTION_LOWER = "Alexa.Actions.Lower"
|
||||
ACTION_OPEN = "Alexa.Actions.Open"
|
||||
ACTION_RAISE = "Alexa.Actions.Raise"
|
||||
|
||||
STATES_OPEN = "Alexa.States.Open"
|
||||
STATES_CLOSED = "Alexa.States.Closed"
|
||||
|
||||
DIRECTIVE_RANGE_SET_VALUE = "SetRangeValue"
|
||||
DIRECTIVE_RANGE_ADJUST_VALUE = "AdjustRangeValue"
|
||||
DIRECTIVE_TOGGLE_TURN_ON = "TurnOn"
|
||||
DIRECTIVE_TOGGLE_TURN_OFF = "TurnOff"
|
||||
DIRECTIVE_MODE_SET_MODE = "SetMode"
|
||||
DIRECTIVE_MODE_ADJUST_MODE = "AdjustMode"
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize an Alexa modeResource."""
|
||||
self._action_mappings = []
|
||||
self._state_mappings = []
|
||||
|
||||
def _add_action_mapping(self, semantics):
|
||||
"""Add action mapping between actions and interface directives."""
|
||||
self._action_mappings.append(semantics)
|
||||
|
||||
def _add_state_mapping(self, semantics):
|
||||
"""Add state mapping between states and interface directives."""
|
||||
self._state_mappings.append(semantics)
|
||||
|
||||
def add_states_to_value(self, states, value):
|
||||
"""Add StatesToValue stateMappings."""
|
||||
self._add_state_mapping(
|
||||
{"@type": self.STATES_TO_VALUE, "states": states, "value": value}
|
||||
)
|
||||
|
||||
def add_states_to_range(self, states, min_value, max_value):
|
||||
"""Add StatesToRange stateMappings."""
|
||||
self._add_state_mapping(
|
||||
{
|
||||
"@type": self.STATES_TO_RANGE,
|
||||
"states": states,
|
||||
"range": {"minimumValue": min_value, "maximumValue": max_value},
|
||||
}
|
||||
)
|
||||
|
||||
def add_action_to_directive(self, actions, directive, payload):
|
||||
"""Add ActionsToDirective actionMappings."""
|
||||
self._add_action_mapping(
|
||||
{
|
||||
"@type": self.ACTIONS_TO_DIRECTIVE,
|
||||
"actions": actions,
|
||||
"directive": {"name": directive, "payload": payload},
|
||||
}
|
||||
)
|
||||
|
||||
def serialize_semantics(self):
|
||||
"""Return semantics object serialized for an API response."""
|
||||
semantics = {}
|
||||
if self._action_mappings:
|
||||
semantics[self.MAPPINGS_ACTION] = self._action_mappings
|
||||
if self._state_mappings:
|
||||
semantics[self.MAPPINGS_STATE] = self._state_mappings
|
||||
|
||||
return semantics
|
@@ -4,7 +4,7 @@ import logging
|
||||
import homeassistant.core as ha
|
||||
|
||||
from .const import API_DIRECTIVE, API_HEADER
|
||||
from .errors import AlexaError, AlexaBridgeUnreachableError
|
||||
from .errors import AlexaBridgeUnreachableError, AlexaError
|
||||
from .handlers import HANDLERS
|
||||
from .messages import AlexaDirective
|
||||
|
||||
|
@@ -13,8 +13,8 @@ from .const import (
|
||||
CONF_ENTITY_CONFIG,
|
||||
CONF_FILTER,
|
||||
)
|
||||
from .state_report import async_enable_proactive_mode
|
||||
from .smart_home import async_handle_message
|
||||
from .state_report import async_enable_proactive_mode
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home"
|
||||
|
@@ -6,8 +6,8 @@ import logging
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import MATCH_ALL, STATE_ON
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import API_CHANGE, Cause
|
||||
from .entities import ENTITY_ADAPTERS
|
||||
|
@@ -2,7 +2,13 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_setup": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u0438\u043d Almond \u0430\u043a\u0430\u0443\u043d\u0442.",
|
||||
"cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430."
|
||||
"cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430.",
|
||||
"missing_configuration": "\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430 \u043a\u0430\u043a \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Almond."
|
||||
},
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
"title": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f"
|
||||
}
|
||||
},
|
||||
"title": "Almond"
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user