diff --git a/.coveragerc b/.coveragerc
index 001729ace5bc6c..9fe3e10c8bc27a 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -50,6 +50,7 @@ omit =
homeassistant/components/asterisk_cdr/mailbox.py
homeassistant/components/asterisk_mbox/*
homeassistant/components/asuswrt/device_tracker.py
+ homeassistant/components/atome/*
homeassistant/components/august/*
homeassistant/components/aurora_abb_powerone/sensor.py
homeassistant/components/automatic/device_tracker.py
@@ -142,6 +143,7 @@ omit =
homeassistant/components/dlna_dmr/media_player.py
homeassistant/components/dnsip/sensor.py
homeassistant/components/dominos/*
+ homeassistant/components/doods/*
homeassistant/components/doorbird/*
homeassistant/components/dovado/*
homeassistant/components/downloader/*
@@ -196,7 +198,6 @@ omit =
homeassistant/components/evohome/*
homeassistant/components/familyhub/camera.py
homeassistant/components/fastdotcom/*
- homeassistant/components/fedex/sensor.py
homeassistant/components/ffmpeg/camera.py
homeassistant/components/fibaro/*
homeassistant/components/filesize/sensor.py
@@ -248,6 +249,7 @@ omit =
homeassistant/components/greeneye_monitor/sensor.py
homeassistant/components/greenwave/light.py
homeassistant/components/group/notify.py
+ homeassistant/components/growatt_server/sensor.py
homeassistant/components/gstreamer/media_player.py
homeassistant/components/gtfs/sensor.py
homeassistant/components/gtt/sensor.py
@@ -286,7 +288,15 @@ omit =
homeassistant/components/hydrawise/*
homeassistant/components/hyperion/light.py
homeassistant/components/ialarm/alarm_control_panel.py
+ homeassistant/components/iaqualink/binary_sensor.py
+ homeassistant/components/iaqualink/climate.py
+ homeassistant/components/iaqualink/light.py
+ homeassistant/components/iaqualink/sensor.py
+ homeassistant/components/iaqualink/switch.py
homeassistant/components/icloud/device_tracker.py
+ homeassistant/components/izone/climate.py
+ homeassistant/components/izone/discovery.py
+ homeassistant/components/izone/__init__.py
homeassistant/components/idteck_prox/*
homeassistant/components/ifttt/*
homeassistant/components/iglo/light.py
@@ -307,6 +317,7 @@ omit =
homeassistant/components/itunes/media_player.py
homeassistant/components/joaoapps_join/*
homeassistant/components/juicenet/*
+ homeassistant/components/kaiterra/*
homeassistant/components/kankun/switch.py
homeassistant/components/keba/*
homeassistant/components/keenetic_ndms2/device_tracker.py
@@ -428,11 +439,14 @@ omit =
homeassistant/components/nuki/lock.py
homeassistant/components/nut/sensor.py
homeassistant/components/nx584/alarm_control_panel.py
+ homeassistant/components/nzbget/__init__.py
homeassistant/components/nzbget/sensor.py
+ homeassistant/components/obihai/*
homeassistant/components/octoprint/*
homeassistant/components/oem/climate.py
homeassistant/components/oasa_telematics/sensor.py
homeassistant/components/ohmconnect/sensor.py
+ homeassistant/components/ombi/*
homeassistant/components/onewire/sensor.py
homeassistant/components/onkyo/media_player.py
homeassistant/components/onvif/camera.py
@@ -469,8 +483,10 @@ omit =
homeassistant/components/pioneer/media_player.py
homeassistant/components/pjlink/media_player.py
homeassistant/components/plaato/*
+ homeassistant/components/plex/__init__.py
homeassistant/components/plex/media_player.py
homeassistant/components/plex/sensor.py
+ homeassistant/components/plex/server.py
homeassistant/components/plugwise/*
homeassistant/components/plum_lightpad/*
homeassistant/components/pocketcasts/sensor.py
@@ -575,6 +591,7 @@ omit =
homeassistant/components/snmp/*
homeassistant/components/sochain/sensor.py
homeassistant/components/socialblade/sensor.py
+ homeassistant/components/solaredge/__init__.py
homeassistant/components/solaredge/sensor.py
homeassistant/components/solaredge_local/sensor.py
homeassistant/components/solax/sensor.py
@@ -590,7 +607,6 @@ omit =
homeassistant/components/spotcrime/sensor.py
homeassistant/components/spotify/media_player.py
homeassistant/components/squeezebox/media_player.py
- homeassistant/components/srp_energy/sensor.py
homeassistant/components/starlingbank/sensor.py
homeassistant/components/steam_online/sensor.py
homeassistant/components/stiebel_eltron/*
@@ -611,7 +627,6 @@ omit =
homeassistant/components/synologydsm/sensor.py
homeassistant/components/syslog/notify.py
homeassistant/components/systemmonitor/sensor.py
- homeassistant/components/sytadin/sensor.py
homeassistant/components/tado/*
homeassistant/components/tado/device_tracker.py
homeassistant/components/tahoma/*
@@ -653,6 +668,7 @@ omit =
homeassistant/components/trackr/device_tracker.py
homeassistant/components/tradfri/*
homeassistant/components/tradfri/light.py
+ homeassistant/components/tradfri/cover.py
homeassistant/components/trafikverket_train/sensor.py
homeassistant/components/trafikverket_weatherstation/sensor.py
homeassistant/components/transmission/*
@@ -670,10 +686,9 @@ omit =
homeassistant/components/ue_smart_radio/media_player.py
homeassistant/components/upcloud/*
homeassistant/components/upnp/*
- homeassistant/components/ups/sensor.py
+ homeassistant/components/upc_connect/*
homeassistant/components/uptimerobot/binary_sensor.py
homeassistant/components/uscis/sensor.py
- homeassistant/components/usps/*
homeassistant/components/vallox/*
homeassistant/components/vasttrafik/sensor.py
homeassistant/components/velbus/__init__.py
@@ -692,6 +707,8 @@ omit =
homeassistant/components/vesync/const.py
homeassistant/components/vesync/switch.py
homeassistant/components/viaggiatreno/sensor.py
+ homeassistant/components/vicare/*
+ homeassistant/components/vivotek/camera.py
homeassistant/components/vizio/media_player.py
homeassistant/components/vlc/media_player.py
homeassistant/components/vlc_telnet/media_player.py
@@ -728,6 +745,7 @@ omit =
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/*
homeassistant/components/yeelightsunflower/light.py
homeassistant/components/yi/camera.py
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index a025a52e849c1c..e78a8e6851c728 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -3,7 +3,7 @@
"name": "Home Assistant Dev",
"context": "..",
"dockerFile": "../Dockerfile.dev",
- "postCreateCommand": "pip3 install -e .",
+ "postCreateCommand": "mkdir -p config && pip3 install -e .",
"appPort": 8123,
"runArgs": ["-e", "GIT_EDITOR=\"code --wait\""],
"extensions": [
diff --git a/.github/stale.yml b/.github/stale.yml
index a1a35e9f3b1ada..44cd95e1f5d710 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -13,6 +13,7 @@ onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- under investigation
+ - Help wanted
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
diff --git a/.gitignore b/.gitignore
index 5389954ca59578..15f0896975da36 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,6 +64,7 @@ nosetests.xml
htmlcov/
test-reports/
test-results.xml
+test-output.xml
# Translations
*.mo
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index f57c182809b93e..151868a1663981 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -19,6 +19,7 @@
"label": "Pytest",
"type": "shell",
"command": "pytest --timeout=10 tests",
+ "dependsOn": ["Install all Test Requirements"],
"group": {
"kind": "test",
"isDefault": true
@@ -85,6 +86,20 @@
"panel": "new"
},
"problemMatcher": []
+ },
+ {
+ "label": "Install all Test Requirements",
+ "type": "shell",
+ "command": "pip3 install -r requirements_test_all.txt -c homeassistant/package_constraints.txt",
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "presentation": {
+ "reveal": "always",
+ "panel": "new"
+ },
+ "problemMatcher": []
}
]
}
diff --git a/CODEOWNERS b/CODEOWNERS
index 7f60243097e239..7e05cdf0b399e7 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -28,6 +28,7 @@ homeassistant/components/arcam_fmj/* @elupus
homeassistant/components/arduino/* @fabaff
homeassistant/components/arest/* @fabaff
homeassistant/components/asuswrt/* @kennedyshead
+homeassistant/components/atome/* @baqs
homeassistant/components/aurora_abb_powerone/* @davet2001
homeassistant/components/auth/* @home-assistant/core
homeassistant/components/automatic/* @armills
@@ -109,10 +110,12 @@ homeassistant/components/google_translate/* @awarecan
homeassistant/components/google_travel_time/* @robbiet480
homeassistant/components/gpsd/* @fabaff
homeassistant/components/group/* @home-assistant/core
+homeassistant/components/growatt_server/* @indykoning
homeassistant/components/gtfs/* @robbiet480
homeassistant/components/harmony/* @ehendrix23
homeassistant/components/hassio/* @home-assistant/hass-io
homeassistant/components/heos/* @andrewsayre
+homeassistant/components/here_travel_time/* @eifinger
homeassistant/components/hikvision/* @mezz64
homeassistant/components/hikvisioncam/* @fbradyirl
homeassistant/components/history/* @home-assistant/core
@@ -128,6 +131,7 @@ homeassistant/components/http/* @home-assistant/core
homeassistant/components/huawei_lte/* @scop
homeassistant/components/huawei_router/* @abmantis
homeassistant/components/hue/* @balloob
+homeassistant/components/iaqualink/* @flz
homeassistant/components/ign_sismologia/* @exxamalte
homeassistant/components/incomfort/* @zxdavb
homeassistant/components/influxdb/* @fabaff
@@ -141,7 +145,9 @@ homeassistant/components/ios/* @robbiet480
homeassistant/components/ipma/* @dgomes
homeassistant/components/iqvia/* @bachya
homeassistant/components/irish_rail_transport/* @ttroy50
+homeassistant/components/izone/* @Swamp-Ig
homeassistant/components/jewish_calendar/* @tsvi
+homeassistant/components/kaiterra/* @Michsior14
homeassistant/components/keba/* @dannerph
homeassistant/components/knx/* @Julius2342
homeassistant/components/kodi/* @armills
@@ -150,9 +156,6 @@ homeassistant/components/lametric/* @robbiet480
homeassistant/components/launch_library/* @ludeeus
homeassistant/components/lcn/* @alengwenus
homeassistant/components/life360/* @pnbruckner
-homeassistant/components/lifx/* @amelchio
-homeassistant/components/lifx_cloud/* @amelchio
-homeassistant/components/lifx_legacy/* @amelchio
homeassistant/components/linky/* @Quentame
homeassistant/components/linux_battery/* @fabaff
homeassistant/components/liveboxplaytv/* @pschmitt
@@ -183,7 +186,6 @@ homeassistant/components/nello/* @pschmitt
homeassistant/components/ness_alarm/* @nickw444
homeassistant/components/nest/* @awarecan
homeassistant/components/netdata/* @fabaff
-homeassistant/components/netgear_lte/* @amelchio
homeassistant/components/nextbus/* @vividboarder
homeassistant/components/nissan_leaf/* @filcole
homeassistant/components/nmbs/* @thibmaek
@@ -192,9 +194,12 @@ homeassistant/components/notify/* @home-assistant/core
homeassistant/components/notion/* @bachya
homeassistant/components/nsw_fuel_station/* @nickw444
homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte
-homeassistant/components/nuki/* @pschmitt
+homeassistant/components/nuki/* @pvizeli
homeassistant/components/nws/* @MatthewFlamm
+homeassistant/components/nzbget/* @chriscla
+homeassistant/components/obihai/* @dshokouhi
homeassistant/components/ohmconnect/* @robbiet480
+homeassistant/components/ombi/* @larssont
homeassistant/components/onboarding/* @home-assistant/core
homeassistant/components/opentherm_gw/* @mvn23
homeassistant/components/openuv/* @bachya
@@ -205,9 +210,10 @@ homeassistant/components/panel_custom/* @home-assistant/frontend
homeassistant/components/panel_iframe/* @home-assistant/frontend
homeassistant/components/persistent_notification/* @home-assistant/core
homeassistant/components/philips_js/* @elupus
-homeassistant/components/pi_hole/* @fabaff
+homeassistant/components/pi_hole/* @fabaff @johnluetke
homeassistant/components/plaato/* @JohNan
homeassistant/components/plant/* @ChristianKuehnel
+homeassistant/components/plex/* @jjlawren
homeassistant/components/plugwise/* @laetificat @CoMPaTech
homeassistant/components/point/* @fredrike
homeassistant/components/ps4/* @ktnrg45
@@ -243,11 +249,10 @@ homeassistant/components/smarthab/* @outadoc
homeassistant/components/smartthings/* @andrewsayre
homeassistant/components/smarty/* @z0mbieprocess
homeassistant/components/smtp/* @fabaff
-homeassistant/components/solaredge_local/* @drobtravels
+homeassistant/components/solaredge_local/* @drobtravels @scheric
homeassistant/components/solax/* @squishykid
homeassistant/components/somfy/* @tetienne
homeassistant/components/songpal/* @rytilahti
-homeassistant/components/sonos/* @amelchio
homeassistant/components/spaceapi/* @fabaff
homeassistant/components/spider/* @peternijssen
homeassistant/components/sql/* @dgomes
@@ -265,7 +270,6 @@ homeassistant/components/switchmate/* @danielhiversen
homeassistant/components/syncthru/* @nielstron
homeassistant/components/synology_srm/* @aerialls
homeassistant/components/syslog/* @fabaff
-homeassistant/components/sytadin/* @gautric
homeassistant/components/tahoma/* @philklei
homeassistant/components/tautulli/* @ludeeus
homeassistant/components/tellduslive/* @fredrike
@@ -287,6 +291,7 @@ homeassistant/components/twentemilieu/* @frenck
homeassistant/components/twilio_call/* @robbiet480
homeassistant/components/twilio_sms/* @robbiet480
homeassistant/components/unifi/* @kane610
+homeassistant/components/upc_connect/* @pvizeli
homeassistant/components/upcloud/* @scop
homeassistant/components/updater/* @home-assistant/core
homeassistant/components/upnp/* @robbiet480
@@ -297,6 +302,7 @@ homeassistant/components/velbus/* @cereal2nd
homeassistant/components/velux/* @Julius2342
homeassistant/components/version/* @fabaff
homeassistant/components/vesync/* @markperdue @webdjoe
+homeassistant/components/vicare/* @oischinger
homeassistant/components/vizio/* @raman325
homeassistant/components/vlc_telnet/* @rodripf
homeassistant/components/waqi/* @andrey-git
@@ -314,6 +320,7 @@ homeassistant/components/xiaomi_miio/* @rytilahti @syssi
homeassistant/components/xiaomi_tv/* @simse
homeassistant/components/xmpp/* @fabaff @flowolf
homeassistant/components/yamaha_musiccast/* @jalmeroth
+homeassistant/components/yandex_transport/* @rishatik92
homeassistant/components/yeelight/* @rytilahti @zewelor
homeassistant/components/yeelightsunflower/* @lindsaymarkward
homeassistant/components/yessssms/* @flowolf
diff --git a/Dockerfile.dev b/Dockerfile.dev
index 00f5576bdbb0fc..eb76fe5b16b038 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -23,9 +23,10 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
WORKDIR /workspaces
-# Install Python dependencies from requirements.txt if it exists
-COPY requirements_test_all.txt homeassistant/package_constraints.txt /workspaces/
-RUN pip3 install -r requirements_test_all.txt -c package_constraints.txt
+# Install Python dependencies from requirements
+COPY requirements_test.txt homeassistant/package_constraints.txt ./
+RUN pip3 install -r requirements_test.txt -c package_constraints.txt \
+ && rm -f requirements_test.txt package_constraints.txt
# Set the default shell to bash instead of sh
ENV SHELL /bin/bash
diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml
index 0ee272f900daa7..13f0915bc56f12 100644
--- a/azure-pipelines-ci.yml
+++ b/azure-pipelines-ci.yml
@@ -112,8 +112,10 @@ stages:
# Find offending deps with `pipdeptree -r -p typing`
pip uninstall -y typing
- script: |
+ set -e
+
. venv/bin/activate
- pytest --timeout=9 --durations=10 --junitxml=test-results.xml -qq -o console_output_style=count -p no:sugar tests
+ pytest --timeout=9 --durations=10 -qq -o console_output_style=count -p no:sugar tests
script/check_dirty
displayName: 'Run pytest for python $(python.container)'
condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain']))
@@ -121,22 +123,11 @@ stages:
set -e
. venv/bin/activate
- pytest --timeout=9 --durations=10 --junitxml=test-results.xml --cov --cov-report=xml -qq -o console_output_style=count -p no:sugar tests
+ pytest --timeout=9 --durations=10 --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests
codecov --token $(codecovToken)
script/check_dirty
displayName: 'Run pytest for python $(python.container) / coverage'
condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain']))
- - task: PublishTestResults@2
- condition: succeededOrFailed()
- inputs:
- testResultsFiles: 'test-results.xml'
- testRunTitle: 'Publish test results for Python $(python.container)'
- - task: PublishCodeCoverageResults@1
- inputs:
- codeCoverageTool: cobertura
- summaryFileLocation: coverage.xml
- displayName: 'publish coverage artifact'
- condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain']))
- stage: 'FullCheck'
dependsOn:
diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml
index 63ce5b707cf6b2..29e68a5d7acc16 100644
--- a/azure-pipelines-release.yml
+++ b/azure-pipelines-release.yml
@@ -43,7 +43,7 @@ stages:
release="$(Build.SourceBranchName)"
created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')"
- if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480)$ ]]; then
+ if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480|bramkragten)$ ]]; then
exit 0
fi
@@ -68,8 +68,8 @@ stages:
- script: python setup.py sdist bdist_wheel
displayName: 'Build package'
- script: |
- TWINE_USERNAME="$(twineUser)"
- TWINE_PASSWORD="$(twinePassword)"
+ export TWINE_USERNAME="$(twineUser)"
+ export TWINE_PASSWORD="$(twinePassword)"
twine upload dist/* --skip-existing
displayName: 'Upload pypi'
diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml
index 83ed75da256336..2fd49c056f7fd6 100644
--- a/azure-pipelines-translation.yml
+++ b/azure-pipelines-translation.yml
@@ -60,6 +60,7 @@ jobs:
displayName: 'Download Translation'
- script: |
git checkout dev
+ git add homeassistant
git commit -am "[ci skip] Translation update"
git push
displayName: 'Update translation'
diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml
index eec3f678981b53..42815d8c8ae779 100644
--- a/azure-pipelines-wheels.yml
+++ b/azure-pipelines-wheels.yml
@@ -45,7 +45,6 @@ jobs:
requirement_files="requirements_wheels.txt requirements_diff.txt"
for requirement_file in ${requirement_files}; do
- sed -i "s|# pytradfri|pytradfri|g" ${requirement_file}
sed -i "s|# pybluez|pybluez|g" ${requirement_file}
sed -i "s|# bluepy|bluepy|g" ${requirement_file}
sed -i "s|# beacontools|beacontools|g" ${requirement_file}
@@ -63,9 +62,15 @@ jobs:
sed -i "s|# homekit|homekit|g" ${requirement_file}
sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file}
sed -i "s|# decora|decora|g" ${requirement_file}
+ sed -i "s|# avion|avion|g" ${requirement_file}
sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file}
sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file}
sed -i "s|# face_recognition|face_recognition|g" ${requirement_file}
sed -i "s|# py_noaa|py_noaa|g" ${requirement_file}
+ sed -i "s|# bme680|bme680|g" ${requirement_file}
+
+ if [[ "$(buildArch)" =~ arm ]]; then
+ sed -i "s|# VL53L1X|VL53L1X|g" ${requirement_file}
+ fi
done
displayName: 'Prepare requirements files for Hass.io'
diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py
index 9fe501078c2a45..f7e24d69884975 100644
--- a/homeassistant/__main__.py
+++ b/homeassistant/__main__.py
@@ -216,7 +216,7 @@ def check_pid(pid_file: str) -> None:
try:
with open(pid_file, "r") as file:
pid = int(file.readline())
- except IOError:
+ except OSError:
# PID File does not exist
return
@@ -239,7 +239,7 @@ def write_pid(pid_file: str) -> None:
try:
with open(pid_file, "w") as file:
file.write(str(pid))
- except IOError:
+ except OSError:
print(f"Fatal Error: Unable to write pid file {pid_file}")
sys.exit(1)
@@ -258,7 +258,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
val = fcntl(_fd, F_GETFD)
if not val & FD_CLOEXEC:
fcntl(_fd, F_SETFD, val | FD_CLOEXEC)
- except IOError:
+ except OSError:
pass
diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py
index 26055032422f0c..6889d17a25fe6d 100644
--- a/homeassistant/auth/models.py
+++ b/homeassistant/auth/models.py
@@ -20,7 +20,7 @@
class Group:
"""A group."""
- name = attr.ib(type=str) # type: Optional[str]
+ name = attr.ib(type=Optional[str])
policy = attr.ib(type=perm_mdl.PolicyType)
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
system_generated = attr.ib(type=bool, default=False)
@@ -30,22 +30,20 @@ class Group:
class User:
"""A user."""
- name = attr.ib(type=str) # type: Optional[str]
+ name = attr.ib(type=Optional[str])
perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, cmp=False)
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
is_owner = attr.ib(type=bool, default=False)
is_active = attr.ib(type=bool, default=False)
system_generated = attr.ib(type=bool, default=False)
- groups = attr.ib(type=List, factory=list, cmp=False) # type: List[Group]
+ groups = attr.ib(type=List[Group], factory=list, cmp=False)
# List of credentials of a user.
- credentials = attr.ib(type=list, factory=list, cmp=False) # type: List[Credentials]
+ credentials = attr.ib(type=List["Credentials"], factory=list, cmp=False)
# Tokens associated with a user.
- refresh_tokens = attr.ib(
- type=dict, factory=dict, cmp=False
- ) # type: Dict[str, RefreshToken]
+ refresh_tokens = attr.ib(type=Dict[str, "RefreshToken"], factory=dict, cmp=False)
_permissions = attr.ib(
type=Optional[perm_mdl.PolicyPermissions], init=False, cmp=False, default=None
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index ef294491141042..7c4ec731b49b93 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -97,6 +97,17 @@ async def async_from_config_dict(
stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop - start)
+ if sys.version_info[:3] < (3, 6, 1):
+ msg = (
+ "Python 3.6.0 support is deprecated and will "
+ "be removed in the first release after October 2. Please "
+ "upgrade Python to 3.6.1 or higher."
+ )
+ _LOGGER.warning(msg)
+ hass.components.persistent_notification.async_create(
+ msg, "Python version", "python_version"
+ )
+
return hass
diff --git a/homeassistant/components/adguard/.translations/es.json b/homeassistant/components/adguard/.translations/es.json
index 971d38f9ab2d26..5886d8e5c5b6fc 100644
--- a/homeassistant/components/adguard/.translations/es.json
+++ b/homeassistant/components/adguard/.translations/es.json
@@ -1,7 +1,30 @@
{
"config": {
"abort": {
- "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente."
- }
+ "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.",
+ "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home."
+ },
+ "error": {
+ "connection_error": "No se conect\u00f3."
+ },
+ "step": {
+ "hassio_confirm": {
+ "description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Hass.io: {addon} ?",
+ "title": "AdGuard Home a trav\u00e9s del complemento Hass.io"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Contrase\u00f1a",
+ "port": "Puerto",
+ "ssl": "AdGuard Home utiliza un certificado SSL",
+ "username": "Nombre de usuario",
+ "verify_ssl": "AdGuard Home utiliza un certificado apropiado"
+ },
+ "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control.",
+ "title": "Enlace su AdGuard Home."
+ }
+ },
+ "title": "AdGuard Home"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/.translations/it.json b/homeassistant/components/adguard/.translations/it.json
index 6cd8767334dc9f..57f81dc1d99ad5 100644
--- a/homeassistant/components/adguard/.translations/it.json
+++ b/homeassistant/components/adguard/.translations/it.json
@@ -1,21 +1,30 @@
{
"config": {
"abort": {
+ "existing_instance_updated": "Configurazione esistente aggiornata.",
"single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home."
},
"error": {
"connection_error": "Impossibile connettersi."
},
"step": {
+ "hassio_confirm": {
+ "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": {
"data": {
"host": "Host",
"password": "Password",
"port": "Porta",
"ssl": "AdGuard Home utilizza un certificato SSL",
- "username": "Nome utente"
- }
+ "username": "Nome utente",
+ "verify_ssl": "AdGuard Home utilizza un certificato appropriato"
+ },
+ "description": "Configura l'istanza di AdGuard Home per consentire il monitoraggio e il controllo.",
+ "title": "Collega la tua AdGuard Home."
}
- }
+ },
+ "title": "AdGuard Home"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/.translations/pl.json b/homeassistant/components/adguard/.translations/pl.json
index e58c901f3643f4..f8f64d542608fe 100644
--- a/homeassistant/components/adguard/.translations/pl.json
+++ b/homeassistant/components/adguard/.translations/pl.json
@@ -22,7 +22,7 @@
"verify_ssl": "AdGuard Home u\u017cywa odpowiedniego certyfikatu."
},
"description": "Skonfiguruj instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i kontrol\u0119.",
- "title": "Po\u0142\u0105cz sw\u00f3j AdGuard Home"
+ "title": "Po\u0142\u0105cz AdGuard Home"
}
},
"title": "AdGuard Home"
diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py
index d769f797da1bfa..aeaa0a62c4bdbb 100644
--- a/homeassistant/components/alexa/capabilities.py
+++ b/homeassistant/components/alexa/capabilities.py
@@ -1,5 +1,4 @@
"""Alexa capabilities."""
-from datetime import datetime
import logging
from homeassistant.const import (
@@ -16,6 +15,7 @@
import homeassistant.components.climate.const as climate
from homeassistant.components import light, fan, cover
import homeassistant.util.color as color_util
+import homeassistant.util.dt as dt_util
from .const import (
API_TEMP_UNITS,
@@ -109,7 +109,7 @@ def serialize_properties(self):
"name": prop_name,
"namespace": self.name(),
"value": prop_value,
- "timeOfSample": datetime.now().strftime(DATE_FORMAT),
+ "timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT),
"uncertaintyInMilliseconds": 0,
}
diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py
index 708d1592e4c0fb..0b5c1243764ce2 100644
--- a/homeassistant/components/alexa/flash_briefings.py
+++ b/homeassistant/components/alexa/flash_briefings.py
@@ -1,9 +1,9 @@
"""Support for Alexa skill service end point."""
import copy
-from datetime import datetime
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
@@ -89,7 +89,7 @@ def get(self, request, briefing_id):
else:
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
- output[ATTR_UPDATE_DATE] = datetime.now().strftime(DATE_FORMAT)
+ output[ATTR_UPDATE_DATE] = dt_util.utcnow().strftime(DATE_FORMAT)
briefing.append(output)
diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py
index 1e636b96ee5205..c72101460c4819 100644
--- a/homeassistant/components/alexa/handlers.py
+++ b/homeassistant/components/alexa/handlers.py
@@ -1,5 +1,4 @@
"""Alexa message handlers."""
-from datetime import datetime
import logging
import math
@@ -28,6 +27,7 @@
TEMP_FAHRENHEIT,
)
import homeassistant.util.color as color_util
+import homeassistant.util.dt as dt_util
from homeassistant.util.decorator import Registry
from homeassistant.util.temperature import convert as convert_temperature
@@ -275,7 +275,7 @@ async def async_api_activate(hass, config, directive, context):
payload = {
"cause": {"type": Cause.VOICE_INTERACTION},
- "timestamp": "%sZ" % (datetime.utcnow().isoformat(),),
+ "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z",
}
return directive.response(
@@ -299,7 +299,7 @@ async def async_api_deactivate(hass, config, directive, context):
payload = {
"cause": {"type": Cause.VOICE_INTERACTION},
- "timestamp": "%sZ" % (datetime.utcnow().isoformat(),),
+ "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z",
}
return directive.response(
diff --git a/homeassistant/components/ambiclimate/.translations/it.json b/homeassistant/components/ambiclimate/.translations/it.json
index b062eb67c1ffea..a13874b36764d5 100644
--- a/homeassistant/components/ambiclimate/.translations/it.json
+++ b/homeassistant/components/ambiclimate/.translations/it.json
@@ -1,7 +1,22 @@
{
"config": {
"abort": {
- "already_setup": "L'account Ambiclimate \u00e8 configurato."
+ "access_token": "Errore sconosciuto durante la generazione di un token di accesso.",
+ "already_setup": "L'account Ambiclimate \u00e8 configurato.",
+ "no_config": "\u00c8 necessario configurare Ambiclimate prima di poter eseguire l'autenticazione con esso. [Leggere le istruzioni] (https://www.home-assistant.io/components/ambiclimate/)."
+ },
+ "create_entry": {
+ "default": "Autenticato con successo con Ambiclimate"
+ },
+ "error": {
+ "follow_link": "Si prega di seguire il link e di autenticarsi prima di premere Invia",
+ "no_token": "Non autenticato con Ambiclimate"
+ },
+ "step": {
+ "auth": {
+ "description": "Segui questo [link]({authorization_url}) e Consenti accesso al tuo account Ambiclimate, quindi torna indietro e premi Invia qui sotto. \n (Assicurati che l'URL di richiamata specificato sia {cb_url})",
+ "title": "Autenticare Ambiclimate"
+ }
},
"title": "Ambiclimate"
}
diff --git a/homeassistant/components/ambiclimate/.translations/no.json b/homeassistant/components/ambiclimate/.translations/no.json
index 567d0b95ff38cb..7bb124ae5433e1 100644
--- a/homeassistant/components/ambiclimate/.translations/no.json
+++ b/homeassistant/components/ambiclimate/.translations/no.json
@@ -14,7 +14,7 @@
},
"step": {
"auth": {
- "description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og Tillat tilgang til din Ambiclimate konto, og kom s\u00e5 tilbake og trykk Send nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})",
+ "description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og Tillat tilgang til din Ambiclimate konto, kom deretter tilbake og trykk Send nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})",
"title": "Autensiere Ambiclimate"
}
},
diff --git a/homeassistant/components/ambiclimate/.translations/pl.json b/homeassistant/components/ambiclimate/.translations/pl.json
index 47e9c9f35b2897..7ba95b007c995a 100644
--- a/homeassistant/components/ambiclimate/.translations/pl.json
+++ b/homeassistant/components/ambiclimate/.translations/pl.json
@@ -14,7 +14,7 @@
},
"step": {
"auth": {
- "description": "Kliknij poni\u017cszy [link]({authorization_url}) i Zezw\u00f3l na dost\u0119p do swojego konta Ambiclimate, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n(Upewnij si\u0119, \u017ce podany adres URL to {cb_url})",
+ "description": "Kliknij poni\u017cszy [link]({authorization_url}) i Zezw\u00f3l na dost\u0119p do konta Ambiclimate, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n(Upewnij si\u0119, \u017ce podany adres URL to {cb_url})",
"title": "Uwierzytelnienie Ambiclimate"
}
},
diff --git a/homeassistant/components/ambiclimate/.translations/ru.json b/homeassistant/components/ambiclimate/.translations/ru.json
index 129579315a29de..a4300e1e5306c5 100644
--- a/homeassistant/components/ambiclimate/.translations/ru.json
+++ b/homeassistant/components/ambiclimate/.translations/ru.json
@@ -3,7 +3,7 @@
"abort": {
"access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.",
"already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.",
- "no_config": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)."
+ "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)."
},
"create_entry": {
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
diff --git a/homeassistant/components/ambient_station/.translations/it.json b/homeassistant/components/ambient_station/.translations/it.json
index f87c987a79fba4..b468ba3673cd5d 100644
--- a/homeassistant/components/ambient_station/.translations/it.json
+++ b/homeassistant/components/ambient_station/.translations/it.json
@@ -13,6 +13,7 @@
},
"title": "Inserisci i tuoi dati"
}
- }
+ },
+ "title": "PWS ambientale"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ambient_station/.translations/pl.json b/homeassistant/components/ambient_station/.translations/pl.json
index 2140b4e29fe27c..6ebd0848a632fe 100644
--- a/homeassistant/components/ambient_station/.translations/pl.json
+++ b/homeassistant/components/ambient_station/.translations/pl.json
@@ -11,7 +11,7 @@
"api_key": "Klucz API",
"app_key": "Klucz aplikacji"
},
- "title": "Wprowad\u017a swoje dane"
+ "title": "Wprowad\u017a dane"
}
},
"title": "Ambient PWS"
diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json
index 2c8fc98e24865a..6643faa85bdeb3 100644
--- a/homeassistant/components/androidtv/manifest.json
+++ b/homeassistant/components/androidtv/manifest.json
@@ -3,7 +3,7 @@
"name": "Androidtv",
"documentation": "https://www.home-assistant.io/components/androidtv",
"requirements": [
- "androidtv==0.0.26"
+ "androidtv==0.0.27"
],
"dependencies": [],
"codeowners": ["@JeffLIrion"]
diff --git a/homeassistant/components/arcam_fmj/.translations/de.json b/homeassistant/components/arcam_fmj/.translations/de.json
new file mode 100644
index 00000000000000..b0ad4660d0fef1
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/.translations/de.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Arcam FMJ"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/.translations/es.json b/homeassistant/components/arcam_fmj/.translations/es.json
new file mode 100644
index 00000000000000..b0ad4660d0fef1
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/.translations/es.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Arcam FMJ"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/.translations/fr.json b/homeassistant/components/arcam_fmj/.translations/fr.json
new file mode 100644
index 00000000000000..b0ad4660d0fef1
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/.translations/fr.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Arcam FMJ"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/.translations/it.json b/homeassistant/components/arcam_fmj/.translations/it.json
new file mode 100644
index 00000000000000..b0ad4660d0fef1
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/.translations/it.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Arcam FMJ"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/.translations/ko.json b/homeassistant/components/arcam_fmj/.translations/ko.json
new file mode 100644
index 00000000000000..b0ad4660d0fef1
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/.translations/ko.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Arcam FMJ"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/.translations/lb.json b/homeassistant/components/arcam_fmj/.translations/lb.json
new file mode 100644
index 00000000000000..b0ad4660d0fef1
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/.translations/lb.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Arcam FMJ"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/.translations/sl.json b/homeassistant/components/arcam_fmj/.translations/sl.json
new file mode 100644
index 00000000000000..b0ad4660d0fef1
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/.translations/sl.json
@@ -0,0 +1,5 @@
+{
+ "config": {
+ "title": "Arcam FMJ"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/atome/__init__.py b/homeassistant/components/atome/__init__.py
new file mode 100644
index 00000000000000..6f524606a817bf
--- /dev/null
+++ b/homeassistant/components/atome/__init__.py
@@ -0,0 +1 @@
+"""Support for Atome devices connected to a Linky Energy Meter."""
diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json
new file mode 100644
index 00000000000000..621faba4fc0a33
--- /dev/null
+++ b/homeassistant/components/atome/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "atome",
+ "name": "Atome",
+ "documentation": "https://www.home-assistant.io/components/atome",
+ "dependencies": [],
+ "codeowners": ["@baqs"],
+ "requirements": ["pyatome==0.1.1"]
+}
diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py
new file mode 100644
index 00000000000000..c98b634bb2111d
--- /dev/null
+++ b/homeassistant/components/atome/sensor.py
@@ -0,0 +1,279 @@
+"""Linky Atome."""
+import logging
+from datetime import timedelta
+
+import voluptuous as vol
+from pyatome.client import AtomeClient
+from pyatome.client import PyAtomeError
+
+from homeassistant.const import (
+ CONF_PASSWORD,
+ CONF_USERNAME,
+ CONF_NAME,
+ DEVICE_CLASS_POWER,
+ POWER_WATT,
+ ENERGY_KILO_WATT_HOUR,
+)
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
+import homeassistant.helpers.config_validation as cv
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_NAME = "atome"
+
+LIVE_SCAN_INTERVAL = timedelta(seconds=30)
+DAILY_SCAN_INTERVAL = timedelta(seconds=150)
+WEEKLY_SCAN_INTERVAL = timedelta(hours=1)
+MONTHLY_SCAN_INTERVAL = timedelta(hours=1)
+YEARLY_SCAN_INTERVAL = timedelta(days=1)
+
+LIVE_NAME = "Atome Live Power"
+DAILY_NAME = "Atome Daily"
+WEEKLY_NAME = "Atome Weekly"
+MONTHLY_NAME = "Atome Monthly"
+YEARLY_NAME = "Atome Yearly"
+
+LIVE_TYPE = "live"
+DAILY_TYPE = "day"
+WEEKLY_TYPE = "week"
+MONTHLY_TYPE = "month"
+YEARLY_TYPE = "year"
+
+ICON = "mdi:flash"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ }
+)
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the Atome sensor."""
+ username = config[CONF_USERNAME]
+ password = config[CONF_PASSWORD]
+
+ try:
+ atome_client = AtomeClient(username, password)
+ atome_client.login()
+ except PyAtomeError as exp:
+ _LOGGER.error(exp)
+ return
+
+ data = AtomeData(atome_client)
+
+ sensors = []
+ sensors.append(AtomeSensor(data, LIVE_NAME, LIVE_TYPE))
+ sensors.append(AtomeSensor(data, DAILY_NAME, DAILY_TYPE))
+ sensors.append(AtomeSensor(data, WEEKLY_NAME, WEEKLY_TYPE))
+ sensors.append(AtomeSensor(data, MONTHLY_NAME, MONTHLY_TYPE))
+ sensors.append(AtomeSensor(data, YEARLY_NAME, YEARLY_TYPE))
+
+ add_entities(sensors, True)
+
+
+class AtomeData:
+ """Stores data retrieved from Neurio sensor."""
+
+ def __init__(self, client: AtomeClient):
+ """Initialize the data."""
+ self.atome_client = client
+ self._live_power = None
+ self._subscribed_power = None
+ self._is_connected = None
+ self._day_usage = None
+ self._day_price = None
+ self._week_usage = None
+ self._week_price = None
+ self._month_usage = None
+ self._month_price = None
+ self._year_usage = None
+ self._year_price = None
+
+ @property
+ def live_power(self):
+ """Return latest active power value."""
+ return self._live_power
+
+ @property
+ def subscribed_power(self):
+ """Return latest active power value."""
+ return self._subscribed_power
+
+ @property
+ def is_connected(self):
+ """Return latest active power value."""
+ return self._is_connected
+
+ @Throttle(LIVE_SCAN_INTERVAL)
+ def update_live_usage(self):
+ """Return current power value."""
+ try:
+ values = self.atome_client.get_live()
+ self._live_power = values["last"]
+ self._subscribed_power = values["subscribed"]
+ self._is_connected = values["isConnected"]
+ _LOGGER.debug(
+ "Updating Atome live data. Got: %d, isConnected: %s, subscribed: %d",
+ self._live_power,
+ self._is_connected,
+ self._subscribed_power,
+ )
+
+ except KeyError as error:
+ _LOGGER.error("Missing last value in values: %s: %s", values, error)
+
+ @property
+ def day_usage(self):
+ """Return latest daily usage value."""
+ return self._day_usage
+
+ @property
+ def day_price(self):
+ """Return latest daily usage value."""
+ return self._day_price
+
+ @Throttle(DAILY_SCAN_INTERVAL)
+ def update_day_usage(self):
+ """Return current daily power usage."""
+ try:
+ values = self.atome_client.get_consumption(DAILY_TYPE)
+ self._day_usage = values["total"] / 1000
+ self._day_price = values["price"]
+ _LOGGER.debug("Updating Atome daily data. Got: %d.", self._day_usage)
+
+ except KeyError as error:
+ _LOGGER.error("Missing last value in values: %s: %s", values, error)
+
+ @property
+ def week_usage(self):
+ """Return latest weekly usage value."""
+ return self._week_usage
+
+ @property
+ def week_price(self):
+ """Return latest weekly usage value."""
+ return self._week_price
+
+ @Throttle(WEEKLY_SCAN_INTERVAL)
+ def update_week_usage(self):
+ """Return current weekly power usage."""
+ try:
+ values = self.atome_client.get_consumption(WEEKLY_TYPE)
+ self._week_usage = values["total"] / 1000
+ self._week_price = values["price"]
+ _LOGGER.debug("Updating Atome weekly data. Got: %d.", self._week_usage)
+
+ except KeyError as error:
+ _LOGGER.error("Missing last value in values: %s: %s", values, error)
+
+ @property
+ def month_usage(self):
+ """Return latest monthly usage value."""
+ return self._month_usage
+
+ @property
+ def month_price(self):
+ """Return latest monthly usage value."""
+ return self._month_price
+
+ @Throttle(MONTHLY_SCAN_INTERVAL)
+ def update_month_usage(self):
+ """Return current monthly power usage."""
+ try:
+ values = self.atome_client.get_consumption(MONTHLY_TYPE)
+ self._month_usage = values["total"] / 1000
+ self._month_price = values["price"]
+ _LOGGER.debug("Updating Atome monthly data. Got: %d.", self._month_usage)
+
+ except KeyError as error:
+ _LOGGER.error("Missing last value in values: %s: %s", values, error)
+
+ @property
+ def year_usage(self):
+ """Return latest yearly usage value."""
+ return self._year_usage
+
+ @property
+ def year_price(self):
+ """Return latest yearly usage value."""
+ return self._year_price
+
+ @Throttle(YEARLY_SCAN_INTERVAL)
+ def update_year_usage(self):
+ """Return current yearly power usage."""
+ try:
+ values = self.atome_client.get_consumption(YEARLY_TYPE)
+ self._year_usage = values["total"] / 1000
+ self._year_price = values["price"]
+ _LOGGER.debug("Updating Atome yearly data. Got: %d.", self._year_usage)
+
+ except KeyError as error:
+ _LOGGER.error("Missing last value in values: %s: %s", values, error)
+
+
+class AtomeSensor(Entity):
+ """Representation of a sensor entity for Atome."""
+
+ def __init__(self, data, name, sensor_type):
+ """Initialize the sensor."""
+ self._name = name
+ self._data = data
+ self._state = None
+ self._attributes = {}
+
+ self._sensor_type = sensor_type
+
+ if sensor_type == LIVE_TYPE:
+ self._unit_of_measurement = POWER_WATT
+ else:
+ self._unit_of_measurement = ENERGY_KILO_WATT_HOUR
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ return self._attributes
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self._unit_of_measurement
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend, if any."""
+ return ICON
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ return DEVICE_CLASS_POWER
+
+ def update(self):
+ """Update device state."""
+ update_function = getattr(self._data, f"update_{self._sensor_type}_usage")
+ update_function()
+
+ if self._sensor_type == LIVE_TYPE:
+ self._state = self._data.live_power
+ self._attributes["subscribed_power"] = self._data.subscribed_power
+ self._attributes["is_connected"] = self._data.is_connected
+ else:
+ self._state = getattr(self._data, f"{self._sensor_type}_usage")
+ self._attributes["price"] = getattr(
+ self._data, f"{self._sensor_type}_price"
+ )
diff --git a/homeassistant/components/auth/.translations/it.json b/homeassistant/components/auth/.translations/it.json
index be06f0209c409d..dbfe4acd6156ab 100644
--- a/homeassistant/components/auth/.translations/it.json
+++ b/homeassistant/components/auth/.translations/it.json
@@ -10,7 +10,7 @@
"step": {
"init": {
"description": "Selezionare uno dei servizi di notifica:",
- "title": "Imposta la password one-time fornita dal componente di notifica"
+ "title": "Imposta la password monouso fornita dal componente di notifica"
},
"setup": {
"description": "\u00c8 stata inviata una password monouso tramite **notify.{notify_service}**. Per favore, inseriscila qui sotto:",
@@ -25,7 +25,7 @@
},
"step": {
"init": {
- "description": "Per attivare l'autenticazione a due fattori utilizzando password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Dopo aver scansionato il codice, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con codice ** ` {code} ` **.",
+ "description": "Per attivare l'autenticazione a due fattori utilizzando le password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \nDopo la scansione, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con il codice ** ` {code} ` **.",
"title": "Imposta l'autenticazione a due fattori usando TOTP"
}
},
diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json
index 6c2e8988d83c58..1cb70519b20f45 100644
--- a/homeassistant/components/auth/.translations/ko.json
+++ b/homeassistant/components/auth/.translations/ko.json
@@ -25,7 +25,7 @@
},
"step": {
"init": {
- "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.",
+ "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [\uad6c\uae00 OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.",
"title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131"
}
},
diff --git a/homeassistant/components/auth/.translations/pl.json b/homeassistant/components/auth/.translations/pl.json
index f0e9f7b71ea449..78610a5324fe39 100644
--- a/homeassistant/components/auth/.translations/pl.json
+++ b/homeassistant/components/auth/.translations/pl.json
@@ -13,7 +13,7 @@
"title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144"
},
"setup": {
- "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez **notify.{notify_service}**. Wpisz je poni\u017cej:",
+ "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez **notify.{notify_service}**. Wprowad\u017a je poni\u017cej:",
"title": "Sprawd\u017a konfiguracj\u0119"
}
},
diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py
index 1cffd361b19210..f0529f126f1e73 100644
--- a/homeassistant/components/automation/__init__.py
+++ b/homeassistant/components/automation/__init__.py
@@ -3,9 +3,13 @@
from functools import partial
import importlib
import logging
+from typing import Any
import voluptuous as vol
+from homeassistant.components.device_automation.exceptions import (
+ InvalidDeviceAutomationConfig,
+)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_NAME,
@@ -31,7 +35,7 @@
from homeassistant.util.dt import parse_datetime, utcnow
-# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
+# mypy: allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
DOMAIN = "automation"
@@ -40,6 +44,7 @@
GROUP_NAME_ALL_AUTOMATIONS = "all automations"
CONF_ALIAS = "alias"
+CONF_DESCRIPTION = "description"
CONF_HIDE_ENTITY = "hide_entity"
CONF_CONDITION = "condition"
@@ -92,6 +97,7 @@ def _platform_validator(config):
# str on purpose
CONF_ID: str,
CONF_ALIAS: cv.string,
+ vol.Optional(CONF_DESCRIPTION): cv.string,
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
@@ -276,11 +282,11 @@ async def async_added_to_hass(self) -> None:
if enable_automation:
await self.async_enable()
- async def async_turn_on(self, **kwargs) -> None:
+ async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on and update the state."""
await self.async_enable()
- async def async_turn_off(self, **kwargs) -> None:
+ async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
await self.async_disable()
@@ -386,7 +392,7 @@ async def _async_process_config(hass, config, component):
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name)
if CONF_CONDITION in config_block:
- cond_func = _async_process_if(hass, config, config_block)
+ cond_func = await _async_process_if(hass, config, config_block)
if cond_func is None:
continue
@@ -437,14 +443,14 @@ async def action(entity_id, variables, context):
return action
-def _async_process_if(hass, config, p_config):
+async def _async_process_if(hass, config, p_config):
"""Process if checks."""
if_configs = p_config.get(CONF_CONDITION)
checks = []
for if_config in if_configs:
try:
- checks.append(condition.async_from_config(if_config, False))
+ checks.append(await condition.async_from_config(hass, if_config, False))
except HomeAssistantError as ex:
_LOGGER.warning("Invalid condition: %s", ex)
return None
@@ -467,7 +473,10 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action):
for conf in trigger_configs:
platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__)
- remove = await platform.async_trigger(hass, conf, action, info)
+ try:
+ remove = await platform.async_trigger(hass, conf, action, info)
+ except InvalidDeviceAutomationConfig:
+ remove = False
if not remove:
_LOGGER.error("Error setting up trigger %s", name)
diff --git a/homeassistant/components/axis/.translations/es.json b/homeassistant/components/axis/.translations/es.json
index 817737eee04d88..d29481a3be96f8 100644
--- a/homeassistant/components/axis/.translations/es.json
+++ b/homeassistant/components/axis/.translations/es.json
@@ -8,6 +8,7 @@
},
"error": {
"already_configured": "El dispositivo ya est\u00e1 configurado",
+ "already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en curso.",
"device_unavailable": "El dispositivo no est\u00e1 disponible",
"faulty_credentials": "Credenciales de usuario incorrectas"
},
diff --git a/homeassistant/components/axis/.translations/it.json b/homeassistant/components/axis/.translations/it.json
index 2498c28ec33aca..e979af0883656f 100644
--- a/homeassistant/components/axis/.translations/it.json
+++ b/homeassistant/components/axis/.translations/it.json
@@ -3,6 +3,7 @@
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
"bad_config_file": "Dati errati dal file di configurazione",
+ "link_local_address": "Gli indirizzi locali di collegamento non sono supportati",
"not_axis_device": "Il dispositivo rilevato non \u00e8 un dispositivo Axis"
},
"error": {
diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py
index b565d05685f979..89449aeab4592f 100644
--- a/homeassistant/components/bbox/device_tracker.py
+++ b/homeassistant/components/bbox/device_tracker.py
@@ -2,6 +2,7 @@
from collections import namedtuple
from datetime import timedelta
import logging
+from typing import List
import voluptuous as vol
@@ -41,12 +42,11 @@ class BboxDeviceScanner(DeviceScanner):
def __init__(self, config):
"""Get host from config."""
- from typing import List # noqa: pylint: disable=unused-import
self.host = config[CONF_HOST]
"""Initialize the scanner."""
- self.last_results = [] # type: List[Device]
+ self.last_results: List[Device] = []
self.success_init = self._update_info()
_LOGGER.info("Scanner initialized")
diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py
index b59b166e41f145..ba38f8d2607dc3 100644
--- a/homeassistant/components/bbox/sensor.py
+++ b/homeassistant/components/bbox/sensor.py
@@ -13,7 +13,7 @@
_LOGGER = logging.getLogger(__name__)
-BANDWIDTH_MEGABITS_SECONDS = "Mb/s" # type: str
+BANDWIDTH_MEGABITS_SECONDS = "Mb/s"
ATTRIBUTION = "Powered by Bouygues Telecom"
diff --git a/homeassistant/components/binary_sensor/.translations/en.json b/homeassistant/components/binary_sensor/.translations/en.json
new file mode 100644
index 00000000000000..6379df936b898d
--- /dev/null
+++ b/homeassistant/components/binary_sensor/.translations/en.json
@@ -0,0 +1,92 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_bat_low": "{entity_name} battery is low",
+ "is_cold": "{entity_name} is cold",
+ "is_connected": "{entity_name} is connected",
+ "is_gas": "{entity_name} is detecting gas",
+ "is_hot": "{entity_name} is hot",
+ "is_light": "{entity_name} is detecting light",
+ "is_locked": "{entity_name} is locked",
+ "is_moist": "{entity_name} is moist",
+ "is_motion": "{entity_name} is detecting motion",
+ "is_moving": "{entity_name} is moving",
+ "is_no_gas": "{entity_name} is not detecting gas",
+ "is_no_light": "{entity_name} is not detecting light",
+ "is_no_motion": "{entity_name} is not detecting motion",
+ "is_no_problem": "{entity_name} is not detecting problem",
+ "is_no_smoke": "{entity_name} is not detecting smoke",
+ "is_no_sound": "{entity_name} is not detecting sound",
+ "is_no_vibration": "{entity_name} is not detecting vibration",
+ "is_not_bat_low": "{entity_name} battery is normal",
+ "is_not_cold": "{entity_name} is not cold",
+ "is_not_connected": "{entity_name} is disconnected",
+ "is_not_hot": "{entity_name} is not hot",
+ "is_not_locked": "{entity_name} is unlocked",
+ "is_not_moist": "{entity_name} is dry",
+ "is_not_moving": "{entity_name} is not moving",
+ "is_not_occupied": "{entity_name} is not occupied",
+ "is_not_open": "{entity_name} is closed",
+ "is_not_plugged_in": "{entity_name} is unplugged",
+ "is_not_powered": "{entity_name} is not powered",
+ "is_not_present": "{entity_name} is not present",
+ "is_not_unsafe": "{entity_name} is safe",
+ "is_occupied": "{entity_name} is occupied",
+ "is_off": "{entity_name} is off",
+ "is_on": "{entity_name} is on",
+ "is_open": "{entity_name} is open",
+ "is_plugged_in": "{entity_name} is plugged in",
+ "is_powered": "{entity_name} is powered",
+ "is_present": "{entity_name} is present",
+ "is_problem": "{entity_name} is detecting problem",
+ "is_smoke": "{entity_name} is detecting smoke",
+ "is_sound": "{entity_name} is detecting sound",
+ "is_unsafe": "{entity_name} is unsafe",
+ "is_vibration": "{entity_name} is detecting vibration"
+ },
+ "trigger_type": {
+ "bat_low": "{entity_name} battery low",
+ "closed": "{entity_name} closed",
+ "cold": "{entity_name} became cold",
+ "connected": "{entity_name} connected",
+ "gas": "{entity_name} started detecting gas",
+ "hot": "{entity_name} became hot",
+ "light": "{entity_name} started detecting light",
+ "locked": "{entity_name} locked",
+ "moist\u00a7": "{entity_name} became moist",
+ "motion": "{entity_name} started detecting motion",
+ "moving": "{entity_name} started moving",
+ "no_gas": "{entity_name} stopped detecting gas",
+ "no_light": "{entity_name} stopped detecting light",
+ "no_motion": "{entity_name} stopped detecting motion",
+ "no_problem": "{entity_name} stopped detecting problem",
+ "no_smoke": "{entity_name} stopped detecting smoke",
+ "no_sound": "{entity_name} stopped detecting sound",
+ "no_vibration": "{entity_name} stopped detecting vibration",
+ "not_bat_low": "{entity_name} battery normal",
+ "not_cold": "{entity_name} became not cold",
+ "not_connected": "{entity_name} disconnected",
+ "not_hot": "{entity_name} became not hot",
+ "not_locked": "{entity_name} unlocked",
+ "not_moist": "{entity_name} became dry",
+ "not_moving": "{entity_name} stopped moving",
+ "not_occupied": "{entity_name} became not occupied",
+ "not_plugged_in": "{entity_name} unplugged",
+ "not_powered": "{entity_name} not powered",
+ "not_present": "{entity_name} not present",
+ "not_unsafe": "{entity_name} became safe",
+ "occupied": "{entity_name} became occupied",
+ "opened": "{entity_name} opened",
+ "plugged_in": "{entity_name} plugged in",
+ "powered": "{entity_name} powered",
+ "present": "{entity_name} present",
+ "problem": "{entity_name} started detecting problem",
+ "smoke": "{entity_name} started detecting smoke",
+ "sound": "{entity_name} started detecting sound",
+ "turned_off": "{entity_name} turned off",
+ "turned_on": "{entity_name} turned on",
+ "unsafe": "{entity_name} became unsafe",
+ "vibration": "{entity_name} started detecting vibration"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/binary_sensor/.translations/no.json b/homeassistant/components/binary_sensor/.translations/no.json
new file mode 100644
index 00000000000000..5a1916bce59c33
--- /dev/null
+++ b/homeassistant/components/binary_sensor/.translations/no.json
@@ -0,0 +1,92 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_bat_low": "{entity_name} batteriniv\u00e5et er lavt",
+ "is_cold": "{entity_name} er kald",
+ "is_connected": "{entity_name} er tilkoblet",
+ "is_gas": "{entity_name} registrerer gass",
+ "is_hot": "{entity_name} er varm",
+ "is_light": "{entity_name} registrerer lys",
+ "is_locked": "{entity_name} er l\u00e5st",
+ "is_moist": "{entity_name} er fuktig",
+ "is_motion": "{entity_name} registrerer bevegelse",
+ "is_moving": "{entity_name} er i bevegelse",
+ "is_no_gas": "{entity_name} registrerer ikke gass",
+ "is_no_light": "{entity_name} registrerer ikke lys",
+ "is_no_motion": "{entity_name} registrerer ikke bevegelse",
+ "is_no_problem": "{entity_name} registrerer ikke et problem",
+ "is_no_smoke": "{entity_name} registrerer ikke r\u00f8yk",
+ "is_no_sound": "{entity_name} registrerer ikke lyd",
+ "is_no_vibration": "{entity_name} registrerer ikke bevegelse",
+ "is_not_bat_low": "{entity_name} batteri er normalt",
+ "is_not_cold": "{entity_name} er ikke kald",
+ "is_not_connected": "{entity_name} er frakoblet",
+ "is_not_hot": "{entity_name} er ikke varm",
+ "is_not_locked": "{entity_name} er ul\u00e5st",
+ "is_not_moist": "{entity_name} er t\u00f8rr",
+ "is_not_moving": "{entity_name} er ikke i bevegelse",
+ "is_not_occupied": "{entity_name} er ledig",
+ "is_not_open": "{entity_name} er lukket",
+ "is_not_plugged_in": "{entity_name} er koblet fra",
+ "is_not_powered": "{entity_name} er spenningsl\u00f8s",
+ "is_not_present": "{entity_name} er ikke tilstede",
+ "is_not_unsafe": "{entity_name} er trygg",
+ "is_occupied": "{entity_name} er opptatt",
+ "is_off": "{entity_name} er sl\u00e5tt av",
+ "is_on": "{entity_name} er sl\u00e5tt p\u00e5",
+ "is_open": "{entity_name} er \u00e5pen",
+ "is_plugged_in": "{entity_name} er koblet til",
+ "is_powered": "{entity_name} er spenningssatt",
+ "is_present": "{entity_name} er tilstede",
+ "is_problem": "{entity_name} registrerer et problem",
+ "is_smoke": "{entity_name} registrerer r\u00f8yk",
+ "is_sound": "{entity_name} registrerer lyd",
+ "is_unsafe": "{entity_name} er utrygg",
+ "is_vibration": "{entity_name} registrerer vibrasjon"
+ },
+ "trigger_type": {
+ "bat_low": "{entity_name} lavt batteri",
+ "closed": "{entity_name} stengt",
+ "cold": "{entity_name} ble kald",
+ "connected": "{entity_name} tilkoblet",
+ "gas": "{entity_name} begynte \u00e5 registrere gass",
+ "hot": "{entity_name} ble varm",
+ "light": "{entity_name} begynte \u00e5 registrere lys",
+ "locked": "{entity_name} l\u00e5st",
+ "moist\u00a7": "{entity_name} ble fuktig",
+ "motion": "{entity_name} begynte \u00e5 registrere bevegelse",
+ "moving": "{entity_name} begynte \u00e5 bevege seg",
+ "no_gas": "{entity_name} sluttet \u00e5 registrere gass",
+ "no_light": "{entity_name} sluttet \u00e5 registrere lys",
+ "no_motion": "{entity_name} sluttet \u00e5 registrere bevegelse",
+ "no_problem": "{entity_name} sluttet \u00e5 registrere problem",
+ "no_smoke": "{entity_name} sluttet \u00e5 registrere r\u00f8yk",
+ "no_sound": "{entity_name} sluttet \u00e5 registrere lyd",
+ "no_vibration": "{entity_name} sluttet \u00e5 registrere vibrasjon",
+ "not_bat_low": "{entity_name} batteri normalt",
+ "not_cold": "{entity_name} ble ikke lenger kald",
+ "not_connected": "{entity_name} koblet fra",
+ "not_hot": "{entity_name} ble ikke lenger varm",
+ "not_locked": "{entity_name} l\u00e5st opp",
+ "not_moist": "{entity_name} ble t\u00f8rr",
+ "not_moving": "{entity_name} sluttet \u00e5 bevege seg",
+ "not_occupied": "{entity_name} ble ledig",
+ "not_plugged_in": "{entity_name} koblet fra",
+ "not_powered": "{entity_name} spenningsl\u00f8s",
+ "not_present": "{entity_name} ikke til stede",
+ "not_unsafe": "{entity_name} ble trygg",
+ "occupied": "{entity_name} ble opptatt",
+ "opened": "{entity_name} \u00e5pnet",
+ "plugged_in": "{entity_name} koblet til",
+ "powered": "{entity_name} spenningssatt",
+ "present": "{entity_name} tilstede",
+ "problem": "{entity_name} begynte \u00e5 registrere et problem",
+ "smoke": "{entity_name} begynte \u00e5 registrere r\u00f8yk",
+ "sound": "{entity_name} begynte \u00e5 registrere lyd",
+ "turned_off": "{entity_name} sl\u00e5tt av",
+ "turned_on": "{entity_name} sl\u00e5tt p\u00e5",
+ "unsafe": "{entity_name} ble usikker",
+ "vibration": "{entity_name} begynte \u00e5 oppdage vibrasjon"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/binary_sensor/device_automation.py b/homeassistant/components/binary_sensor/device_automation.py
new file mode 100644
index 00000000000000..c609c2eb5da4c8
--- /dev/null
+++ b/homeassistant/components/binary_sensor/device_automation.py
@@ -0,0 +1,423 @@
+"""Provides device automations for lights."""
+import voluptuous as vol
+
+import homeassistant.components.automation.state as state
+from homeassistant.components.device_automation.const import (
+ CONF_IS_OFF,
+ CONF_IS_ON,
+ CONF_TURNED_OFF,
+ CONF_TURNED_ON,
+)
+from homeassistant.const import (
+ ATTR_DEVICE_CLASS,
+ CONF_CONDITION,
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ CONF_ENTITY_ID,
+ CONF_PLATFORM,
+ CONF_TYPE,
+)
+from homeassistant.core import split_entity_id
+from homeassistant.helpers.entity_registry import async_entries_for_device
+from homeassistant.helpers import condition, config_validation as cv
+
+from . import (
+ DOMAIN,
+ DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_COLD,
+ DEVICE_CLASS_CONNECTIVITY,
+ DEVICE_CLASS_DOOR,
+ DEVICE_CLASS_GARAGE_DOOR,
+ DEVICE_CLASS_GAS,
+ DEVICE_CLASS_HEAT,
+ DEVICE_CLASS_LIGHT,
+ DEVICE_CLASS_LOCK,
+ DEVICE_CLASS_MOISTURE,
+ DEVICE_CLASS_MOTION,
+ DEVICE_CLASS_MOVING,
+ DEVICE_CLASS_OCCUPANCY,
+ DEVICE_CLASS_OPENING,
+ DEVICE_CLASS_PLUG,
+ DEVICE_CLASS_POWER,
+ DEVICE_CLASS_PRESENCE,
+ DEVICE_CLASS_PROBLEM,
+ DEVICE_CLASS_SAFETY,
+ DEVICE_CLASS_SMOKE,
+ DEVICE_CLASS_SOUND,
+ DEVICE_CLASS_VIBRATION,
+ DEVICE_CLASS_WINDOW,
+)
+
+
+# mypy: allow-untyped-defs, no-check-untyped-defs
+
+DEVICE_CLASS_NONE = "none"
+
+CONF_IS_BAT_LOW = "is_bat_low"
+CONF_IS_NOT_BAT_LOW = "is_not_bat_low"
+CONF_IS_COLD = "is_cold"
+CONF_IS_NOT_COLD = "is_not_cold"
+CONF_IS_CONNECTED = "is_connected"
+CONF_IS_NOT_CONNECTED = "is_not_connected"
+CONF_IS_GAS = "is_gas"
+CONF_IS_NO_GAS = "is_no_gas"
+CONF_IS_HOT = "is_hot"
+CONF_IS_NOT_HOT = "is_not_hot"
+CONF_IS_LIGHT = "is_light"
+CONF_IS_NO_LIGHT = "is_no_light"
+CONF_IS_LOCKED = "is_locked"
+CONF_IS_NOT_LOCKED = "is_not_locked"
+CONF_IS_MOIST = "is_moist"
+CONF_IS_NOT_MOIST = "is_not_moist"
+CONF_IS_MOTION = "is_motion"
+CONF_IS_NO_MOTION = "is_no_motion"
+CONF_IS_MOVING = "is_moving"
+CONF_IS_NOT_MOVING = "is_not_moving"
+CONF_IS_OCCUPIED = "is_occupied"
+CONF_IS_NOT_OCCUPIED = "is_not_occupied"
+CONF_IS_PLUGGED_IN = "is_plugged_in"
+CONF_IS_NOT_PLUGGED_IN = "is_not_plugged_in"
+CONF_IS_POWERED = "is_powered"
+CONF_IS_NOT_POWERED = "is_not_powered"
+CONF_IS_PRESENT = "is_present"
+CONF_IS_NOT_PRESENT = "is_not_present"
+CONF_IS_PROBLEM = "is_problem"
+CONF_IS_NO_PROBLEM = "is_no_problem"
+CONF_IS_UNSAFE = "is_unsafe"
+CONF_IS_NOT_UNSAFE = "is_not_unsafe"
+CONF_IS_SMOKE = "is_smoke"
+CONF_IS_NO_SMOKE = "is_no_smoke"
+CONF_IS_SOUND = "is_sound"
+CONF_IS_NO_SOUND = "is_no_sound"
+CONF_IS_VIBRATION = "is_vibration"
+CONF_IS_NO_VIBRATION = "is_no_vibration"
+CONF_IS_OPEN = "is_open"
+CONF_IS_NOT_OPEN = "is_not_open"
+
+CONF_BAT_LOW = "bat_low"
+CONF_NOT_BAT_LOW = "not_bat_low"
+CONF_COLD = "cold"
+CONF_NOT_COLD = "not_cold"
+CONF_CONNECTED = "connected"
+CONF_NOT_CONNECTED = "not_connected"
+CONF_GAS = "gas"
+CONF_NO_GAS = "no_gas"
+CONF_HOT = "hot"
+CONF_NOT_HOT = "not_hot"
+CONF_LIGHT = "light"
+CONF_NO_LIGHT = "no_light"
+CONF_LOCKED = "locked"
+CONF_NOT_LOCKED = "not_locked"
+CONF_MOIST = "moist"
+CONF_NOT_MOIST = "not_moist"
+CONF_MOTION = "motion"
+CONF_NO_MOTION = "no_motion"
+CONF_MOVING = "moving"
+CONF_NOT_MOVING = "not_moving"
+CONF_OCCUPIED = "occupied"
+CONF_NOT_OCCUPIED = "not_occupied"
+CONF_PLUGGED_IN = "plugged_in"
+CONF_NOT_PLUGGED_IN = "not_plugged_in"
+CONF_POWERED = "powered"
+CONF_NOT_POWERED = "not_powered"
+CONF_PRESENT = "present"
+CONF_NOT_PRESENT = "not_present"
+CONF_PROBLEM = "problem"
+CONF_NO_PROBLEM = "no_problem"
+CONF_UNSAFE = "unsafe"
+CONF_NOT_UNSAFE = "not_unsafe"
+CONF_SMOKE = "smoke"
+CONF_NO_SMOKE = "no_smoke"
+CONF_SOUND = "sound"
+CONF_NO_SOUND = "no_sound"
+CONF_VIBRATION = "vibration"
+CONF_NO_VIBRATION = "no_vibration"
+CONF_OPEN = "open"
+CONF_NOT_OPEN = "not_open"
+
+IS_ON = [
+ CONF_IS_BAT_LOW,
+ CONF_IS_COLD,
+ CONF_IS_CONNECTED,
+ CONF_IS_GAS,
+ CONF_IS_HOT,
+ CONF_IS_LIGHT,
+ CONF_IS_LOCKED,
+ CONF_IS_MOIST,
+ CONF_IS_MOTION,
+ CONF_IS_MOVING,
+ CONF_IS_OCCUPIED,
+ CONF_IS_OPEN,
+ CONF_IS_PLUGGED_IN,
+ CONF_IS_POWERED,
+ CONF_IS_PRESENT,
+ CONF_IS_PROBLEM,
+ CONF_IS_SMOKE,
+ CONF_IS_SOUND,
+ CONF_IS_UNSAFE,
+ CONF_IS_VIBRATION,
+ CONF_IS_ON,
+]
+
+IS_OFF = [
+ CONF_IS_NOT_BAT_LOW,
+ CONF_IS_NOT_COLD,
+ CONF_IS_NOT_CONNECTED,
+ CONF_IS_NOT_HOT,
+ CONF_IS_NOT_LOCKED,
+ CONF_IS_NOT_MOIST,
+ CONF_IS_NOT_MOVING,
+ CONF_IS_NOT_OCCUPIED,
+ CONF_IS_NOT_OPEN,
+ CONF_IS_NOT_PLUGGED_IN,
+ CONF_IS_NOT_POWERED,
+ CONF_IS_NOT_PRESENT,
+ CONF_IS_NOT_UNSAFE,
+ CONF_IS_NO_GAS,
+ CONF_IS_NO_LIGHT,
+ CONF_IS_NO_MOTION,
+ CONF_IS_NO_PROBLEM,
+ CONF_IS_NO_SMOKE,
+ CONF_IS_NO_SOUND,
+ CONF_IS_NO_VIBRATION,
+ CONF_IS_OFF,
+]
+
+TURNED_ON = [
+ CONF_BAT_LOW,
+ CONF_COLD,
+ CONF_CONNECTED,
+ CONF_GAS,
+ CONF_HOT,
+ CONF_LIGHT,
+ CONF_LOCKED,
+ CONF_MOIST,
+ CONF_MOTION,
+ CONF_MOVING,
+ CONF_OCCUPIED,
+ CONF_OPEN,
+ CONF_PLUGGED_IN,
+ CONF_POWERED,
+ CONF_PRESENT,
+ CONF_PROBLEM,
+ CONF_SMOKE,
+ CONF_SOUND,
+ CONF_UNSAFE,
+ CONF_VIBRATION,
+ CONF_TURNED_ON,
+]
+
+TURNED_OFF = [
+ CONF_NOT_BAT_LOW,
+ CONF_NOT_COLD,
+ CONF_NOT_CONNECTED,
+ CONF_NOT_HOT,
+ CONF_NOT_LOCKED,
+ CONF_NOT_MOIST,
+ CONF_NOT_MOVING,
+ CONF_NOT_OCCUPIED,
+ CONF_NOT_OPEN,
+ CONF_NOT_PLUGGED_IN,
+ CONF_NOT_POWERED,
+ CONF_NOT_PRESENT,
+ CONF_NOT_UNSAFE,
+ CONF_NO_GAS,
+ CONF_NO_LIGHT,
+ CONF_NO_MOTION,
+ CONF_NO_PROBLEM,
+ CONF_NO_SMOKE,
+ CONF_NO_SOUND,
+ CONF_NO_VIBRATION,
+ CONF_TURNED_OFF,
+]
+
+ENTITY_CONDITIONS = {
+ DEVICE_CLASS_BATTERY: [
+ {CONF_TYPE: CONF_IS_BAT_LOW},
+ {CONF_TYPE: CONF_IS_NOT_BAT_LOW},
+ ],
+ DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_IS_COLD}, {CONF_TYPE: CONF_IS_NOT_COLD}],
+ DEVICE_CLASS_CONNECTIVITY: [
+ {CONF_TYPE: CONF_IS_CONNECTED},
+ {CONF_TYPE: CONF_IS_NOT_CONNECTED},
+ ],
+ DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}],
+ DEVICE_CLASS_GARAGE_DOOR: [
+ {CONF_TYPE: CONF_IS_OPEN},
+ {CONF_TYPE: CONF_IS_NOT_OPEN},
+ ],
+ DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}, {CONF_TYPE: CONF_IS_NO_GAS}],
+ DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_IS_HOT}, {CONF_TYPE: CONF_IS_NOT_HOT}],
+ DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_IS_LIGHT}, {CONF_TYPE: CONF_IS_NO_LIGHT}],
+ DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_IS_LOCKED}, {CONF_TYPE: CONF_IS_NOT_LOCKED}],
+ DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_IS_MOIST}, {CONF_TYPE: CONF_IS_NOT_MOIST}],
+ DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_IS_MOTION}, {CONF_TYPE: CONF_IS_NO_MOTION}],
+ DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_IS_MOVING}, {CONF_TYPE: CONF_IS_NOT_MOVING}],
+ DEVICE_CLASS_OCCUPANCY: [
+ {CONF_TYPE: CONF_IS_OCCUPIED},
+ {CONF_TYPE: CONF_IS_NOT_OCCUPIED},
+ ],
+ DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}],
+ DEVICE_CLASS_PLUG: [
+ {CONF_TYPE: CONF_IS_PLUGGED_IN},
+ {CONF_TYPE: CONF_IS_NOT_PLUGGED_IN},
+ ],
+ DEVICE_CLASS_POWER: [
+ {CONF_TYPE: CONF_IS_POWERED},
+ {CONF_TYPE: CONF_IS_NOT_POWERED},
+ ],
+ DEVICE_CLASS_PRESENCE: [
+ {CONF_TYPE: CONF_IS_PRESENT},
+ {CONF_TYPE: CONF_IS_NOT_PRESENT},
+ ],
+ DEVICE_CLASS_PROBLEM: [
+ {CONF_TYPE: CONF_IS_PROBLEM},
+ {CONF_TYPE: CONF_IS_NO_PROBLEM},
+ ],
+ DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}],
+ DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}],
+ DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}],
+ DEVICE_CLASS_VIBRATION: [
+ {CONF_TYPE: CONF_IS_VIBRATION},
+ {CONF_TYPE: CONF_IS_NO_VIBRATION},
+ ],
+ DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}],
+ DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_ON}, {CONF_TYPE: CONF_IS_OFF}],
+}
+
+ENTITY_TRIGGERS = {
+ DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BAT_LOW}, {CONF_TYPE: CONF_NOT_BAT_LOW}],
+ DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_COLD}, {CONF_TYPE: CONF_NOT_COLD}],
+ DEVICE_CLASS_CONNECTIVITY: [
+ {CONF_TYPE: CONF_CONNECTED},
+ {CONF_TYPE: CONF_NOT_CONNECTED},
+ ],
+ DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}],
+ DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}],
+ DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}],
+ DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}],
+ DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}],
+ DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_LOCKED}, {CONF_TYPE: CONF_NOT_LOCKED}],
+ DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_MOIST}, {CONF_TYPE: CONF_NOT_MOIST}],
+ DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_MOTION}, {CONF_TYPE: CONF_NO_MOTION}],
+ DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_MOVING}, {CONF_TYPE: CONF_NOT_MOVING}],
+ DEVICE_CLASS_OCCUPANCY: [
+ {CONF_TYPE: CONF_OCCUPIED},
+ {CONF_TYPE: CONF_NOT_OCCUPIED},
+ ],
+ DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}],
+ DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}],
+ DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}],
+ DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}],
+ DEVICE_CLASS_PROBLEM: [{CONF_TYPE: CONF_PROBLEM}, {CONF_TYPE: CONF_NO_PROBLEM}],
+ DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}],
+ DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}],
+ DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}],
+ DEVICE_CLASS_VIBRATION: [
+ {CONF_TYPE: CONF_VIBRATION},
+ {CONF_TYPE: CONF_NO_VIBRATION},
+ ],
+ DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}],
+ DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}],
+}
+
+CONDITION_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_CONDITION): "device",
+ vol.Required(CONF_DEVICE_ID): str,
+ vol.Required(CONF_DOMAIN): DOMAIN,
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON),
+ }
+)
+
+TRIGGER_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_PLATFORM): "device",
+ vol.Required(CONF_DEVICE_ID): str,
+ vol.Required(CONF_DOMAIN): DOMAIN,
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON),
+ }
+)
+
+
+def async_condition_from_config(config, config_validation):
+ """Evaluate state based on configuration."""
+ config = CONDITION_SCHEMA(config)
+ condition_type = config[CONF_TYPE]
+ if condition_type in IS_ON:
+ stat = "on"
+ else:
+ stat = "off"
+ state_config = {
+ condition.CONF_CONDITION: "state",
+ condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
+ condition.CONF_STATE: stat,
+ }
+
+ return condition.state_from_config(state_config, config_validation)
+
+
+async def async_trigger(hass, config, action, automation_info):
+ """Listen for state changes based on configuration."""
+ config = TRIGGER_SCHEMA(config)
+ trigger_type = config[CONF_TYPE]
+ if trigger_type in TURNED_ON:
+ from_state = "off"
+ to_state = "on"
+ else:
+ from_state = "on"
+ to_state = "off"
+ state_config = {
+ state.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
+ state.CONF_FROM: from_state,
+ state.CONF_TO: to_state,
+ }
+
+ return await state.async_trigger(hass, state_config, action, automation_info)
+
+
+def _is_domain(entity, domain):
+ return split_entity_id(entity.entity_id)[0] == domain
+
+
+async def _async_get_automations(hass, device_id, automation_templates, domain):
+ """List device automations."""
+ automations = []
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+
+ entities = async_entries_for_device(entity_registry, device_id)
+ domain_entities = [x for x in entities if _is_domain(x, domain)]
+ for entity in domain_entities:
+ device_class = DEVICE_CLASS_NONE
+ entity_id = entity.entity_id
+ entity = hass.states.get(entity_id)
+ if entity and ATTR_DEVICE_CLASS in entity.attributes:
+ device_class = entity.attributes[ATTR_DEVICE_CLASS]
+ automation_template = automation_templates[device_class]
+
+ for automation in automation_template:
+ automation = dict(automation)
+ automation.update(device_id=device_id, entity_id=entity_id, domain=domain)
+ automations.append(automation)
+
+ return automations
+
+
+async def async_get_conditions(hass, device_id):
+ """List device conditions."""
+ automations = await _async_get_automations(
+ hass, device_id, ENTITY_CONDITIONS, DOMAIN
+ )
+ for automation in automations:
+ automation.update(condition="device")
+ return automations
+
+
+async def async_get_triggers(hass, device_id):
+ """List device triggers."""
+ automations = await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, DOMAIN)
+ for automation in automations:
+ automation.update(platform="device")
+ return automations
diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json
new file mode 100644
index 00000000000000..109a2b1fd45f61
--- /dev/null
+++ b/homeassistant/components/binary_sensor/strings.json
@@ -0,0 +1,93 @@
+{
+ "device_automation": {
+ "condition_type": {
+ "is_bat_low": "{entity_name} battery is low",
+ "is_not_bat_low": "{entity_name} battery is normal",
+ "is_cold": "{entity_name} is cold",
+ "is_not_cold": "{entity_name} is not cold",
+ "is_connected": "{entity_name} is connected",
+ "is_not_connected": "{entity_name} is disconnected",
+ "is_gas": "{entity_name} is detecting gas",
+ "is_no_gas": "{entity_name} is not detecting gas",
+ "is_hot": "{entity_name} is hot",
+ "is_not_hot": "{entity_name} is not hot",
+ "is_light": "{entity_name} is detecting light",
+ "is_no_light": "{entity_name} is not detecting light",
+ "is_locked": "{entity_name} is locked",
+ "is_not_locked": "{entity_name} is unlocked",
+ "is_moist": "{entity_name} is moist",
+ "is_not_moist": "{entity_name} is dry",
+ "is_motion": "{entity_name} is detecting motion",
+ "is_no_motion": "{entity_name} is not detecting motion",
+ "is_moving": "{entity_name} is moving",
+ "is_not_moving": "{entity_name} is not moving",
+ "is_occupied": "{entity_name} is occupied",
+ "is_not_occupied": "{entity_name} is not occupied",
+ "is_plugged_in": "{entity_name} is plugged in",
+ "is_not_plugged_in": "{entity_name} is unplugged",
+ "is_powered": "{entity_name} is powered",
+ "is_not_powered": "{entity_name} is not powered",
+ "is_present": "{entity_name} is present",
+ "is_not_present": "{entity_name} is not present",
+ "is_problem": "{entity_name} is detecting problem",
+ "is_no_problem": "{entity_name} is not detecting problem",
+ "is_unsafe": "{entity_name} is unsafe",
+ "is_not_unsafe": "{entity_name} is safe",
+ "is_smoke": "{entity_name} is detecting smoke",
+ "is_no_smoke": "{entity_name} is not detecting smoke",
+ "is_sound": "{entity_name} is detecting sound",
+ "is_no_sound": "{entity_name} is not detecting sound",
+ "is_vibration": "{entity_name} is detecting vibration",
+ "is_no_vibration": "{entity_name} is not detecting vibration",
+ "is_open": "{entity_name} is open",
+ "is_not_open": "{entity_name} is closed",
+ "is_on": "{entity_name} is on",
+ "is_off": "{entity_name} is off"
+ },
+ "trigger_type": {
+ "bat_low": "{entity_name} battery low",
+ "not_bat_low": "{entity_name} battery normal",
+ "cold": "{entity_name} became cold",
+ "not_cold": "{entity_name} became not cold",
+ "connected": "{entity_name} connected",
+ "not_connected": "{entity_name} disconnected",
+ "gas": "{entity_name} started detecting gas",
+ "no_gas": "{entity_name} stopped detecting gas",
+ "hot": "{entity_name} became hot",
+ "not_hot": "{entity_name} became not hot",
+ "light": "{entity_name} started detecting light",
+ "no_light": "{entity_name} stopped detecting light",
+ "locked": "{entity_name} locked",
+ "not_locked": "{entity_name} unlocked",
+ "moist§": "{entity_name} became moist",
+ "not_moist": "{entity_name} became dry",
+ "motion": "{entity_name} started detecting motion",
+ "no_motion": "{entity_name} stopped detecting motion",
+ "moving": "{entity_name} started moving",
+ "not_moving": "{entity_name} stopped moving",
+ "occupied": "{entity_name} became occupied",
+ "not_occupied": "{entity_name} became not occupied",
+ "plugged_in": "{entity_name} plugged in",
+ "not_plugged_in": "{entity_name} unplugged",
+ "powered": "{entity_name} powered",
+ "not_powered": "{entity_name} not powered",
+ "present": "{entity_name} present",
+ "not_present": "{entity_name} not present",
+ "problem": "{entity_name} started detecting problem",
+ "no_problem": "{entity_name} stopped detecting problem",
+ "unsafe": "{entity_name} became unsafe",
+ "not_unsafe": "{entity_name} became safe",
+ "smoke": "{entity_name} started detecting smoke",
+ "no_smoke": "{entity_name} stopped detecting smoke",
+ "sound": "{entity_name} started detecting sound",
+ "no_sound": "{entity_name} stopped detecting sound",
+ "vibration": "{entity_name} started detecting vibration",
+ "no_vibration": "{entity_name} stopped detecting vibration",
+ "opened": "{entity_name} opened",
+ "closed": "{entity_name} closed",
+ "turned_on": "{entity_name} turned on",
+ "turned_off": "{entity_name} turned off"
+
+ }
+ }
+}
diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py
index e760f91070a163..6a26775b0a8ac7 100644
--- a/homeassistant/components/bluetooth_tracker/device_tracker.py
+++ b/homeassistant/components/bluetooth_tracker/device_tracker.py
@@ -1,25 +1,30 @@
"""Tracking for bluetooth devices."""
+import asyncio
import logging
+from typing import List, Set, Tuple, Optional
+# pylint: disable=import-error
+import bluetooth
+from bt_proximity import BluetoothRSSI
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
-from homeassistant.components.device_tracker.legacy import (
- YAML_DEVICES,
- async_load_config,
-)
from homeassistant.components.device_tracker.const import (
- CONF_TRACK_NEW,
CONF_SCAN_INTERVAL,
- SCAN_INTERVAL,
+ CONF_TRACK_NEW,
DEFAULT_TRACK_NEW,
- SOURCE_TYPE_BLUETOOTH,
DOMAIN,
+ SCAN_INTERVAL,
+ SOURCE_TYPE_BLUETOOTH,
+)
+from homeassistant.components.device_tracker.legacy import (
+ YAML_DEVICES,
+ async_load_config,
)
-import homeassistant.util.dt as dt_util
-from homeassistant.util.async_ import run_coroutine_threadsafe
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.typing import HomeAssistantType
+
_LOGGER = logging.getLogger(__name__)
@@ -42,100 +47,145 @@
)
-def setup_scanner(hass, config, see, discovery_info=None):
+def is_bluetooth_device(device) -> bool:
+ """Check whether a device is a bluetooth device by its mac."""
+ return device.mac and device.mac[:3].upper() == BT_PREFIX
+
+
+def discover_devices(device_id: int) -> List[Tuple[str, str]]:
+ """Discover Bluetooth devices."""
+ result = bluetooth.discover_devices(
+ duration=8,
+ lookup_names=True,
+ flush_cache=True,
+ lookup_class=False,
+ device_id=device_id,
+ )
+ _LOGGER.debug("Bluetooth devices discovered = %d", len(result))
+ return result
+
+
+async def see_device(
+ hass: HomeAssistantType, async_see, mac: str, device_name: str, rssi=None
+) -> None:
+ """Mark a device as seen."""
+ attributes = {}
+ if rssi is not None:
+ attributes["rssi"] = rssi
+
+ await async_see(
+ mac=f"{BT_PREFIX}{mac}",
+ host_name=device_name,
+ attributes=attributes,
+ source_type=SOURCE_TYPE_BLUETOOTH,
+ )
+
+
+async def get_tracking_devices(hass: HomeAssistantType) -> Tuple[Set[str], Set[str]]:
+ """
+ Load all known devices.
+
+ We just need the devices so set consider_home and home range to 0
+ """
+ yaml_path: str = hass.config.path(YAML_DEVICES)
+
+ devices = await async_load_config(yaml_path, hass, 0)
+ bluetooth_devices = [device for device in devices if is_bluetooth_device(device)]
+
+ devices_to_track: Set[str] = {
+ device.mac[3:] for device in bluetooth_devices if device.track
+ }
+ devices_to_not_track: Set[str] = {
+ device.mac[3:] for device in bluetooth_devices if not device.track
+ }
+
+ return devices_to_track, devices_to_not_track
+
+
+def lookup_name(mac: str) -> Optional[str]:
+ """Lookup a Bluetooth device name."""
+ _LOGGER.debug("Scanning %s", mac)
+ return bluetooth.lookup_name(mac, timeout=5)
+
+
+async def async_setup_scanner(
+ hass: HomeAssistantType, config: dict, async_see, discovery_info=None
+):
"""Set up the Bluetooth Scanner."""
- # pylint: disable=import-error
- import bluetooth
- from bt_proximity import BluetoothRSSI
-
- def see_device(mac, name, rssi=None):
- """Mark a device as seen."""
- attributes = {}
- if rssi is not None:
- attributes["rssi"] = rssi
- see(
- mac=f"{BT_PREFIX}{mac}",
- host_name=name,
- attributes=attributes,
- source_type=SOURCE_TYPE_BLUETOOTH,
- )
-
- device_id = config.get(CONF_DEVICE_ID)
-
- def discover_devices():
- """Discover Bluetooth devices."""
- result = bluetooth.discover_devices(
- duration=8,
- lookup_names=True,
- flush_cache=True,
- lookup_class=False,
- device_id=device_id,
- )
- _LOGGER.debug("Bluetooth devices discovered = %d", len(result))
- return result
-
- yaml_path = hass.config.path(YAML_DEVICES)
- devs_to_track = []
- devs_donot_track = []
-
- # Load all known devices.
- # We just need the devices so set consider_home and home range
- # to 0
- for device in run_coroutine_threadsafe(
- async_load_config(yaml_path, hass, 0), hass.loop
- ).result():
- # Check if device is a valid bluetooth device
- if device.mac and device.mac[:3].upper() == BT_PREFIX:
- if device.track:
- devs_to_track.append(device.mac[3:])
- else:
- devs_donot_track.append(device.mac[3:])
+ device_id: int = config.get(CONF_DEVICE_ID)
+ interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
+ request_rssi = config.get(CONF_REQUEST_RSSI, False)
+ update_bluetooth_lock = asyncio.Lock()
# If track new devices is true discover new devices on startup.
- track_new = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
- if track_new:
- for dev in discover_devices():
- if dev[0] not in devs_to_track and dev[0] not in devs_donot_track:
- devs_to_track.append(dev[0])
- see_device(dev[0], dev[1])
+ track_new: bool = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
+ _LOGGER.debug("Tracking new devices is set to %s", track_new)
- interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
+ devices_to_track, devices_to_not_track = await get_tracking_devices(hass)
- request_rssi = config.get(CONF_REQUEST_RSSI, False)
+ if not devices_to_track and not track_new:
+ _LOGGER.debug("No Bluetooth devices to track and not tracking new devices")
+
+ if request_rssi:
+ _LOGGER.debug("Detecting RSSI for devices")
- def update_bluetooth(_):
- """Update Bluetooth and set timer for the next update."""
- update_bluetooth_once()
- track_point_in_utc_time(hass, update_bluetooth, dt_util.utcnow() + interval)
+ async def perform_bluetooth_update():
+ """Discover Bluetooth devices and update status."""
+
+ _LOGGER.debug("Performing Bluetooth devices discovery and update")
+ tasks = []
- def update_bluetooth_once():
- """Lookup Bluetooth device and update status."""
try:
if track_new:
- for dev in discover_devices():
- if dev[0] not in devs_to_track and dev[0] not in devs_donot_track:
- devs_to_track.append(dev[0])
- for mac in devs_to_track:
- _LOGGER.debug("Scanning %s", mac)
- result = bluetooth.lookup_name(mac, timeout=5)
+ devices = await hass.async_add_executor_job(discover_devices, device_id)
+ for mac, device_name in devices:
+ if mac not in devices_to_track and mac not in devices_to_not_track:
+ devices_to_track.add(mac)
+
+ for mac in devices_to_track:
+ device_name = await hass.async_add_executor_job(lookup_name, mac)
+ if device_name is None:
+ # Could not lookup device name
+ continue
+
rssi = None
if request_rssi:
client = BluetoothRSSI(mac)
- rssi = client.request_rssi()
+ rssi = await hass.async_add_executor_job(client.request_rssi)
client.close()
- if result is None:
- # Could not lookup device name
- continue
- see_device(mac, result, rssi)
+
+ tasks.append(see_device(hass, async_see, mac, device_name, rssi))
+
+ if tasks:
+ await asyncio.wait(tasks)
+
except bluetooth.BluetoothError:
_LOGGER.exception("Error looking up Bluetooth device")
- def handle_update_bluetooth(call):
+ async def update_bluetooth(now=None):
+ """Lookup Bluetooth devices and update status."""
+
+ # If an update is in progress, we don't do anything
+ if update_bluetooth_lock.locked():
+ _LOGGER.debug(
+ "Previous execution of update_bluetooth is taking longer than the scheduled update of interval %s",
+ interval,
+ )
+ return
+
+ async with update_bluetooth_lock:
+ await perform_bluetooth_update()
+
+ async def handle_manual_update_bluetooth(call):
"""Update bluetooth devices on demand."""
- update_bluetooth_once()
- update_bluetooth(dt_util.utcnow())
+ await update_bluetooth()
+
+ hass.async_create_task(update_bluetooth())
+ async_track_time_interval(hass, update_bluetooth, interval)
- hass.services.register(DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth)
+ hass.services.async_register(
+ DOMAIN, "bluetooth_tracker_update", handle_manual_update_bluetooth
+ )
return True
diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py
index 20fdfc9ee79f6f..a36b35ea9d4be2 100644
--- a/homeassistant/components/bme680/sensor.py
+++ b/homeassistant/components/bme680/sensor.py
@@ -171,7 +171,7 @@ def _setup_bme680(config):
sensor.select_gas_heater_profile(0)
else:
sensor.set_gas_status(bme680.DISABLE_GAS_MEAS)
- except (RuntimeError, IOError):
+ except (RuntimeError, OSError):
_LOGGER.error("BME680 sensor not detected at 0x%02x", i2c_address)
return None
diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py
index c257470bb2d0e8..8e67da86dc3014 100644
--- a/homeassistant/components/bmw_connected_drive/__init__.py
+++ b/homeassistant/components/bmw_connected_drive/__init__.py
@@ -1,5 +1,4 @@
"""Reads vehicle status from BMW connected drive portal."""
-import datetime
import logging
import voluptuous as vol
@@ -8,6 +7,7 @@
from homeassistant.helpers import discovery
from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv
+import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -100,7 +100,7 @@ def execute_service(call):
# update every UPDATE_INTERVAL minutes, starting now
# this should even out the load on the servers
- now = datetime.datetime.now()
+ now = dt_util.utcnow()
track_utc_time_change(
hass,
cd_account.update,
@@ -142,7 +142,7 @@ def update(self, *_):
self.account.update_vehicle_states()
for listener in self._update_listeners:
listener()
- except IOError as exception:
+ except OSError as exception:
_LOGGER.error(
"Could not connect to the BMW Connected Drive portal. "
"The vehicle state could not be updated."
diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py
index c9cc9b2d33373f..c13de4559847ef 100644
--- a/homeassistant/components/bmw_connected_drive/binary_sensor.py
+++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py
@@ -9,7 +9,7 @@
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {
- "lids": ["Doors", "opening", "mdi:car-door"],
+ "lids": ["Doors", "opening", "mdi:car-door-lock"],
"windows": ["Windows", "opening", "mdi:car-door"],
"door_lock_state": ["Door lock state", "safety", "mdi:car-key"],
"lights_parking": ["Parking lights", "light", "mdi:car-parking-lights"],
@@ -122,8 +122,9 @@ def device_state_attributes(self):
for report in vehicle_state.condition_based_services:
result.update(self._format_cbs_report(report))
elif self._attribute == "check_control_messages":
- check_control_messages = vehicle_state.has_check_control_messages
- if check_control_messages:
+ check_control_messages = vehicle_state.check_control_messages
+ has_check_control_messages = vehicle_state.has_check_control_messages
+ if has_check_control_messages:
cbs_list = []
for message in check_control_messages:
cbs_list.append(message["ccmDescriptionShort"])
@@ -184,9 +185,9 @@ def _format_cbs_report(self, report):
distance = round(
self.hass.config.units.length(report.due_distance, LENGTH_KILOMETERS)
)
- result[f"{service_type} distance"] = "{} {}".format(
- distance, self.hass.config.units.length_unit
- )
+ result[
+ f"{service_type} distance"
+ ] = f"{distance} {self.hass.config.units.length_unit}"
return result
def update_callback(self):
diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py
index 011908d54585e4..96d541b1955337 100644
--- a/homeassistant/components/bmw_connected_drive/sensor.py
+++ b/homeassistant/components/bmw_connected_drive/sensor.py
@@ -17,10 +17,10 @@
ATTR_TO_HA_METRIC = {
"mileage": ["mdi:speedometer", LENGTH_KILOMETERS],
- "remaining_range_total": ["mdi:ruler", LENGTH_KILOMETERS],
- "remaining_range_electric": ["mdi:ruler", LENGTH_KILOMETERS],
- "remaining_range_fuel": ["mdi:ruler", LENGTH_KILOMETERS],
- "max_range_electric": ["mdi:ruler", LENGTH_KILOMETERS],
+ "remaining_range_total": ["mdi:map-marker-distance", LENGTH_KILOMETERS],
+ "remaining_range_electric": ["mdi:map-marker-distance", LENGTH_KILOMETERS],
+ "remaining_range_fuel": ["mdi:map-marker-distance", LENGTH_KILOMETERS],
+ "max_range_electric": ["mdi:map-marker-distance", LENGTH_KILOMETERS],
"remaining_fuel": ["mdi:gas-station", VOLUME_LITERS],
"charging_time_remaining": ["mdi:update", "h"],
"charging_status": ["mdi:battery-charging", None],
@@ -28,10 +28,10 @@
ATTR_TO_HA_IMPERIAL = {
"mileage": ["mdi:speedometer", LENGTH_MILES],
- "remaining_range_total": ["mdi:ruler", LENGTH_MILES],
- "remaining_range_electric": ["mdi:ruler", LENGTH_MILES],
- "remaining_range_fuel": ["mdi:ruler", LENGTH_MILES],
- "max_range_electric": ["mdi:ruler", LENGTH_MILES],
+ "remaining_range_total": ["mdi:map-marker-distance", LENGTH_MILES],
+ "remaining_range_electric": ["mdi:map-marker-distance", LENGTH_MILES],
+ "remaining_range_fuel": ["mdi:map-marker-distance", LENGTH_MILES],
+ "max_range_electric": ["mdi:map-marker-distance", LENGTH_MILES],
"remaining_fuel": ["mdi:gas-station", VOLUME_GALLONS],
"charging_time_remaining": ["mdi:update", "h"],
"charging_status": ["mdi:battery-charging", None],
diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py
index 33444f1099652a..ed22be003ad4fa 100644
--- a/homeassistant/components/bom/sensor.py
+++ b/homeassistant/components/bom/sensor.py
@@ -13,6 +13,7 @@
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
+import homeassistant.util.dt as dt_util
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_MONITORED_CONDITIONS,
@@ -240,7 +241,7 @@ def should_update(self):
# Never updated before, therefore an update should occur.
return True
- now = datetime.datetime.now()
+ now = dt_util.utcnow()
update_due_at = self.last_updated + datetime.timedelta(minutes=35)
return now > update_due_at
@@ -251,8 +252,8 @@ def update(self):
_LOGGER.debug(
"BOM was updated %s minutes ago, skipping update as"
" < 35 minutes, Now: %s, LastUpdate: %s",
- (datetime.datetime.now() - self.last_updated),
- datetime.datetime.now(),
+ (dt_util.utcnow() - self.last_updated),
+ dt_util.utcnow(),
self.last_updated,
)
return
@@ -263,8 +264,10 @@ def update(self):
# set lastupdate using self._data[0] as the first element in the
# array is the latest date in the json
- self.last_updated = datetime.datetime.strptime(
- str(self._data[0]["local_date_time_full"]), "%Y%m%d%H%M%S"
+ self.last_updated = dt_util.as_utc(
+ datetime.datetime.strptime(
+ str(self._data[0]["local_date_time_full"]), "%Y%m%d%H%M%S"
+ )
)
return
diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py
index 1db9e2beaf9376..cdf202bbafd685 100644
--- a/homeassistant/components/buienradar/camera.py
+++ b/homeassistant/components/buienradar/camera.py
@@ -81,13 +81,13 @@ def __init__(self, name: str, dimension: int, delta: float):
# invariant: this condition is private to and owned by this instance.
self._condition = asyncio.Condition()
- self._last_image = None # type: Optional[bytes]
+ self._last_image: Optional[bytes] = None
# value of the last seen last modified header
- self._last_modified = None # type: Optional[str]
+ self._last_modified: Optional[str] = None
# loading status
self._loading = False
# deadline for image refresh - self.delta after last successful load
- self._deadline = None # type: Optional[datetime]
+ self._deadline: Optional[datetime] = None
@property
def name(self) -> str:
diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json
index 32c744c8f20b0e..71dee3afec5ec1 100644
--- a/homeassistant/components/cast/.translations/ko.json
+++ b/homeassistant/components/cast/.translations/ko.json
@@ -1,15 +1,15 @@
{
"config": {
"abort": {
- "no_devices_found": "Googgle Cast \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
- "single_instance_allowed": "\ud558\ub098\uc758 Google Cast \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ "no_devices_found": "\uad6c\uae00 \uce90\uc2a4\ud2b8 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
+ "single_instance_allowed": "\ud558\ub098\uc758 \uad6c\uae00 \uce90\uc2a4\ud2b8\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
"step": {
"confirm": {
- "description": "Google Cast\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
- "title": "Google Cast"
+ "description": "\uad6c\uae00 \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8"
}
},
- "title": "Google Cast"
+ "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py
index cc112984f888ae..4dfb58ef3b7d6e 100644
--- a/homeassistant/components/cast/__init__.py
+++ b/homeassistant/components/cast/__init__.py
@@ -1,6 +1,7 @@
"""Component to embed Google Cast."""
from homeassistant import config_entries
+from . import home_assistant_cast
from .const import DOMAIN
@@ -20,8 +21,10 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, entry):
+async def async_setup_entry(hass, entry: config_entries.ConfigEntry):
"""Set up Cast from a config entry."""
+ await home_assistant_cast.async_setup_ha_cast(hass, entry)
+
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "media_player")
)
diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py
index e9f9ba4c39deac..c6164484dbbb14 100644
--- a/homeassistant/components/cast/const.py
+++ b/homeassistant/components/cast/const.py
@@ -1,3 +1,26 @@
"""Consts for Cast integration."""
DOMAIN = "cast"
+DEFAULT_PORT = 8009
+
+# Stores a threading.Lock that is held by the internal pychromecast discovery.
+INTERNAL_DISCOVERY_RUNNING_KEY = "cast_discovery_running"
+# Stores all ChromecastInfo we encountered through discovery or config as a set
+# If we find a chromecast with a new host, the old one will be removed again.
+KNOWN_CHROMECAST_INFO_KEY = "cast_known_chromecasts"
+# Stores UUIDs of cast devices that were added as entities. Doesn't store
+# None UUIDs.
+ADDED_CAST_DEVICES_KEY = "cast_added_cast_devices"
+# Stores an audio group manager.
+CAST_MULTIZONE_MANAGER_KEY = "cast_multizone_manager"
+
+# Dispatcher signal fired with a ChromecastInfo every time we discover a new
+# Chromecast or receive it through configuration
+SIGNAL_CAST_DISCOVERED = "cast_discovered"
+
+# Dispatcher signal fired with a ChromecastInfo every time a Chromecast is
+# removed
+SIGNAL_CAST_REMOVED = "cast_removed"
+
+# Dispatcher signal fired when a Chromecast should show a Home Assistant Cast view.
+SIGNAL_HASS_CAST_SHOW_VIEW = "cast_show_view"
diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py
new file mode 100644
index 00000000000000..d3097b3cc29fb3
--- /dev/null
+++ b/homeassistant/components/cast/discovery.py
@@ -0,0 +1,99 @@
+"""Deal with Cast discovery."""
+import logging
+import threading
+
+import pychromecast
+
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.dispatcher import dispatcher_send
+
+from .const import (
+ KNOWN_CHROMECAST_INFO_KEY,
+ SIGNAL_CAST_DISCOVERED,
+ INTERNAL_DISCOVERY_RUNNING_KEY,
+ SIGNAL_CAST_REMOVED,
+)
+from .helpers import ChromecastInfo, ChromeCastZeroconf
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def discover_chromecast(hass: HomeAssistant, info: ChromecastInfo):
+ """Discover a Chromecast."""
+ if info in hass.data[KNOWN_CHROMECAST_INFO_KEY]:
+ _LOGGER.debug("Discovered previous chromecast %s", info)
+
+ # Either discovered completely new chromecast or a "moved" one.
+ info = info.fill_out_missing_chromecast_info()
+ _LOGGER.debug("Discovered chromecast %s", info)
+
+ if info.uuid is not None:
+ # Remove previous cast infos with same uuid from known chromecasts.
+ same_uuid = set(
+ x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] if info.uuid == x.uuid
+ )
+ hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid
+
+ hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info)
+ dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info)
+
+
+def _remove_chromecast(hass: HomeAssistant, info: ChromecastInfo):
+ # Removed chromecast
+ _LOGGER.debug("Removed chromecast %s", info)
+
+ dispatcher_send(hass, SIGNAL_CAST_REMOVED, info)
+
+
+def setup_internal_discovery(hass: HomeAssistant) -> None:
+ """Set up the pychromecast internal discovery."""
+ if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data:
+ hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock()
+
+ if not hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].acquire(blocking=False):
+ # Internal discovery is already running
+ return
+
+ def internal_add_callback(name):
+ """Handle zeroconf discovery of a new chromecast."""
+ mdns = listener.services[name]
+ discover_chromecast(
+ hass,
+ ChromecastInfo(
+ service=name,
+ host=mdns[0],
+ port=mdns[1],
+ uuid=mdns[2],
+ model_name=mdns[3],
+ friendly_name=mdns[4],
+ ),
+ )
+
+ def internal_remove_callback(name, mdns):
+ """Handle zeroconf discovery of a removed chromecast."""
+ _remove_chromecast(
+ hass,
+ ChromecastInfo(
+ service=name,
+ host=mdns[0],
+ port=mdns[1],
+ uuid=mdns[2],
+ model_name=mdns[3],
+ friendly_name=mdns[4],
+ ),
+ )
+
+ _LOGGER.debug("Starting internal pychromecast discovery.")
+ listener, browser = pychromecast.start_discovery(
+ internal_add_callback, internal_remove_callback
+ )
+ ChromeCastZeroconf.set_zeroconf(browser.zc)
+
+ def stop_discovery(event):
+ """Stop discovery of new chromecasts."""
+ _LOGGER.debug("Stopping internal pychromecast discovery.")
+ pychromecast.stop_discovery(browser)
+ hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release()
+
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery)
diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py
new file mode 100644
index 00000000000000..ea5c77ebc1afa3
--- /dev/null
+++ b/homeassistant/components/cast/helpers.py
@@ -0,0 +1,246 @@
+"""Helpers to deal with Cast devices."""
+from typing import Optional, Tuple
+
+import attr
+from pychromecast import dial
+
+from .const import DEFAULT_PORT
+
+
+@attr.s(slots=True, frozen=True)
+class ChromecastInfo:
+ """Class to hold all data about a chromecast for creating connections.
+
+ This also has the same attributes as the mDNS fields by zeroconf.
+ """
+
+ host = attr.ib(type=str)
+ port = attr.ib(type=int)
+ service = attr.ib(type=Optional[str], default=None)
+ uuid = attr.ib(
+ type=Optional[str], converter=attr.converters.optional(str), default=None
+ ) # always convert UUID to string if not None
+ manufacturer = attr.ib(type=str, default="")
+ model_name = attr.ib(type=str, default="")
+ friendly_name = attr.ib(type=Optional[str], default=None)
+ is_dynamic_group = attr.ib(type=Optional[bool], default=None)
+
+ @property
+ def is_audio_group(self) -> bool:
+ """Return if this is an audio group."""
+ return self.port != DEFAULT_PORT
+
+ @property
+ def is_information_complete(self) -> bool:
+ """Return if all information is filled out."""
+ want_dynamic_group = self.is_audio_group
+ have_dynamic_group = self.is_dynamic_group is not None
+ have_all_except_dynamic_group = all(
+ attr.astuple(
+ self,
+ filter=attr.filters.exclude(
+ attr.fields(ChromecastInfo).is_dynamic_group
+ ),
+ )
+ )
+ return have_all_except_dynamic_group and (
+ not want_dynamic_group or have_dynamic_group
+ )
+
+ @property
+ def host_port(self) -> Tuple[str, int]:
+ """Return the host+port tuple."""
+ return self.host, self.port
+
+ def fill_out_missing_chromecast_info(self) -> "ChromecastInfo":
+ """Return a new ChromecastInfo object with missing attributes filled in.
+
+ Uses blocking HTTP.
+ """
+ if self.is_information_complete:
+ # We have all information, no need to check HTTP API. Or this is an
+ # audio group, so checking via HTTP won't give us any new information.
+ return self
+
+ # Fill out missing information via HTTP dial.
+ if self.is_audio_group:
+ is_dynamic_group = False
+ http_group_status = None
+ dynamic_groups = []
+ if self.uuid:
+ http_group_status = dial.get_multizone_status(
+ self.host,
+ services=[self.service],
+ zconf=ChromeCastZeroconf.get_zeroconf(),
+ )
+ if http_group_status is not None:
+ dynamic_groups = [
+ str(g.uuid) for g in http_group_status.dynamic_groups
+ ]
+ is_dynamic_group = self.uuid in dynamic_groups
+
+ return ChromecastInfo(
+ service=self.service,
+ host=self.host,
+ port=self.port,
+ uuid=self.uuid,
+ friendly_name=self.friendly_name,
+ manufacturer=self.manufacturer,
+ model_name=self.model_name,
+ is_dynamic_group=is_dynamic_group,
+ )
+
+ http_device_status = dial.get_device_status(
+ self.host, services=[self.service], zconf=ChromeCastZeroconf.get_zeroconf()
+ )
+ if http_device_status is None:
+ # HTTP dial didn't give us any new information.
+ return self
+
+ return ChromecastInfo(
+ service=self.service,
+ host=self.host,
+ port=self.port,
+ uuid=(self.uuid or http_device_status.uuid),
+ friendly_name=(self.friendly_name or http_device_status.friendly_name),
+ manufacturer=(self.manufacturer or http_device_status.manufacturer),
+ model_name=(self.model_name or http_device_status.model_name),
+ )
+
+ def same_dynamic_group(self, other: "ChromecastInfo") -> bool:
+ """Test chromecast info is same dynamic group."""
+ return (
+ self.is_audio_group
+ and other.is_dynamic_group
+ and self.friendly_name == other.friendly_name
+ )
+
+
+class ChromeCastZeroconf:
+ """Class to hold a zeroconf instance."""
+
+ __zconf = None
+
+ @classmethod
+ def set_zeroconf(cls, zconf):
+ """Set zeroconf."""
+ cls.__zconf = zconf
+
+ @classmethod
+ def get_zeroconf(cls):
+ """Get zeroconf."""
+ return cls.__zconf
+
+
+class CastStatusListener:
+ """Helper class to handle pychromecast status callbacks.
+
+ Necessary because a CastDevice entity can create a new socket client
+ and therefore callbacks from multiple chromecast connections can
+ potentially arrive. This class allows invalidating past chromecast objects.
+ """
+
+ def __init__(self, cast_device, chromecast, mz_mgr):
+ """Initialize the status listener."""
+ self._cast_device = cast_device
+ self._uuid = chromecast.uuid
+ self._valid = True
+ self._mz_mgr = mz_mgr
+
+ chromecast.register_status_listener(self)
+ chromecast.socket_client.media_controller.register_status_listener(self)
+ chromecast.register_connection_listener(self)
+ # pylint: disable=protected-access
+ if cast_device._cast_info.is_audio_group:
+ self._mz_mgr.add_multizone(chromecast)
+ else:
+ self._mz_mgr.register_listener(chromecast.uuid, self)
+
+ def new_cast_status(self, cast_status):
+ """Handle reception of a new CastStatus."""
+ if self._valid:
+ self._cast_device.new_cast_status(cast_status)
+
+ def new_media_status(self, media_status):
+ """Handle reception of a new MediaStatus."""
+ if self._valid:
+ self._cast_device.new_media_status(media_status)
+
+ def new_connection_status(self, connection_status):
+ """Handle reception of a new ConnectionStatus."""
+ if self._valid:
+ self._cast_device.new_connection_status(connection_status)
+
+ @staticmethod
+ def added_to_multizone(group_uuid):
+ """Handle the cast added to a group."""
+ pass
+
+ def removed_from_multizone(self, group_uuid):
+ """Handle the cast removed from a group."""
+ if self._valid:
+ self._cast_device.multizone_new_media_status(group_uuid, None)
+
+ def multizone_new_cast_status(self, group_uuid, cast_status):
+ """Handle reception of a new CastStatus for a group."""
+ pass
+
+ def multizone_new_media_status(self, group_uuid, media_status):
+ """Handle reception of a new MediaStatus for a group."""
+ if self._valid:
+ self._cast_device.multizone_new_media_status(group_uuid, media_status)
+
+ def invalidate(self):
+ """Invalidate this status listener.
+
+ All following callbacks won't be forwarded.
+ """
+ # pylint: disable=protected-access
+ if self._cast_device._cast_info.is_audio_group:
+ self._mz_mgr.remove_multizone(self._uuid)
+ else:
+ self._mz_mgr.deregister_listener(self._uuid, self)
+ self._valid = False
+
+
+class DynamicGroupCastStatusListener:
+ """Helper class to handle pychromecast status callbacks.
+
+ Necessary because a CastDevice entity can create a new socket client
+ and therefore callbacks from multiple chromecast connections can
+ potentially arrive. This class allows invalidating past chromecast objects.
+ """
+
+ def __init__(self, cast_device, chromecast, mz_mgr):
+ """Initialize the status listener."""
+ self._cast_device = cast_device
+ self._uuid = chromecast.uuid
+ self._valid = True
+ self._mz_mgr = mz_mgr
+
+ chromecast.register_status_listener(self)
+ chromecast.socket_client.media_controller.register_status_listener(self)
+ chromecast.register_connection_listener(self)
+ self._mz_mgr.add_multizone(chromecast)
+
+ def new_cast_status(self, cast_status):
+ """Handle reception of a new CastStatus."""
+ pass
+
+ def new_media_status(self, media_status):
+ """Handle reception of a new MediaStatus."""
+ if self._valid:
+ self._cast_device.new_dynamic_group_media_status(media_status)
+
+ def new_connection_status(self, connection_status):
+ """Handle reception of a new ConnectionStatus."""
+ if self._valid:
+ self._cast_device.new_dynamic_group_connection_status(connection_status)
+
+ def invalidate(self):
+ """Invalidate this status listener.
+
+ All following callbacks won't be forwarded.
+ """
+ self._mz_mgr.remove_multizone(self._uuid)
+ self._valid = False
diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py
new file mode 100644
index 00000000000000..d5d35ba7c9f933
--- /dev/null
+++ b/homeassistant/components/cast/home_assistant_cast.py
@@ -0,0 +1,74 @@
+"""Home Assistant Cast integration for Cast."""
+from typing import Optional
+
+import voluptuous as vol
+
+from pychromecast.controllers.homeassistant import HomeAssistantController
+
+from homeassistant import auth, config_entries, core
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.helpers import config_validation as cv, dispatcher
+
+from .const import DOMAIN, SIGNAL_HASS_CAST_SHOW_VIEW
+
+SERVICE_SHOW_VIEW = "show_lovelace_view"
+ATTR_VIEW_PATH = "view_path"
+
+
+async def async_setup_ha_cast(
+ hass: core.HomeAssistant, entry: config_entries.ConfigEntry
+):
+ """Set up Home Assistant Cast."""
+ user_id: Optional[str] = entry.data.get("user_id")
+ user: Optional[auth.models.User] = None
+
+ if user_id is not None:
+ user = await hass.auth.async_get_user(user_id)
+
+ if user is None:
+ user = await hass.auth.async_create_system_user(
+ "Home Assistant Cast", [auth.GROUP_ID_ADMIN]
+ )
+ hass.config_entries.async_update_entry(
+ entry, data={**entry.data, "user_id": user.id}
+ )
+
+ if user.refresh_tokens:
+ refresh_token: auth.models.RefreshToken = list(user.refresh_tokens.values())[0]
+ else:
+ refresh_token = await hass.auth.async_create_refresh_token(user)
+
+ async def handle_show_view(call: core.ServiceCall):
+ """Handle a Show View service call."""
+ hass_url = hass.config.api.base_url
+
+ # Home Assistant Cast only works with https urls. If user has no configured
+ # base url, use their remote url.
+ if not hass_url.lower().startswith("https://"):
+ try:
+ hass_url = hass.components.cloud.async_remote_ui_url()
+ except hass.components.cloud.CloudNotAvailable:
+ pass
+
+ controller = HomeAssistantController(
+ # If you are developing Home Assistant Cast, uncomment and set to your dev app id.
+ # app_id="5FE44367",
+ hass_url=hass_url,
+ client_id=None,
+ refresh_token=refresh_token.token,
+ )
+
+ dispatcher.async_dispatcher_send(
+ hass,
+ SIGNAL_HASS_CAST_SHOW_VIEW,
+ controller,
+ call.data[ATTR_ENTITY_ID],
+ call.data[ATTR_VIEW_PATH],
+ )
+
+ hass.helpers.service.async_register_admin_service(
+ DOMAIN,
+ SERVICE_SHOW_VIEW,
+ handle_show_view,
+ vol.Schema({ATTR_ENTITY_ID: cv.entity_id, ATTR_VIEW_PATH: str}),
+ )
diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json
index ff9e8907ec5aeb..84a6a6e2934fd6 100644
--- a/homeassistant/components/cast/manifest.json
+++ b/homeassistant/components/cast/manifest.json
@@ -3,9 +3,7 @@
"name": "Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/cast",
- "requirements": [
- "pychromecast==3.2.2"
- ],
+ "requirements": ["pychromecast==4.0.1"],
"dependencies": [],
"zeroconf": ["_googlecast._tcp.local."],
"codeowners": []
diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py
index af9f39f8ed446f..c2d847fd09bb2b 100644
--- a/homeassistant/components/cast/media_player.py
+++ b/homeassistant/components/cast/media_player.py
@@ -1,10 +1,15 @@
"""Provide functionality to interact with Cast devices on the network."""
import asyncio
import logging
-import threading
-from typing import Optional, Tuple
+from typing import Optional
-import attr
+import pychromecast
+from pychromecast.socket_client import (
+ CONNECTION_STATUS_CONNECTED,
+ CONNECTION_STATUS_DISCONNECTED,
+)
+from pychromecast.controllers.multizone import MultizoneManager
+from pychromecast.controllers.homeassistant import HomeAssistantController
import voluptuous as vol
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice
@@ -35,22 +40,34 @@
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
import homeassistant.util.dt as dt_util
from homeassistant.util.logging import async_create_catching_coro
-from . import DOMAIN as CAST_DOMAIN
-
-DEPENDENCIES = ("cast",)
+from .const import (
+ DOMAIN as CAST_DOMAIN,
+ ADDED_CAST_DEVICES_KEY,
+ SIGNAL_CAST_DISCOVERED,
+ KNOWN_CHROMECAST_INFO_KEY,
+ CAST_MULTIZONE_MANAGER_KEY,
+ DEFAULT_PORT,
+ SIGNAL_CAST_REMOVED,
+ SIGNAL_HASS_CAST_SHOW_VIEW,
+)
+from .helpers import (
+ ChromecastInfo,
+ CastStatusListener,
+ DynamicGroupCastStatusListener,
+ ChromeCastZeroconf,
+)
+from .discovery import setup_internal_discovery, discover_chromecast
_LOGGER = logging.getLogger(__name__)
CONF_IGNORE_CEC = "ignore_cec"
CAST_SPLASH = "https://home-assistant.io/images/cast/splash.png"
-DEFAULT_PORT = 8009
-
SUPPORT_CAST = (
SUPPORT_PAUSE
| SUPPORT_PLAY
@@ -62,24 +79,6 @@
| SUPPORT_VOLUME_SET
)
-# Stores a threading.Lock that is held by the internal pychromecast discovery.
-INTERNAL_DISCOVERY_RUNNING_KEY = "cast_discovery_running"
-# Stores all ChromecastInfo we encountered through discovery or config as a set
-# If we find a chromecast with a new host, the old one will be removed again.
-KNOWN_CHROMECAST_INFO_KEY = "cast_known_chromecasts"
-# Stores UUIDs of cast devices that were added as entities. Doesn't store
-# None UUIDs.
-ADDED_CAST_DEVICES_KEY = "cast_added_cast_devices"
-# Stores an audio group manager.
-CAST_MULTIZONE_MANAGER_KEY = "cast_multizone_manager"
-
-# Dispatcher signal fired with a ChromecastInfo every time we discover a new
-# Chromecast or receive it through configuration
-SIGNAL_CAST_DISCOVERED = "cast_discovered"
-
-# Dispatcher signal fired with a ChromecastInfo every time a Chromecast is
-# removed
-SIGNAL_CAST_REMOVED = "cast_removed"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -89,212 +88,6 @@
)
-@attr.s(slots=True, frozen=True)
-class ChromecastInfo:
- """Class to hold all data about a chromecast for creating connections.
-
- This also has the same attributes as the mDNS fields by zeroconf.
- """
-
- host = attr.ib(type=str)
- port = attr.ib(type=int)
- service = attr.ib(type=Optional[str], default=None)
- uuid = attr.ib(
- type=Optional[str], converter=attr.converters.optional(str), default=None
- ) # always convert UUID to string if not None
- manufacturer = attr.ib(type=str, default="")
- model_name = attr.ib(type=str, default="")
- friendly_name = attr.ib(type=Optional[str], default=None)
- is_dynamic_group = attr.ib(type=Optional[bool], default=None)
-
- @property
- def is_audio_group(self) -> bool:
- """Return if this is an audio group."""
- return self.port != DEFAULT_PORT
-
- @property
- def is_information_complete(self) -> bool:
- """Return if all information is filled out."""
- want_dynamic_group = self.is_audio_group
- have_dynamic_group = self.is_dynamic_group is not None
- have_all_except_dynamic_group = all(
- attr.astuple(
- self,
- filter=attr.filters.exclude(
- attr.fields(ChromecastInfo).is_dynamic_group
- ),
- )
- )
- return have_all_except_dynamic_group and (
- not want_dynamic_group or have_dynamic_group
- )
-
- @property
- def host_port(self) -> Tuple[str, int]:
- """Return the host+port tuple."""
- return self.host, self.port
-
-
-def _is_matching_dynamic_group(
- our_info: ChromecastInfo, new_info: ChromecastInfo
-) -> bool:
- return (
- our_info.is_audio_group
- and new_info.is_dynamic_group
- and our_info.friendly_name == new_info.friendly_name
- )
-
-
-def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo:
- """Fill out missing attributes of ChromecastInfo using blocking HTTP."""
- if info.is_information_complete:
- # We have all information, no need to check HTTP API. Or this is an
- # audio group, so checking via HTTP won't give us any new information.
- return info
-
- # Fill out missing information via HTTP dial.
- from pychromecast import dial
-
- if info.is_audio_group:
- is_dynamic_group = False
- http_group_status = None
- dynamic_groups = []
- if info.uuid:
- http_group_status = dial.get_multizone_status(
- info.host,
- services=[info.service],
- zconf=ChromeCastZeroconf.get_zeroconf(),
- )
- if http_group_status is not None:
- dynamic_groups = [str(g.uuid) for g in http_group_status.dynamic_groups]
- is_dynamic_group = info.uuid in dynamic_groups
-
- return ChromecastInfo(
- service=info.service,
- host=info.host,
- port=info.port,
- uuid=info.uuid,
- friendly_name=info.friendly_name,
- manufacturer=info.manufacturer,
- model_name=info.model_name,
- is_dynamic_group=is_dynamic_group,
- )
-
- http_device_status = dial.get_device_status(
- info.host, services=[info.service], zconf=ChromeCastZeroconf.get_zeroconf()
- )
- if http_device_status is None:
- # HTTP dial didn't give us any new information.
- return info
-
- return ChromecastInfo(
- service=info.service,
- host=info.host,
- port=info.port,
- uuid=(info.uuid or http_device_status.uuid),
- friendly_name=(info.friendly_name or http_device_status.friendly_name),
- manufacturer=(info.manufacturer or http_device_status.manufacturer),
- model_name=(info.model_name or http_device_status.model_name),
- )
-
-
-def _discover_chromecast(hass: HomeAssistantType, info: ChromecastInfo):
- if info in hass.data[KNOWN_CHROMECAST_INFO_KEY]:
- _LOGGER.debug("Discovered previous chromecast %s", info)
-
- # Either discovered completely new chromecast or a "moved" one.
- info = _fill_out_missing_chromecast_info(info)
- _LOGGER.debug("Discovered chromecast %s", info)
-
- if info.uuid is not None:
- # Remove previous cast infos with same uuid from known chromecasts.
- same_uuid = set(
- x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] if info.uuid == x.uuid
- )
- hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid
-
- hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info)
- dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info)
-
-
-def _remove_chromecast(hass: HomeAssistantType, info: ChromecastInfo):
- # Removed chromecast
- _LOGGER.debug("Removed chromecast %s", info)
-
- dispatcher_send(hass, SIGNAL_CAST_REMOVED, info)
-
-
-class ChromeCastZeroconf:
- """Class to hold a zeroconf instance."""
-
- __zconf = None
-
- @classmethod
- def set_zeroconf(cls, zconf):
- """Set zeroconf."""
- cls.__zconf = zconf
-
- @classmethod
- def get_zeroconf(cls):
- """Get zeroconf."""
- return cls.__zconf
-
-
-def _setup_internal_discovery(hass: HomeAssistantType) -> None:
- """Set up the pychromecast internal discovery."""
- if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data:
- hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock()
-
- if not hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].acquire(blocking=False):
- # Internal discovery is already running
- return
-
- import pychromecast
-
- def internal_add_callback(name):
- """Handle zeroconf discovery of a new chromecast."""
- mdns = listener.services[name]
- _discover_chromecast(
- hass,
- ChromecastInfo(
- service=name,
- host=mdns[0],
- port=mdns[1],
- uuid=mdns[2],
- model_name=mdns[3],
- friendly_name=mdns[4],
- ),
- )
-
- def internal_remove_callback(name, mdns):
- """Handle zeroconf discovery of a removed chromecast."""
- _remove_chromecast(
- hass,
- ChromecastInfo(
- service=name,
- host=mdns[0],
- port=mdns[1],
- uuid=mdns[2],
- model_name=mdns[3],
- friendly_name=mdns[4],
- ),
- )
-
- _LOGGER.debug("Starting internal pychromecast discovery.")
- listener, browser = pychromecast.start_discovery(
- internal_add_callback, internal_remove_callback
- )
- ChromeCastZeroconf.set_zeroconf(browser.zc)
-
- def stop_discovery(event):
- """Stop discovery of new chromecasts."""
- _LOGGER.debug("Stopping internal pychromecast discovery.")
- pychromecast.stop_discovery(browser)
- hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release()
-
- hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery)
-
-
@callback
def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo):
"""Create a CastDevice Entity from the chromecast object.
@@ -357,8 +150,6 @@ async def _async_setup_platform(
hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info
):
"""Set up the cast platform."""
- import pychromecast
-
# Import CEC IGNORE attributes
pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, [])
hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set())
@@ -390,9 +181,9 @@ def async_cast_discovered(discover: ChromecastInfo) -> None:
if info is None or info.is_audio_group:
# If we were a) explicitly told to enable discovery or
# b) have an audio group cast device, we need internal discovery.
- hass.async_add_job(_setup_internal_discovery, hass)
+ hass.async_add_executor_job(setup_internal_discovery, hass)
else:
- info = await hass.async_add_job(_fill_out_missing_chromecast_info, info)
+ info = await hass.async_add_executor_job(info.fill_out_missing_chromecast_info)
if info.friendly_name is None:
_LOGGER.debug(
"Cannot retrieve detail information for chromecast"
@@ -400,121 +191,7 @@ def async_cast_discovered(discover: ChromecastInfo) -> None:
info,
)
- hass.async_add_job(_discover_chromecast, hass, info)
-
-
-class CastStatusListener:
- """Helper class to handle pychromecast status callbacks.
-
- Necessary because a CastDevice entity can create a new socket client
- and therefore callbacks from multiple chromecast connections can
- potentially arrive. This class allows invalidating past chromecast objects.
- """
-
- def __init__(self, cast_device, chromecast, mz_mgr):
- """Initialize the status listener."""
- self._cast_device = cast_device
- self._uuid = chromecast.uuid
- self._valid = True
- self._mz_mgr = mz_mgr
-
- chromecast.register_status_listener(self)
- chromecast.socket_client.media_controller.register_status_listener(self)
- chromecast.register_connection_listener(self)
- # pylint: disable=protected-access
- if cast_device._cast_info.is_audio_group:
- self._mz_mgr.add_multizone(chromecast)
- else:
- self._mz_mgr.register_listener(chromecast.uuid, self)
-
- def new_cast_status(self, cast_status):
- """Handle reception of a new CastStatus."""
- if self._valid:
- self._cast_device.new_cast_status(cast_status)
-
- def new_media_status(self, media_status):
- """Handle reception of a new MediaStatus."""
- if self._valid:
- self._cast_device.new_media_status(media_status)
-
- def new_connection_status(self, connection_status):
- """Handle reception of a new ConnectionStatus."""
- if self._valid:
- self._cast_device.new_connection_status(connection_status)
-
- @staticmethod
- def added_to_multizone(group_uuid):
- """Handle the cast added to a group."""
- pass
-
- def removed_from_multizone(self, group_uuid):
- """Handle the cast removed from a group."""
- if self._valid:
- self._cast_device.multizone_new_media_status(group_uuid, None)
-
- def multizone_new_cast_status(self, group_uuid, cast_status):
- """Handle reception of a new CastStatus for a group."""
- pass
-
- def multizone_new_media_status(self, group_uuid, media_status):
- """Handle reception of a new MediaStatus for a group."""
- if self._valid:
- self._cast_device.multizone_new_media_status(group_uuid, media_status)
-
- def invalidate(self):
- """Invalidate this status listener.
-
- All following callbacks won't be forwarded.
- """
- # pylint: disable=protected-access
- if self._cast_device._cast_info.is_audio_group:
- self._mz_mgr.remove_multizone(self._uuid)
- else:
- self._mz_mgr.deregister_listener(self._uuid, self)
- self._valid = False
-
-
-class DynamicGroupCastStatusListener:
- """Helper class to handle pychromecast status callbacks.
-
- Necessary because a CastDevice entity can create a new socket client
- and therefore callbacks from multiple chromecast connections can
- potentially arrive. This class allows invalidating past chromecast objects.
- """
-
- def __init__(self, cast_device, chromecast, mz_mgr):
- """Initialize the status listener."""
- self._cast_device = cast_device
- self._uuid = chromecast.uuid
- self._valid = True
- self._mz_mgr = mz_mgr
-
- chromecast.register_status_listener(self)
- chromecast.socket_client.media_controller.register_status_listener(self)
- chromecast.register_connection_listener(self)
- self._mz_mgr.add_multizone(chromecast)
-
- def new_cast_status(self, cast_status):
- """Handle reception of a new CastStatus."""
- pass
-
- def new_media_status(self, media_status):
- """Handle reception of a new MediaStatus."""
- if self._valid:
- self._cast_device.new_dynamic_group_media_status(media_status)
-
- def new_connection_status(self, connection_status):
- """Handle reception of a new ConnectionStatus."""
- if self._valid:
- self._cast_device.new_dynamic_group_connection_status(connection_status)
-
- def invalidate(self):
- """Invalidate this status listener.
-
- All following callbacks won't be forwarded.
- """
- self._mz_mgr.remove_multizone(self._uuid)
- self._valid = False
+ hass.async_add_executor_job(discover_chromecast, hass, info)
class CastDevice(MediaPlayerDevice):
@@ -525,106 +202,51 @@ class CastDevice(MediaPlayerDevice):
"elected leader" itself.
"""
- def __init__(self, cast_info):
+ def __init__(self, cast_info: ChromecastInfo):
"""Initialize the cast device."""
- import pychromecast # noqa: pylint: disable=unused-import
- self._cast_info = cast_info # type: ChromecastInfo
+ self._cast_info = cast_info
self.services = None
if cast_info.service:
self.services = set()
self.services.add(cast_info.service)
- self._chromecast = None # type: Optional[pychromecast.Chromecast]
+ self._chromecast: Optional[pychromecast.Chromecast] = None
self.cast_status = None
self.media_status = None
self.media_status_received = None
- self._dynamic_group_cast_info = None # type: ChromecastInfo
- self._dynamic_group_cast = None # type: Optional[pychromecast.Chromecast]
+ self._dynamic_group_cast_info: ChromecastInfo = None
+ self._dynamic_group_cast: Optional[pychromecast.Chromecast] = None
self.dynamic_group_media_status = None
self.dynamic_group_media_status_received = None
self.mz_media_status = {}
self.mz_media_status_received = {}
self.mz_mgr = None
- self._available = False # type: bool
- self._dynamic_group_available = False # type: bool
- self._status_listener = None # type: Optional[CastStatusListener]
- self._dynamic_group_status_listener = (
- None
- ) # type: Optional[DynamicGroupCastStatusListener]
+ self._available = False
+ self._dynamic_group_available = False
+ self._status_listener: Optional[CastStatusListener] = None
+ self._dynamic_group_status_listener: Optional[
+ DynamicGroupCastStatusListener
+ ] = None
+ self._hass_cast_controller: Optional[HomeAssistantController] = None
+
self._add_remove_handler = None
self._del_remove_handler = None
+ self._cast_view_remove_handler = None
async def async_added_to_hass(self):
"""Create chromecast object when added to hass."""
-
- @callback
- def async_cast_discovered(discover: ChromecastInfo):
- """Handle discovery of new Chromecast."""
- if self._cast_info.uuid is None:
- # We can't handle empty UUIDs
- return
- if _is_matching_dynamic_group(self._cast_info, discover):
- _LOGGER.debug("Discovered matching dynamic group: %s", discover)
- self.hass.async_create_task(
- async_create_catching_coro(self.async_set_dynamic_group(discover))
- )
- return
-
- if self._cast_info.uuid != discover.uuid:
- # Discovered is not our device.
- return
- if self.services is None:
- _LOGGER.warning(
- "[%s %s (%s:%s)] Received update for manually added Cast",
- self.entity_id,
- self._cast_info.friendly_name,
- self._cast_info.host,
- self._cast_info.port,
- )
- return
- _LOGGER.debug("Discovered chromecast with same UUID: %s", discover)
- self.hass.async_create_task(
- async_create_catching_coro(self.async_set_cast_info(discover))
- )
-
- def async_cast_removed(discover: ChromecastInfo):
- """Handle removal of Chromecast."""
- if self._cast_info.uuid is None:
- # We can't handle empty UUIDs
- return
- if (
- self._dynamic_group_cast_info is not None
- and self._dynamic_group_cast_info.uuid == discover.uuid
- ):
- _LOGGER.debug("Removed matching dynamic group: %s", discover)
- self.hass.async_create_task(
- async_create_catching_coro(self.async_del_dynamic_group())
- )
- return
- if self._cast_info.uuid != discover.uuid:
- # Removed is not our device.
- return
- _LOGGER.debug("Removed chromecast with same UUID: %s", discover)
- self.hass.async_create_task(
- async_create_catching_coro(self.async_del_cast_info(discover))
- )
-
- async def async_stop(event):
- """Disconnect socket on Home Assistant stop."""
- await self._async_disconnect()
-
self._add_remove_handler = async_dispatcher_connect(
- self.hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered
+ self.hass, SIGNAL_CAST_DISCOVERED, self._async_cast_discovered
)
self._del_remove_handler = async_dispatcher_connect(
- self.hass, SIGNAL_CAST_REMOVED, async_cast_removed
+ self.hass, SIGNAL_CAST_REMOVED, self._async_cast_removed
)
- self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop)
+ self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop)
self.hass.async_create_task(
async_create_catching_coro(self.async_set_cast_info(self._cast_info))
)
for info in self.hass.data[KNOWN_CHROMECAST_INFO_KEY]:
- if _is_matching_dynamic_group(self._cast_info, info):
+ if self._cast_info.same_dynamic_group(info):
_LOGGER.debug(
"[%s %s (%s:%s)] Found dynamic group: %s",
self.entity_id,
@@ -638,6 +260,10 @@ async def async_stop(event):
)
break
+ self._cast_view_remove_handler = async_dispatcher_connect(
+ self.hass, SIGNAL_HASS_CAST_SHOW_VIEW, self._handle_signal_show_view
+ )
+
async def async_will_remove_from_hass(self) -> None:
"""Disconnect Chromecast object when removed."""
await self._async_disconnect()
@@ -647,12 +273,16 @@ async def async_will_remove_from_hass(self) -> None:
self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid)
if self._add_remove_handler:
self._add_remove_handler()
+ self._add_remove_handler = None
if self._del_remove_handler:
self._del_remove_handler()
+ self._del_remove_handler = None
+ if self._cast_view_remove_handler:
+ self._cast_view_remove_handler()
+ self._cast_view_remove_handler = None
async def async_set_cast_info(self, cast_info):
"""Set the cast information and set up the chromecast object."""
- import pychromecast
self._cast_info = cast_info
@@ -717,9 +347,8 @@ async def async_set_cast_info(self, cast_info):
self._chromecast = chromecast
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
- from pychromecast.controllers.multizone import MultizoneManager
-
self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager()
+
self.mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY]
self._status_listener = CastStatusListener(self, chromecast, self.mz_mgr)
@@ -744,7 +373,6 @@ async def async_del_cast_info(self, cast_info):
async def async_set_dynamic_group(self, cast_info):
"""Set the cast information and set up the chromecast object."""
- import pychromecast
_LOGGER.debug(
"[%s %s (%s:%s)] Connecting to dynamic group by host %s",
@@ -773,9 +401,8 @@ async def async_set_dynamic_group(self, cast_info):
self._dynamic_group_cast = chromecast
if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data:
- from pychromecast.controllers.multizone import MultizoneManager
-
self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager()
+
mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY]
self._dynamic_group_status_listener = DynamicGroupCastStatusListener(
@@ -839,6 +466,7 @@ def _invalidate(self):
self.mz_media_status = {}
self.mz_media_status_received = {}
self.mz_mgr = None
+ self._hass_cast_controller = None
if self._status_listener is not None:
self._status_listener.invalidate()
self._status_listener = None
@@ -866,11 +494,6 @@ def new_media_status(self, media_status):
def new_connection_status(self, connection_status):
"""Handle updates of connection status."""
- from pychromecast.socket_client import (
- CONNECTION_STATUS_CONNECTED,
- CONNECTION_STATUS_DISCONNECTED,
- )
-
_LOGGER.debug(
"[%s %s (%s:%s)] Received cast device connection status: %s",
self.entity_id,
@@ -901,7 +524,7 @@ def new_connection_status(self, connection_status):
info = self._cast_info
if info.friendly_name is None and not info.is_audio_group:
# We couldn't find friendly_name when the cast was added, retry
- self._cast_info = _fill_out_missing_chromecast_info(info)
+ self._cast_info = info.fill_out_missing_chromecast_info()
self._available = new_available
self.schedule_update_ha_state()
@@ -913,11 +536,6 @@ def new_dynamic_group_media_status(self, media_status):
def new_dynamic_group_connection_status(self, connection_status):
"""Handle updates of connection status."""
- from pychromecast.socket_client import (
- CONNECTION_STATUS_CONNECTED,
- CONNECTION_STATUS_DISCONNECTED,
- )
-
_LOGGER.debug(
"[%s %s (%s:%s)] Received dynamic group connection status: %s",
self.entity_id,
@@ -991,7 +609,6 @@ def _media_controller(self):
def turn_on(self):
"""Turn on the cast device."""
- import pychromecast
if not self._chromecast.is_idle:
# Already turned on
@@ -1276,3 +893,69 @@ def media_position_updated_at(self):
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return self._cast_info.uuid
+
+ async def _async_cast_discovered(self, discover: ChromecastInfo):
+ """Handle discovery of new Chromecast."""
+ if self._cast_info.uuid is None:
+ # We can't handle empty UUIDs
+ return
+
+ if self._cast_info.same_dynamic_group(discover):
+ _LOGGER.debug("Discovered matching dynamic group: %s", discover)
+ await self.async_set_dynamic_group(discover)
+ return
+
+ if self._cast_info.uuid != discover.uuid:
+ # Discovered is not our device.
+ return
+
+ if self.services is None:
+ _LOGGER.warning(
+ "[%s %s (%s:%s)] Received update for manually added Cast",
+ self.entity_id,
+ self._cast_info.friendly_name,
+ self._cast_info.host,
+ self._cast_info.port,
+ )
+ return
+
+ _LOGGER.debug("Discovered chromecast with same UUID: %s", discover)
+ await self.async_set_cast_info(discover)
+
+ async def _async_cast_removed(self, discover: ChromecastInfo):
+ """Handle removal of Chromecast."""
+ if self._cast_info.uuid is None:
+ # We can't handle empty UUIDs
+ return
+
+ if (
+ self._dynamic_group_cast_info is not None
+ and self._dynamic_group_cast_info.uuid == discover.uuid
+ ):
+ _LOGGER.debug("Removed matching dynamic group: %s", discover)
+ await self.async_del_dynamic_group()
+ return
+
+ if self._cast_info.uuid != discover.uuid:
+ # Removed is not our device.
+ return
+
+ _LOGGER.debug("Removed chromecast with same UUID: %s", discover)
+ await self.async_del_cast_info(discover)
+
+ async def _async_stop(self, event):
+ """Disconnect socket on Home Assistant stop."""
+ await self._async_disconnect()
+
+ def _handle_signal_show_view(
+ self, controller: HomeAssistantController, entity_id: str, view_path: str
+ ):
+ """Handle a show view signal."""
+ if entity_id != self.entity_id:
+ return
+
+ if self._hass_cast_controller is None:
+ self._hass_cast_controller = controller
+ self._chromecast.register_handler(controller)
+
+ self._hass_cast_controller.show_lovelace_view(view_path)
diff --git a/homeassistant/components/cast/services.yaml b/homeassistant/components/cast/services.yaml
new file mode 100644
index 00000000000000..24bc7b16a903c7
--- /dev/null
+++ b/homeassistant/components/cast/services.yaml
@@ -0,0 +1,9 @@
+show_lovelace_view:
+ description: Show a Lovelace view on a Chromecast.
+ fields:
+ entity_id:
+ description: Media Player entity to show the Lovelace view on.
+ example: "media_player.kitchen"
+ view_path:
+ description: The path of the Lovelace view to show.
+ example: downstairs
diff --git a/homeassistant/components/cert_expiry/.translations/de.json b/homeassistant/components/cert_expiry/.translations/de.json
new file mode 100644
index 00000000000000..344abe13067c42
--- /dev/null
+++ b/homeassistant/components/cert_expiry/.translations/de.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "host_port_exists": "Diese Kombination aus Host und Port ist bereits konfiguriert."
+ },
+ "error": {
+ "certificate_fetch_failed": "Zertifikat kann von dieser Kombination aus Host und Port nicht abgerufen werden",
+ "connection_timeout": "Zeit\u00fcberschreitung beim Herstellen einer Verbindung mit diesem Host",
+ "host_port_exists": "Diese Kombination aus Host und Port ist bereits konfiguriert.",
+ "resolve_failed": "Dieser Host kann nicht aufgel\u00f6st werden"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Der Hostname des Zertifikats",
+ "name": "Der Name des Zertifikats",
+ "port": "Der Port des Zertifikats"
+ },
+ "title": "Definieren Sie das zu testende Zertifikat"
+ }
+ },
+ "title": "Zertifikatsablauf"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json
index b6aa1cefb02aed..873dfee9a92bb9 100644
--- a/homeassistant/components/cert_expiry/.translations/en.json
+++ b/homeassistant/components/cert_expiry/.translations/en.json
@@ -5,7 +5,7 @@
},
"error": {
"certificate_fetch_failed": "Can not fetch certificate from this host and port combination",
- "connection_timeout": "Timeout whemn connecting to this host",
+ "connection_timeout": "Timeout when connecting to this host",
"host_port_exists": "This host and port combination is already configured",
"resolve_failed": "This host can not be resolved"
},
diff --git a/homeassistant/components/cert_expiry/.translations/es.json b/homeassistant/components/cert_expiry/.translations/es.json
new file mode 100644
index 00000000000000..b10518646ac907
--- /dev/null
+++ b/homeassistant/components/cert_expiry/.translations/es.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada"
+ },
+ "error": {
+ "certificate_fetch_failed": "No se puede obtener el certificado de esta combinaci\u00f3n de host y puerto",
+ "connection_timeout": "Tiempo de espera agotado al conectar a este host",
+ "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada",
+ "resolve_failed": "Este host no se puede resolver"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "El nombre de host del certificado",
+ "name": "El nombre del certificado",
+ "port": "El puerto del certificado"
+ },
+ "title": "Defina el certificado para probar"
+ }
+ },
+ "title": "Caducidad del certificado"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cert_expiry/.translations/fr.json b/homeassistant/components/cert_expiry/.translations/fr.json
new file mode 100644
index 00000000000000..a3536902c76d26
--- /dev/null
+++ b/homeassistant/components/cert_expiry/.translations/fr.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e"
+ },
+ "error": {
+ "certificate_fetch_failed": "Impossible de r\u00e9cup\u00e9rer le certificat de cette combinaison h\u00f4te / port",
+ "connection_timeout": "D\u00e9lai d'attente lors de la connexion \u00e0 cet h\u00f4te",
+ "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e",
+ "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Le nom d'h\u00f4te du certificat",
+ "name": "Le nom du certificat",
+ "port": "Le port du certificat"
+ },
+ "title": "D\u00e9finir le certificat \u00e0 tester"
+ }
+ },
+ "title": "Expiration du certificat"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cert_expiry/.translations/it.json b/homeassistant/components/cert_expiry/.translations/it.json
new file mode 100644
index 00000000000000..73749382dd9bca
--- /dev/null
+++ b/homeassistant/components/cert_expiry/.translations/it.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata"
+ },
+ "error": {
+ "certificate_fetch_failed": "Non \u00e8 possibile recuperare il certificato da questa combinazione di host e porta",
+ "connection_timeout": "Tempo scaduto collegandosi a questo host",
+ "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata",
+ "resolve_failed": "Questo host non pu\u00f2 essere risolto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "L'hostname del certificato",
+ "name": "Il nome del certificato",
+ "port": "La porta del certificato"
+ },
+ "title": "Definire il certificato da testare"
+ }
+ },
+ "title": "Scadenza certificato"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cert_expiry/.translations/ko.json b/homeassistant/components/cert_expiry/.translations/ko.json
new file mode 100644
index 00000000000000..a807d32a6fbaaa
--- /dev/null
+++ b/homeassistant/components/cert_expiry/.translations/ko.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "certificate_fetch_failed": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uc5d0\uc11c \uc778\uc99d\uc11c\ub97c \uac00\uc838 \uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "connection_timeout": "\ud638\uc2a4\ud2b8 \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4",
+ "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "resolve_failed": "\ud638\uc2a4\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\uc778\uc99d\uc11c\uc758 \ud638\uc2a4\ud2b8 \uc774\ub984",
+ "name": "\uc778\uc99d\uc11c\uc758 \uc774\ub984",
+ "port": "\uc778\uc99d\uc11c\uc758 \ud3ec\ud2b8"
+ },
+ "title": "\uc778\uc99d\uc11c \uc815\uc758 \ud14c\uc2a4\ud2b8 \ub300\uc0c1"
+ }
+ },
+ "title": "\uc778\uc99d\uc11c \ub9cc\ub8cc"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cert_expiry/.translations/lb.json b/homeassistant/components/cert_expiry/.translations/lb.json
new file mode 100644
index 00000000000000..9620526e363d9d
--- /dev/null
+++ b/homeassistant/components/cert_expiry/.translations/lb.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert"
+ },
+ "error": {
+ "certificate_fetch_failed": "Kann keen Zertifikat vun d\u00ebsen Host a Port recuper\u00e9ieren",
+ "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen.",
+ "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert",
+ "resolve_failed": "D\u00ebsen Host kann net opgel\u00e9ist ginn"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Den Hostnumm vum Zertifikat",
+ "name": "De Numm vum Zertifikat",
+ "port": "De Port vum Zertifikat"
+ },
+ "title": "W\u00e9ieen Zertifikat soll getest ginn"
+ }
+ },
+ "title": "Zertifikat Verfallsdatum"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cert_expiry/.translations/no.json b/homeassistant/components/cert_expiry/.translations/no.json
index e095cc360a0f4e..73e899106c1063 100644
--- a/homeassistant/components/cert_expiry/.translations/no.json
+++ b/homeassistant/components/cert_expiry/.translations/no.json
@@ -5,7 +5,7 @@
},
"error": {
"certificate_fetch_failed": "Kan ikke hente sertifikat fra denne verts- og portkombinasjonen",
- "connection_timeout": "Timeout n\u00e5r det kobles til denne verten",
+ "connection_timeout": "Tidsavbrudd n\u00e5r du kobler til denne verten",
"host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert",
"resolve_failed": "Denne verten kan ikke l\u00f8ses"
},
diff --git a/homeassistant/components/cert_expiry/.translations/sl.json b/homeassistant/components/cert_expiry/.translations/sl.json
new file mode 100644
index 00000000000000..3774956330a52a
--- /dev/null
+++ b/homeassistant/components/cert_expiry/.translations/sl.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana"
+ },
+ "error": {
+ "certificate_fetch_failed": "Iz te kombinacije gostitelja in vrat ni mogo\u010de pridobiti potrdila",
+ "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem je potekla",
+ "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana",
+ "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Ime gostitelja potrdila",
+ "name": "Ime potrdila",
+ "port": "Vrata potrdila"
+ },
+ "title": "Dolo\u010dite potrdilo za testiranje"
+ }
+ },
+ "title": "Veljavnost certifikata"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cert_expiry/.translations/zh-Hans.json b/homeassistant/components/cert_expiry/.translations/zh-Hans.json
new file mode 100644
index 00000000000000..07affc990a8173
--- /dev/null
+++ b/homeassistant/components/cert_expiry/.translations/zh-Hans.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "connection_timeout": "\u8fde\u63a5\u5230\u6b64\u4e3b\u673a\u65f6\u7684\u8d85\u65f6"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u8bc1\u4e66\u7684\u4e3b\u673a\u540d",
+ "name": "\u8bc1\u4e66\u7684\u540d\u79f0",
+ "port": "\u8bc1\u4e66\u7684\u7aef\u53e3"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py
index ab68d5ba08bc43..7c7efea7333120 100644
--- a/homeassistant/components/cert_expiry/__init__.py
+++ b/homeassistant/components/cert_expiry/__init__.py
@@ -1,7 +1,5 @@
"""The cert_expiry component."""
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import EVENT_HOMEASSISTANT_START
-from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@@ -13,13 +11,7 @@ async def async_setup(hass, config):
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Load the saved entities."""
- @callback
- def async_start(_):
- """Load the entry after the start event."""
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, "sensor")
- )
-
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start)
-
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, "sensor")
+ )
return True
diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py
index dd3463fff95c4e..d73762ce882647 100644
--- a/homeassistant/components/cert_expiry/config_flow.py
+++ b/homeassistant/components/cert_expiry/config_flow.py
@@ -38,10 +38,12 @@ def _prt_in_configuration_exists(self, user_input) -> bool:
return True
return False
- def _test_connection(self, user_input=None):
+ async def _test_connection(self, user_input=None):
"""Test connection to the server and try to get the certtificate."""
try:
- get_cert(user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT))
+ await self.hass.async_add_executor_job(
+ get_cert, user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT)
+ )
return True
except socket.gaierror:
self._errors[CONF_HOST] = "resolve_failed"
@@ -59,7 +61,7 @@ async def async_step_user(self, user_input=None):
if self._prt_in_configuration_exists(user_input):
self._errors[CONF_HOST] = "host_port_exists"
else:
- if self._test_connection(user_input):
+ if await self._test_connection(user_input):
host = user_input[CONF_HOST]
name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME))
prt = user_input.get(CONF_PORT, DEFAULT_PORT)
diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py
index fccfb295c0fff2..b564cff7338584 100644
--- a/homeassistant/components/cert_expiry/sensor.py
+++ b/homeassistant/components/cert_expiry/sensor.py
@@ -9,7 +9,12 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT
+from homeassistant.const import (
+ CONF_NAME,
+ CONF_HOST,
+ CONF_PORT,
+ EVENT_HOMEASSISTANT_START,
+)
from homeassistant.helpers.entity import Entity
from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT
@@ -82,6 +87,15 @@ def available(self):
"""Icon to use in the frontend, if any."""
return self._available
+ async def async_added_to_hass(self):
+ """Once the entity is added we should update to get the initial data loaded."""
+
+ def do_update(_):
+ """Run the update method when the start event was fired."""
+ self.update()
+
+ self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update)
+
def update(self):
"""Fetch the certificate information."""
try:
diff --git a/homeassistant/components/cert_expiry/strings.json b/homeassistant/components/cert_expiry/strings.json
index 8943643e8b392e..3e2fea2342e260 100644
--- a/homeassistant/components/cert_expiry/strings.json
+++ b/homeassistant/components/cert_expiry/strings.json
@@ -14,7 +14,7 @@
"error": {
"host_port_exists": "This host and port combination is already configured",
"resolve_failed": "This host can not be resolved",
- "connection_timeout": "Timeout whemn connecting to this host",
+ "connection_timeout": "Timeout when connecting to this host",
"certificate_fetch_failed": "Can not fetch certificate from this host and port combination"
},
"abort": {
diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py
index 68f0d77e307a82..e86ec02040e3b3 100644
--- a/homeassistant/components/concord232/alarm_control_panel.py
+++ b/homeassistant/components/concord232/alarm_control_panel.py
@@ -69,7 +69,6 @@ def __init__(self, url, name, code, mode):
self._url = url
self._alarm = concord232_client.Client(self._url)
self._alarm.partitions = self._alarm.list_partitions()
- self._alarm.last_partition_update = datetime.datetime.now()
@property
def name(self):
diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py
index 10643f134d70e8..1a406d743b7cfa 100644
--- a/homeassistant/components/concord232/binary_sensor.py
+++ b/homeassistant/components/concord232/binary_sensor.py
@@ -12,6 +12,7 @@
)
from homeassistant.const import CONF_HOST, CONF_PORT
import homeassistant.helpers.config_validation as cv
+import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -53,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.debug("Initializing client")
client = concord232_client.Client(f"http://{host}:{port}")
client.zones = client.list_zones()
- client.last_zone_update = datetime.datetime.now()
+ client.last_zone_update = dt_util.utcnow()
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
@@ -128,11 +129,11 @@ def is_on(self):
def update(self):
"""Get updated stats from API."""
- last_update = datetime.datetime.now() - self._client.last_zone_update
+ last_update = dt_util.utcnow() - self._client.last_zone_update
_LOGGER.debug("Zone: %s ", self._zone)
if last_update > datetime.timedelta(seconds=1):
self._client.zones = self._client.list_zones()
- self._client.last_zone_update = datetime.datetime.now()
+ self._client.last_zone_update = dt_util.utcnow()
_LOGGER.debug("Updated from zone: %s", self._zone["name"])
if hasattr(self._client, "zones"):
diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py
index 7ca71fc4f93dfb..17efdba3fb573f 100644
--- a/homeassistant/components/config/automation.py
+++ b/homeassistant/components/config/automation.py
@@ -53,7 +53,7 @@ def _write_value(self, hass, data, config_key, new_value):
# Iterate through some keys that we want to have ordered in the output
updated_value = OrderedDict()
- for key in ("id", "alias", "trigger", "condition", "action"):
+ for key in ("id", "alias", "description", "trigger", "condition", "action"):
if key in cur_value:
updated_value[key] = cur_value[key]
if key in new_value:
diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py
index d491765bb00f57..8d2b4430fe110c 100644
--- a/homeassistant/components/cover/__init__.py
+++ b/homeassistant/components/cover/__init__.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import functools as ft
import logging
+from typing import Any
import voluptuous as vol
@@ -33,7 +34,7 @@
)
-# mypy: allow-untyped-calls, allow-incomplete-defs, allow-untyped-defs
+# mypy: allow-untyped-calls, allow-untyped-defs
_LOGGER = logging.getLogger(__name__)
@@ -263,7 +264,7 @@ def is_closed(self):
"""Return if the cover is closed or not."""
raise NotImplementedError()
- def open_cover(self, **kwargs):
+ def open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
raise NotImplementedError()
@@ -274,7 +275,7 @@ def async_open_cover(self, **kwargs):
"""
return self.hass.async_add_job(ft.partial(self.open_cover, **kwargs))
- def close_cover(self, **kwargs):
+ def close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
raise NotImplementedError()
@@ -285,7 +286,7 @@ def async_close_cover(self, **kwargs):
"""
return self.hass.async_add_job(ft.partial(self.close_cover, **kwargs))
- def toggle(self, **kwargs) -> None:
+ def toggle(self, **kwargs: Any) -> None:
"""Toggle the entity."""
if self.is_closed:
self.open_cover(**kwargs)
@@ -323,7 +324,7 @@ def async_stop_cover(self, **kwargs):
"""
return self.hass.async_add_job(ft.partial(self.stop_cover, **kwargs))
- def open_cover_tilt(self, **kwargs):
+ def open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
pass
@@ -334,7 +335,7 @@ def async_open_cover_tilt(self, **kwargs):
"""
return self.hass.async_add_job(ft.partial(self.open_cover_tilt, **kwargs))
- def close_cover_tilt(self, **kwargs):
+ def close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
pass
@@ -369,7 +370,7 @@ def async_stop_cover_tilt(self, **kwargs):
"""
return self.hass.async_add_job(ft.partial(self.stop_cover_tilt, **kwargs))
- def toggle_tilt(self, **kwargs) -> None:
+ def toggle_tilt(self, **kwargs: Any) -> None:
"""Toggle the entity."""
if self.current_cover_tilt_position == 0:
self.open_cover_tilt(**kwargs)
diff --git a/homeassistant/components/daikin/.translations/ko.json b/homeassistant/components/daikin/.translations/ko.json
index 2291d46800d849..4b1d1bd86e5b8a 100644
--- a/homeassistant/components/daikin/.translations/ko.json
+++ b/homeassistant/components/daikin/.translations/ko.json
@@ -10,10 +10,10 @@
"data": {
"host": "\ud638\uc2a4\ud2b8"
},
- "description": "Daikin AC \uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
- "title": "Daikin AC \uad6c\uc131"
+ "description": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8 \uad6c\uc131"
}
},
- "title": "Daikin AC"
+ "title": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py
index 4f000253245f1f..d4e7e7ec63a97c 100644
--- a/homeassistant/components/darksky/sensor.py
+++ b/homeassistant/components/darksky/sensor.py
@@ -371,7 +371,7 @@
CONDITION_PICTURES = {
"clear-day": ["/static/images/darksky/weather-sunny.svg", "mdi:weather-sunny"],
- "clear-night": ["/static/images/darksky/weather-night.svg", "mdi:weather-sunny"],
+ "clear-night": ["/static/images/darksky/weather-night.svg", "mdi:weather-night"],
"rain": ["/static/images/darksky/weather-pouring.svg", "mdi:weather-pouring"],
"snow": ["/static/images/darksky/weather-snowy.svg", "mdi:weather-snowy"],
"sleet": ["/static/images/darksky/weather-hail.svg", "mdi:weather-snowy-rainy"],
diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json
index a02a6ff422338f..f3eead4aae023a 100644
--- a/homeassistant/components/deconz/.translations/bg.json
+++ b/homeassistant/components/deconz/.translations/bg.json
@@ -40,5 +40,34 @@
}
},
"title": "deCONZ"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "\u0418 \u0434\u0432\u0430\u0442\u0430 \u0431\u0443\u0442\u043e\u043d\u0430",
+ "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d",
+ "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d",
+ "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d",
+ "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d",
+ "close": "\u0417\u0430\u0442\u0432\u0430\u0440\u044f\u043d\u0435",
+ "dim_down": "\u0417\u0430\u0442\u044a\u043c\u043d\u044f\u0432\u0430\u043d\u0435",
+ "dim_up": "\u041e\u0441\u0432\u0435\u0442\u044f\u0432\u0430\u043d\u0435",
+ "left": "\u041b\u044f\u0432\u043e",
+ "open": "\u041e\u0442\u0432\u0430\u0440\u044f\u043d\u0435",
+ "right": "\u0414\u044f\u0441\u043d\u043e",
+ "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438",
+ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e",
+ "remote_button_long_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e",
+ "remote_button_long_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435",
+ "remote_button_quadruple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e",
+ "remote_button_quintuple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e",
+ "remote_button_rotated": "\u0417\u0430\u0432\u044a\u0440\u0442\u044f\u043d \u0431\u0443\u0442\u043e\u043d \"{subtype}\"",
+ "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442",
+ "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442",
+ "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e",
+ "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json
index 56ae59c78ba031..d36de4acc1e96c 100644
--- a/homeassistant/components/deconz/.translations/ca.json
+++ b/homeassistant/components/deconz/.translations/ca.json
@@ -41,6 +41,35 @@
},
"title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee"
},
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "Ambd\u00f3s botons",
+ "button_1": "Primer bot\u00f3",
+ "button_2": "Segon bot\u00f3",
+ "button_3": "Tercer bot\u00f3",
+ "button_4": "Quart bot\u00f3",
+ "close": "Tanca",
+ "dim_down": "Atenua la brillantor",
+ "dim_up": "Augmenta la brillantor",
+ "left": "Esquerra",
+ "open": "Obert",
+ "right": "Dreta",
+ "turn_off": "Desactiva",
+ "turn_on": "Activa"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades consecutives",
+ "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut continuament",
+ "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut",
+ "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades consecutives",
+ "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades consecutives",
+ "remote_button_rotated": "Bot\u00f3 \"{subtype}\" girat",
+ "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut",
+ "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat",
+ "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives",
+ "remote_gyro_activated": "Dispositiu sacsejat"
+ }
+ },
"options": {
"step": {
"async_step_deconz_devices": {
@@ -48,7 +77,14 @@
"allow_clip_sensor": "Permet sensors deCONZ CLIP",
"allow_deconz_groups": "Permet grups de llums deCONZ"
},
- "description": "Configura la visibilitat dels tipus de dispositius deCONZ"
+ "description": "Configura la visibilitat dels tipus dels dispositius deCONZ"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "Permet sensors deCONZ CLIP",
+ "allow_deconz_groups": "Permet grups de llums deCONZ"
+ },
+ "description": "Configura la visibilitat dels tipus dels dispositius deCONZ"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json
index 1b595924106cfd..6b74c09107a098 100644
--- a/homeassistant/components/deconz/.translations/da.json
+++ b/homeassistant/components/deconz/.translations/da.json
@@ -41,6 +41,24 @@
},
"title": "deCONZ Zigbee gateway"
},
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "Begge knapper",
+ "button_1": "F\u00f8rste knap",
+ "button_2": "Anden knap",
+ "button_3": "Tredje knap",
+ "button_4": "Fjerde knap",
+ "close": "Luk",
+ "dim_down": "D\u00e6mp ned",
+ "dim_up": "D\u00e6mp op",
+ "left": "Venstre",
+ "open": "\u00c5ben",
+ "right": "H\u00f8jre"
+ },
+ "trigger_type": {
+ "remote_gyro_activated": "Enhed rystet"
+ }
+ },
"options": {
"step": {
"async_step_deconz_devices": {
diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json
index 5902d2a3bf3619..97e25e28965c5f 100644
--- a/homeassistant/components/deconz/.translations/de.json
+++ b/homeassistant/components/deconz/.translations/de.json
@@ -49,6 +49,13 @@
"allow_deconz_groups": "deCONZ-Lichtgruppen zulassen"
},
"description": "Konfigurieren der Sichtbarkeit von deCONZ-Ger\u00e4tetypen"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "deCONZ CLIP-Sensoren zulassen",
+ "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen"
+ },
+ "description": "Sichtbarkeit der deCONZ-Ger\u00e4tetypen konfigurieren"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json
index 272a6f5d1be2ed..ead71db8c27d89 100644
--- a/homeassistant/components/deconz/.translations/en.json
+++ b/homeassistant/components/deconz/.translations/en.json
@@ -41,6 +41,35 @@
},
"title": "deCONZ Zigbee gateway"
},
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "Both buttons",
+ "button_1": "First button",
+ "button_2": "Second button",
+ "button_3": "Third button",
+ "button_4": "Fourth button",
+ "close": "Close",
+ "dim_down": "Dim down",
+ "dim_up": "Dim up",
+ "left": "Left",
+ "open": "Open",
+ "right": "Right",
+ "turn_off": "Turn off",
+ "turn_on": "Turn on"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "\"{subtype}\" button double clicked",
+ "remote_button_long_press": "\"{subtype}\" button continuously pressed",
+ "remote_button_long_release": "\"{subtype}\" button released after long press",
+ "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked",
+ "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked",
+ "remote_button_rotated": "Button rotated \"{subtype}\"",
+ "remote_button_short_press": "\"{subtype}\" button pressed",
+ "remote_button_short_release": "\"{subtype}\" button released",
+ "remote_button_triple_press": "\"{subtype}\" button triple clicked",
+ "remote_gyro_activated": "Device shaken"
+ }
+ },
"options": {
"step": {
"async_step_deconz_devices": {
diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json
index 8bcf03914cee84..1bc6c8211a268d 100644
--- a/homeassistant/components/deconz/.translations/es.json
+++ b/homeassistant/components/deconz/.translations/es.json
@@ -2,7 +2,9 @@
"config": {
"abort": {
"already_configured": "El puente ya esta configurado",
+ "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en curso.",
"no_bridges": "No se han descubierto puentes deCONZ",
+ "not_deconz_bridge": "No es un puente deCONZ",
"one_instance_only": "El componente solo admite una instancia de deCONZ",
"updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host"
},
@@ -39,12 +41,43 @@
},
"title": "Pasarela Zigbee deCONZ"
},
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "Ambos botones",
+ "button_1": "Primer bot\u00f3n",
+ "button_2": "Segundo bot\u00f3n",
+ "button_3": "Tercer bot\u00f3n",
+ "button_4": "Cuarto bot\u00f3n",
+ "close": "Cerrar",
+ "dim_down": "Bajar la intensidad",
+ "dim_up": "Subir la intensidad",
+ "left": "Izquierda",
+ "open": "Abierto",
+ "right": "Derecha",
+ "turn_off": "Apagar",
+ "turn_on": "Encender"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces consecutivas",
+ "remote_button_long_press": "bot\u00f3n \"{subtype}\" pulsado continuamente",
+ "remote_button_long_release": "Bot\u00f3n \"{subtype}\" liberado despu\u00e9s de un rato pulsado",
+ "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas",
+ "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas",
+ "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado",
+ "remote_button_short_press": "bot\u00f3n \"{subtype}\" pulsado",
+ "remote_button_short_release": "bot\u00f3n \"{subtype}\" liberado",
+ "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas",
+ "remote_gyro_activated": "Dispositivo sacudido"
+ }
+ },
"options": {
"step": {
"async_step_deconz_devices": {
"data": {
+ "allow_clip_sensor": "Permitir sensores deCONZ CLIP",
"allow_deconz_groups": "Permitir grupos de luz deCONZ"
- }
+ },
+ "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ"
},
"deconz_devices": {
"data": {
diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json
index 9b98914314a749..cc6d22945dcb79 100644
--- a/homeassistant/components/deconz/.translations/fr.json
+++ b/homeassistant/components/deconz/.translations/fr.json
@@ -40,5 +40,52 @@
}
},
"title": "Passerelle deCONZ Zigbee"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "Les deux boutons",
+ "button_1": "Premier bouton",
+ "button_2": "Deuxi\u00e8me bouton",
+ "button_3": "Troisi\u00e8me bouton",
+ "button_4": "Quatri\u00e8me bouton",
+ "close": "Ferm\u00e9",
+ "dim_down": "Assombrir",
+ "dim_up": "\u00c9claircir",
+ "left": "Gauche",
+ "open": "Ouvert",
+ "right": "Droite",
+ "turn_off": "\u00c9teint",
+ "turn_on": "Allum\u00e9"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "Bouton \"{subtype}\" double cliqu\u00e9",
+ "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement",
+ "remote_button_long_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9 apr\u00e8s appui long",
+ "remote_button_quadruple_press": "Bouton \"{subtype}\" quadruple cliqu\u00e9",
+ "remote_button_quintuple_press": "Bouton \"{subtype}\" quintuple cliqu\u00e9",
+ "remote_button_rotated": "Bouton \"{subtype}\" tourn\u00e9",
+ "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9",
+ "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9",
+ "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9",
+ "remote_gyro_activated": "Appareil secou\u00e9"
+ }
+ },
+ "options": {
+ "step": {
+ "async_step_deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP",
+ "allow_deconz_groups": "Autoriser les groupes de lumi\u00e8res deCONZ"
+ },
+ "description": "Configurer la visibilit\u00e9 des appareils de type deCONZ"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP",
+ "allow_deconz_groups": "Autoriser les groupes de lumi\u00e8res deCONZ"
+ },
+ "description": "Configurer la visibilit\u00e9 des appareils de type deCONZ"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json
index f15a2ddf26536a..7a2b8832864e2b 100644
--- a/homeassistant/components/deconz/.translations/it.json
+++ b/homeassistant/components/deconz/.translations/it.json
@@ -2,7 +2,9 @@
"config": {
"abort": {
"already_configured": "Il Bridge \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Il flusso di configurazione per bridge \u00e8 gi\u00e0 in corso.",
"no_bridges": "Nessun bridge deCONZ rilevato",
+ "not_deconz_bridge": "Non \u00e8 un bridge deCONZ",
"one_instance_only": "Il componente supporto solo un'istanza di deCONZ",
"updated_instance": "Istanza deCONZ aggiornata con nuovo indirizzo host"
},
@@ -21,12 +23,12 @@
"init": {
"data": {
"host": "Host",
- "port": "Porta (valore di default: '80')"
+ "port": "Porta"
},
"title": "Definisci il gateway deCONZ"
},
"link": {
- "description": "Sblocca il tuo gateway deCONZ per registrarlo in Home Assistant.\n\n1. Vai nelle impostazioni di sistema di deCONZ\n2. Premi il bottone \"Unlock Gateway\"",
+ "description": "Sblocca il tuo gateway deCONZ per registrarti con Home Assistant.\n\n1. Vai a Impostazioni deCONZ -> Gateway -> Avanzate\n2. Premere il pulsante \"Autentica app\"",
"title": "Collega con deCONZ"
},
"options": {
@@ -38,5 +40,52 @@
}
},
"title": "Gateway Zigbee deCONZ"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "Entrambi",
+ "button_1": "Primo",
+ "button_2": "Secondo pulsante",
+ "button_3": "Terzo pulsante",
+ "button_4": "Quarto pulsante",
+ "close": "Chiudere",
+ "dim_down": "Diminuire luminosit\u00e0",
+ "dim_up": "Aumentare luminosit\u00e0",
+ "left": "Sinistra",
+ "open": "Aperto",
+ "right": "Destra",
+ "turn_off": "Spegnere",
+ "turn_on": "Accendere"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte",
+ "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente",
+ "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione",
+ "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte",
+ "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte",
+ "remote_button_rotated": "Pulsante ruotato \"{subtype}\"",
+ "remote_button_short_press": "Pulsante \"{subtype}\" premuto",
+ "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato",
+ "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte",
+ "remote_gyro_activated": "Dispositivo in vibrazione"
+ }
+ },
+ "options": {
+ "step": {
+ "async_step_deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "Consentire sensori CLIP deCONZ",
+ "allow_deconz_groups": "Consentire gruppi luce deCONZ"
+ },
+ "description": "Configurare la visibilit\u00e0 dei tipi di dispositivi deCONZ"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "Consentire sensori CLIP deCONZ",
+ "allow_deconz_groups": "Consentire gruppi luce deCONZ"
+ },
+ "description": "Configurare la visibilit\u00e0 dei tipi di dispositivi deCONZ"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json
index 4bf845d50e5fd2..923a2beb2ffba2 100644
--- a/homeassistant/components/deconz/.translations/ko.json
+++ b/homeassistant/components/deconz/.translations/ko.json
@@ -40,5 +40,52 @@
}
},
"title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "\ub450 \uac1c",
+ "button_1": "\uccab \ubc88\uc9f8",
+ "button_2": "\ub450 \ubc88\uc9f8",
+ "button_3": "\uc138 \ubc88\uc9f8",
+ "button_4": "\ub124 \ubc88\uc9f8",
+ "close": "\ub2eb\uae30",
+ "dim_down": "\uc5b4\ub461\uac8c \ud558\uae30",
+ "dim_up": "\ubc1d\uac8c \ud558\uae30",
+ "left": "\uc67c\ucabd",
+ "open": "\uc5f4\uae30",
+ "right": "\uc624\ub978\ucabd",
+ "turn_off": "\ub044\uae30",
+ "turn_on": "\ucf1c\uae30"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984",
+ "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984",
+ "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc",
+ "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984",
+ "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984",
+ "remote_button_rotated": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804",
+ "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984",
+ "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc",
+ "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984",
+ "remote_gyro_activated": "\uae30\uae30 \ud754\ub4e6"
+ }
+ },
+ "options": {
+ "step": {
+ "async_step_deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "deCONZ CLIP \uc13c\uc11c \ud5c8\uc6a9",
+ "allow_deconz_groups": "deCONZ \ub77c\uc774\ud2b8 \uadf8\ub8f9 \ud5c8\uc6a9"
+ },
+ "description": "deCONZ \uae30\uae30 \uc720\ud615\uc758 \ud45c\uc2dc \uc5ec\ubd80 \uad6c\uc131"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "deCONZ CLIP \uc13c\uc11c \ud5c8\uc6a9",
+ "allow_deconz_groups": "deCONZ \ub77c\uc774\ud2b8 \uadf8\ub8f9 \ud5c8\uc6a9"
+ },
+ "description": "deCONZ \uae30\uae30 \uc720\ud615\uc758 \ud45c\uc2dc \uc5ec\ubd80 \uad6c\uc131"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json
index 60a27304d78437..1a03143f11edfd 100644
--- a/homeassistant/components/deconz/.translations/lb.json
+++ b/homeassistant/components/deconz/.translations/lb.json
@@ -23,7 +23,7 @@
"init": {
"data": {
"host": "Host",
- "port": "Port (Standard Wert: '80')"
+ "port": "Port"
},
"title": "deCONZ gateway d\u00e9fin\u00e9ieren"
},
@@ -40,5 +40,52 @@
}
},
"title": "deCONZ Zigbee gateway"
+ },
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "B\u00e9id Kn\u00e4ppchen",
+ "button_1": "\u00c9ischte Kn\u00e4ppchen",
+ "button_2": "Zweete Kn\u00e4ppchen",
+ "button_3": "Dr\u00ebtte Kn\u00e4ppchen",
+ "button_4": "V\u00e9ierte Kn\u00e4ppchen",
+ "close": "Zoumaachen",
+ "dim_down": "Erhellen",
+ "dim_up": "Verd\u00e4ischteren",
+ "left": "L\u00e9nks",
+ "open": "Op",
+ "right": "Riets",
+ "turn_off": "Ausschalten",
+ "turn_on": "Uschalten"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "\"{subtype}\" Kn\u00e4ppche zwee mol gedr\u00e9ckt",
+ "remote_button_long_press": "\"{subtype}\" Kn\u00e4ppche permanent gedr\u00e9ckt",
+ "remote_button_long_release": "\"{subtype}\" Kn\u00e4ppche no laangem unhalen lassgelooss",
+ "remote_button_quadruple_press": "\"{subtype}\" Kn\u00e4ppche v\u00e9ier mol gedr\u00e9ckt",
+ "remote_button_quintuple_press": "\"{subtype}\" Kn\u00e4ppche f\u00ebnnef mol gedr\u00e9ckt",
+ "remote_button_rotated": "Kn\u00e4ppche gedr\u00e9int \"{subtype}\"",
+ "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt",
+ "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss",
+ "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt",
+ "remote_gyro_activated": "Apparat ger\u00ebselt"
+ }
+ },
+ "options": {
+ "step": {
+ "async_step_deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "deCONZ Clip Sensoren erlaben",
+ "allow_deconz_groups": "deCONZ Luucht Gruppen erlaben"
+ },
+ "description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "deCONZ Clip Sensoren erlaben",
+ "allow_deconz_groups": "deCONZ Luucht Gruppen erlaben"
+ },
+ "description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json
index 785fba4ffc0e02..116f6254b37213 100644
--- a/homeassistant/components/deconz/.translations/nl.json
+++ b/homeassistant/components/deconz/.translations/nl.json
@@ -41,6 +41,35 @@
},
"title": "deCONZ Zigbee gateway"
},
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "Beide knoppen",
+ "button_1": "Eerste knop",
+ "button_2": "Tweede knop",
+ "button_3": "Derde knop",
+ "button_4": "Vierde knop",
+ "close": "Sluiten",
+ "dim_down": "Dim omlaag",
+ "dim_up": "Dim omhoog",
+ "left": "Links",
+ "open": "Open",
+ "right": "Rechts",
+ "turn_off": "Uitschakelen",
+ "turn_on": "Inschakelen"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "\"{subtype}\" knop dubbel geklikt",
+ "remote_button_long_press": "\" {subtype} \" knop continu ingedrukt",
+ "remote_button_long_release": "\"{subtype}\" knop losgelaten na lang indrukken van de knop",
+ "remote_button_quadruple_press": "\" {subtype} \" knop viervoudig aangeklikt",
+ "remote_button_quintuple_press": "\" {subtype} \" knop vijf keer aangeklikt",
+ "remote_button_rotated": "Knop gedraaid \" {subtype} \"",
+ "remote_button_short_press": "\" {subtype} \" knop ingedrukt",
+ "remote_button_short_release": "\"{subtype}\" knop losgelaten",
+ "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt",
+ "remote_gyro_activated": "Apparaat geschud"
+ }
+ },
"options": {
"step": {
"async_step_deconz_devices": {
@@ -49,6 +78,13 @@
"allow_deconz_groups": "DeCONZ-lichtgroepen toestaan"
},
"description": "De zichtbaarheid van deCONZ-apparaattypen configureren"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "DeCONZ CLIP sensoren toestaan",
+ "allow_deconz_groups": "Sta deCONZ-lichtgroepen toe"
+ },
+ "description": "Configureer de zichtbaarheid van deCONZ-apparaattypen"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json
index 8798248224a582..3968c1f00c58d2 100644
--- a/homeassistant/components/deconz/.translations/no.json
+++ b/homeassistant/components/deconz/.translations/no.json
@@ -41,6 +41,35 @@
},
"title": "deCONZ Zigbee gateway"
},
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "Begge knappene",
+ "button_1": "F\u00f8rste knapp",
+ "button_2": "Andre knapp",
+ "button_3": "Tredje knapp",
+ "button_4": "Fjerde knapp",
+ "close": "Lukk",
+ "dim_down": "Dimm ned",
+ "dim_up": "Dimm opp",
+ "left": "Venstre",
+ "open": "\u00c5pen",
+ "right": "H\u00f8yre",
+ "turn_off": "Skru av",
+ "turn_on": "Sl\u00e5 p\u00e5"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket",
+ "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket",
+ "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk",
+ "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket",
+ "remote_button_quintuple_press": "\" {subtype} \" - knappen femdobbelt klikket",
+ "remote_button_rotated": "Knappen roterte \" {subtype} \"",
+ "remote_button_short_press": "\" {subtype} \" -knappen ble trykket",
+ "remote_button_short_release": "\" {subtype} \" -knappen ble utgitt",
+ "remote_button_triple_press": "\" {subtype} \"-knappen trippel klikket",
+ "remote_gyro_activated": "Enhet er ristet"
+ }
+ },
"options": {
"step": {
"async_step_deconz_devices": {
@@ -49,6 +78,13 @@
"allow_deconz_groups": "Tillat deCONZ lys grupper"
},
"description": "Konfigurere synlighet av deCONZ enhetstyper"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "Tillat deCONZ CLIP-sensorer",
+ "allow_deconz_groups": "Tillat deCONZ lys grupper"
+ },
+ "description": "Konfigurere synlighet av deCONZ enhetstyper"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json
index 0f2009a46b687b..70c33cf3c02f4f 100644
--- a/homeassistant/components/deconz/.translations/pl.json
+++ b/homeassistant/components/deconz/.translations/pl.json
@@ -28,7 +28,7 @@
"title": "Zdefiniuj bramk\u0119 deCONZ"
},
"link": {
- "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawie\u0144 systemu deCONZ \n 2. Naci\u015bnij przycisk \"Odblokuj bramk\u0119\"",
+ "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawienia deCONZ > bramka > Zaawansowane\n 2. Naci\u015bnij przycisk \"Uwierzytelnij aplikacj\u0119\"",
"title": "Po\u0142\u0105cz z deCONZ"
},
"options": {
@@ -41,6 +41,35 @@
},
"title": "Brama deCONZ Zigbee"
},
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "Oba przyciski",
+ "button_1": "Pierwszy przycisk",
+ "button_2": "Drugi przycisk",
+ "button_3": "Trzeci przycisk",
+ "button_4": "Czwarty przycisk",
+ "close": "Zamknij",
+ "dim_down": "Przyciemnienie",
+ "dim_up": "Przyciemnienie",
+ "left": "Lewo",
+ "open": "Otw\u00f3rz",
+ "right": "Prawo",
+ "turn_off": "Wy\u0142\u0105cz",
+ "turn_on": "W\u0142\u0105cz"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "Przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty",
+ "remote_button_long_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y",
+ "remote_button_long_release": "Przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu",
+ "remote_button_quadruple_press": "Przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty",
+ "remote_button_quintuple_press": "Przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty",
+ "remote_button_rotated": "Przycisk obr\u00f3cony \"{subtype}\"",
+ "remote_button_short_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty",
+ "remote_button_short_release": "Przycisk \"{subtype}\" zwolniony",
+ "remote_button_triple_press": "Przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty",
+ "remote_gyro_activated": "Urz\u0105dzenie potrz\u0105\u015bni\u0119te"
+ }
+ },
"options": {
"step": {
"async_step_deconz_devices": {
@@ -49,6 +78,13 @@
"allow_deconz_groups": "Zezwalaj na grupy \u015bwiate\u0142 deCONZ"
},
"description": "Skonfiguruj widoczno\u015b\u0107 urz\u0105dze\u0144 deCONZ"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "Zezwalaj na czujniki deCONZ CLIP",
+ "allow_deconz_groups": "Zezwalaj na grupy \u015bwiate\u0142 deCONZ"
+ },
+ "description": "Skonfiguruj widoczno\u015b\u0107 typ\u00f3w urz\u0105dze\u0144 deCONZ"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json
index ee7208cdf1731a..558fd9e5897e7f 100644
--- a/homeassistant/components/deconz/.translations/ru.json
+++ b/homeassistant/components/deconz/.translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430",
+ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
"already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.",
"no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b",
"not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ",
@@ -25,7 +25,7 @@
"host": "\u0425\u043e\u0441\u0442",
"port": "\u041f\u043e\u0440\u0442"
},
- "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ"
+ "title": "deCONZ"
},
"link": {
"description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb",
@@ -41,6 +41,35 @@
},
"title": "deCONZ"
},
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "\u041e\u0431\u0435 \u043a\u043d\u043e\u043f\u043a\u0438",
+ "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430",
+ "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e",
+ "dim_down": "\u0423\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c",
+ "dim_up": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c",
+ "left": "\u041d\u0430\u043b\u0435\u0432\u043e",
+ "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e",
+ "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e",
+ "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
+ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b",
+ "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430",
+ "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f",
+ "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430",
+ "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437",
+ "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0451\u0440\u043d\u0443\u0442\u0430",
+ "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430",
+ "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430",
+ "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b",
+ "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438"
+ }
+ },
"options": {
"step": {
"async_step_deconz_devices": {
@@ -49,6 +78,13 @@
"allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 deCONZ CLIP",
+ "allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json
index 4a30e9d34d113a..9aebb2a556f6cd 100644
--- a/homeassistant/components/deconz/.translations/sl.json
+++ b/homeassistant/components/deconz/.translations/sl.json
@@ -41,6 +41,35 @@
},
"title": "deCONZ Zigbee prehod"
},
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "Oba gumba",
+ "button_1": "Prvi gumb",
+ "button_2": "Drugi gumb",
+ "button_3": "Tretji gumb",
+ "button_4": "\u010cetrti gumb",
+ "close": "Zapri",
+ "dim_down": "Zatemnite",
+ "dim_up": "pove\u010dajte mo\u010d",
+ "left": "Levo",
+ "open": "Odprto",
+ "right": "Desno",
+ "turn_off": "Ugasni",
+ "turn_on": "Pri\u017egi"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "Dvakrat kliknete gumb \"{subtype}\"",
+ "remote_button_long_press": "\"{subtype}\" gumb neprekinjeno pritisnjen",
+ "remote_button_long_release": "\"{subtype}\" gumb spro\u0161\u010den po dolgem pritisku",
+ "remote_button_quadruple_press": "\"{subtype}\" gumb \u0161tirikrat kliknjen",
+ "remote_button_quintuple_press": "\"{subtype}\" gumb petkrat kliknjen",
+ "remote_button_rotated": "Gumb \"{subtype}\" zasukan",
+ "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb",
+ "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den",
+ "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen",
+ "remote_gyro_activated": "Naprava se je pretresla"
+ }
+ },
"options": {
"step": {
"async_step_deconz_devices": {
@@ -49,6 +78,13 @@
"allow_deconz_groups": "Dovolite deCONZ skupine lu\u010di"
},
"description": "Konfiguracija vidnosti tipov naprav deCONZ"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "Dovoli deCONZ CLIP senzorje",
+ "allow_deconz_groups": "Dovolite deCONZ skupine lu\u010di"
+ },
+ "description": "Konfiguracija vidnosti tipov naprav deCONZ"
}
}
}
diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json
index 53d6f76a60161a..f024386aa0f873 100644
--- a/homeassistant/components/deconz/.translations/zh-Hant.json
+++ b/homeassistant/components/deconz/.translations/zh-Hant.json
@@ -41,6 +41,35 @@
},
"title": "deCONZ Zigbee \u9598\u9053\u5668"
},
+ "device_automation": {
+ "trigger_subtype": {
+ "both_buttons": "\u5169\u500b\u6309\u9215",
+ "button_1": "\u7b2c\u4e00\u500b\u6309\u9215",
+ "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215",
+ "button_3": "\u7b2c\u4e09\u500b\u6309\u9215",
+ "button_4": "\u7b2c\u56db\u500b\u6309\u9215",
+ "close": "\u95dc\u9589",
+ "dim_down": "\u8abf\u6697",
+ "dim_up": "\u8abf\u4eae",
+ "left": "\u5de6",
+ "open": "\u958b\u555f",
+ "right": "\u53f3",
+ "turn_off": "\u95dc\u9589",
+ "turn_on": "\u958b\u555f"
+ },
+ "trigger_type": {
+ "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca",
+ "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b",
+ "remote_button_long_release": "\u9577\u6309\u5f8c\u91cb\u653e \"{subtype}\" \u6309\u9215",
+ "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u9ede\u64ca",
+ "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u9ede\u64ca",
+ "remote_button_rotated": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215",
+ "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b",
+ "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e",
+ "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca",
+ "remote_gyro_activated": "\u8a2d\u5099\u6416\u6643"
+ }
+ },
"options": {
"step": {
"async_step_deconz_devices": {
@@ -49,6 +78,13 @@
"allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44"
},
"description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b"
+ },
+ "deconz_devices": {
+ "data": {
+ "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668",
+ "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44"
+ },
+ "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b"
}
}
}
diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py
index 68974d12253f6e..558b0fe4205147 100644
--- a/homeassistant/components/deconz/__init__.py
+++ b/homeassistant/components/deconz/__init__.py
@@ -1,78 +1,20 @@
"""Support for deCONZ devices."""
import voluptuous as vol
-from homeassistant import config_entries
-from homeassistant.const import (
- CONF_API_KEY,
- CONF_HOST,
- CONF_PORT,
- EVENT_HOMEASSISTANT_STOP,
-)
-from homeassistant.helpers import config_validation as cv
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP
-# Loading the config flow file will register the flow
from .config_flow import get_master_gateway
-from .const import (
- CONF_ALLOW_CLIP_SENSOR,
- CONF_ALLOW_DECONZ_GROUPS,
- CONF_BRIDGEID,
- CONF_MASTER_GATEWAY,
- DEFAULT_PORT,
- DOMAIN,
- _LOGGER,
-)
+from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DOMAIN
from .gateway import DeconzGateway
+from .services import async_setup_services, async_unload_services
CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.Schema(
- {
- vol.Optional(CONF_API_KEY): cv.string,
- vol.Optional(CONF_HOST): cv.string,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
- }
- )
- },
- extra=vol.ALLOW_EXTRA,
-)
-
-SERVICE_DECONZ = "configure"
-
-SERVICE_FIELD = "field"
-SERVICE_ENTITY = "entity"
-SERVICE_DATA = "data"
-
-SERVICE_SCHEMA = vol.All(
- vol.Schema(
- {
- vol.Optional(SERVICE_ENTITY): cv.entity_id,
- vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"),
- vol.Required(SERVICE_DATA): dict,
- vol.Optional(CONF_BRIDGEID): str,
- }
- ),
- cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD),
+ {DOMAIN: vol.Schema({}, extra=vol.ALLOW_EXTRA)}, extra=vol.ALLOW_EXTRA
)
-SERVICE_DEVICE_REFRESH = "device_refresh"
-
-SERVICE_DEVICE_REFRESCH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGEID): str}))
-
async def async_setup(hass, config):
- """Load configuration for deCONZ component.
-
- Discovery has loaded the component if DOMAIN is not present in config.
- """
- if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config:
- deconz_config = config[DOMAIN]
- hass.async_create_task(
- hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": config_entries.SOURCE_IMPORT},
- data=deconz_config,
- )
- )
+ """Old way of setting up deCONZ integrations."""
return True
@@ -86,7 +28,7 @@ async def async_setup_entry(hass, config_entry):
hass.data[DOMAIN] = {}
if not config_entry.options:
- await async_populate_options(hass, config_entry)
+ await async_update_master_gateway(hass, config_entry)
gateway = DeconzGateway(hass, config_entry)
@@ -97,100 +39,10 @@ async def async_setup_entry(hass, config_entry):
await gateway.async_update_device_registry()
- async def async_configure(call):
- """Set attribute of device in deCONZ.
-
- Entity is used to resolve to a device path (e.g. '/lights/1').
- Field is a string representing either a full path
- (e.g. '/lights/1/state') when entity is not specified, or a
- subpath (e.g. '/state') when used together with entity.
- Data is a json object with what data you want to alter
- e.g. data={'on': true}.
- {
- "field": "/lights/1/state",
- "data": {"on": true}
- }
- See Dresden Elektroniks REST API documentation for details:
- http://dresden-elektronik.github.io/deconz-rest-doc/rest/
- """
- field = call.data.get(SERVICE_FIELD, "")
- entity_id = call.data.get(SERVICE_ENTITY)
- data = call.data[SERVICE_DATA]
-
- gateway = get_master_gateway(hass)
- if CONF_BRIDGEID in call.data:
- gateway = hass.data[DOMAIN][call.data[CONF_BRIDGEID]]
-
- if entity_id:
- try:
- field = gateway.deconz_ids[entity_id] + field
- except KeyError:
- _LOGGER.error("Could not find the entity %s", entity_id)
- return
-
- await gateway.api.async_put_state(field, data)
-
- hass.services.async_register(
- DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA
- )
-
- async def async_refresh_devices(call):
- """Refresh available devices from deCONZ."""
- gateway = get_master_gateway(hass)
- if CONF_BRIDGEID in call.data:
- gateway = hass.data[DOMAIN][call.data[CONF_BRIDGEID]]
-
- groups = set(gateway.api.groups.keys())
- lights = set(gateway.api.lights.keys())
- scenes = set(gateway.api.scenes.keys())
- sensors = set(gateway.api.sensors.keys())
-
- await gateway.api.async_load_parameters()
-
- gateway.async_add_device_callback(
- "group",
- [
- group
- for group_id, group in gateway.api.groups.items()
- if group_id not in groups
- ],
- )
-
- gateway.async_add_device_callback(
- "light",
- [
- light
- for light_id, light in gateway.api.lights.items()
- if light_id not in lights
- ],
- )
-
- gateway.async_add_device_callback(
- "scene",
- [
- scene
- for scene_id, scene in gateway.api.scenes.items()
- if scene_id not in scenes
- ],
- )
-
- gateway.async_add_device_callback(
- "sensor",
- [
- sensor
- for sensor_id, sensor in gateway.api.sensors.items()
- if sensor_id not in sensors
- ],
- )
-
- hass.services.async_register(
- DOMAIN,
- SERVICE_DEVICE_REFRESH,
- async_refresh_devices,
- schema=SERVICE_DEVICE_REFRESCH_SCHEMA,
- )
+ await async_setup_services(hass)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown)
+
return True
@@ -199,29 +51,28 @@ async def async_unload_entry(hass, config_entry):
gateway = hass.data[DOMAIN].pop(config_entry.data[CONF_BRIDGEID])
if not hass.data[DOMAIN]:
- hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
- hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH)
+ await async_unload_services(hass)
elif gateway.master:
- await async_populate_options(hass, config_entry)
+ await async_update_master_gateway(hass, config_entry)
new_master_gateway = next(iter(hass.data[DOMAIN].values()))
- await async_populate_options(hass, new_master_gateway.config_entry)
+ await async_update_master_gateway(hass, new_master_gateway.config_entry)
return await gateway.async_reset()
-async def async_populate_options(hass, config_entry):
- """Populate default options for gateway.
+async def async_update_master_gateway(hass, config_entry):
+ """Update master gateway boolean.
Called by setup_entry and unload_entry.
Makes sure there is always one master available.
"""
master = not get_master_gateway(hass)
- options = {
- CONF_MASTER_GATEWAY: master,
- CONF_ALLOW_CLIP_SENSOR: config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, False),
- CONF_ALLOW_DECONZ_GROUPS: config_entry.data.get(CONF_ALLOW_DECONZ_GROUPS, True),
- }
+ old_options = dict(config_entry.options)
+
+ new_options = {CONF_MASTER_GATEWAY: master}
+
+ options = {**old_options, **new_options}
hass.config_entries.async_update_entry(config_entry, options=options)
diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py
index 0b5d3173812ba7..b81ecdc5164a75 100644
--- a/homeassistant/components/deconz/binary_sensor.py
+++ b/homeassistant/components/deconz/binary_sensor.py
@@ -2,13 +2,13 @@
from pydeconz.sensor import Presence, Vibration
from homeassistant.components.binary_sensor import BinarySensorDevice
-from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE
+from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR
from .deconz_device import DeconzDevice
-from .gateway import get_gateway_from_config_entry
+from .gateway import get_gateway_from_config_entry, DeconzEntityHandler
ATTR_ORIENTATION = "orientation"
ATTR_TILTANGLE = "tiltangle"
@@ -17,13 +17,14 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ platforms."""
- pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ binary sensor."""
gateway = get_gateway_from_config_entry(hass, config_entry)
+ entity_handler = DeconzEntityHandler(gateway)
+
@callback
def async_add_sensor(sensors):
"""Add binary sensor from deCONZ."""
@@ -31,17 +32,16 @@ def async_add_sensor(sensors):
for sensor in sensors:
- if sensor.BINARY and not (
- not gateway.allow_clip_sensor and sensor.type.startswith("CLIP")
- ):
-
- entities.append(DeconzBinarySensor(sensor, gateway))
+ if sensor.BINARY:
+ new_sensor = DeconzBinarySensor(sensor, gateway)
+ entity_handler.add_entity(new_sensor)
+ entities.append(new_sensor)
async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
- hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor
+ hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor
)
)
@@ -55,7 +55,7 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
def async_update_callback(self, force_update=False):
"""Update the sensor's state."""
changed = set(self._device.changed_keys)
- keys = {"battery", "on", "reachable", "state"}
+ keys = {"on", "reachable", "state"}
if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@@ -78,8 +78,6 @@ def icon(self):
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
attr = {}
- if self._device.battery:
- attr[ATTR_BATTERY_LEVEL] = self._device.battery
if self._device.on is not None:
attr[ATTR_ON] = self._device.on
diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py
index a72c29019598e7..b7a1ebce22ad48 100644
--- a/homeassistant/components/deconz/climate.py
+++ b/homeassistant/components/deconz/climate.py
@@ -8,7 +8,7 @@
HVAC_MODE_OFF,
SUPPORT_TARGET_TEMPERATURE,
)
-from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, TEMP_CELSIUS
+from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -21,7 +21,6 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ platforms."""
- pass
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -38,17 +37,14 @@ def async_add_climate(sensors):
for sensor in sensors:
- if sensor.type in Thermostat.ZHATYPE and not (
- not gateway.allow_clip_sensor and sensor.type.startswith("CLIP")
- ):
-
+ if sensor.type in Thermostat.ZHATYPE:
entities.append(DeconzThermostat(sensor, gateway))
async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
- hass, gateway.async_event_new_device(NEW_SENSOR), async_add_climate
+ hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_climate
)
)
@@ -123,9 +119,6 @@ def device_state_attributes(self):
"""Return the state attributes of the thermostat."""
attr = {}
- if self._device.battery:
- attr[ATTR_BATTERY_LEVEL] = self._device.battery
-
if self._device.offset:
attr[ATTR_OFFSET] = self._device.offset
diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py
index 60d47a0a4e22b6..c63b1721393222 100644
--- a/homeassistant/components/deconz/config_flow.py
+++ b/homeassistant/components/deconz/config_flow.py
@@ -1,6 +1,5 @@
"""Config flow to configure deCONZ component."""
import asyncio
-from copy import copy
import async_timeout
import voluptuous as vol
@@ -17,6 +16,8 @@
CONF_ALLOW_CLIP_SENSOR,
CONF_ALLOW_DECONZ_GROUPS,
CONF_BRIDGEID,
+ DEFAULT_ALLOW_CLIP_SENSOR,
+ DEFAULT_ALLOW_DECONZ_GROUPS,
DEFAULT_PORT,
DOMAIN,
)
@@ -190,31 +191,12 @@ async def async_step_ssdp(self, discovery_info):
# pylint: disable=unsupported-assignment-operation
self.context[CONF_BRIDGEID] = bridgeid
- deconz_config = {
+ self.deconz_config = {
CONF_HOST: discovery_info[CONF_HOST],
CONF_PORT: discovery_info[CONF_PORT],
}
- return await self.async_step_import(deconz_config)
-
- async def async_step_import(self, import_config):
- """Import a deCONZ bridge as a config entry.
-
- This flow is triggered by `async_setup` for configured bridges.
- This flow is also triggered by `async_step_discovery`.
-
- This will execute for any bridge that does not have a
- config entry yet (based on host).
-
- If an API key is provided, we will create an entry.
- Otherwise we will delegate to `link` step which
- will ask user to link the bridge.
- """
- self.deconz_config = import_config
- if CONF_API_KEY not in import_config:
- return await self.async_step_link()
-
- return await self._create_entry()
+ return await self.async_step_link()
async def async_step_hassio(self, user_input=None):
"""Prepare configuration for a Hass.io deCONZ bridge.
@@ -256,7 +238,7 @@ class DeconzOptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry):
"""Initialize deCONZ options flow."""
self.config_entry = config_entry
- self.options = copy(config_entry.options)
+ self.options = dict(config_entry.options)
async def async_step_init(self, user_input=None):
"""Manage the deCONZ options."""
@@ -277,11 +259,15 @@ async def async_step_deconz_devices(self, user_input=None):
{
vol.Optional(
CONF_ALLOW_CLIP_SENSOR,
- default=self.config_entry.options[CONF_ALLOW_CLIP_SENSOR],
+ default=self.config_entry.options.get(
+ CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR
+ ),
): bool,
vol.Optional(
CONF_ALLOW_DECONZ_GROUPS,
- default=self.config_entry.options[CONF_ALLOW_DECONZ_GROUPS],
+ default=self.config_entry.options.get(
+ CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS
+ ),
): bool,
}
),
diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py
index ef152aa2b708f0..62879a82724b03 100644
--- a/homeassistant/components/deconz/const.py
+++ b/homeassistant/components/deconz/const.py
@@ -7,7 +7,7 @@
DEFAULT_PORT = 80
DEFAULT_ALLOW_CLIP_SENSOR = False
-DEFAULT_ALLOW_DECONZ_GROUPS = False
+DEFAULT_ALLOW_DECONZ_GROUPS = True
CONF_ALLOW_CLIP_SENSOR = "allow_clip_sensor"
CONF_ALLOW_DECONZ_GROUPS = "allow_deconz_groups"
diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py
index be4088a5c86592..bcd408c25a7591 100644
--- a/homeassistant/components/deconz/cover.py
+++ b/homeassistant/components/deconz/cover.py
@@ -17,7 +17,6 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ platforms."""
- pass
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -40,7 +39,7 @@ def async_add_cover(lights):
gateway.listeners.append(
async_dispatcher_connect(
- hass, gateway.async_event_new_device(NEW_LIGHT), async_add_cover
+ hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_cover
)
)
diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py
index 389ed11e437126..68daee6cf260ca 100644
--- a/homeassistant/components/deconz/deconz_device.py
+++ b/homeassistant/components/deconz/deconz_device.py
@@ -7,69 +7,105 @@
from .const import DOMAIN as DECONZ_DOMAIN
-class DeconzDevice(Entity):
- """Representation of a deCONZ device."""
+class DeconzBase:
+ """Common base for deconz entities and events."""
def __init__(self, device, gateway):
"""Set up device and add update callback to get data from websocket."""
self._device = device
self.gateway = gateway
+ self.listeners = []
+
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this device."""
+ return self._device.uniqueid
+
+ @property
+ def serial(self):
+ """Return a serial number for this device."""
+ if self._device.uniqueid is None or self._device.uniqueid.count(":") != 7:
+ return None
+
+ return self._device.uniqueid.split("-", 1)[0]
+
+ @property
+ def device_info(self):
+ """Return a device description for device registry."""
+ if self.serial is None:
+ return None
+
+ bridgeid = self.gateway.api.config.bridgeid
+
+ return {
+ "connections": {(CONNECTION_ZIGBEE, self.serial)},
+ "identifiers": {(DECONZ_DOMAIN, self.serial)},
+ "manufacturer": self._device.manufacturer,
+ "model": self._device.modelid,
+ "name": self._device.name,
+ "sw_version": self._device.swversion,
+ "via_device": (DECONZ_DOMAIN, bridgeid),
+ }
+
+
+class DeconzDevice(DeconzBase, Entity):
+ """Representation of a deCONZ device."""
+
+ def __init__(self, device, gateway):
+ """Set up device and add update callback to get data from websocket."""
+ super().__init__(device, gateway)
+
self.unsub_dispatcher = None
+ @property
+ def entity_registry_enabled_default(self):
+ """Return if the entity should be enabled when first added to the entity registry."""
+ if not self.gateway.option_allow_clip_sensor and self._device.type.startswith(
+ "CLIP"
+ ):
+ return False
+
+ if (
+ not self.gateway.option_allow_deconz_groups
+ and self._device.type == "LightGroup"
+ ):
+ return False
+
+ return True
+
async def async_added_to_hass(self):
"""Subscribe to device events."""
self._device.register_async_callback(self.async_update_callback)
self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id
- self.unsub_dispatcher = async_dispatcher_connect(
- self.hass, self.gateway.event_reachable, self.async_update_callback
+ self.listeners.append(
+ async_dispatcher_connect(
+ self.hass, self.gateway.signal_reachable, self.async_update_callback
+ )
)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
self._device.remove_callback(self.async_update_callback)
del self.gateway.deconz_ids[self.entity_id]
- self.unsub_dispatcher()
+ for unsub_dispatcher in self.listeners:
+ unsub_dispatcher()
@callback
def async_update_callback(self, force_update=False):
"""Update the device's state."""
self.async_schedule_update_ha_state()
- @property
- def name(self):
- """Return the name of the device."""
- return self._device.name
-
- @property
- def unique_id(self):
- """Return a unique identifier for this device."""
- return self._device.uniqueid
-
@property
def available(self):
"""Return True if device is available."""
return self.gateway.available and self._device.reachable
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._device.name
+
@property
def should_poll(self):
"""No polling needed."""
return False
-
- @property
- def device_info(self):
- """Return a device description for device registry."""
- if self._device.uniqueid is None or self._device.uniqueid.count(":") != 7:
- return None
-
- serial = self._device.uniqueid.split("-", 1)[0]
- bridgeid = self.gateway.api.config.bridgeid
-
- return {
- "connections": {(CONNECTION_ZIGBEE, serial)},
- "identifiers": {(DECONZ_DOMAIN, serial)},
- "manufacturer": self._device.manufacturer,
- "model": self._device.modelid,
- "name": self._device.name,
- "sw_version": self._device.swversion,
- "via_device": (DECONZ_DOMAIN, bridgeid),
- }
diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py
new file mode 100644
index 00000000000000..31588db1f23833
--- /dev/null
+++ b/homeassistant/components/deconz/deconz_event.py
@@ -0,0 +1,61 @@
+"""Representation of a deCONZ remote."""
+from homeassistant.const import CONF_EVENT, CONF_ID
+from homeassistant.core import callback
+from homeassistant.util import slugify
+
+from .const import _LOGGER
+from .deconz_device import DeconzBase
+
+CONF_DECONZ_EVENT = "deconz_event"
+CONF_UNIQUE_ID = "unique_id"
+
+
+class DeconzEvent(DeconzBase):
+ """When you want signals instead of entities.
+
+ Stateless sensors such as remotes are expected to generate an event
+ instead of a sensor entity in hass.
+ """
+
+ def __init__(self, device, gateway):
+ """Register callback that will be used for signals."""
+ super().__init__(device, gateway)
+
+ self._device.register_async_callback(self.async_update_callback)
+
+ self.device_id = None
+ self.event_id = slugify(self._device.name)
+ _LOGGER.debug("deCONZ event created: %s", self.event_id)
+
+ @property
+ def device(self):
+ """Return Event device."""
+ return self._device
+
+ @callback
+ def async_will_remove_from_hass(self) -> None:
+ """Disconnect event object when removed."""
+ self._device.remove_callback(self.async_update_callback)
+ self._device = None
+
+ @callback
+ def async_update_callback(self, force_update=False):
+ """Fire the event if reason is that state is updated."""
+ if "state" in self._device.changed_keys:
+ data = {
+ CONF_ID: self.event_id,
+ CONF_UNIQUE_ID: self.serial,
+ CONF_EVENT: self._device.state,
+ }
+ self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data)
+
+ async def async_update_device_registry(self):
+ """Update device registry."""
+ device_registry = (
+ await self.gateway.hass.helpers.device_registry.async_get_registry()
+ )
+
+ entry = device_registry.async_get_or_create(
+ config_entry_id=self.gateway.config_entry.entry_id, **self.device_info
+ )
+ self.device_id = entry.id
diff --git a/homeassistant/components/deconz/device_automation.py b/homeassistant/components/deconz/device_automation.py
new file mode 100644
index 00000000000000..28f36b8f431ea8
--- /dev/null
+++ b/homeassistant/components/deconz/device_automation.py
@@ -0,0 +1,254 @@
+"""Provides device automations for deconz events."""
+import voluptuous as vol
+
+import homeassistant.components.automation.event as event
+
+from homeassistant.components.device_automation.exceptions import (
+ InvalidDeviceAutomationConfig,
+)
+from homeassistant.const import (
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ CONF_EVENT,
+ CONF_PLATFORM,
+ CONF_TYPE,
+)
+
+from . import DOMAIN
+from .config_flow import configured_gateways
+from .deconz_event import CONF_DECONZ_EVENT, CONF_UNIQUE_ID
+from .gateway import get_gateway_from_config_entry
+
+CONF_SUBTYPE = "subtype"
+
+CONF_SHORT_PRESS = "remote_button_short_press"
+CONF_SHORT_RELEASE = "remote_button_short_release"
+CONF_LONG_PRESS = "remote_button_long_press"
+CONF_LONG_RELEASE = "remote_button_long_release"
+CONF_DOUBLE_PRESS = "remote_button_double_press"
+CONF_TRIPLE_PRESS = "remote_button_triple_press"
+CONF_QUADRUPLE_PRESS = "remote_button_quadruple_press"
+CONF_QUINTUPLE_PRESS = "remote_button_quintuple_press"
+CONF_ROTATED = "remote_button_rotated"
+CONF_SHAKE = "remote_gyro_activated"
+
+CONF_TURN_ON = "turn_on"
+CONF_TURN_OFF = "turn_off"
+CONF_DIM_UP = "dim_up"
+CONF_DIM_DOWN = "dim_down"
+CONF_LEFT = "left"
+CONF_RIGHT = "right"
+CONF_OPEN = "open"
+CONF_CLOSE = "close"
+CONF_BOTH_BUTTONS = "both_buttons"
+CONF_BUTTON_1 = "button_1"
+CONF_BUTTON_2 = "button_2"
+CONF_BUTTON_3 = "button_3"
+CONF_BUTTON_4 = "button_4"
+
+HUE_DIMMER_REMOTE_MODEL = "RWL021"
+HUE_DIMMER_REMOTE = {
+ (CONF_SHORT_PRESS, CONF_TURN_ON): 1000,
+ (CONF_SHORT_RELEASE, CONF_TURN_ON): 1002,
+ (CONF_LONG_PRESS, CONF_TURN_ON): 1001,
+ (CONF_LONG_RELEASE, CONF_TURN_ON): 1003,
+ (CONF_SHORT_PRESS, CONF_DIM_UP): 2000,
+ (CONF_SHORT_RELEASE, CONF_DIM_UP): 2002,
+ (CONF_LONG_PRESS, CONF_DIM_UP): 2001,
+ (CONF_LONG_RELEASE, CONF_DIM_UP): 2003,
+ (CONF_SHORT_PRESS, CONF_DIM_DOWN): 3000,
+ (CONF_SHORT_RELEASE, CONF_DIM_DOWN): 3002,
+ (CONF_LONG_PRESS, CONF_DIM_DOWN): 3001,
+ (CONF_LONG_RELEASE, CONF_DIM_DOWN): 3003,
+ (CONF_SHORT_PRESS, CONF_TURN_OFF): 4000,
+ (CONF_SHORT_RELEASE, CONF_TURN_OFF): 4002,
+ (CONF_LONG_PRESS, CONF_TURN_OFF): 4001,
+ (CONF_LONG_RELEASE, CONF_TURN_OFF): 4003,
+}
+
+HUE_TAP_REMOTE_MODEL = "ZGPSWITCH"
+HUE_TAP_REMOTE = {
+ (CONF_SHORT_PRESS, CONF_BUTTON_1): 34,
+ (CONF_SHORT_PRESS, CONF_BUTTON_2): 16,
+ (CONF_SHORT_PRESS, CONF_BUTTON_3): 17,
+ (CONF_SHORT_PRESS, CONF_BUTTON_4): 18,
+}
+
+TRADFRI_ON_OFF_SWITCH_MODEL = "TRADFRI on/off switch"
+TRADFRI_ON_OFF_SWITCH = {
+ (CONF_SHORT_PRESS, CONF_TURN_ON): 1002,
+ (CONF_LONG_PRESS, CONF_TURN_ON): 1001,
+ (CONF_LONG_RELEASE, CONF_TURN_ON): 1003,
+ (CONF_SHORT_PRESS, CONF_TURN_OFF): 2002,
+ (CONF_LONG_PRESS, CONF_TURN_OFF): 2001,
+ (CONF_LONG_RELEASE, CONF_TURN_OFF): 2003,
+}
+
+TRADFRI_OPEN_CLOSE_REMOTE_MODEL = "TRADFRI open/close remote"
+TRADFRI_OPEN_CLOSE_REMOTE = {
+ (CONF_SHORT_PRESS, CONF_OPEN): 1002,
+ (CONF_LONG_PRESS, CONF_OPEN): 1003,
+ (CONF_SHORT_PRESS, CONF_CLOSE): 2002,
+ (CONF_LONG_PRESS, CONF_CLOSE): 2003,
+}
+
+TRADFRI_REMOTE_MODEL = "TRADFRI remote control"
+TRADFRI_REMOTE = {
+ (CONF_SHORT_PRESS, CONF_TURN_ON): 1002,
+ (CONF_LONG_PRESS, CONF_TURN_ON): 1001,
+ (CONF_SHORT_PRESS, CONF_DIM_UP): 2002,
+ (CONF_LONG_PRESS, CONF_DIM_UP): 2001,
+ (CONF_LONG_RELEASE, CONF_DIM_UP): 2003,
+ (CONF_SHORT_PRESS, CONF_DIM_DOWN): 3002,
+ (CONF_LONG_PRESS, CONF_DIM_DOWN): 3001,
+ (CONF_LONG_RELEASE, CONF_DIM_DOWN): 3003,
+ (CONF_SHORT_PRESS, CONF_LEFT): 4002,
+ (CONF_LONG_PRESS, CONF_LEFT): 4001,
+ (CONF_LONG_RELEASE, CONF_LEFT): 4003,
+ (CONF_SHORT_PRESS, CONF_RIGHT): 5002,
+ (CONF_LONG_PRESS, CONF_RIGHT): 5001,
+ (CONF_LONG_RELEASE, CONF_RIGHT): 5003,
+}
+
+TRADFRI_WIRELESS_DIMMER_MODEL = "TRADFRI wireless dimmer"
+TRADFRI_WIRELESS_DIMMER = {
+ (CONF_ROTATED, CONF_LEFT): 3002,
+ (CONF_ROTATED, CONF_RIGHT): 2002,
+}
+
+AQARA_DOUBLE_WALL_SWITCH_MODEL = "lumi.remote.b286acn01"
+AQARA_DOUBLE_WALL_SWITCH = {
+ (CONF_SHORT_PRESS, CONF_LEFT): 1002,
+ (CONF_LONG_PRESS, CONF_LEFT): 1001,
+ (CONF_DOUBLE_PRESS, CONF_LEFT): 1004,
+ (CONF_SHORT_PRESS, CONF_RIGHT): 2002,
+ (CONF_LONG_PRESS, CONF_RIGHT): 2001,
+ (CONF_DOUBLE_PRESS, CONF_RIGHT): 2004,
+ (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): 3002,
+ (CONF_LONG_PRESS, CONF_BOTH_BUTTONS): 3001,
+ (CONF_DOUBLE_PRESS, CONF_BOTH_BUTTONS): 3004,
+}
+
+AQARA_MINI_SWITCH_MODEL = "lumi.remote.b1acn01"
+AQARA_MINI_SWITCH = {
+ (CONF_SHORT_PRESS, CONF_TURN_ON): 1002,
+ (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004,
+ (CONF_LONG_PRESS, CONF_TURN_ON): 1001,
+ (CONF_LONG_RELEASE, CONF_TURN_ON): 1003,
+}
+
+AQARA_ROUND_SWITCH_MODEL = "lumi.sensor_switch"
+AQARA_ROUND_SWITCH = {
+ (CONF_SHORT_PRESS, CONF_TURN_ON): 1000,
+ (CONF_SHORT_RELEASE, CONF_TURN_ON): 1002,
+ (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004,
+ (CONF_TRIPLE_PRESS, CONF_TURN_ON): 1005,
+ (CONF_QUADRUPLE_PRESS, CONF_TURN_ON): 1006,
+ (CONF_QUINTUPLE_PRESS, CONF_TURN_ON): 1010,
+ (CONF_LONG_PRESS, CONF_TURN_ON): 1001,
+ (CONF_LONG_RELEASE, CONF_TURN_ON): 1003,
+}
+
+AQARA_SQUARE_SWITCH_MODEL = "lumi.sensor_switch.aq3"
+AQARA_SQUARE_SWITCH = {
+ (CONF_SHORT_PRESS, CONF_TURN_ON): 1002,
+ (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004,
+ (CONF_LONG_PRESS, CONF_TURN_ON): 1001,
+ (CONF_LONG_RELEASE, CONF_TURN_ON): 1003,
+ (CONF_SHAKE, ""): 1007,
+}
+
+REMOTES = {
+ HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE,
+ HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE,
+ TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH,
+ TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE,
+ TRADFRI_REMOTE_MODEL: TRADFRI_REMOTE,
+ TRADFRI_WIRELESS_DIMMER_MODEL: TRADFRI_WIRELESS_DIMMER,
+ AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH,
+ AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH,
+ AQARA_ROUND_SWITCH_MODEL: AQARA_ROUND_SWITCH,
+ AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH,
+}
+
+TRIGGER_SCHEMA = vol.All(
+ vol.Schema(
+ {
+ vol.Required(CONF_DEVICE_ID): str,
+ vol.Required(CONF_DOMAIN): DOMAIN,
+ vol.Required(CONF_PLATFORM): "device",
+ vol.Required(CONF_TYPE): str,
+ vol.Required(CONF_SUBTYPE): str,
+ }
+ )
+)
+
+
+def _get_deconz_event_from_device_id(hass, device_id):
+ """Resolve deconz event from device id."""
+ deconz_config_entries = configured_gateways(hass)
+ for config_entry in deconz_config_entries.values():
+
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+ for deconz_event in gateway.events:
+
+ if device_id == deconz_event.device_id:
+ return deconz_event
+
+ return None
+
+
+async def async_trigger(hass, config, action, automation_info):
+ """Listen for state changes based on configuration."""
+ config = TRIGGER_SCHEMA(config)
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get(config[CONF_DEVICE_ID])
+
+ trigger = (config[CONF_TYPE], config[CONF_SUBTYPE])
+
+ if device.model not in REMOTES and trigger not in REMOTES[device.model]:
+ raise InvalidDeviceAutomationConfig
+
+ trigger = REMOTES[device.model][trigger]
+
+ deconz_event = _get_deconz_event_from_device_id(hass, device.id)
+ if deconz_event is None:
+ raise InvalidDeviceAutomationConfig
+
+ event_id = deconz_event.serial
+
+ state_config = {
+ event.CONF_EVENT_TYPE: CONF_DECONZ_EVENT,
+ event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger},
+ }
+
+ return await event.async_trigger(hass, state_config, action, automation_info)
+
+
+async def async_get_triggers(hass, device_id):
+ """List device triggers.
+
+ Make sure device is a supported remote model.
+ Retrieve the deconz event object matching device entry.
+ Generate device trigger list.
+ """
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get(device_id)
+
+ if device.model not in REMOTES:
+ return
+
+ triggers = []
+ for trigger, subtype in REMOTES[device.model].keys():
+ triggers.append(
+ {
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_PLATFORM: "device",
+ CONF_TYPE: trigger,
+ CONF_SUBTYPE: subtype,
+ }
+ )
+
+ return triggers
diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py
index 2117f8dc6bb3cf..75898b0fdab819 100644
--- a/homeassistant/components/deconz/gateway.py
+++ b/homeassistant/components/deconz/gateway.py
@@ -3,18 +3,20 @@
import async_timeout
from pydeconz import DeconzSession, errors
-from pydeconz.sensor import Switch
from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.const import CONF_EVENT, CONF_HOST, CONF_ID
-from homeassistant.core import EventOrigin, callback
+from homeassistant.const import CONF_HOST
+from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
-from homeassistant.util import slugify
+from homeassistant.helpers.entity_registry import (
+ async_get_registry,
+ DISABLED_CONFIG_ENTRY,
+)
from .const import (
_LOGGER,
@@ -22,11 +24,13 @@
CONF_ALLOW_DECONZ_GROUPS,
CONF_BRIDGEID,
CONF_MASTER_GATEWAY,
+ DEFAULT_ALLOW_CLIP_SENSOR,
+ DEFAULT_ALLOW_DECONZ_GROUPS,
DOMAIN,
NEW_DEVICE,
- NEW_SENSOR,
SUPPORTED_PLATFORMS,
)
+
from .errors import AuthenticationRequired, CannotConnect
@@ -61,14 +65,18 @@ def master(self) -> bool:
return self.config_entry.options[CONF_MASTER_GATEWAY]
@property
- def allow_clip_sensor(self) -> bool:
+ def option_allow_clip_sensor(self) -> bool:
"""Allow loading clip sensor from gateway."""
- return self.config_entry.options.get(CONF_ALLOW_CLIP_SENSOR, True)
+ return self.config_entry.options.get(
+ CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR
+ )
@property
- def allow_deconz_groups(self) -> bool:
+ def option_allow_deconz_groups(self) -> bool:
"""Allow loading deCONZ groups from gateway."""
- return self.config_entry.options.get(CONF_ALLOW_DECONZ_GROUPS, True)
+ return self.config_entry.options.get(
+ CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS
+ )
async def async_update_device_registry(self):
"""Update device registry."""
@@ -109,45 +117,52 @@ async def async_setup(self):
)
)
- self.listeners.append(
- async_dispatcher_connect(
- hass, self.async_event_new_device(NEW_SENSOR), self.async_add_remote
- )
- )
-
- self.async_add_remote(self.api.sensors.values())
-
self.api.start()
- self.config_entry.add_update_listener(self.async_new_address_callback)
+ self.config_entry.add_update_listener(self.async_new_address)
+ self.config_entry.add_update_listener(self.async_options_updated)
return True
@staticmethod
- async def async_new_address_callback(hass, entry):
+ async def async_new_address(hass, entry):
"""Handle signals of gateway getting new address.
This is a static method because a class method (bound method),
can not be used with weak references.
"""
- gateway = hass.data[DOMAIN][entry.data[CONF_BRIDGEID]]
- gateway.api.close()
- gateway.api.host = entry.data[CONF_HOST]
- gateway.api.start()
+ gateway = get_gateway_from_config_entry(hass, entry)
+ if gateway.api.host != entry.data[CONF_HOST]:
+ gateway.api.close()
+ gateway.api.host = entry.data[CONF_HOST]
+ gateway.api.start()
@property
- def event_reachable(self):
+ def signal_reachable(self):
"""Gateway specific event to signal a change in connection status."""
- return f"deconz_reachable_{self.bridgeid}"
+ return f"deconz-reachable-{self.bridgeid}"
@callback
def async_connection_status_callback(self, available):
"""Handle signals of gateway connection status."""
self.available = available
- async_dispatcher_send(self.hass, self.event_reachable, True)
+ async_dispatcher_send(self.hass, self.signal_reachable, True)
+
+ @property
+ def signal_options_update(self):
+ """Event specific per deCONZ entry to signal new options."""
+ return f"deconz-options-{self.bridgeid}"
+
+ @staticmethod
+ async def async_options_updated(hass, entry):
+ """Triggered by config entry options updates."""
+ gateway = get_gateway_from_config_entry(hass, entry)
+
+ registry = await async_get_registry(hass)
+ async_dispatcher_send(hass, gateway.signal_options_update, registry)
@callback
- def async_event_new_device(self, device_type):
+ def async_signal_new_device(self, device_type):
"""Gateway specific event to signal new device."""
return NEW_DEVICE[device_type].format(self.bridgeid)
@@ -157,18 +172,9 @@ def async_add_device_callback(self, device_type, device):
if not isinstance(device, list):
device = [device]
async_dispatcher_send(
- self.hass, self.async_event_new_device(device_type), device
+ self.hass, self.async_signal_new_device(device_type), device
)
- @callback
- def async_add_remote(self, sensors):
- """Set up remote from deCONZ."""
- for sensor in sensors:
- if sensor.type in Switch.ZHATYPE and not (
- not self.allow_clip_sensor and sensor.type.startswith("CLIP")
- ):
- self.events.append(DeconzEvent(self.hass, sensor))
-
@callback
def shutdown(self, event):
"""Wrap the call to deconz.close.
@@ -178,11 +184,8 @@ def shutdown(self, event):
self.api.close()
async def async_reset(self):
- """Reset this gateway to default state.
-
- Will cancel any scheduled setup retry and will unload
- the config entry.
- """
+ """Reset this gateway to default state."""
+ self.api.async_connection_status_callback = None
self.api.close()
for component in SUPPORTED_PLATFORMS:
@@ -196,7 +199,7 @@ async def async_reset(self):
for event in self.events:
event.async_will_remove_from_hass()
- self.events.remove(event)
+ self.events.clear()
self.deconz_ids = {}
return True
@@ -229,31 +232,36 @@ async def get_gateway(
raise CannotConnect
-class DeconzEvent:
- """When you want signals instead of entities.
+class DeconzEntityHandler:
+ """Platform entity handler to help with updating disabled by."""
- Stateless sensors such as remotes are expected to generate an event
- instead of a sensor entity in hass.
- """
+ def __init__(self, gateway):
+ """Create an entity handler."""
+ self.gateway = gateway
+ self._entities = []
- def __init__(self, hass, device):
- """Register callback that will be used for signals."""
- self._hass = hass
- self._device = device
- self._device.register_async_callback(self.async_update_callback)
- self._event = f"deconz_{CONF_EVENT}"
- self._id = slugify(self._device.name)
- _LOGGER.debug("deCONZ event created: %s", self._id)
+ gateway.listeners.append(
+ async_dispatcher_connect(
+ gateway.hass, gateway.signal_options_update, self.update_entity_registry
+ )
+ )
@callback
- def async_will_remove_from_hass(self) -> None:
- """Disconnect event object when removed."""
- self._device.remove_callback(self.async_update_callback)
- self._device = None
+ def add_entity(self, entity):
+ """Add a new entity to handler."""
+ self._entities.append(entity)
@callback
- def async_update_callback(self, force_update=False):
- """Fire the event if reason is that state is updated."""
- if "state" in self._device.changed_keys:
- data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
- self._hass.bus.async_fire(self._event, data, EventOrigin.remote)
+ def update_entity_registry(self, entity_registry):
+ """Update entity registry disabled by status."""
+ for entity in self._entities:
+
+ if entity.entity_registry_enabled_default != entity.enabled:
+ disabled_by = None
+
+ if entity.enabled:
+ disabled_by = DISABLED_CONFIG_ENTRY
+
+ entity_registry.async_update_entity(
+ entity.registry_entry.entity_id, disabled_by=disabled_by
+ )
diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py
index b68aa6f07796d6..bf4b05089a84c0 100644
--- a/homeassistant/components/deconz/light.py
+++ b/homeassistant/components/deconz/light.py
@@ -29,18 +29,19 @@
SWITCH_TYPES,
)
from .deconz_device import DeconzDevice
-from .gateway import get_gateway_from_config_entry
+from .gateway import get_gateway_from_config_entry, DeconzEntityHandler
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ platforms."""
- pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ lights and groups from a config entry."""
gateway = get_gateway_from_config_entry(hass, config_entry)
+ entity_handler = DeconzEntityHandler(gateway)
+
@callback
def async_add_light(lights):
"""Add light from deCONZ."""
@@ -54,7 +55,7 @@ def async_add_light(lights):
gateway.listeners.append(
async_dispatcher_connect(
- hass, gateway.async_event_new_device(NEW_LIGHT), async_add_light
+ hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_light
)
)
@@ -64,14 +65,16 @@ def async_add_group(groups):
entities = []
for group in groups:
- if group.lights and gateway.allow_deconz_groups:
- entities.append(DeconzGroup(group, gateway))
+ if group.lights:
+ new_group = DeconzGroup(group, gateway)
+ entity_handler.add_entity(new_group)
+ entities.append(new_group)
async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
- hass, gateway.async_event_new_device(NEW_GROUP), async_add_group
+ hass, gateway.async_signal_new_device(NEW_GROUP), async_add_group
)
)
@@ -190,9 +193,6 @@ def device_state_attributes(self):
attributes = {}
attributes["is_deconz_group"] = self._device.type == "LightGroup"
- if self._device.type == "LightGroup":
- attributes["all_on"] = self._device.all_on
-
return attributes
@@ -203,9 +203,7 @@ def __init__(self, device, gateway):
"""Set up group and create an unique id."""
super().__init__(device, gateway)
- self._unique_id = "{}-{}".format(
- self.gateway.api.config.bridgeid, self._device.deconz_id
- )
+ self._unique_id = f"{self.gateway.api.config.bridgeid}-{self._device.deconz_id}"
@property
def unique_id(self):
@@ -224,3 +222,11 @@ def device_info(self):
"name": self._device.name,
"via_device": (DECONZ_DOMAIN, bridgeid),
}
+
+ @property
+ def device_state_attributes(self):
+ """Return the device state attributes."""
+ attributes = dict(super().device_state_attributes)
+ attributes["all_on"] = self._device.all_on
+
+ return attributes
diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py
index ede60e3ef453ed..a84e799d44d47b 100644
--- a/homeassistant/components/deconz/scene.py
+++ b/homeassistant/components/deconz/scene.py
@@ -9,7 +9,6 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ platforms."""
- pass
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -28,7 +27,7 @@ def async_add_scene(scenes):
gateway.listeners.append(
async_dispatcher_connect(
- hass, gateway.async_event_new_device(NEW_SCENE), async_add_scene
+ hass, gateway.async_signal_new_device(NEW_SCENE), async_add_scene
)
)
@@ -49,6 +48,7 @@ async def async_added_to_hass(self):
async def async_will_remove_from_hass(self) -> None:
"""Disconnect scene object when removed."""
+ del self.gateway.deconz_ids[self.entity_id]
self._scene = None
async def async_activate(self):
diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py
index dad3c25cc38d0a..cc3f3de3170c66 100644
--- a/homeassistant/components/deconz/sensor.py
+++ b/homeassistant/components/deconz/sensor.py
@@ -1,19 +1,14 @@
"""Support for deCONZ sensors."""
-from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch
-
-from homeassistant.const import (
- ATTR_BATTERY_LEVEL,
- ATTR_TEMPERATURE,
- ATTR_VOLTAGE,
- DEVICE_CLASS_BATTERY,
-)
+from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch, Thermostat
+
+from homeassistant.const import ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.util import slugify
from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR
from .deconz_device import DeconzDevice
-from .gateway import get_gateway_from_config_entry
+from .deconz_event import DeconzEvent
+from .gateway import get_gateway_from_config_entry, DeconzEntityHandler
ATTR_CURRENT = "current"
ATTR_POWER = "power"
@@ -23,36 +18,53 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ platforms."""
- pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ sensors."""
gateway = get_gateway_from_config_entry(hass, config_entry)
+ batteries = set()
+ entity_handler = DeconzEntityHandler(gateway)
+
@callback
def async_add_sensor(sensors):
- """Add sensors from deCONZ."""
+ """Add sensors from deCONZ.
+
+ Create DeconzEvent if part of ZHAType list.
+ Create DeconzSensor if not a ZHAType and not a binary sensor.
+ Create DeconzBattery if sensor has a battery attribute.
+ """
entities = []
for sensor in sensors:
- if not sensor.BINARY and not (
- not gateway.allow_clip_sensor and sensor.type.startswith("CLIP")
- ):
+ if sensor.type in Switch.ZHATYPE:
- if sensor.type in Switch.ZHATYPE:
- if sensor.battery:
- entities.append(DeconzBattery(sensor, gateway))
+ if gateway.option_allow_clip_sensor or not sensor.type.startswith(
+ "CLIP"
+ ):
+ new_event = DeconzEvent(sensor, gateway)
+ hass.async_create_task(new_event.async_update_device_registry())
+ gateway.events.append(new_event)
- else:
- entities.append(DeconzSensor(sensor, gateway))
+ elif not sensor.BINARY and sensor.type not in Thermostat.ZHATYPE:
+
+ new_sensor = DeconzSensor(sensor, gateway)
+ entity_handler.add_entity(new_sensor)
+ entities.append(new_sensor)
+
+ if sensor.battery:
+ new_battery = DeconzBattery(sensor, gateway)
+ if new_battery.unique_id not in batteries:
+ batteries.add(new_battery.unique_id)
+ entities.append(new_battery)
async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
- hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor
+ hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor
)
)
@@ -66,7 +78,7 @@ class DeconzSensor(DeconzDevice):
def async_update_callback(self, force_update=False):
"""Update the sensor's state."""
changed = set(self._device.changed_keys)
- keys = {"battery", "on", "reachable", "state"}
+ keys = {"on", "reachable", "state"}
if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@@ -94,8 +106,6 @@ def unit_of_measurement(self):
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
attr = {}
- if self._device.battery:
- attr[ATTR_BATTERY_LEVEL] = self._device.battery
if self._device.on is not None:
attr[ATTR_ON] = self._device.on
@@ -122,13 +132,6 @@ def device_state_attributes(self):
class DeconzBattery(DeconzDevice):
"""Battery class for when a device is only represented as an event."""
- def __init__(self, device, gateway):
- """Register dispatcher callback for update of battery state."""
- super().__init__(device, gateway)
-
- self._name = "{} {}".format(self._device.name, "Battery Level")
- self._unit_of_measurement = "%"
-
@callback
def async_update_callback(self, force_update=False):
"""Update the battery's state, if needed."""
@@ -137,6 +140,11 @@ def async_update_callback(self, force_update=False):
if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this device."""
+ return f"{self.serial}-battery"
+
@property
def state(self):
"""Return the state of the battery."""
@@ -145,7 +153,7 @@ def state(self):
@property
def name(self):
"""Return the name of the battery."""
- return self._name
+ return f"{self._device.name} Battery Level"
@property
def device_class(self):
@@ -155,10 +163,16 @@ def device_class(self):
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
- return self._unit_of_measurement
+ return "%"
@property
def device_state_attributes(self):
"""Return the state attributes of the battery."""
- attr = {ATTR_EVENT_ID: slugify(self._device.name)}
+ attr = {}
+
+ if self._device.type in Switch.ZHATYPE:
+ for event in self.gateway.events:
+ if self._device == event.device:
+ attr[ATTR_EVENT_ID] = event.event_id
+
return attr
diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py
new file mode 100644
index 00000000000000..3498b46d879c8f
--- /dev/null
+++ b/homeassistant/components/deconz/services.py
@@ -0,0 +1,158 @@
+"""deCONZ services."""
+import voluptuous as vol
+
+from homeassistant.helpers import config_validation as cv
+
+from .config_flow import get_master_gateway
+from .const import CONF_BRIDGEID, DOMAIN, _LOGGER
+
+DECONZ_SERVICES = "deconz_services"
+
+SERVICE_FIELD = "field"
+SERVICE_ENTITY = "entity"
+SERVICE_DATA = "data"
+
+SERVICE_CONFIGURE_DEVICE = "configure"
+SERVICE_CONFIGURE_DEVICE_SCHEMA = vol.All(
+ vol.Schema(
+ {
+ vol.Optional(SERVICE_ENTITY): cv.entity_id,
+ vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"),
+ vol.Required(SERVICE_DATA): dict,
+ vol.Optional(CONF_BRIDGEID): str,
+ }
+ ),
+ cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD),
+)
+
+SERVICE_DEVICE_REFRESH = "device_refresh"
+SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGEID): str}))
+
+
+async def async_setup_services(hass):
+ """Set up services for deCONZ integration."""
+ if hass.data.get(DECONZ_SERVICES, False):
+ return
+
+ hass.data[DECONZ_SERVICES] = True
+
+ async def async_call_deconz_service(service_call):
+ """Call correct deCONZ service."""
+ service = service_call.service
+ service_data = service_call.data
+
+ if service == SERVICE_CONFIGURE_DEVICE:
+ await async_configure_service(hass, service_data)
+
+ elif service == SERVICE_DEVICE_REFRESH:
+ await async_refresh_devices_service(hass, service_data)
+
+ hass.services.async_register(
+ DOMAIN,
+ SERVICE_CONFIGURE_DEVICE,
+ async_call_deconz_service,
+ schema=SERVICE_CONFIGURE_DEVICE_SCHEMA,
+ )
+
+ hass.services.async_register(
+ DOMAIN,
+ SERVICE_DEVICE_REFRESH,
+ async_call_deconz_service,
+ schema=SERVICE_DEVICE_REFRESH_SCHEMA,
+ )
+
+
+async def async_unload_services(hass):
+ """Unload deCONZ services."""
+ if not hass.data.get(DECONZ_SERVICES):
+ return
+
+ hass.data[DECONZ_SERVICES] = False
+
+ hass.services.async_remove(DOMAIN, SERVICE_CONFIGURE_DEVICE)
+ hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH)
+
+
+async def async_configure_service(hass, data):
+ """Set attribute of device in deCONZ.
+
+ Entity is used to resolve to a device path (e.g. '/lights/1').
+ Field is a string representing either a full path
+ (e.g. '/lights/1/state') when entity is not specified, or a
+ subpath (e.g. '/state') when used together with entity.
+ Data is a json object with what data you want to alter
+ e.g. data={'on': true}.
+ {
+ "field": "/lights/1/state",
+ "data": {"on": true}
+ }
+ See Dresden Elektroniks REST API documentation for details:
+ http://dresden-elektronik.github.io/deconz-rest-doc/rest/
+ """
+ bridgeid = data.get(CONF_BRIDGEID)
+ field = data.get(SERVICE_FIELD, "")
+ entity_id = data.get(SERVICE_ENTITY)
+ data = data[SERVICE_DATA]
+
+ gateway = get_master_gateway(hass)
+ if bridgeid:
+ gateway = hass.data[DOMAIN][bridgeid]
+
+ if entity_id:
+ try:
+ field = gateway.deconz_ids[entity_id] + field
+ except KeyError:
+ _LOGGER.error("Could not find the entity %s", entity_id)
+ return
+
+ await gateway.api.async_put_state(field, data)
+
+
+async def async_refresh_devices_service(hass, data):
+ """Refresh available devices from deCONZ."""
+ gateway = get_master_gateway(hass)
+ if CONF_BRIDGEID in data:
+ gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]]
+
+ groups = set(gateway.api.groups.keys())
+ lights = set(gateway.api.lights.keys())
+ scenes = set(gateway.api.scenes.keys())
+ sensors = set(gateway.api.sensors.keys())
+
+ await gateway.api.async_load_parameters()
+
+ gateway.async_add_device_callback(
+ "group",
+ [
+ group
+ for group_id, group in gateway.api.groups.items()
+ if group_id not in groups
+ ],
+ )
+
+ gateway.async_add_device_callback(
+ "light",
+ [
+ light
+ for light_id, light in gateway.api.lights.items()
+ if light_id not in lights
+ ],
+ )
+
+ gateway.async_add_device_callback(
+ "scene",
+ [
+ scene
+ for scene_id, scene in gateway.api.scenes.items()
+ if scene_id not in scenes
+ ],
+ )
+
+ gateway.async_add_device_callback(
+ "sensor",
+ [
+ sensor
+ for sensor_id, sensor in gateway.api.sensors.items()
+ if sensor_id not in sensors
+ ],
+ )
diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json
index 7081f816e6ae08..00aa463349cf68 100644
--- a/homeassistant/components/deconz/strings.json
+++ b/homeassistant/components/deconz/strings.json
@@ -51,5 +51,34 @@
}
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "remote_button_short_press": "\"{subtype}\" button pressed",
+ "remote_button_short_release": "\"{subtype}\" button released",
+ "remote_button_long_press": "\"{subtype}\" button continuously pressed",
+ "remote_button_long_release": "\"{subtype}\" button released after long press",
+ "remote_button_double_press": "\"{subtype}\" button double clicked",
+ "remote_button_triple_press": "\"{subtype}\" button triple clicked",
+ "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked",
+ "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked",
+ "remote_button_rotated": "Button rotated \"{subtype}\"",
+ "remote_gyro_activated": "Device shaken"
+ },
+ "trigger_subtype": {
+ "turn_on": "Turn on",
+ "turn_off": "Turn off",
+ "dim_up": "Dim up",
+ "dim_down": "Dim down",
+ "left": "Left",
+ "right": "Right",
+ "open": "Open",
+ "close": "Close",
+ "both_buttons": "Both buttons",
+ "button_1": "First button",
+ "button_2": "Second button",
+ "button_3": "Third button",
+ "button_4": "Fourth button"
+ }
}
}
diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py
index 7ce40789802f3d..1b51256580aeb7 100644
--- a/homeassistant/components/deconz/switch.py
+++ b/homeassistant/components/deconz/switch.py
@@ -10,7 +10,6 @@
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ platforms."""
- pass
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -37,7 +36,7 @@ def async_add_switch(lights):
gateway.listeners.append(
async_dispatcher_connect(
- hass, gateway.async_event_new_device(NEW_LIGHT), async_add_switch
+ hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_switch
)
)
diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py
index b81a2193bb54ab..2253f261ad2cef 100644
--- a/homeassistant/components/demo/weather.py
+++ b/homeassistant/components/demo/weather.py
@@ -1,5 +1,5 @@
"""Demo platform that offers fake meteorological data."""
-from datetime import datetime, timedelta
+from datetime import timedelta
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
@@ -10,6 +10,7 @@
WeatherEntity,
)
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
+import homeassistant.util.dt as dt_util
CONDITION_CLASSES = {
"cloudy": [],
@@ -147,7 +148,7 @@ def attribution(self):
@property
def forecast(self):
"""Return the forecast."""
- reftime = datetime.now().replace(hour=16, minute=00)
+ reftime = dt_util.now().replace(hour=16, minute=00)
forecast_data = []
for entry in self._forecast:
diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py
index 018e1286d8bc53..9508dd9c849a93 100644
--- a/homeassistant/components/device_automation/__init__.py
+++ b/homeassistant/components/device_automation/__init__.py
@@ -1,12 +1,16 @@
"""Helpers for device automations."""
import asyncio
import logging
+from typing import Callable, cast
import voluptuous as vol
from homeassistant.components import websocket_api
-from homeassistant.core import split_entity_id
+from homeassistant.const import CONF_DOMAIN
+from homeassistant.core import split_entity_id, HomeAssistant
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_registry import async_entries_for_device
+from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_integration, IntegrationNotFound
DOMAIN = "device_automation"
@@ -16,14 +20,34 @@
async def async_setup(hass, config):
"""Set up device automation."""
+ hass.components.websocket_api.async_register_command(
+ websocket_device_automation_list_actions
+ )
+ hass.components.websocket_api.async_register_command(
+ websocket_device_automation_list_conditions
+ )
hass.components.websocket_api.async_register_command(
websocket_device_automation_list_triggers
)
return True
-async def _async_get_device_automation_triggers(hass, domain, device_id):
- """List device triggers."""
+async def async_device_condition_from_config(
+ hass: HomeAssistant, config: ConfigType, config_validation: bool = True
+) -> Callable[..., bool]:
+ """Wrap action method with state based condition."""
+ if config_validation:
+ config = cv.DEVICE_CONDITION_SCHEMA(config)
+ integration = await async_get_integration(hass, config[CONF_DOMAIN])
+ platform = integration.get_platform("device_automation")
+ return cast(
+ Callable[..., bool],
+ platform.async_condition_from_config(config, config_validation), # type: ignore
+ )
+
+
+async def _async_get_device_automations_from_domain(hass, domain, fname, device_id):
+ """List device automations."""
integration = None
try:
integration = await async_get_integration(hass, domain)
@@ -37,19 +61,19 @@ async def _async_get_device_automation_triggers(hass, domain, device_id):
# The domain does not have device automations
return None
- if hasattr(platform, "async_get_triggers"):
- return await platform.async_get_triggers(hass, device_id)
+ if hasattr(platform, fname):
+ return await getattr(platform, fname)(hass, device_id)
-async def async_get_device_automation_triggers(hass, device_id):
- """List device triggers."""
+async def _async_get_device_automations(hass, fname, device_id):
+ """List device automations."""
device_registry, entity_registry = await asyncio.gather(
hass.helpers.device_registry.async_get_registry(),
hass.helpers.entity_registry.async_get_registry(),
)
domains = set()
- triggers = []
+ automations = []
device = device_registry.async_get(device_id)
for entry_id in device.config_entries:
config_entry = hass.config_entries.async_get_entry(entry_id)
@@ -59,17 +83,47 @@ async def async_get_device_automation_triggers(hass, device_id):
for entity in entities:
domains.add(split_entity_id(entity.entity_id)[0])
- device_triggers = await asyncio.gather(
+ device_automations = await asyncio.gather(
*(
- _async_get_device_automation_triggers(hass, domain, device_id)
+ _async_get_device_automations_from_domain(hass, domain, fname, device_id)
for domain in domains
)
)
- for device_trigger in device_triggers:
- if device_trigger is not None:
- triggers.extend(device_trigger)
+ for device_automation in device_automations:
+ if device_automation is not None:
+ automations.extend(device_automation)
- return triggers
+ return automations
+
+
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "device_automation/action/list",
+ vol.Required("device_id"): str,
+ }
+)
+async def websocket_device_automation_list_actions(hass, connection, msg):
+ """Handle request for device actions."""
+ device_id = msg["device_id"]
+ actions = await _async_get_device_automations(hass, "async_get_actions", device_id)
+ connection.send_result(msg["id"], actions)
+
+
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "device_automation/condition/list",
+ vol.Required("device_id"): str,
+ }
+)
+async def websocket_device_automation_list_conditions(hass, connection, msg):
+ """Handle request for device conditions."""
+ device_id = msg["device_id"]
+ conditions = await _async_get_device_automations(
+ hass, "async_get_conditions", device_id
+ )
+ connection.send_result(msg["id"], conditions)
@websocket_api.async_response
@@ -82,5 +136,7 @@ async def async_get_device_automation_triggers(hass, device_id):
async def websocket_device_automation_list_triggers(hass, connection, msg):
"""Handle request for device triggers."""
device_id = msg["device_id"]
- triggers = await async_get_device_automation_triggers(hass, device_id)
- connection.send_result(msg["id"], {"triggers": triggers})
+ triggers = await _async_get_device_automations(
+ hass, "async_get_triggers", device_id
+ )
+ connection.send_result(msg["id"], triggers)
diff --git a/homeassistant/components/device_automation/const.py b/homeassistant/components/device_automation/const.py
new file mode 100644
index 00000000000000..40bfc4ca0a13e4
--- /dev/null
+++ b/homeassistant/components/device_automation/const.py
@@ -0,0 +1,8 @@
+"""Constants for device automations."""
+CONF_IS_OFF = "is_off"
+CONF_IS_ON = "is_on"
+CONF_TOGGLE = "toggle"
+CONF_TURN_OFF = "turn_off"
+CONF_TURN_ON = "turn_on"
+CONF_TURNED_OFF = "turned_off"
+CONF_TURNED_ON = "turned_on"
diff --git a/homeassistant/components/device_automation/exceptions.py b/homeassistant/components/device_automation/exceptions.py
new file mode 100644
index 00000000000000..2f7c0df01876f7
--- /dev/null
+++ b/homeassistant/components/device_automation/exceptions.py
@@ -0,0 +1,6 @@
+"""Device automation exceptions."""
+from homeassistant.exceptions import HomeAssistantError
+
+
+class InvalidDeviceAutomationConfig(HomeAssistantError):
+ """When device automation config is invalid."""
diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py
new file mode 100644
index 00000000000000..1593e70771aea3
--- /dev/null
+++ b/homeassistant/components/device_automation/toggle_entity.py
@@ -0,0 +1,186 @@
+"""Device automation helpers for toggle entity."""
+import voluptuous as vol
+
+import homeassistant.components.automation.state as state
+from homeassistant.components.device_automation.const import (
+ CONF_IS_OFF,
+ CONF_IS_ON,
+ CONF_TOGGLE,
+ CONF_TURN_OFF,
+ CONF_TURN_ON,
+ CONF_TURNED_OFF,
+ CONF_TURNED_ON,
+)
+from homeassistant.core import split_entity_id
+from homeassistant.const import (
+ CONF_CONDITION,
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ CONF_ENTITY_ID,
+ CONF_PLATFORM,
+ CONF_TYPE,
+)
+from homeassistant.helpers.entity_registry import async_entries_for_device
+from homeassistant.helpers import condition, config_validation as cv, service
+
+ENTITY_ACTIONS = [
+ {
+ # Turn entity off
+ CONF_TYPE: CONF_TURN_OFF
+ },
+ {
+ # Turn entity on
+ CONF_TYPE: CONF_TURN_ON
+ },
+ {
+ # Toggle entity
+ CONF_TYPE: CONF_TOGGLE
+ },
+]
+
+ENTITY_CONDITIONS = [
+ {
+ # True when entity is turned off
+ CONF_CONDITION: "device",
+ CONF_TYPE: CONF_IS_OFF,
+ },
+ {
+ # True when entity is turned on
+ CONF_CONDITION: "device",
+ CONF_TYPE: CONF_IS_ON,
+ },
+]
+
+ENTITY_TRIGGERS = [
+ {
+ # Trigger when entity is turned off
+ CONF_PLATFORM: "device",
+ CONF_TYPE: CONF_TURNED_OFF,
+ },
+ {
+ # Trigger when entity is turned on
+ CONF_PLATFORM: "device",
+ CONF_TYPE: CONF_TURNED_ON,
+ },
+]
+
+ACTION_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_DEVICE_ID): str,
+ vol.Required(CONF_DOMAIN): str,
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In([CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]),
+ }
+)
+
+CONDITION_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_CONDITION): "device",
+ vol.Required(CONF_DEVICE_ID): str,
+ vol.Required(CONF_DOMAIN): str,
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]),
+ }
+)
+
+TRIGGER_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_PLATFORM): "device",
+ vol.Required(CONF_DEVICE_ID): str,
+ vol.Required(CONF_DOMAIN): str,
+ vol.Required(CONF_ENTITY_ID): cv.entity_id,
+ vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]),
+ }
+)
+
+
+def _is_domain(entity, domain):
+ return split_entity_id(entity.entity_id)[0] == domain
+
+
+async def async_call_action_from_config(hass, config, variables, context, domain):
+ """Change state based on configuration."""
+ config = ACTION_SCHEMA(config)
+ action_type = config[CONF_TYPE]
+ if action_type == CONF_TURN_ON:
+ action = "turn_on"
+ elif action_type == CONF_TURN_OFF:
+ action = "turn_off"
+ else:
+ action = "toggle"
+
+ service_action = {
+ service.CONF_SERVICE: "{}.{}".format(domain, action),
+ CONF_ENTITY_ID: config[CONF_ENTITY_ID],
+ }
+
+ await service.async_call_from_config(
+ hass, service_action, blocking=True, variables=variables, context=context
+ )
+
+
+def async_condition_from_config(config, config_validation):
+ """Evaluate state based on configuration."""
+ condition_type = config[CONF_TYPE]
+ if condition_type == CONF_IS_ON:
+ stat = "on"
+ else:
+ stat = "off"
+ state_config = {
+ condition.CONF_CONDITION: "state",
+ condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
+ condition.CONF_STATE: stat,
+ }
+
+ return condition.state_from_config(state_config, config_validation)
+
+
+async def async_attach_trigger(hass, config, action, automation_info):
+ """Listen for state changes based on configuration."""
+ trigger_type = config[CONF_TYPE]
+ if trigger_type == CONF_TURNED_ON:
+ from_state = "off"
+ to_state = "on"
+ else:
+ from_state = "on"
+ to_state = "off"
+ state_config = {
+ state.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
+ state.CONF_FROM: from_state,
+ state.CONF_TO: to_state,
+ }
+
+ return await state.async_trigger(hass, state_config, action, automation_info)
+
+
+async def _async_get_automations(hass, device_id, automation_templates, domain):
+ """List device automations."""
+ automations = []
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+
+ entities = async_entries_for_device(entity_registry, device_id)
+ domain_entities = [x for x in entities if _is_domain(x, domain)]
+ for entity in domain_entities:
+ for automation in automation_templates:
+ automation = dict(automation)
+ automation.update(
+ device_id=device_id, entity_id=entity.entity_id, domain=domain
+ )
+ automations.append(automation)
+
+ return automations
+
+
+async def async_get_actions(hass, device_id, domain):
+ """List device actions."""
+ return await _async_get_automations(hass, device_id, ENTITY_ACTIONS, domain)
+
+
+async def async_get_conditions(hass, device_id, domain):
+ """List device conditions."""
+ return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS, domain)
+
+
+async def async_get_triggers(hass, device_id, domain):
+ """List device triggers."""
+ return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain)
diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py
index 1b71b44369da14..9a058cfacc10bb 100644
--- a/homeassistant/components/device_sun_light_trigger/__init__.py
+++ b/homeassistant/components/device_sun_light_trigger/__init__.py
@@ -63,12 +63,14 @@ async def async_setup(hass, config):
device_tracker = hass.components.device_tracker
group = hass.components.group
light = hass.components.light
+ person = hass.components.person
conf = config[DOMAIN]
disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF)
light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS)
light_profile = conf.get(CONF_LIGHT_PROFILE)
device_group = conf.get(CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES)
device_entity_ids = group.get_entity_ids(device_group, device_tracker.DOMAIN)
+ device_entity_ids.extend(group.get_entity_ids(device_group, person.DOMAIN))
if not device_entity_ids:
logger.error("No devices found to track")
diff --git a/homeassistant/components/device_sun_light_trigger/manifest.json b/homeassistant/components/device_sun_light_trigger/manifest.json
index abe5a1d500cb81..40ab85bc1e5fbb 100644
--- a/homeassistant/components/device_sun_light_trigger/manifest.json
+++ b/homeassistant/components/device_sun_light_trigger/manifest.json
@@ -6,7 +6,8 @@
"dependencies": [
"device_tracker",
"group",
- "light"
+ "light",
+ "person"
],
"codeowners": []
}
diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py
index 460f11984096ca..9e53c2e0cea24e 100644
--- a/homeassistant/components/device_tracker/config_entry.py
+++ b/homeassistant/components/device_tracker/config_entry.py
@@ -18,7 +18,7 @@
async def async_setup_entry(hass, entry):
"""Set up an entry."""
- component = hass.data.get(DOMAIN) # type: Optional[EntityComponent]
+ component: Optional[EntityComponent] = hass.data.get(DOMAIN)
if component is None:
component = hass.data[DOMAIN] = EntityComponent(LOGGER, DOMAIN, hass)
diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py
index 2bfd0c41a47bfd..5c186cc12a1182 100644
--- a/homeassistant/components/device_tracker/legacy.py
+++ b/homeassistant/components/device_tracker/legacy.py
@@ -327,15 +327,15 @@ async def async_init_single_device(dev):
class Device(RestoreEntity):
"""Represent a tracked device."""
- host_name = None # type: str
- location_name = None # type: str
- gps = None # type: GPSType
- gps_accuracy = 0 # type: int
- last_seen = None # type: dt_util.dt.datetime
- consider_home = None # type: dt_util.dt.timedelta
- battery = None # type: int
- attributes = None # type: dict
- icon = None # type: str
+ host_name: str = None
+ location_name: str = None
+ gps: GPSType = None
+ gps_accuracy: int = 0
+ last_seen: dt_util.dt.datetime = None
+ consider_home: dt_util.dt.timedelta = None
+ battery: int = None
+ attributes: dict = None
+ icon: str = None
# Track if the last update of this device was HOME.
last_update_home = False
diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py
index e6edb5f63ac6a0..6c9f05dead7349 100644
--- a/homeassistant/components/device_tracker/setup.py
+++ b/homeassistant/components/device_tracker/setup.py
@@ -147,7 +147,7 @@ def async_setup_scanner_platform(
scanner.hass = hass
# Initial scan of each mac we also tell about host name for config
- seen = set() # type: Any
+ seen: Any = set()
async def async_device_tracker_scan(now: dt_util.dt.datetime):
"""Handle interval matches."""
diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py
index 5f1fd335d45d89..15fcfc15338da2 100644
--- a/homeassistant/components/discovery/__init__.py
+++ b/homeassistant/components/discovery/__init__.py
@@ -36,6 +36,7 @@
SERVICE_MOBILE_APP = "hass_mobile_app"
SERVICE_NETGEAR = "netgear_router"
SERVICE_OCTOPRINT = "octoprint"
+SERVICE_PLEX = "plex_mediaserver"
SERVICE_ROKU = "roku"
SERVICE_SABNZBD = "sabnzbd"
SERVICE_SAMSUNG_PRINTER = "samsung_printer"
@@ -49,6 +50,7 @@
SERVICE_DAIKIN: "daikin",
SERVICE_TELLDUSLIVE: "tellduslive",
SERVICE_IGD: "upnp",
+ SERVICE_PLEX: "plex",
}
SERVICE_HANDLERS = {
@@ -68,7 +70,6 @@
SERVICE_FREEBOX: ("freebox", None),
SERVICE_YEELIGHT: ("yeelight", None),
"panasonic_viera": ("media_player", "panasonic_viera"),
- "plex_mediaserver": ("media_player", "plex"),
"yamaha": ("media_player", "yamaha"),
"logitech_mediaserver": ("media_player", "squeezebox"),
"directv": ("media_player", "directv"),
diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json
index 4e7b11767beb5b..bf05d5c7f63fdd 100644
--- a/homeassistant/components/dlna_dmr/manifest.json
+++ b/homeassistant/components/dlna_dmr/manifest.json
@@ -3,7 +3,7 @@
"name": "Dlna dmr",
"documentation": "https://www.home-assistant.io/components/dlna_dmr",
"requirements": [
- "async-upnp-client==0.14.10"
+ "async-upnp-client==0.14.11"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py
index c7c488950cc747..5dd7ab7a88a74d 100644
--- a/homeassistant/components/dlna_dmr/media_player.py
+++ b/homeassistant/components/dlna_dmr/media_player.py
@@ -1,6 +1,5 @@
"""Support for DLNA DMR (Device Media Renderer)."""
import asyncio
-from datetime import datetime
from datetime import timedelta
import functools
import logging
@@ -43,6 +42,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.helpers.config_validation as cv
+import homeassistant.util.dt as dt_util
from homeassistant.util import get_local_ip
_LOGGER = logging.getLogger(__name__)
@@ -241,14 +241,14 @@ async def async_update(self):
return
# do we need to (re-)subscribe?
- now = datetime.now()
+ now = dt_util.utcnow()
should_renew = (
self._subscription_renew_time and now >= self._subscription_renew_time
)
if should_renew or not was_available and self._available:
try:
timeout = await self._device.async_subscribe_services()
- self._subscription_renew_time = datetime.now() + timeout / 2
+ self._subscription_renew_time = dt_util.utcnow() + timeout / 2
except (asyncio.TimeoutError, aiohttp.ClientError):
self._available = False
_LOGGER.debug("Could not (re)subscribe")
diff --git a/homeassistant/components/doods/__init__.py b/homeassistant/components/doods/__init__.py
new file mode 100644
index 00000000000000..b6edb9be87bda0
--- /dev/null
+++ b/homeassistant/components/doods/__init__.py
@@ -0,0 +1 @@
+"""The doods component."""
diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py
new file mode 100644
index 00000000000000..850eae76040f2a
--- /dev/null
+++ b/homeassistant/components/doods/image_processing.py
@@ -0,0 +1,363 @@
+"""Support for the DOODS service."""
+import io
+import logging
+import time
+
+import voluptuous as vol
+from PIL import Image, ImageDraw
+from pydoods import PyDOODS
+
+from homeassistant.components.image_processing import (
+ CONF_CONFIDENCE,
+ CONF_ENTITY_ID,
+ CONF_NAME,
+ CONF_SOURCE,
+ PLATFORM_SCHEMA,
+ ImageProcessingEntity,
+)
+from homeassistant.core import split_entity_id
+from homeassistant.helpers import template
+import homeassistant.helpers.config_validation as cv
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_MATCHES = "matches"
+ATTR_SUMMARY = "summary"
+ATTR_TOTAL_MATCHES = "total_matches"
+
+CONF_URL = "url"
+CONF_AUTH_KEY = "auth_key"
+CONF_DETECTOR = "detector"
+CONF_LABELS = "labels"
+CONF_AREA = "area"
+CONF_TOP = "top"
+CONF_BOTTOM = "bottom"
+CONF_RIGHT = "right"
+CONF_LEFT = "left"
+CONF_FILE_OUT = "file_out"
+
+AREA_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_BOTTOM, default=1): cv.small_float,
+ vol.Optional(CONF_LEFT, default=0): cv.small_float,
+ vol.Optional(CONF_RIGHT, default=1): cv.small_float,
+ vol.Optional(CONF_TOP, default=0): cv.small_float,
+ }
+)
+
+LABEL_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_NAME): cv.string,
+ vol.Optional(CONF_AREA): AREA_SCHEMA,
+ vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100),
+ }
+)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_URL): cv.string,
+ vol.Required(CONF_DETECTOR): cv.string,
+ vol.Optional(CONF_AUTH_KEY, default=""): cv.string,
+ vol.Optional(CONF_FILE_OUT, default=[]): vol.All(cv.ensure_list, [cv.template]),
+ vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100),
+ vol.Optional(CONF_LABELS, default=[]): vol.All(
+ cv.ensure_list, [vol.Any(cv.string, LABEL_SCHEMA)]
+ ),
+ vol.Optional(CONF_AREA): AREA_SCHEMA,
+ }
+)
+
+
+def draw_box(draw, box, img_width, img_height, text="", color=(255, 255, 0)):
+ """Draw bounding box on image."""
+ ymin, xmin, ymax, xmax = box
+ (left, right, top, bottom) = (
+ xmin * img_width,
+ xmax * img_width,
+ ymin * img_height,
+ ymax * img_height,
+ )
+ draw.line(
+ [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)],
+ width=5,
+ fill=color,
+ )
+ if text:
+ draw.text((left, abs(top - 15)), text, fill=color)
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the Doods client."""
+ url = config[CONF_URL]
+ auth_key = config[CONF_AUTH_KEY]
+ detector_name = config[CONF_DETECTOR]
+
+ doods = PyDOODS(url, auth_key)
+ response = doods.get_detectors()
+ if not isinstance(response, dict):
+ _LOGGER.warning("Could not connect to doods server: %s", url)
+ return
+
+ detector = {}
+ for server_detector in response["detectors"]:
+ if server_detector["name"] == detector_name:
+ detector = server_detector
+ break
+
+ if not detector:
+ _LOGGER.warning(
+ "Detector %s is not supported by doods server %s", detector_name, url
+ )
+ return
+
+ entities = []
+ for camera in config[CONF_SOURCE]:
+ entities.append(
+ Doods(
+ hass,
+ camera[CONF_ENTITY_ID],
+ camera.get(CONF_NAME),
+ doods,
+ detector,
+ config,
+ )
+ )
+ add_entities(entities)
+
+
+class Doods(ImageProcessingEntity):
+ """Doods image processing service client."""
+
+ def __init__(self, hass, camera_entity, name, doods, detector, config):
+ """Initialize the DOODS entity."""
+ self.hass = hass
+ self._camera_entity = camera_entity
+ if name:
+ self._name = name
+ else:
+ name = split_entity_id(camera_entity)[1]
+ self._name = f"Doods {name}"
+ self._doods = doods
+ self._file_out = config[CONF_FILE_OUT]
+ self._detector_name = detector["name"]
+
+ # detector config and aspect ratio
+ self._width = None
+ self._height = None
+ self._aspect = None
+ if detector["width"] and detector["height"]:
+ self._width = detector["width"]
+ self._height = detector["height"]
+ self._aspect = self._width / self._height
+
+ # the base confidence
+ dconfig = {}
+ confidence = config[CONF_CONFIDENCE]
+
+ # handle labels and specific detection areas
+ labels = config[CONF_LABELS]
+ self._label_areas = {}
+ for label in labels:
+ if isinstance(label, dict):
+ label_name = label[CONF_NAME]
+ if label_name not in detector["labels"] and label_name != "*":
+ _LOGGER.warning("Detector does not support label %s", label_name)
+ continue
+
+ # Label Confidence
+ label_confidence = label[CONF_CONFIDENCE]
+ if label_name not in dconfig or dconfig[label_name] > label_confidence:
+ dconfig[label_name] = label_confidence
+
+ # Label area
+ label_area = label.get(CONF_AREA)
+ self._label_areas[label_name] = [0, 0, 1, 1]
+ if label_area:
+ self._label_areas[label_name] = [
+ label_area[CONF_TOP],
+ label_area[CONF_LEFT],
+ label_area[CONF_BOTTOM],
+ label_area[CONF_RIGHT],
+ ]
+ else:
+ if label not in detector["labels"] and label != "*":
+ _LOGGER.warning("Detector does not support label %s", label)
+ continue
+ self._label_areas[label] = [0, 0, 1, 1]
+ if label not in dconfig or dconfig[label] > confidence:
+ dconfig[label] = confidence
+
+ if not dconfig:
+ dconfig["*"] = confidence
+
+ # Handle global detection area
+ self._area = [0, 0, 1, 1]
+ area_config = config.get(CONF_AREA)
+ if area_config:
+ self._area = [
+ area_config[CONF_TOP],
+ area_config[CONF_LEFT],
+ area_config[CONF_BOTTOM],
+ area_config[CONF_RIGHT],
+ ]
+
+ template.attach(hass, self._file_out)
+
+ self._dconfig = dconfig
+ self._matches = {}
+ self._total_matches = 0
+ self._last_image = None
+
+ @property
+ def camera_entity(self):
+ """Return camera entity id from process pictures."""
+ return self._camera_entity
+
+ @property
+ def name(self):
+ """Return the name of the image processor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the entity."""
+ return self._total_matches
+
+ @property
+ def device_state_attributes(self):
+ """Return device specific state attributes."""
+ return {
+ ATTR_MATCHES: self._matches,
+ ATTR_SUMMARY: {
+ label: len(values) for label, values in self._matches.items()
+ },
+ ATTR_TOTAL_MATCHES: self._total_matches,
+ }
+
+ def _save_image(self, image, matches, paths):
+ img = Image.open(io.BytesIO(bytearray(image))).convert("RGB")
+ img_width, img_height = img.size
+ draw = ImageDraw.Draw(img)
+
+ # Draw custom global region/area
+ if self._area != [0, 0, 1, 1]:
+ draw_box(
+ draw, self._area, img_width, img_height, "Detection Area", (0, 255, 255)
+ )
+
+ for label, values in matches.items():
+
+ # Draw custom label regions/areas
+ if label in self._label_areas and self._label_areas[label] != [0, 0, 1, 1]:
+ box_label = f"{label.capitalize()} Detection Area"
+ draw_box(
+ draw,
+ self._label_areas[label],
+ img_width,
+ img_height,
+ box_label,
+ (0, 255, 0),
+ )
+
+ # Draw detected objects
+ for instance in values:
+ box_label = f'{label} {instance["score"]:.1f}%'
+ # Already scaled, use 1 for width and height
+ draw_box(
+ draw,
+ instance["box"],
+ img_width,
+ img_height,
+ box_label,
+ (255, 255, 0),
+ )
+
+ for path in paths:
+ _LOGGER.info("Saving results image to %s", path)
+ img.save(path)
+
+ def process_image(self, image):
+ """Process the image."""
+ img = Image.open(io.BytesIO(bytearray(image)))
+ img_width, img_height = img.size
+
+ if self._aspect and abs((img_width / img_height) - self._aspect) > 0.1:
+ _LOGGER.debug(
+ "The image aspect: %s and the detector aspect: %s differ by more than 0.1",
+ (img_width / img_height),
+ self._aspect,
+ )
+
+ # Run detection
+ start = time.time()
+ response = self._doods.detect(
+ image, dconfig=self._dconfig, detector_name=self._detector_name
+ )
+ _LOGGER.debug(
+ "doods detect: %s response: %s duration: %s",
+ self._dconfig,
+ response,
+ time.time() - start,
+ )
+
+ matches = {}
+ total_matches = 0
+
+ if not response or "error" in response:
+ if "error" in response:
+ _LOGGER.error(response["error"])
+ self._matches = matches
+ self._total_matches = total_matches
+ return
+
+ for detection in response["detections"]:
+ score = detection["confidence"]
+ boxes = [
+ detection["top"],
+ detection["left"],
+ detection["bottom"],
+ detection["right"],
+ ]
+ label = detection["label"]
+
+ # Exclude unlisted labels
+ if "*" not in self._dconfig and label not in self._dconfig:
+ continue
+
+ # Exclude matches outside global area definition
+ if (
+ boxes[0] < self._area[0]
+ or boxes[1] < self._area[1]
+ or boxes[2] > self._area[2]
+ or boxes[3] > self._area[3]
+ ):
+ continue
+
+ # Exclude matches outside label specific area definition
+ if self._label_areas and (
+ boxes[0] < self._label_areas[label][0]
+ or boxes[1] < self._label_areas[label][1]
+ or boxes[2] > self._label_areas[label][2]
+ or boxes[3] > self._label_areas[label][3]
+ ):
+ continue
+
+ if label not in matches:
+ matches[label] = []
+ matches[label].append({"score": float(score), "box": boxes})
+ total_matches += 1
+
+ # Save Images
+ if total_matches and self._file_out:
+ paths = []
+ for path_template in self._file_out:
+ if isinstance(path_template, template.Template):
+ paths.append(
+ path_template.render(camera_entity=self._camera_entity)
+ )
+ else:
+ paths.append(path_template)
+ self._save_image(image, matches, paths)
+
+ self._matches = matches
+ self._total_matches = total_matches
diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json
new file mode 100644
index 00000000000000..75c1bd3dcd3814
--- /dev/null
+++ b/homeassistant/components/doods/manifest.json
@@ -0,0 +1,10 @@
+{
+ "domain": "doods",
+ "name": "DOODS - Distributed Outside Object Detection Service",
+ "documentation": "https://www.home-assistant.io/components/doods",
+ "requirements": [
+ "pydoods==1.0.2"
+ ],
+ "dependencies": [],
+ "codeowners": []
+}
diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py
index eaae3f1923636d..457c319d9e1654 100644
--- a/homeassistant/components/doorbird/camera.py
+++ b/homeassistant/components/doorbird/camera.py
@@ -8,6 +8,7 @@
from homeassistant.components.camera import Camera, SUPPORT_STREAM
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+import homeassistant.util.dt as dt_util
from . import DOMAIN as DOORBIRD_DOMAIN
@@ -77,7 +78,7 @@ def name(self):
async def async_camera_image(self):
"""Pull a still image from the camera."""
- now = datetime.datetime.now()
+ now = dt_util.utcnow()
if self._last_image and now - self._last_update < self._interval:
return self._last_image
diff --git a/homeassistant/components/doorbird/switch.py b/homeassistant/components/doorbird/switch.py
index 643e006dfef0ed..7a0dfa82e76eef 100644
--- a/homeassistant/components/doorbird/switch.py
+++ b/homeassistant/components/doorbird/switch.py
@@ -3,6 +3,7 @@
import logging
from homeassistant.components.switch import SwitchDevice
+import homeassistant.util.dt as dt_util
from . import DOMAIN as DOORBIRD_DOMAIN
@@ -66,7 +67,7 @@ def turn_on(self, **kwargs):
else:
self._state = self._doorstation.device.energize_relay(self._relay)
- now = datetime.datetime.now()
+ now = dt_util.utcnow()
self._assume_off = now + self._time
def turn_off(self, **kwargs):
@@ -75,6 +76,6 @@ def turn_off(self, **kwargs):
def update(self):
"""Wait for the correct amount of assumed time to pass."""
- if self._state and self._assume_off <= datetime.datetime.now():
+ if self._state and self._assume_off <= dt_util.utcnow():
self._state = False
self._assume_off = datetime.datetime.min
diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py
index 66c58b828826cb..95c5513ecaf94e 100644
--- a/homeassistant/components/ebox/sensor.py
+++ b/homeassistant/components/ebox/sensor.py
@@ -26,10 +26,10 @@
_LOGGER = logging.getLogger(__name__)
-GIGABITS = "Gb" # type: str
-PRICE = "CAD" # type: str
-DAYS = "days" # type: str
-PERCENT = "%" # type: str
+GIGABITS = "Gb"
+PRICE = "CAD"
+DAYS = "days"
+PERCENT = "%"
DEFAULT_NAME = "EBox"
diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py
index ac156e040d7332..4bc79e7bd39c19 100644
--- a/homeassistant/components/ebusd/sensor.py
+++ b/homeassistant/components/ebusd/sensor.py
@@ -3,6 +3,7 @@
import datetime
from homeassistant.helpers.entity import Entity
+import homeassistant.util.dt as dt_util
from .const import DOMAIN
@@ -68,9 +69,7 @@ def device_state_attributes(self):
if index < len(time_frame):
parsed = datetime.datetime.strptime(time_frame[index], "%H:%M")
parsed = parsed.replace(
- datetime.datetime.now().year,
- datetime.datetime.now().month,
- datetime.datetime.now().day,
+ dt_util.now().year, dt_util.now().month, dt_util.now().day
)
schedule[item[0]] = parsed.isoformat()
return schedule
diff --git a/homeassistant/components/egardia/__init__.py b/homeassistant/components/egardia/__init__.py
index e17ea8f065d10a..9e11f522dd53d0 100644
--- a/homeassistant/components/egardia/__init__.py
+++ b/homeassistant/components/egardia/__init__.py
@@ -110,7 +110,7 @@ def setup(hass, config):
server = egardiaserver.EgardiaServer("", rs_port)
bound = server.bind()
if not bound:
- raise IOError(
+ raise OSError(
"Binding error occurred while " + "starting EgardiaServer."
)
hass.data[EGARDIA_SERVER] = server
@@ -123,7 +123,7 @@ def handle_stop_event(event):
# listen to home assistant stop event
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event)
- except IOError:
+ except OSError:
_LOGGER.error("Binding error occurred while starting EgardiaServer")
return False
diff --git a/homeassistant/components/emulated_roku/.translations/it.json b/homeassistant/components/emulated_roku/.translations/it.json
index cba89add799481..8f39309264a6ca 100644
--- a/homeassistant/components/emulated_roku/.translations/it.json
+++ b/homeassistant/components/emulated_roku/.translations/it.json
@@ -6,8 +6,12 @@
"step": {
"user": {
"data": {
+ "advertise_ip": "Pubblicizza IP",
+ "advertise_port": "Pubblicizza porta",
"host_ip": "Indirizzo IP dell'host",
- "name": "Nome"
+ "listen_port": "Porta di ascolto",
+ "name": "Nome",
+ "upnp_bind_multicast": "Associa multicast (Vero / Falso)"
},
"title": "Definisci la configurazione del server"
}
diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py
index 2cc46632ddaf58..13784e24d77fc9 100644
--- a/homeassistant/components/enphase_envoy/sensor.py
+++ b/homeassistant/components/enphase_envoy/sensor.py
@@ -9,6 +9,7 @@
from homeassistant.const import (
CONF_IP_ADDRESS,
CONF_MONITORED_CONDITIONS,
+ CONF_NAME,
POWER_WATT,
ENERGY_WATT_HOUR,
)
@@ -44,6 +45,7 @@
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All(
cv.ensure_list, [vol.In(list(SENSORS))]
),
+ vol.Optional(CONF_NAME, default=""): cv.string,
}
)
@@ -54,6 +56,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
ip_address = config[CONF_IP_ADDRESS]
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
+ name = config[CONF_NAME]
entities = []
# Iterate through the list of sensors
@@ -66,14 +69,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
Envoy(
ip_address,
condition,
- "{} {}".format(SENSORS[condition][0], inverter),
+ f"{name}{SENSORS[condition][0]} {inverter}",
SENSORS[condition][1],
)
)
else:
entities.append(
Envoy(
- ip_address, condition, SENSORS[condition][0], SENSORS[condition][1]
+ ip_address,
+ condition,
+ f"{name}{SENSORS[condition][0]}",
+ SENSORS[condition][1],
)
)
async_add_entities(entities)
diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json
index 0625fd4c27f6d9..2ae2006512b03a 100644
--- a/homeassistant/components/environment_canada/manifest.json
+++ b/homeassistant/components/environment_canada/manifest.json
@@ -3,7 +3,7 @@
"name": "Environment Canada",
"documentation": "https://www.home-assistant.io/components/environment_canada",
"requirements": [
- "env_canada==0.0.24"
+ "env_canada==0.0.25"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py
index 2413edaebce060..244fda61656f88 100644
--- a/homeassistant/components/environment_canada/sensor.py
+++ b/homeassistant/components/environment_canada/sensor.py
@@ -68,7 +68,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
ec_data = ECData(coordinates=(lat, lon), language=config.get(CONF_LANGUAGE))
sensor_list = list(ec_data.conditions.keys()) + list(ec_data.alerts.keys())
- sensor_list.remove("icon_code")
add_entities([ECSensor(sensor_type, ec_data) for sensor_type in sensor_list], True)
diff --git a/homeassistant/components/esphome/.translations/es.json b/homeassistant/components/esphome/.translations/es.json
index 88730a18554e9a..70d766cf4c0544 100644
--- a/homeassistant/components/esphome/.translations/es.json
+++ b/homeassistant/components/esphome/.translations/es.json
@@ -8,6 +8,7 @@
"invalid_password": "\u00a1Contrase\u00f1a incorrecta!",
"resolve_error": "No se puede resolver la direcci\u00f3n de ESP. Si el error persiste, configura una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "Desplom\u00e9: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/.translations/it.json b/homeassistant/components/esphome/.translations/it.json
index b9088c2eadc34e..bb77e87f6a1c38 100644
--- a/homeassistant/components/esphome/.translations/it.json
+++ b/homeassistant/components/esphome/.translations/it.json
@@ -18,7 +18,7 @@
"title": "Inserisci la password"
},
"discovery_confirm": {
- "description": "Vuoi aggiungere il nodo ESPHome ` {name} ` a Home Assistant?",
+ "description": "Vuoi aggiungere il nodo ESPHome `{name}` a Home Assistant?",
"title": "Trovato nodo ESPHome"
},
"user": {
diff --git a/homeassistant/components/esphome/.translations/lb.json b/homeassistant/components/esphome/.translations/lb.json
index 955b050bc5b19d..882b67823ba288 100644
--- a/homeassistant/components/esphome/.translations/lb.json
+++ b/homeassistant/components/esphome/.translations/lb.json
@@ -14,7 +14,7 @@
"data": {
"password": "Passwuert"
},
- "description": "Gitt d'Passwuert vun \u00e4rer Konfiguratioun an.",
+ "description": "Gitt d'Passwuert vun \u00e4rer Konfiguratioun an fir {name}.",
"title": "Passwuert aginn"
},
"discovery_confirm": {
diff --git a/homeassistant/components/esphome/.translations/pl.json b/homeassistant/components/esphome/.translations/pl.json
index c8e6012ea94582..9394b5af543cb2 100644
--- a/homeassistant/components/esphome/.translations/pl.json
+++ b/homeassistant/components/esphome/.translations/pl.json
@@ -26,7 +26,7 @@
"host": "Host",
"port": "Port"
},
- "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia swojego [ESPHome](https://esphomelib.com/) w\u0119z\u0142a.",
+ "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia [ESPHome](https://esphomelib.com/) w\u0119z\u0142a.",
"title": "ESPHome"
}
},
diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py
index 182d4003e30664..bc06aba94ead87 100644
--- a/homeassistant/components/esphome/__init__.py
+++ b/homeassistant/components/esphome/__init__.py
@@ -203,7 +203,7 @@ async def try_connect(tries: int = 0, is_disconnect: bool = True) -> None:
# When removing/disconnecting manually
return
- data = hass.data[DOMAIN][entry.entry_id] # type: RuntimeEntryData
+ data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id]
for disconnect_cb in data.disconnect_callbacks:
disconnect_cb()
data.disconnect_callbacks = []
@@ -326,7 +326,7 @@ async def _cleanup_instance(
hass: HomeAssistantType, entry: ConfigEntry
) -> RuntimeEntryData:
"""Cleanup the esphome client if it exists."""
- data = hass.data[DATA_KEY].pop(entry.entry_id) # type: RuntimeEntryData
+ data: RuntimeEntryData = hass.data[DATA_KEY].pop(entry.entry_id)
if data.reconnect_task is not None:
data.reconnect_task.cancel()
for disconnect_cb in data.disconnect_callbacks:
@@ -363,7 +363,7 @@ async def platform_async_setup_entry(
This method is in charge of receiving, distributing and storing
info and state updates.
"""
- entry_data = hass.data[DOMAIN][entry.entry_id] # type: RuntimeEntryData
+ entry_data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id]
entry_data.info[component_key] = {}
entry_data.state[component_key] = {}
@@ -468,7 +468,7 @@ def __init__(self, entry_id: str, component_key: str, key: int):
self._entry_id = entry_id
self._component_key = component_key
self._key = key
- self._remove_callbacks = [] # type: List[Callable[[], None]]
+ self._remove_callbacks: List[Callable[[], None]] = []
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py
index 35389d055d6708..9680ed46acdf5d 100644
--- a/homeassistant/components/esphome/config_flow.py
+++ b/homeassistant/components/esphome/config_flow.py
@@ -19,9 +19,9 @@ class EsphomeFlowHandler(config_entries.ConfigFlow):
def __init__(self):
"""Initialize flow."""
- self._host = None # type: Optional[str]
- self._port = None # type: Optional[int]
- self._password = None # type: Optional[str]
+ self._host: Optional[str] = None
+ self._port: Optional[int] = None
+ self._password: Optional[str] = None
async def async_step_user(
self, user_input: Optional[ConfigType] = None, error: Optional[str] = None
@@ -94,9 +94,7 @@ async def async_step_zeroconf(self, user_input: ConfigType):
already_configured = True
elif entry.entry_id in self.hass.data.get(DATA_KEY, {}):
# Does a config entry with this name already exist?
- data = self.hass.data[DATA_KEY][
- entry.entry_id
- ] # type: RuntimeEntryData
+ data: RuntimeEntryData = self.hass.data[DATA_KEY][entry.entry_id]
# Node names are unique in the network
if data.device_info is not None:
already_configured = data.device_info.name == node_name
diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py
index e455d5581d1e3d..1205521706eb21 100644
--- a/homeassistant/components/esphome/light.py
+++ b/homeassistant/components/esphome/light.py
@@ -74,7 +74,7 @@ async def async_turn_on(self, **kwargs) -> None:
red, green, blue = color_util.color_hsv_to_RGB(hue, sat, 100)
data["rgb"] = (red / 255, green / 255, blue / 255)
if ATTR_FLASH in kwargs:
- data["flash"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]]
+ data["flash_length"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]]
if ATTR_TRANSITION in kwargs:
data["transition_length"] = kwargs[ATTR_TRANSITION]
if ATTR_BRIGHTNESS in kwargs:
diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py
index 50d698f733656f..82f4d37938c7d8 100644
--- a/homeassistant/components/fan/__init__.py
+++ b/homeassistant/components/fan/__init__.py
@@ -55,23 +55,21 @@
"speed_list": ATTR_SPEED_LIST,
"oscillating": ATTR_OSCILLATING,
"current_direction": ATTR_DIRECTION,
-} # type: dict
+}
FAN_SET_SPEED_SCHEMA = ENTITY_SERVICE_SCHEMA.extend(
{vol.Required(ATTR_SPEED): cv.string}
-) # type: dict
+)
-FAN_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend(
- {vol.Optional(ATTR_SPEED): cv.string}
-) # type: dict
+FAN_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({vol.Optional(ATTR_SPEED): cv.string})
FAN_OSCILLATE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend(
{vol.Required(ATTR_OSCILLATING): cv.boolean}
-) # type: dict
+)
FAN_SET_DIRECTION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend(
{vol.Optional(ATTR_DIRECTION): cv.string}
-) # type: dict
+)
@bind_hass
@@ -198,7 +196,7 @@ def current_direction(self) -> Optional[str]:
@property
def state_attributes(self) -> dict:
"""Return optional state attributes."""
- data = {} # type: dict
+ data = {}
for prop, attr in PROP_TO_ATTR.items():
if not hasattr(self, prop):
diff --git a/homeassistant/components/fedex/__init__.py b/homeassistant/components/fedex/__init__.py
deleted file mode 100644
index d685ab50372de5..00000000000000
--- a/homeassistant/components/fedex/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The fedex component."""
diff --git a/homeassistant/components/fedex/manifest.json b/homeassistant/components/fedex/manifest.json
deleted file mode 100644
index b34a8b8383ef85..00000000000000
--- a/homeassistant/components/fedex/manifest.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "domain": "fedex",
- "name": "Fedex",
- "documentation": "https://www.home-assistant.io/components/fedex",
- "requirements": [
- "fedexdeliverymanager==1.0.6"
- ],
- "dependencies": [],
- "codeowners": []
-}
diff --git a/homeassistant/components/fedex/sensor.py b/homeassistant/components/fedex/sensor.py
deleted file mode 100644
index 2f499e52e234e5..00000000000000
--- a/homeassistant/components/fedex/sensor.py
+++ /dev/null
@@ -1,120 +0,0 @@
-"""Sensor for Fedex packages."""
-import logging
-from collections import defaultdict
-from datetime import timedelta
-
-import voluptuous as vol
-
-import homeassistant.helpers.config_validation as cv
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
- CONF_NAME,
- CONF_USERNAME,
- CONF_PASSWORD,
- ATTR_ATTRIBUTION,
- CONF_SCAN_INTERVAL,
-)
-from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle
-from homeassistant.util import slugify
-from homeassistant.util.dt import now, parse_date
-
-_LOGGER = logging.getLogger(__name__)
-
-COOKIE = "fedexdeliverymanager_cookies.pickle"
-
-DOMAIN = "fedex"
-
-ICON = "mdi:package-variant-closed"
-
-STATUS_DELIVERED = "delivered"
-
-SCAN_INTERVAL = timedelta(seconds=1800)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_NAME): cv.string,
- }
-)
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Fedex platform."""
- import fedexdeliverymanager
-
- _LOGGER.warning(
- "The fedex integration is deprecated and will be removed "
- "in Home Assistant 0.100.0. For more information see ADR-0004:"
- "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md"
- )
-
- name = config.get(CONF_NAME)
- update_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
-
- try:
- cookie = hass.config.path(COOKIE)
- session = fedexdeliverymanager.get_session(
- config.get(CONF_USERNAME), config.get(CONF_PASSWORD), cookie_path=cookie
- )
- except fedexdeliverymanager.FedexError:
- _LOGGER.exception("Could not connect to Fedex Delivery Manager")
- return False
-
- add_entities([FedexSensor(session, name, update_interval)], True)
-
-
-class FedexSensor(Entity):
- """Fedex Sensor."""
-
- def __init__(self, session, name, interval):
- """Initialize the sensor."""
- self._session = session
- self._name = name
- self._attributes = None
- self._state = None
- self.update = Throttle(interval)(self._update)
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name or DOMAIN
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return "packages"
-
- def _update(self):
- """Update device state."""
- import fedexdeliverymanager
-
- status_counts = defaultdict(int)
- for package in fedexdeliverymanager.get_packages(self._session):
- status = slugify(package["primary_status"])
- skip = (
- status == STATUS_DELIVERED
- and parse_date(package["delivery_date"]) < now().date()
- )
- if skip:
- continue
- status_counts[status] += 1
- self._attributes = {ATTR_ATTRIBUTION: fedexdeliverymanager.ATTRIBUTION}
- self._attributes.update(status_counts)
- self._state = sum(status_counts.values())
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- return self._attributes
-
- @property
- def icon(self):
- """Icon to use in the frontend."""
- return ICON
diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py
index e85b45db4d3aa0..8814a2406c52a9 100644
--- a/homeassistant/components/fido/sensor.py
+++ b/homeassistant/components/fido/sensor.py
@@ -25,10 +25,10 @@
_LOGGER = logging.getLogger(__name__)
-KILOBITS = "Kb" # type: str
-PRICE = "CAD" # type: str
-MESSAGES = "messages" # type: str
-MINUTES = "minutes" # type: str
+KILOBITS = "Kb"
+PRICE = "CAD"
+MESSAGES = "messages"
+MINUTES = "minutes"
DEFAULT_NAME = "Fido"
diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py
index 008337f88eb077..376ea2c0f9d4fc 100644
--- a/homeassistant/components/fints/sensor.py
+++ b/homeassistant/components/fints/sensor.py
@@ -162,11 +162,11 @@ class FinTsAccount(Entity):
def __init__(self, client: FinTsClient, account, name: str) -> None:
"""Initialize a FinTs balance account."""
- self._client = client # type: FinTsClient
+ self._client = client
self._account = account
- self._name = name # type: str
- self._balance = None # type: float
- self._currency = None # type: str
+ self._name = name
+ self._balance: float = None
+ self._currency: str = None
@property
def should_poll(self) -> bool:
@@ -222,11 +222,11 @@ class FinTsHoldingsAccount(Entity):
def __init__(self, client: FinTsClient, account, name: str) -> None:
"""Initialize a FinTs holdings account."""
- self._client = client # type: FinTsClient
- self._name = name # type: str
+ self._client = client
+ self._name = name
self._account = account
self._holdings = []
- self._total = None # type: float
+ self._total: float = None
@property
def should_poll(self) -> bool:
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 7298ce8c1d086a..8ef662ec878f90 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -4,6 +4,7 @@
import mimetypes
import os
import pathlib
+from typing import Optional, Set, Tuple
from aiohttp import web, web_urldispatcher, hdrs
import voluptuous as vol
@@ -22,7 +23,7 @@
from .storage import async_setup_frontend_storage
-# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs
+# mypy: allow-untyped-defs, no-check-untyped-defs
# Fix mimetypes for borked Windows machines
# https://github.com/home-assistant/home-assistant-polymer/issues/3336
@@ -400,7 +401,9 @@ def url_for(self, **kwargs: str) -> URL:
"""Construct url for resource with additional params."""
return URL("/")
- async def resolve(self, request: web.Request):
+ async def resolve(
+ self, request: web.Request
+ ) -> Tuple[Optional[web_urldispatcher.UrlMappingMatchInfo], Set[str]]:
"""Resolve resource.
Return (UrlMappingMatchInfo, allowed_methods) pair.
@@ -447,7 +450,7 @@ def get_template(self):
return tpl
- async def get(self, request: web.Request):
+ async def get(self, request: web.Request) -> web.Response:
"""Serve the index page for panel pages."""
hass = request.app["hass"]
diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json
index 2b17091ba5cb94..896867fcb172eb 100644
--- a/homeassistant/components/frontend/manifest.json
+++ b/homeassistant/components/frontend/manifest.json
@@ -2,9 +2,7 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/components/frontend",
- "requirements": [
- "home-assistant-frontend==20190901.0"
- ],
+ "requirements": ["home-assistant-frontend==20190919.0"],
"dependencies": [
"api",
"auth",
@@ -14,7 +12,5 @@
"system_log",
"websocket_api"
],
- "codeowners": [
- "@home-assistant/frontend"
- ]
+ "codeowners": ["@home-assistant/frontend"]
}
diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json
index 12f7c266840bf6..f2110ffb2f0220 100644
--- a/homeassistant/components/geniushub/manifest.json
+++ b/homeassistant/components/geniushub/manifest.json
@@ -3,7 +3,7 @@
"name": "Genius Hub",
"documentation": "https://www.home-assistant.io/components/geniushub",
"requirements": [
- "geniushub-client==0.6.5"
+ "geniushub-client==0.6.13"
],
"dependencies": [],
"codeowners": ["@zxdavb"]
diff --git a/homeassistant/components/geonetnz_quakes/.translations/de.json b/homeassistant/components/geonetnz_quakes/.translations/de.json
new file mode 100644
index 00000000000000..7c6fd08af96c8d
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/.translations/de.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Standort bereits registriert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "mmi": "MMI",
+ "radius": "Radius"
+ },
+ "title": "F\u00fcllen Sie Ihre Filterdaten aus."
+ }
+ },
+ "title": "GeoNet NZ Erdbeben"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/.translations/es.json b/homeassistant/components/geonetnz_quakes/.translations/es.json
new file mode 100644
index 00000000000000..f6f592675ab33e
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/.translations/es.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Ubicaci\u00f3n ya registrada"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "mmi": "MMI",
+ "radius": "Radio"
+ },
+ "title": "Complete todos los campos requeridos"
+ }
+ },
+ "title": "GeoNet NZ Quakes"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/.translations/fr.json b/homeassistant/components/geonetnz_quakes/.translations/fr.json
new file mode 100644
index 00000000000000..74ae5541754ef7
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/.translations/fr.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Emplacement d\u00e9j\u00e0 enregistr\u00e9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "mmi": "MMI",
+ "radius": "Rayon"
+ },
+ "title": "Remplissez les d\u00e9tails de votre filtre."
+ }
+ },
+ "title": "GeoNet NZ Quakes"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/.translations/it.json b/homeassistant/components/geonetnz_quakes/.translations/it.json
new file mode 100644
index 00000000000000..2a019aa39d94ae
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/.translations/it.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Localit\u00e0 gi\u00e0 registrata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "mmi": "Intensit\u00e0 in Scala Mercalli Modificata",
+ "radius": "Raggio"
+ },
+ "title": "Inserisci i tuoi dettagli del filtro."
+ }
+ },
+ "title": "GeoNet NZ Quakes"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/.translations/ko.json b/homeassistant/components/geonetnz_quakes/.translations/ko.json
new file mode 100644
index 00000000000000..26caa2ebe54cad
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/.translations/ko.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "\uc704\uce58\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "mmi": "MMI",
+ "radius": "\ubc18\uacbd"
+ },
+ "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694"
+ }
+ },
+ "title": "GeoNet NZ Quakes"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/.translations/lb.json b/homeassistant/components/geonetnz_quakes/.translations/lb.json
new file mode 100644
index 00000000000000..2499befecbb822
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/.translations/lb.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Standuert ass scho registr\u00e9iert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "mmi": "MMI",
+ "radius": "Radius"
+ },
+ "title": "F\u00ebllt \u00e4r Filter D\u00e9tailer aus."
+ }
+ },
+ "title": "GeoNet NZ Quakes"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/.translations/no.json b/homeassistant/components/geonetnz_quakes/.translations/no.json
new file mode 100644
index 00000000000000..40b695d6f51488
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/.translations/no.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Beliggenhet allerede er registrert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "mmi": "MMI",
+ "radius": "Radius"
+ },
+ "title": "Fyll ut filterdetaljene."
+ }
+ },
+ "title": "GeoNet NZ Quakes"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/.translations/sl.json b/homeassistant/components/geonetnz_quakes/.translations/sl.json
new file mode 100644
index 00000000000000..bdd05d339535b2
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/.translations/sl.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Lokacija je \u017ee registrirana"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "mmi": "MMI",
+ "radius": "Radij"
+ },
+ "title": "Izpolnite podrobnosti filtra."
+ }
+ },
+ "title": "GeoNet NZ Potresi"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json b/homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json
new file mode 100644
index 00000000000000..3786b03f41fc3e
--- /dev/null
+++ b/homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "\u586b\u5199\u60a8\u7684filter\u8be6\u7ec6\u4fe1\u606f\u3002"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py
index 1385d5e59a768b..90b4b386f37e52 100644
--- a/homeassistant/components/glances/sensor.py
+++ b/homeassistant/components/glances/sensor.py
@@ -197,18 +197,20 @@ async def async_update(self):
elif self.type == "cpu_temp":
for sensor in value["sensors"]:
if sensor["label"] in [
- "CPU",
+ "amdgpu 1",
+ "aml_thermal",
+ "Core 0",
+ "Core 1",
"CPU Temperature",
- "Package id 0",
- "Physical id 0",
- "cpu_thermal 1",
+ "CPU",
"cpu-thermal 1",
+ "cpu_thermal 1",
"exynos-therm 1",
- "soc_thermal 1",
+ "Package id 0",
+ "Physical id 0",
+ "radeon 1",
"soc-thermal 1",
- "aml_thermal",
- "Core 0",
- "Core 1",
+ "soc_thermal 1",
]:
self._state = sensor["value"]
elif self.type == "docker_active":
diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py
index 24502462512fba..d68650fb6384c3 100644
--- a/homeassistant/components/google_assistant/http.py
+++ b/homeassistant/components/google_assistant/http.py
@@ -93,7 +93,7 @@ def __init__(self, config):
async def post(self, request: Request) -> Response:
"""Handle Google Assistant requests."""
- message = await request.json() # type: dict
+ message: dict = await request.json()
result = await async_handle_message(
request.app["hass"], self.config, request["hass_user"].id, message
)
diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py
index 2cb440f9181fea..6ab6d937b51e84 100644
--- a/homeassistant/components/google_assistant/smart_home.py
+++ b/homeassistant/components/google_assistant/smart_home.py
@@ -24,7 +24,7 @@
async def async_handle_message(hass, config, user_id, message):
"""Handle incoming API messages."""
- request_id = message.get("requestId") # type: str
+ request_id: str = message.get("requestId")
data = RequestData(config, user_id, request_id)
@@ -38,7 +38,7 @@ async def async_handle_message(hass, config, user_id, message):
async def _process(hass, data, message):
"""Process a message."""
- inputs = message.get("inputs") # type: list
+ inputs: list = message.get("inputs")
if len(inputs) != 1:
return {
diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py
index 5fa7d49b885b1c..2afa18af32e6a7 100644
--- a/homeassistant/components/google_assistant/trait.py
+++ b/homeassistant/components/google_assistant/trait.py
@@ -308,7 +308,7 @@ def sync_attributes(self):
if features & light.SUPPORT_COLOR_TEMP:
# Max Kelvin is Min Mireds K = 1000000 / mireds
- # Min Kevin is Max Mireds K = 1000000 / mireds
+ # Min Kelvin is Max Mireds K = 1000000 / mireds
response["colorTemperatureRange"] = {
"temperatureMaxK": color_util.color_temperature_mired_to_kelvin(
attrs.get(light.ATTR_MIN_MIREDS)
diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py
index f0d29d923c8ed1..0b1291d4045f42 100644
--- a/homeassistant/components/group/light.py
+++ b/homeassistant/components/group/light.py
@@ -1,4 +1,5 @@
"""This platform allows several lights to be grouped into one light."""
+import asyncio
from collections import Counter
import itertools
import logging
@@ -19,6 +20,7 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.util import color as color_util
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -75,19 +77,19 @@ class LightGroup(light.Light):
def __init__(self, name: str, entity_ids: List[str]) -> None:
"""Initialize a light group."""
- self._name = name # type: str
- self._entity_ids = entity_ids # type: List[str]
- self._is_on = False # type: bool
- self._available = False # type: bool
- self._brightness = None # type: Optional[int]
- self._hs_color = None # type: Optional[Tuple[float, float]]
- self._color_temp = None # type: Optional[int]
- self._min_mireds = 154 # type: Optional[int]
- self._max_mireds = 500 # type: Optional[int]
- self._white_value = None # type: Optional[int]
- self._effect_list = None # type: Optional[List[str]]
- self._effect = None # type: Optional[str]
- self._supported_features = 0 # type: int
+ self._name = name
+ self._entity_ids = entity_ids
+ self._is_on = False
+ self._available = False
+ self._brightness: Optional[int] = None
+ self._hs_color: Optional[Tuple[float, float]] = None
+ self._color_temp: Optional[int] = None
+ self._min_mireds: Optional[int] = 154
+ self._max_mireds: Optional[int] = 500
+ self._white_value: Optional[int] = None
+ self._effect_list: Optional[List[str]] = None
+ self._effect: Optional[str] = None
+ self._supported_features: int = 0
self._async_unsub_state_changed = None
async def async_added_to_hass(self) -> None:
@@ -179,6 +181,7 @@ def should_poll(self) -> bool:
async def async_turn_on(self, **kwargs):
"""Forward the turn_on command to all lights in the light group."""
data = {ATTR_ENTITY_ID: self._entity_ids}
+ emulate_color_temp_entity_ids = []
if ATTR_BRIGHTNESS in kwargs:
data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS]
@@ -189,6 +192,23 @@ async def async_turn_on(self, **kwargs):
if ATTR_COLOR_TEMP in kwargs:
data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP]
+ # Create a new entity list to mutate
+ updated_entities = list(self._entity_ids)
+
+ # Walk through initial entity ids, split entity lists by support
+ for entity_id in self._entity_ids:
+ state = self.hass.states.get(entity_id)
+ if not state:
+ continue
+ support = state.attributes.get(ATTR_SUPPORTED_FEATURES)
+ # Only pass color temperature to supported entity_ids
+ if bool(support & SUPPORT_COLOR) and not bool(
+ support & SUPPORT_COLOR_TEMP
+ ):
+ emulate_color_temp_entity_ids.append(entity_id)
+ updated_entities.remove(entity_id)
+ data[ATTR_ENTITY_ID] = updated_entities
+
if ATTR_WHITE_VALUE in kwargs:
data[ATTR_WHITE_VALUE] = kwargs[ATTR_WHITE_VALUE]
@@ -201,8 +221,32 @@ async def async_turn_on(self, **kwargs):
if ATTR_FLASH in kwargs:
data[ATTR_FLASH] = kwargs[ATTR_FLASH]
- await self.hass.services.async_call(
- light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True
+ if not emulate_color_temp_entity_ids:
+ await self.hass.services.async_call(
+ light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True
+ )
+ return
+
+ emulate_color_temp_data = data.copy()
+ temp_k = color_util.color_temperature_mired_to_kelvin(
+ emulate_color_temp_data[ATTR_COLOR_TEMP]
+ )
+ hs_color = color_util.color_temperature_to_hs(temp_k)
+ emulate_color_temp_data[ATTR_HS_COLOR] = hs_color
+ del emulate_color_temp_data[ATTR_COLOR_TEMP]
+
+ emulate_color_temp_data[ATTR_ENTITY_ID] = emulate_color_temp_entity_ids
+
+ await asyncio.gather(
+ self.hass.services.async_call(
+ light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True
+ ),
+ self.hass.services.async_call(
+ light.DOMAIN,
+ light.SERVICE_TURN_ON,
+ emulate_color_temp_data,
+ blocking=True,
+ ),
)
async def async_turn_off(self, **kwargs):
diff --git a/homeassistant/components/growatt_server/__init__.py b/homeassistant/components/growatt_server/__init__.py
new file mode 100644
index 00000000000000..14205e8d9ba3ce
--- /dev/null
+++ b/homeassistant/components/growatt_server/__init__.py
@@ -0,0 +1 @@
+"""The Growatt server PV inverter sensor integration."""
diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json
new file mode 100644
index 00000000000000..a6a1d2b8aebb7a
--- /dev/null
+++ b/homeassistant/components/growatt_server/manifest.json
@@ -0,0 +1,12 @@
+{
+ "domain": "growatt_server",
+ "name": "Growatt Server",
+ "documentation": "https://www.home-assistant.io/components/growatt_server/",
+ "requirements": [
+ "growattServer==0.0.1"
+ ],
+ "dependencies": [],
+ "codeowners": [
+ "@indykoning"
+ ]
+}
diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py
new file mode 100644
index 00000000000000..3b7109222a4abb
--- /dev/null
+++ b/homeassistant/components/growatt_server/sensor.py
@@ -0,0 +1,189 @@
+"""Read status of growatt inverters."""
+import re
+import json
+import logging
+import datetime
+
+import growattServer
+import voluptuous as vol
+
+from homeassistant.util import Throttle
+from homeassistant.helpers.entity import Entity
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_PLANT_ID = "plant_id"
+DEFAULT_PLANT_ID = "0"
+DEFAULT_NAME = "Growatt"
+SCAN_INTERVAL = datetime.timedelta(minutes=5)
+
+TOTAL_SENSOR_TYPES = {
+ "total_money_today": ("Total money today", "€", "plantMoneyText", None),
+ "total_money_total": ("Money lifetime", "€", "totalMoneyText", None),
+ "total_energy_today": ("Energy Today", "kWh", "todayEnergy", "power"),
+ "total_output_power": ("Output Power", "W", "invTodayPpv", "power"),
+ "total_energy_output": ("Lifetime energy output", "kWh", "totalEnergy", "power"),
+ "total_maximum_output": ("Maximum power", "W", "nominalPower", "power"),
+}
+
+INVERTER_SENSOR_TYPES = {
+ "inverter_energy_today": ("Energy today", "kWh", "e_today", "power"),
+ "inverter_energy_total": ("Lifetime energy output", "kWh", "e_total", "power"),
+ "inverter_voltage_input_1": ("Input 1 voltage", "V", "vpv1", None),
+ "inverter_amperage_input_1": ("Input 1 Amperage", "A", "ipv1", None),
+ "inverter_wattage_input_1": ("Input 1 Wattage", "W", "ppv1", "power"),
+ "inverter_voltage_input_2": ("Input 2 voltage", "V", "vpv2", None),
+ "inverter_amperage_input_2": ("Input 2 Amperage", "A", "ipv2", None),
+ "inverter_wattage_input_2": ("Input 2 Wattage", "W", "ppv2", "power"),
+ "inverter_voltage_input_3": ("Input 3 voltage", "V", "vpv3", None),
+ "inverter_amperage_input_3": ("Input 3 Amperage", "A", "ipv3", None),
+ "inverter_wattage_input_3": ("Input 3 Wattage", "W", "ppv3", "power"),
+ "inverter_internal_wattage": ("Internal wattage", "W", "ppv", "power"),
+ "inverter_reactive_voltage": ("Reactive voltage", "V", "vacr", None),
+ "inverter_inverter_reactive_amperage": ("Reactive amperage", "A", "iacr", None),
+ "inverter_frequency": ("AC frequency", "Hz", "fac", None),
+ "inverter_current_wattage": ("Output power", "W", "pac", "power"),
+ "inverter_current_reactive_wattage": ("Reactive wattage", "W", "pacr", "power"),
+}
+
+SENSOR_TYPES = {**TOTAL_SENSOR_TYPES, **INVERTER_SENSOR_TYPES}
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_PLANT_ID, default=DEFAULT_PLANT_ID): cv.string,
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ }
+)
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the Growatt sensor."""
+ username = config[CONF_USERNAME]
+ password = config[CONF_PASSWORD]
+ plant_id = config[CONF_PLANT_ID]
+ name = config[CONF_NAME]
+
+ api = growattServer.GrowattApi()
+
+ # Log in to api and fetch first plant if no plant id is defined.
+ login_response = api.login(username, password)
+ if not login_response["success"] and login_response["errCode"] == "102":
+ _LOGGER.error("Username or Password may be incorrect!")
+ return
+ user_id = login_response["userId"]
+ if plant_id == DEFAULT_PLANT_ID:
+ plant_info = api.plant_list(user_id)
+ plant_id = plant_info["data"][0]["plantId"]
+
+ # Get a list of inverters for specified plant to add sensors for.
+ inverters = api.inverter_list(plant_id)
+ entities = []
+ probe = GrowattData(api, username, password, plant_id, True)
+ for sensor in TOTAL_SENSOR_TYPES:
+ entities.append(
+ GrowattInverter(probe, f"{name} Total", sensor, f"{plant_id}-{sensor}")
+ )
+
+ # Add sensors for each inverter in the specified plant.
+ for inverter in inverters:
+ probe = GrowattData(api, username, password, inverter["deviceSn"], False)
+ for sensor in INVERTER_SENSOR_TYPES:
+ entities.append(
+ GrowattInverter(
+ probe,
+ f"{inverter['deviceAilas']}",
+ sensor,
+ f"{inverter['deviceSn']}-{sensor}",
+ )
+ )
+
+ add_entities(entities, True)
+
+
+class GrowattInverter(Entity):
+ """Representation of a Growatt Sensor."""
+
+ def __init__(self, probe, name, sensor, unique_id):
+ """Initialize a PVOutput sensor."""
+ self.sensor = sensor
+ self.probe = probe
+ self._name = name
+ self._state = None
+ self._unique_id = unique_id
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return f"{self._name} {SENSOR_TYPES[self.sensor][0]}"
+
+ @property
+ def unique_id(self):
+ """Return the unique id of the sensor."""
+ return self._unique_id
+
+ @property
+ def icon(self):
+ """Return the icon of the sensor."""
+ return "mdi:solar-power"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self.probe.get_data(SENSOR_TYPES[self.sensor][2])
+
+ @property
+ def device_class(self):
+ """Return the device class of the sensor."""
+ return SENSOR_TYPES[self.sensor][3]
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this entity, if any."""
+ return SENSOR_TYPES[self.sensor][1]
+
+ def update(self):
+ """Get the latest data from the Growat API and updates the state."""
+ self.probe.update()
+
+
+class GrowattData:
+ """The class for handling data retrieval."""
+
+ def __init__(self, api, username, password, inverter_id, is_total=False):
+ """Initialize the probe."""
+
+ self.is_total = is_total
+ self.api = api
+ self.inverter_id = inverter_id
+ self.data = {}
+ self.username = username
+ self.password = password
+
+ @Throttle(SCAN_INTERVAL)
+ def update(self):
+ """Update probe data."""
+ self.api.login(self.username, self.password)
+ _LOGGER.debug("Updating data for %s", self.inverter_id)
+ try:
+ if self.is_total:
+ total_info = self.api.plant_info(self.inverter_id)
+ del total_info["deviceList"]
+ # PlantMoneyText comes in as "3.1/€" remove anything that isn't part of the number
+ total_info["plantMoneyText"] = re.sub(
+ r"[^\d.,]", "", total_info["plantMoneyText"]
+ )
+ self.data = total_info
+ else:
+ inverter_info = self.api.inverter_detail(self.inverter_id)
+ self.data = inverter_info["data"]
+ except json.decoder.JSONDecodeError:
+ _LOGGER.error("Unable to fetch data from Growatt server")
+
+ def get_data(self, variable):
+ """Get the data."""
+ return self.data.get(variable)
diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py
index d70e1016f07e84..086545f0c761d5 100644
--- a/homeassistant/components/gtfs/sensor.py
+++ b/homeassistant/components/gtfs/sensor.py
@@ -122,7 +122,7 @@ def get_next_departure(
include_tomorrow: bool = False,
) -> dict:
"""Get the next departure for the given schedule."""
- now = datetime.datetime.now() + offset
+ now = dt_util.now().replace(tzinfo=None) + offset
now_date = now.strftime(dt_util.DATE_STR_FORMAT)
yesterday = now - datetime.timedelta(days=1)
yesterday_date = yesterday.strftime(dt_util.DATE_STR_FORMAT)
@@ -256,7 +256,7 @@ def get_next_departure(
_LOGGER.debug("Timetable: %s", sorted(timetable.keys()))
- item = {} # type: dict
+ item = {}
for key in sorted(timetable.keys()):
if dt_util.parse_datetime(key) > now:
item = timetable[key]
@@ -393,11 +393,11 @@ def __init__(
self._available = False
self._icon = ICON
self._name = ""
- self._state = None # type: Optional[str]
- self._attributes = {} # type: dict
+ self._state: Optional[str] = None
+ self._attributes = {}
self._agency = None
- self._departure = {} # type: dict
+ self._departure = {}
self._destination = None
self._origin = None
self._route = None
diff --git a/homeassistant/components/hangouts/.translations/it.json b/homeassistant/components/hangouts/.translations/it.json
index ad8dafd17ec74a..ff0a8238d49caa 100644
--- a/homeassistant/components/hangouts/.translations/it.json
+++ b/homeassistant/components/hangouts/.translations/it.json
@@ -14,14 +14,16 @@
"data": {
"2fa": "2FA Pin"
},
+ "description": "Vuoto",
"title": "Autenticazione a due fattori"
},
"user": {
"data": {
"authorization_code": "Codice di autorizzazione (necessario per l'autenticazione manuale)",
- "email": "Indirizzo email",
+ "email": "Indirizzo E-mail",
"password": "Password"
},
+ "description": "Vuoto",
"title": "Accesso a Google Hangouts"
}
},
diff --git a/homeassistant/components/hangouts/.translations/ko.json b/homeassistant/components/hangouts/.translations/ko.json
index e045f3359d1542..3b1c755b3588c1 100644
--- a/homeassistant/components/hangouts/.translations/ko.json
+++ b/homeassistant/components/hangouts/.translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "already_configured": "Google Hangouts \uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
+ "already_configured": "\uad6c\uae00 \ud589\uc544\uc6c3\uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
"unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
},
"error": {
@@ -24,9 +24,9 @@
"password": "\ube44\ubc00\ubc88\ud638"
},
"description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.",
- "title": "Google Hangouts \ub85c\uadf8\uc778"
+ "title": "\uad6c\uae00 \ud589\uc544\uc6c3 \ub85c\uadf8\uc778"
}
},
- "title": "Google Hangouts"
+ "title": "\uad6c\uae00 \ud589\uc544\uc6c3"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py
index 35f866b3d813aa..9fc3e2fa58e8fb 100644
--- a/homeassistant/components/hangouts/hangouts_bot.py
+++ b/homeassistant/components/hangouts/hangouts_bot.py
@@ -293,7 +293,7 @@ async def _async_send_message(self, message, targets, data):
if self.hass.config.is_allowed_path(uri):
try:
image_file = open(uri, "rb")
- except IOError as error:
+ except OSError as error:
_LOGGER.error(
"Image file I/O error(%s): %s", error.errno, error.strerror
)
diff --git a/homeassistant/components/heos/.translations/ca.json b/homeassistant/components/heos/.translations/ca.json
index 05d95116b10f6b..60bd780547c8cc 100644
--- a/homeassistant/components/heos/.translations/ca.json
+++ b/homeassistant/components/heos/.translations/ca.json
@@ -4,7 +4,7 @@
"already_setup": "Nom\u00e9s pots configurar una \u00fanica connexi\u00f3 de Heos tot i que aquesta ja pot controlar tots els dispositius de la xarxa."
},
"error": {
- "connection_failure": "No es pot connectar amb l'amfitri\u00f3 especificat."
+ "connection_failure": "No s'ha pogut connectar amb l'amfitri\u00f3 especificat."
},
"step": {
"user": {
diff --git a/homeassistant/components/heos/.translations/it.json b/homeassistant/components/heos/.translations/it.json
index 20a4060add4ba5..824f7c3fb50d83 100644
--- a/homeassistant/components/heos/.translations/it.json
+++ b/homeassistant/components/heos/.translations/it.json
@@ -16,6 +16,6 @@
"title": "Connetti a Heos"
}
},
- "title": "Heos"
+ "title": "HEOS"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/heos/.translations/lb.json b/homeassistant/components/heos/.translations/lb.json
index 416f0878de46a3..cfe1d347b0cc20 100644
--- a/homeassistant/components/heos/.translations/lb.json
+++ b/homeassistant/components/heos/.translations/lb.json
@@ -16,6 +16,6 @@
"title": "Mat Heos verbannen"
}
},
- "title": "Heos"
+ "title": "HEOS"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py
new file mode 100755
index 00000000000000..9a5c8ec32aca43
--- /dev/null
+++ b/homeassistant/components/here_travel_time/__init__.py
@@ -0,0 +1 @@
+"""The here_travel_time component."""
diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json
new file mode 100755
index 00000000000000..e26e2e1d6ea57c
--- /dev/null
+++ b/homeassistant/components/here_travel_time/manifest.json
@@ -0,0 +1,12 @@
+{
+ "domain": "here_travel_time",
+ "name": "HERE travel time",
+ "documentation": "https://www.home-assistant.io/components/here_travel_time",
+ "requirements": [
+ "herepy==0.6.3.1"
+ ],
+ "dependencies": [],
+ "codeowners": [
+ "@eifinger"
+ ]
+ }
diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py
new file mode 100755
index 00000000000000..ba4908fe85c3ac
--- /dev/null
+++ b/homeassistant/components/here_travel_time/sensor.py
@@ -0,0 +1,431 @@
+"""Support for HERE travel time sensors."""
+from datetime import timedelta
+import logging
+from typing import Callable, Dict, Optional, Union
+
+import herepy
+import voluptuous as vol
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import (
+ ATTR_ATTRIBUTION,
+ ATTR_LATITUDE,
+ ATTR_LONGITUDE,
+ CONF_MODE,
+ CONF_NAME,
+ CONF_UNIT_SYSTEM,
+ CONF_UNIT_SYSTEM_IMPERIAL,
+ CONF_UNIT_SYSTEM_METRIC,
+)
+from homeassistant.core import HomeAssistant, State
+from homeassistant.helpers import location
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_DESTINATION_LATITUDE = "destination_latitude"
+CONF_DESTINATION_LONGITUDE = "destination_longitude"
+CONF_DESTINATION_ENTITY_ID = "destination_entity_id"
+CONF_ORIGIN_LATITUDE = "origin_latitude"
+CONF_ORIGIN_LONGITUDE = "origin_longitude"
+CONF_ORIGIN_ENTITY_ID = "origin_entity_id"
+CONF_APP_ID = "app_id"
+CONF_APP_CODE = "app_code"
+CONF_TRAFFIC_MODE = "traffic_mode"
+CONF_ROUTE_MODE = "route_mode"
+
+DEFAULT_NAME = "HERE Travel Time"
+
+TRAVEL_MODE_BICYCLE = "bicycle"
+TRAVEL_MODE_CAR = "car"
+TRAVEL_MODE_PEDESTRIAN = "pedestrian"
+TRAVEL_MODE_PUBLIC = "publicTransport"
+TRAVEL_MODE_PUBLIC_TIME_TABLE = "publicTransportTimeTable"
+TRAVEL_MODE_TRUCK = "truck"
+TRAVEL_MODE = [
+ TRAVEL_MODE_BICYCLE,
+ TRAVEL_MODE_CAR,
+ TRAVEL_MODE_PEDESTRIAN,
+ TRAVEL_MODE_PUBLIC,
+ TRAVEL_MODE_PUBLIC_TIME_TABLE,
+ TRAVEL_MODE_TRUCK,
+]
+
+TRAVEL_MODES_PUBLIC = [TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE]
+TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK]
+TRAVEL_MODES_NON_VEHICLE = [TRAVEL_MODE_BICYCLE, TRAVEL_MODE_PEDESTRIAN]
+
+TRAFFIC_MODE_ENABLED = "traffic_enabled"
+TRAFFIC_MODE_DISABLED = "traffic_disabled"
+
+ROUTE_MODE_FASTEST = "fastest"
+ROUTE_MODE_SHORTEST = "shortest"
+ROUTE_MODE = [ROUTE_MODE_FASTEST, ROUTE_MODE_SHORTEST]
+
+ICON_BICYCLE = "mdi:bike"
+ICON_CAR = "mdi:car"
+ICON_PEDESTRIAN = "mdi:walk"
+ICON_PUBLIC = "mdi:bus"
+ICON_TRUCK = "mdi:truck"
+
+UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL]
+
+ATTR_DURATION = "duration"
+ATTR_DISTANCE = "distance"
+ATTR_ROUTE = "route"
+ATTR_ORIGIN = "origin"
+ATTR_DESTINATION = "destination"
+
+ATTR_MODE = "mode"
+ATTR_UNIT_SYSTEM = CONF_UNIT_SYSTEM
+ATTR_TRAFFIC_MODE = CONF_TRAFFIC_MODE
+
+ATTR_DURATION_IN_TRAFFIC = "duration_in_traffic"
+ATTR_ORIGIN_NAME = "origin_name"
+ATTR_DESTINATION_NAME = "destination_name"
+
+UNIT_OF_MEASUREMENT = "min"
+
+SCAN_INTERVAL = timedelta(minutes=5)
+
+TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"]
+
+NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input"
+
+COORDINATE_SCHEMA = vol.Schema(
+ {
+ vol.Inclusive(CONF_DESTINATION_LATITUDE, "coordinates"): cv.latitude,
+ vol.Inclusive(CONF_DESTINATION_LONGITUDE, "coordinates"): cv.longitude,
+ }
+)
+
+PLATFORM_SCHEMA = vol.All(
+ cv.has_at_least_one_key(CONF_DESTINATION_LATITUDE, CONF_DESTINATION_ENTITY_ID),
+ cv.has_at_least_one_key(CONF_ORIGIN_LATITUDE, CONF_ORIGIN_ENTITY_ID),
+ PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_APP_ID): cv.string,
+ vol.Required(CONF_APP_CODE): cv.string,
+ vol.Inclusive(
+ CONF_DESTINATION_LATITUDE, "destination_coordinates"
+ ): cv.latitude,
+ vol.Inclusive(
+ CONF_DESTINATION_LONGITUDE, "destination_coordinates"
+ ): cv.longitude,
+ vol.Exclusive(CONF_DESTINATION_LATITUDE, "destination"): cv.latitude,
+ vol.Exclusive(CONF_DESTINATION_ENTITY_ID, "destination"): cv.entity_id,
+ vol.Inclusive(CONF_ORIGIN_LATITUDE, "origin_coordinates"): cv.latitude,
+ vol.Inclusive(CONF_ORIGIN_LONGITUDE, "origin_coordinates"): cv.longitude,
+ vol.Exclusive(CONF_ORIGIN_LATITUDE, "origin"): cv.latitude,
+ vol.Exclusive(CONF_ORIGIN_ENTITY_ID, "origin"): cv.entity_id,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODE),
+ vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In(
+ ROUTE_MODE
+ ),
+ vol.Optional(CONF_TRAFFIC_MODE, default=False): cv.boolean,
+ vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS),
+ }
+ ),
+)
+
+
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: Dict[str, Union[str, bool]],
+ async_add_entities: Callable,
+ discovery_info: None = None,
+) -> None:
+ """Set up the HERE travel time platform."""
+
+ app_id = config[CONF_APP_ID]
+ app_code = config[CONF_APP_CODE]
+ here_client = herepy.RoutingApi(app_id, app_code)
+
+ if not await hass.async_add_executor_job(
+ _are_valid_client_credentials, here_client
+ ):
+ _LOGGER.error(
+ "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token."
+ )
+ return
+
+ if config.get(CONF_ORIGIN_LATITUDE) is not None:
+ origin = f"{config[CONF_ORIGIN_LATITUDE]},{config[CONF_ORIGIN_LONGITUDE]}"
+ else:
+ origin = config[CONF_ORIGIN_ENTITY_ID]
+
+ if config.get(CONF_DESTINATION_LATITUDE) is not None:
+ destination = (
+ f"{config[CONF_DESTINATION_LATITUDE]},{config[CONF_DESTINATION_LONGITUDE]}"
+ )
+ else:
+ destination = config[CONF_DESTINATION_ENTITY_ID]
+
+ travel_mode = config[CONF_MODE]
+ traffic_mode = config[CONF_TRAFFIC_MODE]
+ route_mode = config[CONF_ROUTE_MODE]
+ name = config[CONF_NAME]
+ units = config.get(CONF_UNIT_SYSTEM, hass.config.units.name)
+
+ here_data = HERETravelTimeData(
+ here_client, travel_mode, traffic_mode, route_mode, units
+ )
+
+ sensor = HERETravelTimeSensor(name, origin, destination, here_data)
+
+ async_add_entities([sensor], True)
+
+
+def _are_valid_client_credentials(here_client: herepy.RoutingApi) -> bool:
+ """Check if the provided credentials are correct using defaults."""
+ known_working_origin = [38.9, -77.04833]
+ known_working_destination = [39.0, -77.1]
+ try:
+ here_client.car_route(
+ known_working_origin,
+ known_working_destination,
+ [
+ herepy.RouteMode[ROUTE_MODE_FASTEST],
+ herepy.RouteMode[TRAVEL_MODE_CAR],
+ herepy.RouteMode[TRAFFIC_MODE_DISABLED],
+ ],
+ )
+ except herepy.InvalidCredentialsError:
+ return False
+ return True
+
+
+class HERETravelTimeSensor(Entity):
+ """Representation of a HERE travel time sensor."""
+
+ def __init__(
+ self, name: str, origin: str, destination: str, here_data: "HERETravelTimeData"
+ ) -> None:
+ """Initialize the sensor."""
+ self._name = name
+ self._here_data = here_data
+ self._unit_of_measurement = UNIT_OF_MEASUREMENT
+ self._origin_entity_id = None
+ self._destination_entity_id = None
+ self._attrs = {
+ ATTR_UNIT_SYSTEM: self._here_data.units,
+ ATTR_MODE: self._here_data.travel_mode,
+ ATTR_TRAFFIC_MODE: self._here_data.traffic_mode,
+ }
+
+ # Check if location is a trackable entity
+ if origin.split(".", 1)[0] in TRACKABLE_DOMAINS:
+ self._origin_entity_id = origin
+ else:
+ self._here_data.origin = origin
+
+ if destination.split(".", 1)[0] in TRACKABLE_DOMAINS:
+ self._destination_entity_id = destination
+ else:
+ self._here_data.destination = destination
+
+ @property
+ def state(self) -> Optional[str]:
+ """Return the state of the sensor."""
+ if self._here_data.traffic_mode:
+ if self._here_data.traffic_time is not None:
+ return str(round(self._here_data.traffic_time / 60))
+ if self._here_data.base_time is not None:
+ return str(round(self._here_data.base_time / 60))
+
+ return None
+
+ @property
+ def name(self) -> str:
+ """Get the name of the sensor."""
+ return self._name
+
+ @property
+ def device_state_attributes(
+ self
+ ) -> Optional[Dict[str, Union[None, float, str, bool]]]:
+ """Return the state attributes."""
+ if self._here_data.base_time is None:
+ return None
+
+ res = self._attrs
+ if self._here_data.attribution is not None:
+ res[ATTR_ATTRIBUTION] = self._here_data.attribution
+ res[ATTR_DURATION] = self._here_data.base_time / 60
+ res[ATTR_DISTANCE] = self._here_data.distance
+ res[ATTR_ROUTE] = self._here_data.route
+ res[ATTR_DURATION_IN_TRAFFIC] = self._here_data.traffic_time / 60
+ res[ATTR_ORIGIN] = self._here_data.origin
+ res[ATTR_DESTINATION] = self._here_data.destination
+ res[ATTR_ORIGIN_NAME] = self._here_data.origin_name
+ res[ATTR_DESTINATION_NAME] = self._here_data.destination_name
+ return res
+
+ @property
+ def unit_of_measurement(self) -> str:
+ """Return the unit this state is expressed in."""
+ return self._unit_of_measurement
+
+ @property
+ def icon(self) -> str:
+ """Icon to use in the frontend depending on travel_mode."""
+ if self._here_data.travel_mode == TRAVEL_MODE_BICYCLE:
+ return ICON_BICYCLE
+ if self._here_data.travel_mode == TRAVEL_MODE_PEDESTRIAN:
+ return ICON_PEDESTRIAN
+ if self._here_data.travel_mode in TRAVEL_MODES_PUBLIC:
+ return ICON_PUBLIC
+ if self._here_data.travel_mode == TRAVEL_MODE_TRUCK:
+ return ICON_TRUCK
+ return ICON_CAR
+
+ async def async_update(self) -> None:
+ """Update Sensor Information."""
+ # Convert device_trackers to HERE friendly location
+ if self._origin_entity_id is not None:
+ self._here_data.origin = await self._get_location_from_entity(
+ self._origin_entity_id
+ )
+
+ if self._destination_entity_id is not None:
+ self._here_data.destination = await self._get_location_from_entity(
+ self._destination_entity_id
+ )
+
+ await self.hass.async_add_executor_job(self._here_data.update)
+
+ async def _get_location_from_entity(self, entity_id: str) -> Optional[str]:
+ """Get the location from the entity state or attributes."""
+ entity = self.hass.states.get(entity_id)
+
+ if entity is None:
+ _LOGGER.error("Unable to find entity %s", entity_id)
+ return None
+
+ # Check if the entity has location attributes
+ if location.has_location(entity):
+ return self._get_location_from_attributes(entity)
+
+ # Check if device is in a zone
+ zone_entity = self.hass.states.get("zone.{}".format(entity.state))
+ if location.has_location(zone_entity):
+ _LOGGER.debug(
+ "%s is in %s, getting zone location", entity_id, zone_entity.entity_id
+ )
+ return self._get_location_from_attributes(zone_entity)
+
+ # If zone was not found in state then use the state as the location
+ if entity_id.startswith("sensor."):
+ return entity.state
+
+ @staticmethod
+ def _get_location_from_attributes(entity: State) -> str:
+ """Get the lat/long string from an entities attributes."""
+ attr = entity.attributes
+ return "{},{}".format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE))
+
+
+class HERETravelTimeData:
+ """HERETravelTime data object."""
+
+ def __init__(
+ self,
+ here_client: herepy.RoutingApi,
+ travel_mode: str,
+ traffic_mode: bool,
+ route_mode: str,
+ units: str,
+ ) -> None:
+ """Initialize herepy."""
+ self.origin = None
+ self.destination = None
+ self.travel_mode = travel_mode
+ self.traffic_mode = traffic_mode
+ self.route_mode = route_mode
+ self.attribution = None
+ self.traffic_time = None
+ self.distance = None
+ self.route = None
+ self.base_time = None
+ self.origin_name = None
+ self.destination_name = None
+ self.units = units
+ self._client = here_client
+
+ def update(self) -> None:
+ """Get the latest data from HERE."""
+ if self.traffic_mode:
+ traffic_mode = TRAFFIC_MODE_ENABLED
+ else:
+ traffic_mode = TRAFFIC_MODE_DISABLED
+
+ if self.destination is not None and self.origin is not None:
+ # Convert location to HERE friendly location
+ destination = self.destination.split(",")
+ origin = self.origin.split(",")
+
+ _LOGGER.debug(
+ "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s",
+ origin,
+ destination,
+ herepy.RouteMode[self.route_mode],
+ herepy.RouteMode[self.travel_mode],
+ herepy.RouteMode[traffic_mode],
+ )
+ try:
+ response = self._client.car_route(
+ origin,
+ destination,
+ [
+ herepy.RouteMode[self.route_mode],
+ herepy.RouteMode[self.travel_mode],
+ herepy.RouteMode[traffic_mode],
+ ],
+ )
+ except herepy.NoRouteFoundError:
+ # Better error message for cryptic no route error codes
+ _LOGGER.error(NO_ROUTE_ERROR_MESSAGE)
+ return
+
+ _LOGGER.debug("Raw response is: %s", response.response)
+
+ # pylint: disable=no-member
+ source_attribution = response.response.get("sourceAttribution")
+ if source_attribution is not None:
+ self.attribution = self._build_hass_attribution(source_attribution)
+ # pylint: disable=no-member
+ route = response.response["route"]
+ summary = route[0]["summary"]
+ waypoint = route[0]["waypoint"]
+ self.base_time = summary["baseTime"]
+ if self.travel_mode in TRAVEL_MODES_VEHICLE:
+ self.traffic_time = summary["trafficTime"]
+ else:
+ self.traffic_time = self.base_time
+ distance = summary["distance"]
+ if self.units == CONF_UNIT_SYSTEM_IMPERIAL:
+ # Convert to miles.
+ self.distance = distance / 1609.344
+ else:
+ # Convert to kilometers
+ self.distance = distance / 1000
+ # pylint: disable=no-member
+ self.route = response.route_short
+ self.origin_name = waypoint[0]["mappedRoadName"]
+ self.destination_name = waypoint[1]["mappedRoadName"]
+
+ @staticmethod
+ def _build_hass_attribution(source_attribution: Dict) -> Optional[str]:
+ """Build a hass frontend ready string out of the sourceAttribution."""
+ suppliers = source_attribution.get("supplier")
+ if suppliers is not None:
+ supplier_titles = []
+ for supplier in suppliers:
+ title = supplier.get("title")
+ if title is not None:
+ supplier_titles.append(title)
+ joined_supplier_titles = ",".join(supplier_titles)
+ attribution = f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind."
+ return attribution
diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json
index ea3e801ac536f4..ebb0895bd7a100 100644
--- a/homeassistant/components/homekit/manifest.json
+++ b/homeassistant/components/homekit/manifest.json
@@ -3,7 +3,7 @@
"name": "Homekit",
"documentation": "https://www.home-assistant.io/components/homekit",
"requirements": [
- "HAP-python==2.5.0"
+ "HAP-python==2.6.0"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/homekit_controller/.translations/es.json b/homeassistant/components/homekit_controller/.translations/es.json
index 642e76fd1dd0d3..67f6daa8469d8e 100644
--- a/homeassistant/components/homekit_controller/.translations/es.json
+++ b/homeassistant/components/homekit_controller/.translations/es.json
@@ -3,6 +3,7 @@
"abort": {
"accessory_not_found_error": "No se puede a\u00f1adir el emparejamiento porque ya no se puede encontrar el dispositivo.",
"already_configured": "El accesorio ya est\u00e1 configurado con este controlador.",
+ "already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en curso.",
"already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicia el accesorio e int\u00e9ntalo de nuevo.",
"ignored_model": "El soporte de HomeKit para este modelo est\u00e1 bloqueado ya que est\u00e1 disponible una integraci\u00f3n nativa m\u00e1s completa.",
"invalid_config_entry": "Este dispositivo se muestra como listo para vincular, pero ya existe una entrada que causa conflicto en Home Assistant y se debe eliminar primero.",
@@ -23,7 +24,7 @@
"data": {
"pairing_code": "C\u00f3digo de vinculaci\u00f3n"
},
- "description": "Introduce tu c\u00f3digo de vinculaci\u00f3n de HomeKit para usar este accesorio",
+ "description": "Introduce tu c\u00f3digo de vinculaci\u00f3n de HomeKit (en este formato XXX-XX-XXX) para usar este accesorio",
"title": "Vincular con accesorio HomeKit"
},
"user": {
diff --git a/homeassistant/components/homekit_controller/.translations/fr.json b/homeassistant/components/homekit_controller/.translations/fr.json
index 15e50a4012701c..7f0566ddd42365 100644
--- a/homeassistant/components/homekit_controller/.translations/fr.json
+++ b/homeassistant/components/homekit_controller/.translations/fr.json
@@ -24,7 +24,7 @@
"data": {
"pairing_code": "Code d\u2019appairage"
},
- "description": "Entrez votre code de jumelage HomeKit pour utiliser cet accessoire.",
+ "description": "Entrez votre code de jumelage HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire.",
"title": "Appairer avec l'accessoire HomeKit"
},
"user": {
diff --git a/homeassistant/components/homekit_controller/.translations/it.json b/homeassistant/components/homekit_controller/.translations/it.json
index a1d460d12dcfa8..7ed026a529c2fd 100644
--- a/homeassistant/components/homekit_controller/.translations/it.json
+++ b/homeassistant/components/homekit_controller/.translations/it.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "accessory_not_found_error": "Impossibile aggiungere l'abbinamento in quanto non \u00e8 pi\u00f9 possibile trovare il dispositivo.",
"already_configured": "L'accessorio \u00e8 gi\u00e0 configurato con questo controller.",
"already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.",
"already_paired": "Questo accessorio \u00e8 gi\u00e0 associato a un altro dispositivo. Si prega di resettare l'accessorio e riprovare.",
@@ -10,6 +11,10 @@
},
"error": {
"authentication_error": "Codice HomeKit errato. Per favore, controllate e riprovate.",
+ "busy_error": "Il dispositivo ha rifiutato di aggiungere l'abbinamento in quanto \u00e8 gi\u00e0 associato a un altro controller.",
+ "max_peers_error": "Il dispositivo ha rifiutato di aggiungere l'abbinamento in quanto non dispone di una memoria libera per esso.",
+ "max_tries_error": "Il dispositivo ha rifiutato di aggiungere l'abbinamento poich\u00e9 ha ricevuto pi\u00f9 di 100 tentativi di autenticazione non riusciti.",
+ "pairing_failed": "Si \u00e8 verificato un errore non gestito durante il tentativo di abbinamento con questo dispositivo. Potrebbe trattarsi di un errore temporaneo o il dispositivo potrebbe non essere attualmente supportato.",
"unable_to_pair": "Impossibile abbinare, per favore riprova.",
"unknown_error": "Il dispositivo ha riportato un errore sconosciuto. L'abbinamento non \u00e8 riuscito."
},
@@ -19,7 +24,7 @@
"data": {
"pairing_code": "Codice di abbinamento"
},
- "description": "Inserisci il codice di abbinamento HomeKit per usare questo accessorio",
+ "description": "Immettere il codice di abbinamento HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio",
"title": "Abbina con accessorio HomeKit"
},
"user": {
diff --git a/homeassistant/components/homekit_controller/.translations/lb.json b/homeassistant/components/homekit_controller/.translations/lb.json
index 97efd428a0469e..ca7bce44508243 100644
--- a/homeassistant/components/homekit_controller/.translations/lb.json
+++ b/homeassistant/components/homekit_controller/.translations/lb.json
@@ -24,7 +24,7 @@
"data": {
"pairing_code": "Pairing Code"
},
- "description": "Gitt \u00e4ren HomeKit pairing Code an fir d\u00ebsen Accessoire ze benotzen",
+ "description": "Gitt \u00e4ren HomeKit pairing Code (am Format XXX-XX-XXX) an fir d\u00ebsen Accessoire ze benotzen",
"title": "Mam HomeKit Accessoire verbannen"
},
"user": {
diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hans.json b/homeassistant/components/homekit_controller/.translations/zh-Hans.json
index 8d064622f7e468..d9fdc8f91c2e1f 100644
--- a/homeassistant/components/homekit_controller/.translations/zh-Hans.json
+++ b/homeassistant/components/homekit_controller/.translations/zh-Hans.json
@@ -23,7 +23,7 @@
"data": {
"pairing_code": "\u914d\u5bf9\u4ee3\u7801"
},
- "description": "\u8f93\u5165\u60a8\u7684 HomeKit \u914d\u5bf9\u4ee3\u7801\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6",
+ "description": "\u8f93\u5165\u60a8\u7684HomeKit\u914d\u5bf9\u4ee3\u7801\uff08\u683c\u5f0f\u4e3aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6",
"title": "\u4e0e HomeKit \u914d\u4ef6\u914d\u5bf9"
},
"user": {
diff --git a/homeassistant/components/homematicip_cloud/.translations/it.json b/homeassistant/components/homematicip_cloud/.translations/it.json
index 6e6d7c8a59fe06..c7f1af21f2270a 100644
--- a/homeassistant/components/homematicip_cloud/.translations/it.json
+++ b/homeassistant/components/homematicip_cloud/.translations/it.json
@@ -15,7 +15,7 @@
"init": {
"data": {
"hapid": "ID del punto di accesso (SGTIN)",
- "name": "Nome (facoltativo, utilizzato come prefisso del nome per tutti i dispositivi)",
+ "name": "Nome (opzionale, usato come prefisso del nome per tutti i dispositivi)",
"pin": "Codice Pin (opzionale)"
},
"title": "Scegli punto di accesso HomematicIP"
diff --git a/homeassistant/components/homematicip_cloud/.translations/lb.json b/homeassistant/components/homematicip_cloud/.translations/lb.json
index 2cad909a7ee54e..f8ae990d36442a 100644
--- a/homeassistant/components/homematicip_cloud/.translations/lb.json
+++ b/homeassistant/components/homematicip_cloud/.translations/lb.json
@@ -21,7 +21,7 @@
"title": "HomematicIP Accesspoint auswielen"
},
"link": {
- "description": "Dr\u00e9ckt de bloen Kn\u00e4ppchen um Accesspoint an den Submit Kn\u00e4ppchen fir d'HomematicIP mam Home Assistant ze registr\u00e9ieren.",
+ "description": "Dr\u00e9ckt de bloen Kn\u00e4ppchen um Accesspoint an den Submit Kn\u00e4ppchen fir d'HomematicIP mam Home Assistant ze registr\u00e9ieren.\n\n",
"title": "Accesspoint verbannen"
}
},
diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py
index d6bc24d21edf18..4ac4614379b905 100644
--- a/homeassistant/components/homematicip_cloud/binary_sensor.py
+++ b/homeassistant/components/homematicip_cloud/binary_sensor.py
@@ -38,19 +38,30 @@
from homeassistant.core import HomeAssistant
from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice
-from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_MODEL_TYPE
+from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP, ATTR_MODEL_TYPE
_LOGGER = logging.getLogger(__name__)
ATTR_LOW_BATTERY = "low_battery"
-ATTR_MOTIONDETECTED = "motion detected"
-ATTR_PRESENCEDETECTED = "presence detected"
-ATTR_POWERMAINSFAILURE = "power mains failure"
-ATTR_WINDOWSTATE = "window state"
-ATTR_MOISTUREDETECTED = "moisture detected"
-ATTR_WATERLEVELDETECTED = "water level detected"
-ATTR_SMOKEDETECTORALARM = "smoke detector alarm"
+ATTR_MOISTURE_DETECTED = "moisture_detected"
+ATTR_MOTION_DETECTED = "motion_detected"
+ATTR_POWER_MAINS_FAILURE = "power_mains_failure"
+ATTR_PRESENCE_DETECTED = "presence_detected"
+ATTR_SMOKE_DETECTOR_ALARM = "smoke_detector_alarm"
ATTR_TODAY_SUNSHINE_DURATION = "today_sunshine_duration_in_minutes"
+ATTR_WATER_LEVEL_DETECTED = "water_level_detected"
+ATTR_WINDOW_STATE = "window_state"
+
+GROUP_ATTRIBUTES = {
+ "lowBat": ATTR_LOW_BATTERY,
+ "modelType": ATTR_MODEL_TYPE,
+ "moistureDetected": ATTR_MOISTURE_DETECTED,
+ "motionDetected": ATTR_MOTION_DETECTED,
+ "powerMainsFailure": ATTR_POWER_MAINS_FAILURE,
+ "presenceDetected": ATTR_PRESENCE_DETECTED,
+ "unreach": ATTR_GROUP_MEMBER_UNREACHABLE,
+ "waterlevelDetected": ATTR_WATER_LEVEL_DETECTED,
+}
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@@ -118,8 +129,6 @@ def device_class(self) -> str:
@property
def is_on(self) -> bool:
"""Return true if the contact interface is on/open."""
- if hasattr(self._device, "sabotage") and self._device.sabotage:
- return True
if self._device.windowState is None:
return None
return self._device.windowState != WindowState.CLOSED
@@ -136,8 +145,6 @@ def device_class(self) -> str:
@property
def is_on(self) -> bool:
"""Return true if the shutter contact is on/open."""
- if hasattr(self._device, "sabotage") and self._device.sabotage:
- return True
if self._device.windowState is None:
return None
return self._device.windowState != WindowState.CLOSED
@@ -154,8 +161,6 @@ def device_class(self) -> str:
@property
def is_on(self) -> bool:
"""Return true if motion is detected."""
- if hasattr(self._device, "sabotage") and self._device.sabotage:
- return True
return self._device.motionDetected
@@ -170,8 +175,6 @@ def device_class(self) -> str:
@property
def is_on(self) -> bool:
"""Return true if presence is detected."""
- if hasattr(self._device, "sabotage") and self._device.sabotage:
- return True
return self._device.presenceDetected
@@ -259,13 +262,13 @@ def is_on(self) -> bool:
@property
def device_state_attributes(self):
"""Return the state attributes of the illuminance sensor."""
- attr = super().device_state_attributes
- if (
- hasattr(self._device, "todaySunshineDuration")
- and self._device.todaySunshineDuration
- ):
- attr[ATTR_TODAY_SUNSHINE_DURATION] = self._device.todaySunshineDuration
- return attr
+ state_attr = super().device_state_attributes
+
+ today_sunshine_duration = getattr(self._device, "todaySunshineDuration", None)
+ if today_sunshine_duration:
+ state_attr[ATTR_TODAY_SUNSHINE_DURATION] = today_sunshine_duration
+
+ return state_attr
class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice):
@@ -309,21 +312,18 @@ def available(self) -> bool:
@property
def device_state_attributes(self):
"""Return the state attributes of the security zone group."""
- attr = {ATTR_MODEL_TYPE: self._device.modelType}
+ state_attr = {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: True}
- if self._device.motionDetected:
- attr[ATTR_MOTIONDETECTED] = True
- if self._device.presenceDetected:
- attr[ATTR_PRESENCEDETECTED] = True
+ for attr, attr_key in GROUP_ATTRIBUTES.items():
+ attr_value = getattr(self._device, attr, None)
+ if attr_value:
+ state_attr[attr_key] = attr_value
- if (
- self._device.windowState is not None
- and self._device.windowState != WindowState.CLOSED
- ):
- attr[ATTR_WINDOWSTATE] = str(self._device.windowState)
- if self._device.unreach:
- attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True
- return attr
+ window_state = getattr(self._device, "windowState", None)
+ if window_state and window_state != WindowState.CLOSED:
+ state_attr[ATTR_WINDOW_STATE] = str(window_state)
+
+ return state_attr
@property
def is_on(self) -> bool:
@@ -356,23 +356,13 @@ def __init__(self, home: AsyncHome, device) -> None:
@property
def device_state_attributes(self):
"""Return the state attributes of the security group."""
- attr = super().device_state_attributes
-
- if self._device.powerMainsFailure:
- attr[ATTR_POWERMAINSFAILURE] = True
- if self._device.moistureDetected:
- attr[ATTR_MOISTUREDETECTED] = True
- if self._device.waterlevelDetected:
- attr[ATTR_WATERLEVELDETECTED] = True
- if self._device.lowBat:
- attr[ATTR_LOW_BATTERY] = True
- if (
- self._device.smokeDetectorAlarmType is not None
- and self._device.smokeDetectorAlarmType != SmokeDetectorAlarmType.IDLE_OFF
- ):
- attr[ATTR_SMOKEDETECTORALARM] = str(self._device.smokeDetectorAlarmType)
+ state_attr = super().device_state_attributes
+
+ smoke_detector_at = getattr(self._device, "smokeDetectorAlarmType", None)
+ if smoke_detector_at and smoke_detector_at != SmokeDetectorAlarmType.IDLE_OFF:
+ state_attr[ATTR_SMOKE_DETECTOR_ALARM] = str(smoke_detector_at)
- return attr
+ return state_attr
@property
def is_on(self) -> bool:
diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py
index 5eeb14b635946c..05853d4b260bca 100644
--- a/homeassistant/components/homematicip_cloud/device.py
+++ b/homeassistant/components/homematicip_cloud/device.py
@@ -12,6 +12,7 @@
ATTR_MODEL_TYPE = "model_type"
ATTR_ID = "id"
+ATTR_IS_GROUP = "is_group"
# RSSI HAP -> Device
ATTR_RSSI_DEVICE = "rssi_device"
# RSSI Device -> HAP
@@ -131,4 +132,6 @@ def device_state_attributes(self):
if attr_value:
state_attr[attr_key] = attr_value
+ state_attr[ATTR_IS_GROUP] = False
+
return state_attr
diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py
index bc7b12f9653ea5..42ff6d30478fb4 100644
--- a/homeassistant/components/homematicip_cloud/light.py
+++ b/homeassistant/components/homematicip_cloud/light.py
@@ -93,13 +93,15 @@ class HomematicipLightMeasuring(HomematicipLight):
@property
def device_state_attributes(self):
"""Return the state attributes of the generic device."""
- attr = super().device_state_attributes
- if self._device.currentPowerConsumption > 0.05:
- attr[ATTR_POWER_CONSUMPTION] = round(
- self._device.currentPowerConsumption, 2
- )
- attr[ATTR_ENERGY_COUNTER] = round(self._device.energyCounter, 2)
- return attr
+ state_attr = super().device_state_attributes
+
+ current_power_consumption = self._device.currentPowerConsumption
+ if current_power_consumption > 0.05:
+ state_attr[ATTR_POWER_CONSUMPTION] = round(current_power_consumption, 2)
+
+ state_attr[ATTR_ENERGY_COUNTER] = round(self._device.energyCounter, 2)
+
+ return state_attr
class HomematicipDimmer(HomematicipGenericDevice, Light):
@@ -187,15 +189,17 @@ def hs_color(self) -> tuple:
@property
def device_state_attributes(self):
"""Return the state attributes of the generic device."""
- attr = super().device_state_attributes
+ state_attr = super().device_state_attributes
+
if self.is_on:
- attr[ATTR_COLOR_NAME] = self._func_channel.simpleRGBColorState
- return attr
+ state_attr[ATTR_COLOR_NAME] = self._func_channel.simpleRGBColorState
+
+ return state_attr
@property
def name(self) -> str:
"""Return the name of the generic device."""
- return "{} {}".format(super().name, "Notification")
+ return f"{super().name} Notification"
@property
def supported_features(self) -> int:
diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py
index c15b3121d3a63e..770921288b9341 100644
--- a/homeassistant/components/homematicip_cloud/sensor.py
+++ b/homeassistant/components/homematicip_cloud/sensor.py
@@ -10,6 +10,7 @@
AsyncMotionDetectorIndoor,
AsyncMotionDetectorOutdoor,
AsyncMotionDetectorPushButton,
+ AsyncPassageDetector,
AsyncPlugableSwitchMeasuring,
AsyncPresenceDetectorIndoor,
AsyncTemperatureHumiditySensorDisplay,
@@ -34,10 +35,12 @@
from homeassistant.core import HomeAssistant
from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice
-from .device import ATTR_MODEL_TYPE
+from .device import ATTR_IS_GROUP, ATTR_MODEL_TYPE
_LOGGER = logging.getLogger(__name__)
+ATTR_LEFT_COUNTER = "left_counter"
+ATTR_RIGHT_COUNTER = "right_counter"
ATTR_TEMPERATURE_OFFSET = "temperature_offset"
ATTR_WIND_DIRECTION = "wind_direction"
ATTR_WIND_DIRECTION_VARIATION = "wind_direction_variation_in_degree"
@@ -100,6 +103,8 @@ async def async_setup_entry(
devices.append(HomematicipWindspeedSensor(home, device))
if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)):
devices.append(HomematicipTodayRainSensor(home, device))
+ if isinstance(device, AsyncPassageDetector):
+ devices.append(HomematicipPassageDetectorDeltaCounter(home, device))
if devices:
async_add_entities(devices)
@@ -145,8 +150,8 @@ def unit_of_measurement(self) -> str:
@property
def device_state_attributes(self):
- """Return the state attributes of the security zone group."""
- return {ATTR_MODEL_TYPE: self._device.modelType}
+ """Return the state attributes of the access point."""
+ return {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: False}
class HomematicipHeatingThermostat(HomematicipGenericDevice):
@@ -229,13 +234,13 @@ def unit_of_measurement(self) -> str:
@property
def device_state_attributes(self):
"""Return the state attributes of the windspeed sensor."""
- attr = super().device_state_attributes
- if (
- hasattr(self._device, "temperatureOffset")
- and self._device.temperatureOffset
- ):
- attr[ATTR_TEMPERATURE_OFFSET] = self._device.temperatureOffset
- return attr
+ state_attr = super().device_state_attributes
+
+ temperature_offset = getattr(self._device, "temperatureOffset", None)
+ if temperature_offset:
+ state_attr[ATTR_TEMPERATURE_OFFSET] = temperature_offset
+
+ return state_attr
class HomematicipIlluminanceSensor(HomematicipGenericDevice):
@@ -307,15 +312,17 @@ def unit_of_measurement(self) -> str:
@property
def device_state_attributes(self):
"""Return the state attributes of the wind speed sensor."""
- attr = super().device_state_attributes
- if hasattr(self._device, "windDirection") and self._device.windDirection:
- attr[ATTR_WIND_DIRECTION] = _get_wind_direction(self._device.windDirection)
- if (
- hasattr(self._device, "windDirectionVariation")
- and self._device.windDirectionVariation
- ):
- attr[ATTR_WIND_DIRECTION_VARIATION] = self._device.windDirectionVariation
- return attr
+ state_attr = super().device_state_attributes
+
+ wind_direction = getattr(self._device, "windDirection", None)
+ if wind_direction:
+ state_attr[ATTR_WIND_DIRECTION] = _get_wind_direction(wind_direction)
+
+ wind_direction_variation = getattr(self._device, "windDirectionVariation", None)
+ if wind_direction_variation:
+ state_attr[ATTR_WIND_DIRECTION_VARIATION] = wind_direction_variation
+
+ return state_attr
class HomematicipTodayRainSensor(HomematicipGenericDevice):
@@ -336,6 +343,29 @@ def unit_of_measurement(self) -> str:
return "mm"
+class HomematicipPassageDetectorDeltaCounter(HomematicipGenericDevice):
+ """Representation of a HomematicIP passage detector delta counter."""
+
+ def __init__(self, home: AsyncHome, device) -> None:
+ """Initialize the device."""
+ super().__init__(home, device)
+
+ @property
+ def state(self) -> int:
+ """Representation of the HomematicIP passage detector delta counter value."""
+ return self._device.leftRightCounterDelta
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the delta counter."""
+ state_attr = super().device_state_attributes
+
+ state_attr[ATTR_LEFT_COUNTER] = self._device.leftCounter
+ state_attr[ATTR_RIGHT_COUNTER] = self._device.rightCounter
+
+ return state_attr
+
+
def _get_wind_direction(wind_direction_degree: float) -> str:
"""Convert wind direction degree to named direction."""
if 11.25 <= wind_direction_degree < 33.75:
diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py
index 6d19087781daef..ababf793f0ce6e 100644
--- a/homeassistant/components/homematicip_cloud/switch.py
+++ b/homeassistant/components/homematicip_cloud/switch.py
@@ -19,7 +19,7 @@
from homeassistant.core import HomeAssistant
from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice
-from .device import ATTR_GROUP_MEMBER_UNREACHABLE
+from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP
_LOGGER = logging.getLogger(__name__)
@@ -113,10 +113,10 @@ def available(self) -> bool:
@property
def device_state_attributes(self):
"""Return the state attributes of the switch-group."""
- attr = {}
+ state_attr = {ATTR_IS_GROUP: True}
if self._device.unreach:
- attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True
- return attr
+ state_attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True
+ return state_attr
async def async_turn_on(self, **kwargs):
"""Turn the group on."""
diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py
index 463e1bfb7410f1..ed9098559a344c 100644
--- a/homeassistant/components/homematicip_cloud/weather.py
+++ b/homeassistant/components/homematicip_cloud/weather.py
@@ -7,6 +7,7 @@
AsyncWeatherSensorPro,
)
from homematicip.aio.home import AsyncHome
+from homematicip.base.enums import WeatherCondition
from homeassistant.components.weather import WeatherEntity
from homeassistant.config_entries import ConfigEntry
@@ -17,6 +18,24 @@
_LOGGER = logging.getLogger(__name__)
+HOME_WEATHER_CONDITION = {
+ WeatherCondition.CLEAR: "sunny",
+ WeatherCondition.LIGHT_CLOUDY: "partlycloudy",
+ WeatherCondition.CLOUDY: "cloudy",
+ WeatherCondition.CLOUDY_WITH_RAIN: "rainy",
+ WeatherCondition.CLOUDY_WITH_SNOW_RAIN: "snowy-rainy",
+ WeatherCondition.HEAVILY_CLOUDY: "cloudy",
+ WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN: "rainy",
+ WeatherCondition.HEAVILY_CLOUDY_WITH_STRONG_RAIN: "snowy-rainy",
+ WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW: "snowy",
+ WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW_RAIN: "snowy-rainy",
+ WeatherCondition.HEAVILY_CLOUDY_WITH_THUNDER: "lightning",
+ WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN_AND_THUNDER: "lightning-rainy",
+ WeatherCondition.FOGGY: "fog",
+ WeatherCondition.STRONG_WIND: "windy",
+ WeatherCondition.UNKNOWN: "",
+}
+
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the HomematicIP Cloud weather sensor."""
@@ -35,6 +54,8 @@ async def async_setup_entry(
elif isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus)):
devices.append(HomematicipWeatherSensor(home, device))
+ devices.append(HomematicipHomeWeather(home))
+
if devices:
async_add_entities(devices)
@@ -79,7 +100,7 @@ def attribution(self) -> str:
@property
def condition(self) -> str:
"""Return the current condition."""
- if hasattr(self._device, "raining") and self._device.raining:
+ if getattr(self._device, "raining", None):
return "rainy"
if self._device.storm:
return "windy"
@@ -95,3 +116,57 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor):
def wind_bearing(self) -> float:
"""Return the wind bearing."""
return self._device.windDirection
+
+
+class HomematicipHomeWeather(HomematicipGenericDevice, WeatherEntity):
+ """representation of a HomematicIP Cloud home weather."""
+
+ def __init__(self, home: AsyncHome) -> None:
+ """Initialize the home weather."""
+ home.weather.modelType = "HmIP-Home-Weather"
+ super().__init__(home, home)
+
+ @property
+ def available(self) -> bool:
+ """Device available."""
+ return self._home.connected
+
+ @property
+ def name(self) -> str:
+ """Return the name of the sensor."""
+ return f"Weather {self._home.location.city}"
+
+ @property
+ def temperature(self) -> float:
+ """Return the platform temperature."""
+ return self._device.weather.temperature
+
+ @property
+ def temperature_unit(self) -> str:
+ """Return the unit of measurement."""
+ return TEMP_CELSIUS
+
+ @property
+ def humidity(self) -> int:
+ """Return the humidity."""
+ return self._device.weather.humidity
+
+ @property
+ def wind_speed(self) -> float:
+ """Return the wind speed."""
+ return round(self._device.weather.windSpeed, 1)
+
+ @property
+ def wind_bearing(self) -> float:
+ """Return the wind bearing."""
+ return self._device.weather.windDirection
+
+ @property
+ def attribution(self) -> str:
+ """Return the attribution."""
+ return "Powered by Homematic IP"
+
+ @property
+ def condition(self) -> str:
+ """Return the current condition."""
+ return HOME_WEATHER_CONDITION.get(self._device.weather.weatherCondition)
diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py
index d8fa8853c7f18c..7d1e24f369800f 100644
--- a/homeassistant/components/http/ban.py
+++ b/homeassistant/components/http/ban.py
@@ -18,7 +18,7 @@
from .const import KEY_REAL_IP
-# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs
+# mypy: allow-untyped-defs, no-check-untyped-defs
_LOGGER = logging.getLogger(__name__)
@@ -165,7 +165,7 @@ def __init__(self, ip_ban: str, banned_at: Optional[datetime] = None) -> None:
self.banned_at = banned_at or datetime.utcnow()
-async def async_load_ip_bans_config(hass: HomeAssistant, path: str):
+async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> List[IpBan]:
"""Load list of banned IPs from config file."""
ip_list: List[IpBan] = []
@@ -188,7 +188,7 @@ async def async_load_ip_bans_config(hass: HomeAssistant, path: str):
return ip_list
-def update_ip_bans_config(path: str, ip_ban: IpBan):
+def update_ip_bans_config(path: str, ip_ban: IpBan) -> None:
"""Update config file with new banned IP address."""
with open(path, "a") as out:
ip_ = {
diff --git a/homeassistant/components/hue/.translations/es.json b/homeassistant/components/hue/.translations/es.json
index 56e7ed62e9dc8a..3ec9ed871d3dca 100644
--- a/homeassistant/components/hue/.translations/es.json
+++ b/homeassistant/components/hue/.translations/es.json
@@ -3,9 +3,11 @@
"abort": {
"all_configured": "Todos los puentes Philips Hue ya est\u00e1n configurados",
"already_configured": "El puente ya esta configurado",
+ "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en curso.",
"cannot_connect": "No se puede conectar al puente",
"discover_timeout": "No se han descubierto puentes Philips Hue",
"no_bridges": "No se han descubierto puentes Philips Hue.",
+ "not_hue_bridge": "No es un puente Hue",
"unknown": "Se produjo un error desconocido"
},
"error": {
diff --git a/homeassistant/components/hue/.translations/it.json b/homeassistant/components/hue/.translations/it.json
index 72b2fd6445bf37..5dd64364c1080f 100644
--- a/homeassistant/components/hue/.translations/it.json
+++ b/homeassistant/components/hue/.translations/it.json
@@ -1,11 +1,13 @@
{
"config": {
"abort": {
- "all_configured": "Tutti i bridge Philips Hue sono gi\u00e0 configurati",
- "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato",
+ "all_configured": "Tutti i bridge di Philips Hue sono gi\u00e0 configurati",
+ "already_configured": "Il bridge \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Il flusso di configurazione per bridge \u00e8 gi\u00e0 in corso.",
"cannot_connect": "Impossibile connettersi al bridge",
"discover_timeout": "Impossibile trovare i bridge Hue",
- "no_bridges": "Nessun bridge Hue di Philips trovato",
+ "no_bridges": "Nessun bridge di Philips Hue trovato",
+ "not_hue_bridge": "Non \u00e8 un bridge Hue",
"unknown": "Si \u00e8 verificato un errore"
},
"error": {
@@ -24,6 +26,6 @@
"title": "Collega Hub"
}
},
- "title": "Philips Hue Bridge"
+ "title": "Philips Hue"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hydroquebec/sensor.py b/homeassistant/components/hydroquebec/sensor.py
index fd713e8b7a7937..c3ad79c1c9866e 100644
--- a/homeassistant/components/hydroquebec/sensor.py
+++ b/homeassistant/components/hydroquebec/sensor.py
@@ -28,9 +28,9 @@
_LOGGER = logging.getLogger(__name__)
KILOWATT_HOUR = ENERGY_KILO_WATT_HOUR
-PRICE = "CAD" # type: str
-DAYS = "days" # type: str
-CONF_CONTRACT = "contract" # type: str
+PRICE = "CAD"
+DAYS = "days"
+CONF_CONTRACT = "contract"
DEFAULT_NAME = "HydroQuebec"
diff --git a/homeassistant/components/iaqualink/.translations/bg.json b/homeassistant/components/iaqualink/.translations/bg.json
new file mode 100644
index 00000000000000..5b37bde3ee3a81
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/bg.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\u041c\u043e\u0436\u0435 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0441 iAqualink."
+ },
+ "error": {
+ "connection_failure": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 iAqualink. \u041f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u0430",
+ "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 / \u0438\u043c\u0435\u0439\u043b \u0430\u0434\u0440\u0435\u0441"
+ },
+ "description": "\u041c\u043e\u043b\u044f \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f iAqualink \u0430\u043a\u0430\u0443\u043d\u0442.",
+ "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/ca.json b/homeassistant/components/iaqualink/.translations/ca.json
new file mode 100644
index 00000000000000..a5456c7b0cd0a9
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/ca.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Nom\u00e9s pots configurar una \u00fanica connexi\u00f3 d'iAqualink."
+ },
+ "error": {
+ "connection_failure": "No s'ha pogut connectar amb iAqualink. Comprova el nom d'usuari i la contrasenya."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrasenya",
+ "username": "Nom d'usuari / Correu electr\u00f2nic"
+ },
+ "description": "Introdueix el nom d'usuari i la contrasenya del teu compte d'iAqualink.",
+ "title": "Connexi\u00f3 amb iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/da.json b/homeassistant/components/iaqualink/.translations/da.json
new file mode 100644
index 00000000000000..a1e1c20cbc5e5f
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/da.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Du kan kun konfigurere en enkelt iAqualink-forbindelse."
+ },
+ "error": {
+ "connection_failure": "Kan ikke oprette forbindelse til iAqualink. Kontroller dit brugernavn og din adgangskode."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Adgangskode",
+ "username": "Brugernavn / e-mail-adresse"
+ },
+ "description": "Indtast brugernavn og adgangskode til din iAqualink-konto.",
+ "title": "Opret forbindelse til iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/en.json b/homeassistant/components/iaqualink/.translations/en.json
new file mode 100644
index 00000000000000..4972c3d3ff7d06
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/en.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "You can only configure a single iAqualink connection."
+ },
+ "error": {
+ "connection_failure": "Unable to connect to iAqualink. Check your username and password."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Username / Email Address"
+ },
+ "description": "Please enter the username and password for your iAqualink account.",
+ "title": "Connect to iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/es.json b/homeassistant/components/iaqualink/.translations/es.json
new file mode 100644
index 00000000000000..698be68bd78e9c
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/es.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Solo puede configurar una \u00fanica conexi\u00f3n iAqualink."
+ },
+ "error": {
+ "connection_failure": "No se puede conectar a iAqualink. Verifica tu nombre de usuario y contrase\u00f1a."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "username": "Usuario / correo electr\u00f3nico"
+ },
+ "description": "Por favor, introduzca el nombre de usuario y la contrase\u00f1a de su cuenta de iAqualink.",
+ "title": "Con\u00e9ctese a iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/fr.json b/homeassistant/components/iaqualink/.translations/fr.json
new file mode 100644
index 00000000000000..97971b99e9f7ab
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/fr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Vous ne pouvez configurer qu'une seule connexion iAqualink."
+ },
+ "error": {
+ "connection_failure": "Impossible de se connecter \u00e0 iAqualink. V\u00e9rifiez votre nom d'utilisateur et votre mot de passe."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Nom d'utilisateur / adresse e-mail"
+ },
+ "description": "Veuillez saisir le nom d'utilisateur et le mot de passe de votre compte iAqualink.",
+ "title": "Se connecter \u00e0 iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/it.json b/homeassistant/components/iaqualink/.translations/it.json
new file mode 100644
index 00000000000000..73d840bdbd376c
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/it.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\u00c8 possibile configurare una sola connessione iAqualink."
+ },
+ "error": {
+ "connection_failure": "Impossibile connettersi a iAqualink. Controllare il nome utente e la password."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Nome Utente / Indirizzo E-mail"
+ },
+ "description": "Inserisci il nome utente e la password del tuo account iAqualink.",
+ "title": "Collegati a iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/ko.json b/homeassistant/components/iaqualink/.translations/ko.json
new file mode 100644
index 00000000000000..9b2519077e26ce
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/ko.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\ud558\ub098\uc758 iAqualink \uc5f0\uacb0\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "connection_failure": "iAqualink \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984 / \uc774\uba54\uc77c \uc8fc\uc18c"
+ },
+ "description": "iAqualink \uacc4\uc815\uc758 \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "iAqualink \uc5f0\uacb0"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/lb.json b/homeassistant/components/iaqualink/.translations/lb.json
new file mode 100644
index 00000000000000..4beb11214bc2c8
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/lb.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Dir k\u00ebnnt n\u00ebmmen eng eenzeg iAqualink Verbindung konfigur\u00e9ieren."
+ },
+ "error": {
+ "connection_failure": "Kann sech net mat iAqualink verbannen. Iwwerpr\u00e9ift \u00e4ren Benotzernumm an Passwuert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwuert",
+ "username": "Benotzernumm / E-Mail Adresse"
+ },
+ "description": "Gitt den Benotznumm an d'Passwuert fir \u00e4ren iAqualink Kont un.",
+ "title": "Mat iAqualink verbannen"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/nl.json b/homeassistant/components/iaqualink/.translations/nl.json
new file mode 100644
index 00000000000000..c0a515bb741e00
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/nl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "U kunt slechts \u00e9\u00e9n iAqualink-verbinding configureren."
+ },
+ "error": {
+ "connection_failure": "Kan geen verbinding maken met iAqualink. Controleer je gebruikersnaam en wachtwoord."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Wachtwoord",
+ "username": "Gebruikersnaam/E-mailadres"
+ },
+ "description": "Voer de gebruikersnaam en het wachtwoord voor uw iAqualink-account in.",
+ "title": "Verbinding maken met iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/no.json b/homeassistant/components/iaqualink/.translations/no.json
new file mode 100644
index 00000000000000..9d464a6d516c55
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/no.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Du kan bare konfigurere en enkel iAqualink-tilkobling."
+ },
+ "error": {
+ "connection_failure": "Kan ikke koble til iAqualink. Sjekk brukernavnet og passordet ditt."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passord",
+ "username": "Brukernavn / E-postadresse"
+ },
+ "description": "Vennligst skriv inn brukernavn og passord for iAqualink-kontoen din.",
+ "title": "Koble til iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/pl.json b/homeassistant/components/iaqualink/.translations/pl.json
new file mode 100644
index 00000000000000..211a65f5ccb4fb
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/pl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno po\u0142\u0105czenie iAqualink."
+ },
+ "error": {
+ "connection_failure": "Nie mo\u017cna po\u0142\u0105czy\u0107 z iAqualink. Sprawd\u017a nazw\u0119 u\u017cytkownika i has\u0142o."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika / adres e-mail"
+ },
+ "description": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o do konta iAqualink.",
+ "title": "Po\u0142\u0105cz z iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/ru.json b/homeassistant/components/iaqualink/.translations/ru.json
new file mode 100644
index 00000000000000..35444dd422b379
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/ru.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ },
+ "error": {
+ "connection_failure": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a iAqualink. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0412\u0430\u0448 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.",
+ "title": "Jandy iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/sl.json b/homeassistant/components/iaqualink/.translations/sl.json
new file mode 100644
index 00000000000000..e2a7f94b3d8a70
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/sl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Konfigurirate lahko samo eno povezavo iAqualink."
+ },
+ "error": {
+ "connection_failure": "Ne morete vzpostaviti povezave z iAqualink. Preverite va\u0161e uporabni\u0161ko ime in geslo."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Geslo",
+ "username": "Uporabni\u0161ko ime / e-po\u0161tni naslov"
+ },
+ "description": "Prosimo, vnesite uporabni\u0161ko ime in geslo za iAqualink ra\u010dun.",
+ "title": "Pove\u017eite se z iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/.translations/zh-Hant.json b/homeassistant/components/iaqualink/.translations/zh-Hant.json
new file mode 100644
index 00000000000000..146088b4eff9c0
--- /dev/null
+++ b/homeassistant/components/iaqualink/.translations/zh-Hant.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 iAqualink \u9023\u7dda\u3002"
+ },
+ "error": {
+ "connection_failure": "\u7121\u6cd5\u9023\u7dda\u81f3 iAqualink\uff0c\u8acb\u78ba\u8a8d\u60a8\u7684\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u5bc6\u78bc",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31 / \u96fb\u5b50\u90f5\u4ef6"
+ },
+ "description": "\u8acb\u8f38\u5165 iAqualink \u5e33\u865f\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002",
+ "title": "\u9023\u7dda\u81f3 iAqualink"
+ }
+ },
+ "title": "Jandy iAqualink"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py
new file mode 100644
index 00000000000000..dec91186be2869
--- /dev/null
+++ b/homeassistant/components/iaqualink/__init__.py
@@ -0,0 +1,212 @@
+"""Component to embed Aqualink devices."""
+import asyncio
+from functools import wraps
+import logging
+
+from aiohttp import CookieJar
+import voluptuous as vol
+
+from iaqualink import (
+ AqualinkBinarySensor,
+ AqualinkClient,
+ AqualinkDevice,
+ AqualinkLight,
+ AqualinkLoginException,
+ AqualinkSensor,
+ AqualinkThermostat,
+ AqualinkToggle,
+)
+
+from homeassistant import config_entries
+from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
+from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.core import callback
+from homeassistant.helpers.aiohttp_client import async_create_clientsession
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_connect,
+ async_dispatcher_send,
+)
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.event import async_track_time_interval
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+
+from .const import DOMAIN, UPDATE_INTERVAL
+
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_CONFIG = "config"
+PARALLEL_UPDATES = 0
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ }
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+async def async_setup(hass: HomeAssistantType, config: ConfigType) -> None:
+ """Set up the Aqualink component."""
+ conf = config.get(DOMAIN)
+
+ hass.data[DOMAIN] = {}
+
+ if conf is not None:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf
+ )
+ )
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None:
+ """Set up Aqualink from a config entry."""
+ username = entry.data[CONF_USERNAME]
+ password = entry.data[CONF_PASSWORD]
+
+ # These will contain the initialized devices
+ binary_sensors = hass.data[DOMAIN][BINARY_SENSOR_DOMAIN] = []
+ climates = hass.data[DOMAIN][CLIMATE_DOMAIN] = []
+ lights = hass.data[DOMAIN][LIGHT_DOMAIN] = []
+ sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = []
+ switches = hass.data[DOMAIN][SWITCH_DOMAIN] = []
+
+ session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True))
+ aqualink = AqualinkClient(username, password, session)
+ try:
+ await aqualink.login()
+ except AqualinkLoginException as login_exception:
+ _LOGGER.error("Exception raised while attempting to login: %s", login_exception)
+ return False
+
+ systems = await aqualink.get_systems()
+ systems = list(systems.values())
+ if not systems:
+ _LOGGER.error("No systems detected or supported")
+ return False
+
+ # Only supporting the first system for now.
+ devices = await systems[0].get_devices()
+
+ for dev in devices.values():
+ if isinstance(dev, AqualinkThermostat):
+ climates += [dev]
+ elif isinstance(dev, AqualinkLight):
+ lights += [dev]
+ elif isinstance(dev, AqualinkBinarySensor):
+ binary_sensors += [dev]
+ elif isinstance(dev, AqualinkSensor):
+ sensors += [dev]
+ elif isinstance(dev, AqualinkToggle):
+ switches += [dev]
+
+ forward_setup = hass.config_entries.async_forward_entry_setup
+ if binary_sensors:
+ _LOGGER.debug("Got %s binary sensors: %s", len(binary_sensors), binary_sensors)
+ hass.async_create_task(forward_setup(entry, BINARY_SENSOR_DOMAIN))
+ if climates:
+ _LOGGER.debug("Got %s climates: %s", len(climates), climates)
+ hass.async_create_task(forward_setup(entry, CLIMATE_DOMAIN))
+ if lights:
+ _LOGGER.debug("Got %s lights: %s", len(lights), lights)
+ hass.async_create_task(forward_setup(entry, LIGHT_DOMAIN))
+ if sensors:
+ _LOGGER.debug("Got %s sensors: %s", len(sensors), sensors)
+ hass.async_create_task(forward_setup(entry, SENSOR_DOMAIN))
+ if switches:
+ _LOGGER.debug("Got %s switches: %s", len(switches), switches)
+ hass.async_create_task(forward_setup(entry, SWITCH_DOMAIN))
+
+ async def _async_systems_update(now):
+ """Refresh internal state for all systems."""
+ await systems[0].update()
+ async_dispatcher_send(hass, DOMAIN)
+
+ async_track_time_interval(hass, _async_systems_update, UPDATE_INTERVAL)
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ forward_unload = hass.config_entries.async_forward_entry_unload
+
+ tasks = []
+
+ if hass.data[DOMAIN][BINARY_SENSOR_DOMAIN]:
+ tasks += [forward_unload(entry, BINARY_SENSOR_DOMAIN)]
+ if hass.data[DOMAIN][CLIMATE_DOMAIN]:
+ tasks += [forward_unload(entry, CLIMATE_DOMAIN)]
+ if hass.data[DOMAIN][LIGHT_DOMAIN]:
+ tasks += [forward_unload(entry, LIGHT_DOMAIN)]
+ if hass.data[DOMAIN][SENSOR_DOMAIN]:
+ tasks += [forward_unload(entry, SENSOR_DOMAIN)]
+ if hass.data[DOMAIN][SWITCH_DOMAIN]:
+ tasks += [forward_unload(entry, SWITCH_DOMAIN)]
+
+ hass.data[DOMAIN].clear()
+
+ return all(await asyncio.gather(*tasks))
+
+
+def refresh_system(func):
+ """Force update all entities after state change."""
+
+ @wraps(func)
+ async def wrapper(self, *args, **kwargs):
+ """Call decorated function and send update signal to all entities."""
+ await func(self, *args, **kwargs)
+ async_dispatcher_send(self.hass, DOMAIN)
+
+ return wrapper
+
+
+class AqualinkEntity(Entity):
+ """Abstract class for all Aqualink platforms.
+
+ Entity state is updated via the interval timer within the integration.
+ Any entity state change via the iaqualink library triggers an internal
+ state refresh which is then propagated to all the entities in the system
+ via the refresh_system decorator above to the _update_callback in this
+ class.
+ """
+
+ def __init__(self, dev: AqualinkDevice):
+ """Initialize the entity."""
+ self.dev = dev
+
+ async def async_added_to_hass(self) -> None:
+ """Set up a listener when this entity is added to HA."""
+ async_dispatcher_connect(self.hass, DOMAIN, self._update_callback)
+
+ @callback
+ def _update_callback(self) -> None:
+ self.async_schedule_update_ha_state()
+
+ @property
+ def should_poll(self) -> bool:
+ """Return False as entities shouldn't be polled.
+
+ Entities are checked periodically as the integration runs periodic
+ updates on a timer.
+ """
+ return False
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique identifier for this entity."""
+ return f"{self.dev.system.serial}_{self.dev.name}"
diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py
new file mode 100644
index 00000000000000..09c9322a58764b
--- /dev/null
+++ b/homeassistant/components/iaqualink/binary_sensor.py
@@ -0,0 +1,48 @@
+"""Support for Aqualink temperature sensors."""
+import logging
+
+from homeassistant.components.binary_sensor import (
+ BinarySensorDevice,
+ DEVICE_CLASS_COLD,
+ DOMAIN,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import AqualinkEntity
+from .const import DOMAIN as AQUALINK_DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+PARALLEL_UPDATES = 0
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up discovered binary sensors."""
+ devs = []
+ for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]:
+ devs.append(HassAqualinkBinarySensor(dev))
+ async_add_entities(devs, True)
+
+
+class HassAqualinkBinarySensor(AqualinkEntity, BinarySensorDevice):
+ """Representation of a binary sensor."""
+
+ @property
+ def name(self) -> str:
+ """Return the name of the binary sensor."""
+ return self.dev.label
+
+ @property
+ def is_on(self) -> bool:
+ """Return whether the binary sensor is on or not."""
+ return self.dev.is_on
+
+ @property
+ def device_class(self) -> str:
+ """Return the class of the binary sensor."""
+ if self.name == "Freeze Protection":
+ return DEVICE_CLASS_COLD
+ return None
diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py
new file mode 100644
index 00000000000000..f41d17837c2f6c
--- /dev/null
+++ b/homeassistant/components/iaqualink/climate.py
@@ -0,0 +1,132 @@
+"""Support for Aqualink Thermostats."""
+import logging
+from typing import List, Optional
+
+from iaqualink import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState
+from iaqualink.const import (
+ AQUALINK_TEMP_CELSIUS_HIGH,
+ AQUALINK_TEMP_CELSIUS_LOW,
+ AQUALINK_TEMP_FAHRENHEIT_HIGH,
+ AQUALINK_TEMP_FAHRENHEIT_LOW,
+)
+
+from homeassistant.components.climate import ClimateDevice
+from homeassistant.components.climate.const import (
+ DOMAIN,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_OFF,
+ SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import AqualinkEntity, refresh_system
+from .const import DOMAIN as AQUALINK_DOMAIN, CLIMATE_SUPPORTED_MODES
+
+_LOGGER = logging.getLogger(__name__)
+
+PARALLEL_UPDATES = 0
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up discovered switches."""
+ devs = []
+ for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]:
+ devs.append(HassAqualinkThermostat(dev))
+ async_add_entities(devs, True)
+
+
+class HassAqualinkThermostat(AqualinkEntity, ClimateDevice):
+ """Representation of a thermostat."""
+
+ @property
+ def name(self) -> str:
+ """Return the name of the thermostat."""
+ return self.dev.label.split(" ")[0]
+
+ @property
+ def supported_features(self) -> int:
+ """Return the list of supported features."""
+ return SUPPORT_TARGET_TEMPERATURE
+
+ @property
+ def hvac_modes(self) -> List[str]:
+ """Return the list of supported HVAC modes."""
+ return CLIMATE_SUPPORTED_MODES
+
+ @property
+ def pump(self) -> AqualinkPump:
+ """Return the pump device for the current thermostat."""
+ pump = f"{self.name.lower()}_pump"
+ return self.dev.system.devices[pump]
+
+ @property
+ def hvac_mode(self) -> str:
+ """Return the current HVAC mode."""
+ state = AqualinkState(self.heater.state)
+ if state == AqualinkState.ON:
+ return HVAC_MODE_HEAT
+ return HVAC_MODE_OFF
+
+ @refresh_system
+ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
+ """Turn the underlying heater switch on or off."""
+ if hvac_mode == HVAC_MODE_HEAT:
+ await self.heater.turn_on()
+ elif hvac_mode == HVAC_MODE_OFF:
+ await self.heater.turn_off()
+ else:
+ _LOGGER.warning("Unknown operation mode: %s", hvac_mode)
+
+ @property
+ def temperature_unit(self) -> str:
+ """Return the unit of measurement."""
+ if self.dev.system.temp_unit == "F":
+ return TEMP_FAHRENHEIT
+ return TEMP_CELSIUS
+
+ @property
+ def min_temp(self) -> int:
+ """Return the minimum temperature supported by the thermostat."""
+ if self.temperature_unit == TEMP_FAHRENHEIT:
+ return AQUALINK_TEMP_FAHRENHEIT_LOW
+ return AQUALINK_TEMP_CELSIUS_LOW
+
+ @property
+ def max_temp(self) -> int:
+ """Return the minimum temperature supported by the thermostat."""
+ if self.temperature_unit == TEMP_FAHRENHEIT:
+ return AQUALINK_TEMP_FAHRENHEIT_HIGH
+ return AQUALINK_TEMP_CELSIUS_HIGH
+
+ @property
+ def target_temperature(self) -> float:
+ """Return the current target temperature."""
+ return float(self.dev.state)
+
+ @refresh_system
+ async def async_set_temperature(self, **kwargs) -> None:
+ """Set new target temperature."""
+ await self.dev.set_temperature(int(kwargs[ATTR_TEMPERATURE]))
+
+ @property
+ def sensor(self) -> AqualinkSensor:
+ """Return the sensor device for the current thermostat."""
+ sensor = f"{self.name.lower()}_temp"
+ return self.dev.system.devices[sensor]
+
+ @property
+ def current_temperature(self) -> Optional[float]:
+ """Return the current temperature."""
+ if self.sensor.state != "":
+ return float(self.sensor.state)
+ return None
+
+ @property
+ def heater(self) -> AqualinkHeater:
+ """Return the heater device for the current thermostat."""
+ heater = f"{self.name.lower()}_heater"
+ return self.dev.system.devices[heater]
diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py
new file mode 100644
index 00000000000000..ec83477d253a04
--- /dev/null
+++ b/homeassistant/components/iaqualink/config_flow.py
@@ -0,0 +1,52 @@
+"""Config flow to configure zone component."""
+from typing import Optional
+
+import voluptuous as vol
+
+from iaqualink import AqualinkClient, AqualinkLoginException
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+from homeassistant.helpers import ConfigType
+
+from .const import DOMAIN
+
+
+@config_entries.HANDLERS.register(DOMAIN)
+class AqualinkFlowHandler(config_entries.ConfigFlow):
+ """Aqualink config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def async_step_user(self, user_input: Optional[ConfigType] = None):
+ """Handle a flow start."""
+ # Supporting a single account.
+ entries = self.hass.config_entries.async_entries(DOMAIN)
+ if entries:
+ return self.async_abort(reason="already_setup")
+
+ errors = {}
+
+ if user_input is not None:
+ username = user_input[CONF_USERNAME]
+ password = user_input[CONF_PASSWORD]
+
+ try:
+ aqualink = AqualinkClient(username, password)
+ await aqualink.login()
+ return self.async_create_entry(title=username, data=user_input)
+ except AqualinkLoginException:
+ errors["base"] = "connection_failure"
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
+ ),
+ errors=errors,
+ )
+
+ async def async_step_import(self, user_input: Optional[ConfigType] = None):
+ """Occurs when an entry is setup through config."""
+ return await self.async_step_user(user_input)
diff --git a/homeassistant/components/iaqualink/const.py b/homeassistant/components/iaqualink/const.py
new file mode 100644
index 00000000000000..219eb9129944f2
--- /dev/null
+++ b/homeassistant/components/iaqualink/const.py
@@ -0,0 +1,8 @@
+"""Constants for the the iaqualink component."""
+from datetime import timedelta
+
+from homeassistant.components.climate.const import HVAC_MODE_HEAT, HVAC_MODE_OFF
+
+DOMAIN = "iaqualink"
+CLIMATE_SUPPORTED_MODES = [HVAC_MODE_HEAT, HVAC_MODE_OFF]
+UPDATE_INTERVAL = timedelta(seconds=30)
diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py
new file mode 100644
index 00000000000000..813af7863f1700
--- /dev/null
+++ b/homeassistant/components/iaqualink/light.py
@@ -0,0 +1,101 @@
+"""Support for Aqualink pool lights."""
+import logging
+
+from iaqualink import AqualinkLightEffect
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ ATTR_EFFECT,
+ DOMAIN,
+ SUPPORT_BRIGHTNESS,
+ SUPPORT_EFFECT,
+ Light,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import AqualinkEntity, refresh_system
+from .const import DOMAIN as AQUALINK_DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+PARALLEL_UPDATES = 0
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up discovered lights."""
+ devs = []
+ for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]:
+ devs.append(HassAqualinkLight(dev))
+ async_add_entities(devs, True)
+
+
+class HassAqualinkLight(AqualinkEntity, Light):
+ """Representation of a light."""
+
+ @property
+ def name(self) -> str:
+ """Return the name of the light."""
+ return self.dev.label
+
+ @property
+ def is_on(self) -> bool:
+ """Return whether the light is on or off."""
+ return self.dev.is_on
+
+ @refresh_system
+ async def async_turn_on(self, **kwargs) -> None:
+ """Turn on the light.
+
+ This handles brightness and light effects for lights that do support
+ them.
+ """
+ brightness = kwargs.get(ATTR_BRIGHTNESS)
+ effect = kwargs.get(ATTR_EFFECT)
+
+ # For now I'm assuming lights support either effects or brightness.
+ if effect:
+ effect = AqualinkLightEffect[effect].value
+ await self.dev.set_effect(effect)
+ elif brightness:
+ # Aqualink supports percentages in 25% increments.
+ pct = int(round(brightness * 4.0 / 255)) * 25
+ await self.dev.set_brightness(pct)
+ else:
+ await self.dev.turn_on()
+
+ @refresh_system
+ async def async_turn_off(self, **kwargs) -> None:
+ """Turn off the light."""
+ await self.dev.turn_off()
+
+ @property
+ def brightness(self) -> int:
+ """Return current brightness of the light.
+
+ The scale needs converting between 0-100 and 0-255.
+ """
+ return self.dev.brightness * 255 / 100
+
+ @property
+ def effect(self) -> str:
+ """Return the current light effect if supported."""
+ return AqualinkLightEffect(self.dev.effect).name
+
+ @property
+ def effect_list(self) -> list:
+ """Return supported light effects."""
+ return list(AqualinkLightEffect.__members__)
+
+ @property
+ def supported_features(self) -> int:
+ """Return the list of features supported by the light."""
+ if self.dev.is_dimmer:
+ return SUPPORT_BRIGHTNESS
+
+ if self.dev.is_color:
+ return SUPPORT_EFFECT
+
+ return 0
diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json
new file mode 100644
index 00000000000000..25e02536897915
--- /dev/null
+++ b/homeassistant/components/iaqualink/manifest.json
@@ -0,0 +1,13 @@
+{
+ "domain": "iaqualink",
+ "name": "Jandy iAqualink",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/components/iaqualink/",
+ "dependencies": [],
+ "codeowners": [
+ "@flz"
+ ],
+ "requirements": [
+ "iaqualink==0.2.9"
+ ]
+}
diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py
new file mode 100644
index 00000000000000..81021d0b4471d4
--- /dev/null
+++ b/homeassistant/components/iaqualink/sensor.py
@@ -0,0 +1,53 @@
+"""Support for Aqualink temperature sensors."""
+import logging
+from typing import Optional
+
+from homeassistant.components.sensor import DOMAIN
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import AqualinkEntity
+from .const import DOMAIN as AQUALINK_DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+PARALLEL_UPDATES = 0
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up discovered sensors."""
+ devs = []
+ for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]:
+ devs.append(HassAqualinkSensor(dev))
+ async_add_entities(devs, True)
+
+
+class HassAqualinkSensor(AqualinkEntity):
+ """Representation of a sensor."""
+
+ @property
+ def name(self) -> str:
+ """Return the name of the sensor."""
+ return self.dev.label
+
+ @property
+ def unit_of_measurement(self) -> str:
+ """Return the measurement unit for the sensor."""
+ if self.dev.system.temp_unit == "F":
+ return TEMP_FAHRENHEIT
+ return TEMP_CELSIUS
+
+ @property
+ def state(self) -> str:
+ """Return the state of the sensor."""
+ return int(self.dev.state) if self.dev.state != "" else None
+
+ @property
+ def device_class(self) -> Optional[str]:
+ """Return the class of the sensor."""
+ if self.dev.name.endswith("_temp"):
+ return DEVICE_CLASS_TEMPERATURE
+ return None
diff --git a/homeassistant/components/iaqualink/strings.json b/homeassistant/components/iaqualink/strings.json
new file mode 100644
index 00000000000000..4c706522198c9f
--- /dev/null
+++ b/homeassistant/components/iaqualink/strings.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "title": "Jandy iAqualink",
+ "step": {
+ "user": {
+ "title": "Connect to iAqualink",
+ "description": "Please enter the username and password for your iAqualink account.",
+ "data": {
+ "username": "Username / Email Address",
+ "password": "Password"
+ }
+ }
+ },
+ "error": {
+ "connection_failure": "Unable to connect to iAqualink. Check your username and password."
+ },
+ "abort": {
+ "already_setup": "You can only configure a single iAqualink connection."
+ }
+ }
+}
diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py
new file mode 100644
index 00000000000000..8efb473cf54d35
--- /dev/null
+++ b/homeassistant/components/iaqualink/switch.py
@@ -0,0 +1,59 @@
+"""Support for Aqualink pool feature switches."""
+import logging
+
+from homeassistant.components.switch import DOMAIN, SwitchDevice
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import AqualinkEntity, refresh_system
+from .const import DOMAIN as AQUALINK_DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+PARALLEL_UPDATES = 0
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up discovered switches."""
+ devs = []
+ for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]:
+ devs.append(HassAqualinkSwitch(dev))
+ async_add_entities(devs, True)
+
+
+class HassAqualinkSwitch(AqualinkEntity, SwitchDevice):
+ """Representation of a switch."""
+
+ @property
+ def name(self) -> str:
+ """Return the name of the switch."""
+ return self.dev.label
+
+ @property
+ def icon(self) -> str:
+ """Return an icon based on the switch type."""
+ if self.name == "Cleaner":
+ return "mdi:robot-vacuum"
+ if self.name == "Waterfall" or self.name.endswith("Dscnt"):
+ return "mdi:fountain"
+ if self.name.endswith("Pump") or self.name.endswith("Blower"):
+ return "mdi:fan"
+ if self.name.endswith("Heater"):
+ return "mdi:radiator"
+
+ @property
+ def is_on(self) -> bool:
+ """Return whether the switch is on or not."""
+ return self.dev.is_on
+
+ @refresh_system
+ async def async_turn_on(self, **kwargs) -> None:
+ """Turn on the switch."""
+ await self.dev.turn_on()
+
+ @refresh_system
+ async def async_turn_off(self, **kwargs) -> None:
+ """Turn off the switch."""
+ await self.dev.turn_off()
diff --git a/homeassistant/components/ifttt/.translations/it.json b/homeassistant/components/ifttt/.translations/it.json
index e5dc76b7923cb5..d6faf60d618ed7 100644
--- a/homeassistant/components/ifttt/.translations/it.json
+++ b/homeassistant/components/ifttt/.translations/it.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "not_internet_accessible": "Home Assistant deve essere accessibile da internet per ricevere messaggi IFTTT",
+ "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi IFTTT.",
"one_instance_allowed": "\u00c8 necessaria una sola istanza."
},
"create_entry": {
diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json
index 20652ddd046375..feda5da732cab7 100644
--- a/homeassistant/components/influxdb/manifest.json
+++ b/homeassistant/components/influxdb/manifest.json
@@ -3,10 +3,10 @@
"name": "Influxdb",
"documentation": "https://www.home-assistant.io/components/influxdb",
"requirements": [
- "influxdb==5.2.0"
+ "influxdb==5.2.3"
],
"dependencies": [],
"codeowners": [
"@fabaff"
]
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py
index 83e4c089b9a541..ee74b369629b75 100644
--- a/homeassistant/components/ios/notify.py
+++ b/homeassistant/components/ios/notify.py
@@ -1,5 +1,4 @@
"""Support for iOS push notifications."""
-from datetime import datetime, timezone
import logging
import requests
@@ -25,7 +24,7 @@ def log_rate_limits(hass, target, resp, level=20):
"""Output rate limit log line at given level."""
rate_limits = resp["rateLimits"]
resetsAt = dt_util.parse_datetime(rate_limits["resetsAt"])
- resetsAtTime = resetsAt - datetime.now(timezone.utc)
+ resetsAtTime = resetsAt - dt_util.utcnow()
rate_limit_msg = (
"iOS push notification rate limits for %s: "
"%d sent, %d allowed, %d errors, "
diff --git a/homeassistant/components/iperf3/manifest.json b/homeassistant/components/iperf3/manifest.json
index e35be24fc8089f..0547628b4bfd2c 100644
--- a/homeassistant/components/iperf3/manifest.json
+++ b/homeassistant/components/iperf3/manifest.json
@@ -3,7 +3,7 @@
"name": "Iperf3",
"documentation": "https://www.home-assistant.io/components/iperf3",
"requirements": [
- "iperf3==0.1.10"
+ "iperf3==0.1.11"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/iqvia/.translations/it.json b/homeassistant/components/iqvia/.translations/it.json
index 37079cf571dbbf..492654c660c5b0 100644
--- a/homeassistant/components/iqvia/.translations/it.json
+++ b/homeassistant/components/iqvia/.translations/it.json
@@ -9,6 +9,7 @@
"data": {
"zip_code": "CAP"
},
+ "description": "Compila il tuo CAP americano o canadese.",
"title": "IQVIA"
}
},
diff --git a/homeassistant/components/iqvia/.translations/pl.json b/homeassistant/components/iqvia/.translations/pl.json
index 7a6e9a8a915634..b528cdeb70f3b0 100644
--- a/homeassistant/components/iqvia/.translations/pl.json
+++ b/homeassistant/components/iqvia/.translations/pl.json
@@ -9,7 +9,7 @@
"data": {
"zip_code": "Kod pocztowy"
},
- "description": "Wprowad\u017a sw\u00f3j ameryka\u0144ski lub kanadyjski kod pocztowy.",
+ "description": "Wprowad\u017a ameryka\u0144ski lub kanadyjski kod pocztowy.",
"title": "IQVIA"
}
},
diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py
index e1d24fa5551041..727ec91dc37cd3 100644
--- a/homeassistant/components/isy994/__init__.py
+++ b/homeassistant/components/isy994/__init__.py
@@ -459,7 +459,7 @@ class ISYDevice(Entity):
"""Representation of an ISY994 device."""
_attrs = {}
- _name = None # type: str
+ _name: str = None
def __init__(self, node) -> None:
"""Initialize the insteon device."""
diff --git a/homeassistant/components/izone/.translations/ca.json b/homeassistant/components/izone/.translations/ca.json
new file mode 100644
index 00000000000000..b80d9bee4e271a
--- /dev/null
+++ b/homeassistant/components/izone/.translations/ca.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "No s'han trobat dispositius iZone a la xarxa.",
+ "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de iZone."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vols configurar iZone?",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/.translations/en.json b/homeassistant/components/izone/.translations/en.json
new file mode 100644
index 00000000000000..5293ad2a1fec34
--- /dev/null
+++ b/homeassistant/components/izone/.translations/en.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "No iZone devices found on the network.",
+ "single_instance_allowed": "Only a single configuration of iZone is necessary."
+ },
+ "step": {
+ "confirm": {
+ "description": "Do you want to set up iZone?",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/.translations/fr.json b/homeassistant/components/izone/.translations/fr.json
new file mode 100644
index 00000000000000..c90416b0619746
--- /dev/null
+++ b/homeassistant/components/izone/.translations/fr.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Aucun p\u00e9riph\u00e9rique iZone trouv\u00e9 sur le r\u00e9seau.",
+ "single_instance_allowed": "Une seule configuration d'iZone est n\u00e9cessaire."
+ },
+ "step": {
+ "confirm": {
+ "description": "Voulez-vous configurer iZone?",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/.translations/it.json b/homeassistant/components/izone/.translations/it.json
new file mode 100644
index 00000000000000..5498624a061ed4
--- /dev/null
+++ b/homeassistant/components/izone/.translations/it.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Nessun dispositivo iZone trovato in rete.",
+ "single_instance_allowed": "\u00c8 necessaria una sola configurazione di iZone."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vuoi configurare iZone?",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/.translations/ko.json b/homeassistant/components/izone/.translations/ko.json
new file mode 100644
index 00000000000000..69b8ce8a31ea35
--- /dev/null
+++ b/homeassistant/components/izone/.translations/ko.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "iZone \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
+ "single_instance_allowed": "\ud558\ub098\uc758 iZone \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "confirm": {
+ "description": "iZone \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/.translations/lb.json b/homeassistant/components/izone/.translations/lb.json
new file mode 100644
index 00000000000000..c6e075683ad159
--- /dev/null
+++ b/homeassistant/components/izone/.translations/lb.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Keng iZone Apparater am Netzwierk fonnt.",
+ "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun iZone ass n\u00e9ideg."
+ },
+ "step": {
+ "confirm": {
+ "description": "Soll iZone konfigur\u00e9iert ginn?",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/.translations/no.json b/homeassistant/components/izone/.translations/no.json
new file mode 100644
index 00000000000000..fcd5c1df019a6b
--- /dev/null
+++ b/homeassistant/components/izone/.translations/no.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Finner ingen iZone-enheter p\u00e5 nettverket.",
+ "single_instance_allowed": "Bare en enkelt konfigurasjon av iZone er n\u00f8dvendig."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vil du konfigurere iZone?",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/.translations/pl.json b/homeassistant/components/izone/.translations/pl.json
new file mode 100644
index 00000000000000..4f90cf71cbcb04
--- /dev/null
+++ b/homeassistant/components/izone/.translations/pl.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 iZone.",
+ "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja iZone."
+ },
+ "step": {
+ "confirm": {
+ "description": "Chcesz skonfigurowa\u0107 iZone?",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/.translations/ru.json b/homeassistant/components/izone/.translations/ru.json
new file mode 100644
index 00000000000000..7e632c8dd62119
--- /dev/null
+++ b/homeassistant/components/izone/.translations/ru.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 iZone \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
+ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c iZone?",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/.translations/zh-Hant.json b/homeassistant/components/izone/.translations/zh-Hant.json
new file mode 100644
index 00000000000000..7448100158e8ee
--- /dev/null
+++ b/homeassistant/components/izone/.translations/zh-Hant.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 iZone \u8a2d\u5099\u3002",
+ "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 iZone \u5373\u53ef\u3002"
+ },
+ "step": {
+ "confirm": {
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a iZone\uff1f",
+ "title": "iZone"
+ }
+ },
+ "title": "iZone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py
new file mode 100644
index 00000000000000..7f80fb077cf92c
--- /dev/null
+++ b/homeassistant/components/izone/__init__.py
@@ -0,0 +1,67 @@
+"""
+Platform for the iZone AC.
+
+For more details about this component, please refer to the documentation
+https://home-assistant.io/components/izone/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_EXCLUDE
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+
+from .const import IZONE, DATA_CONFIG
+from .discovery import async_start_discovery_service, async_stop_discovery_service
+
+_LOGGER = logging.getLogger(__name__)
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ IZONE: vol.Schema(
+ {
+ vol.Optional(CONF_EXCLUDE, default=[]): vol.All(
+ cv.ensure_list, [cv.string]
+ )
+ }
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+async def async_setup(hass: HomeAssistantType, config: ConfigType):
+ """Register the iZone component config."""
+ conf = config.get(IZONE)
+ if not conf:
+ return True
+
+ hass.data[DATA_CONFIG] = conf
+
+ # Explicitly added in the config file, create a config entry.
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ IZONE, context={"source": config_entries.SOURCE_IMPORT}
+ )
+ )
+
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up from a config entry."""
+ await async_start_discovery_service(hass)
+
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, "climate")
+ )
+ return True
+
+
+async def async_unload_entry(hass, entry):
+ """Unload the config entry and stop discovery process."""
+ await async_stop_discovery_service(hass)
+ await hass.config_entries.async_forward_entry_unload(entry, "climate")
+ return True
diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py
new file mode 100644
index 00000000000000..c932c66627bcae
--- /dev/null
+++ b/homeassistant/components/izone/climate.py
@@ -0,0 +1,546 @@
+"""Support for the iZone HVAC."""
+import logging
+from typing import Optional, List
+
+from pizone import Zone, Controller
+
+from homeassistant.core import callback
+from homeassistant.components.climate import ClimateDevice
+from homeassistant.components.climate.const import (
+ HVAC_MODE_HEAT_COOL,
+ HVAC_MODE_COOL,
+ HVAC_MODE_DRY,
+ HVAC_MODE_FAN_ONLY,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_OFF,
+ FAN_LOW,
+ FAN_MEDIUM,
+ FAN_HIGH,
+ FAN_AUTO,
+ PRESET_ECO,
+ PRESET_NONE,
+ SUPPORT_FAN_MODE,
+ SUPPORT_PRESET_MODE,
+ SUPPORT_TARGET_TEMPERATURE,
+)
+from homeassistant.const import (
+ ATTR_TEMPERATURE,
+ PRECISION_HALVES,
+ TEMP_CELSIUS,
+ CONF_EXCLUDE,
+)
+from homeassistant.helpers.temperature import display_temp as show_temp
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from .const import (
+ DATA_DISCOVERY_SERVICE,
+ IZONE,
+ DISPATCH_CONTROLLER_DISCOVERED,
+ DISPATCH_CONTROLLER_DISCONNECTED,
+ DISPATCH_CONTROLLER_RECONNECTED,
+ DISPATCH_CONTROLLER_UPDATE,
+ DISPATCH_ZONE_UPDATE,
+ DATA_CONFIG,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+_IZONE_FAN_TO_HA = {
+ Controller.Fan.LOW: FAN_LOW,
+ Controller.Fan.MED: FAN_MEDIUM,
+ Controller.Fan.HIGH: FAN_HIGH,
+ Controller.Fan.AUTO: FAN_AUTO,
+}
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, config: ConfigType, async_add_entities
+):
+ """Initialize an IZone Controller."""
+ disco = hass.data[DATA_DISCOVERY_SERVICE]
+
+ @callback
+ def init_controller(ctrl: Controller):
+ """Register the controller device and the containing zones."""
+ conf = hass.data.get(DATA_CONFIG) # type: ConfigType
+
+ # Filter out any entities excluded in the config file
+ if conf and ctrl.device_uid in conf[CONF_EXCLUDE]:
+ _LOGGER.info("Controller UID=%s ignored as excluded", ctrl.device_uid)
+ return
+ _LOGGER.info("Controller UID=%s discovered", ctrl.device_uid)
+
+ device = ControllerDevice(ctrl)
+ async_add_entities([device])
+ async_add_entities(device.zones.values())
+
+ # create any components not yet created
+ for controller in disco.pi_disco.controllers.values():
+ init_controller(controller)
+
+ # connect to register any further components
+ async_dispatcher_connect(hass, DISPATCH_CONTROLLER_DISCOVERED, init_controller)
+
+ return True
+
+
+class ControllerDevice(ClimateDevice):
+ """Representation of iZone Controller."""
+
+ def __init__(self, controller: Controller) -> None:
+ """Initialise ControllerDevice."""
+ self._controller = controller
+
+ self._supported_features = SUPPORT_FAN_MODE
+
+ if (
+ controller.ras_mode == "master" and controller.zone_ctrl == 13
+ ) or controller.ras_mode == "RAS":
+ self._supported_features |= SUPPORT_TARGET_TEMPERATURE
+
+ self._state_to_pizone = {
+ HVAC_MODE_COOL: Controller.Mode.COOL,
+ HVAC_MODE_HEAT: Controller.Mode.HEAT,
+ HVAC_MODE_HEAT_COOL: Controller.Mode.AUTO,
+ HVAC_MODE_FAN_ONLY: Controller.Mode.VENT,
+ HVAC_MODE_DRY: Controller.Mode.DRY,
+ }
+ if controller.free_air_enabled:
+ self._supported_features |= SUPPORT_PRESET_MODE
+
+ self._fan_to_pizone = {}
+ for fan in controller.fan_modes:
+ self._fan_to_pizone[_IZONE_FAN_TO_HA[fan]] = fan
+ self._available = True
+
+ self._device_info = {
+ "identifiers": {(IZONE, self.unique_id)},
+ "name": self.name,
+ "manufacturer": "IZone",
+ "model": self._controller.sys_type,
+ }
+
+ # Create the zones
+ self.zones = {}
+ for zone in controller.zones:
+ self.zones[zone] = ZoneDevice(self, zone)
+
+ async def async_added_to_hass(self):
+ """Call on adding to hass."""
+ # Register for connect/disconnect/update events
+ @callback
+ def controller_disconnected(ctrl: Controller, ex: Exception) -> None:
+ """Disconnected from controller."""
+ if ctrl is not self._controller:
+ return
+ self.set_available(False, ex)
+
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass, DISPATCH_CONTROLLER_DISCONNECTED, controller_disconnected
+ )
+ )
+
+ @callback
+ def controller_reconnected(ctrl: Controller) -> None:
+ """Reconnected to controller."""
+ if ctrl is not self._controller:
+ return
+ self.set_available(True)
+
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass, DISPATCH_CONTROLLER_RECONNECTED, controller_reconnected
+ )
+ )
+
+ @callback
+ def controller_update(ctrl: Controller) -> None:
+ """Handle controller data updates."""
+ if ctrl is not self._controller:
+ return
+ self.async_schedule_update_ha_state()
+
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass, DISPATCH_CONTROLLER_UPDATE, controller_update
+ )
+ )
+
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return self._available
+
+ @callback
+ def set_available(self, available: bool, ex: Exception = None) -> None:
+ """
+ Set availability for the controller.
+
+ Also sets zone availability as they follow the same availability.
+ """
+ if self.available == available:
+ return
+
+ if available:
+ _LOGGER.info("Reconnected controller %s ", self._controller.device_uid)
+ else:
+ _LOGGER.info(
+ "Controller %s disconnected due to exception: %s",
+ self._controller.device_uid,
+ ex,
+ )
+
+ self._available = available
+ self.async_schedule_update_ha_state()
+ for zone in self.zones.values():
+ zone.async_schedule_update_ha_state()
+
+ @property
+ def device_info(self):
+ """Return the device info for the iZone system."""
+ return self._device_info
+
+ @property
+ def unique_id(self):
+ """Return the ID of the controller device."""
+ return self._controller.device_uid
+
+ @property
+ def name(self) -> str:
+ """Return the name of the entity."""
+ return f"iZone Controller {self._controller.device_uid}"
+
+ @property
+ def should_poll(self) -> bool:
+ """Return True if entity has to be polled for state.
+
+ False if entity pushes its state to HA.
+ """
+ return False
+
+ @property
+ def supported_features(self) -> int:
+ """Return the list of supported features."""
+ return self._supported_features
+
+ @property
+ def temperature_unit(self) -> str:
+ """Return the unit of measurement which this thermostat uses."""
+ return TEMP_CELSIUS
+
+ @property
+ def precision(self) -> float:
+ """Return the precision of the system."""
+ return PRECISION_HALVES
+
+ @property
+ def device_state_attributes(self):
+ """Return the optional state attributes."""
+ return {
+ "supply_temperature": show_temp(
+ self.hass,
+ self.supply_temperature,
+ self.temperature_unit,
+ self.precision,
+ ),
+ "temp_setpoint": show_temp(
+ self.hass,
+ self._controller.temp_setpoint,
+ self.temperature_unit,
+ self.precision,
+ ),
+ }
+
+ @property
+ def hvac_mode(self) -> str:
+ """Return current operation ie. heat, cool, idle."""
+ if not self._controller.is_on:
+ return HVAC_MODE_OFF
+ mode = self._controller.mode
+ for (key, value) in self._state_to_pizone.items():
+ if value == mode:
+ return key
+ assert False, "Should be unreachable"
+
+ @property
+ def hvac_modes(self) -> List[str]:
+ """Return the list of available operation modes."""
+ if self._controller.free_air:
+ return [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY]
+ return [HVAC_MODE_OFF, *self._state_to_pizone]
+
+ @property
+ def preset_mode(self):
+ """Eco mode is external air."""
+ return PRESET_ECO if self._controller.free_air else PRESET_NONE
+
+ @property
+ def preset_modes(self):
+ """Available preset modes, normal or eco."""
+ if self._controller.free_air_enabled:
+ return [PRESET_NONE, PRESET_ECO]
+ return [PRESET_NONE]
+
+ @property
+ def current_temperature(self) -> Optional[float]:
+ """Return the current temperature."""
+ if self._controller.mode == Controller.Mode.FREE_AIR:
+ return self._controller.temp_supply
+ return self._controller.temp_return
+
+ @property
+ def target_temperature(self) -> Optional[float]:
+ """Return the temperature we try to reach."""
+ if not self._supported_features & SUPPORT_TARGET_TEMPERATURE:
+ return None
+ return self._controller.temp_setpoint
+
+ @property
+ def supply_temperature(self) -> float:
+ """Return the current supply, or in duct, temperature."""
+ return self._controller.temp_supply
+
+ @property
+ def target_temperature_step(self) -> Optional[float]:
+ """Return the supported step of target temperature."""
+ return 0.5
+
+ @property
+ def fan_mode(self) -> Optional[str]:
+ """Return the fan setting."""
+ return _IZONE_FAN_TO_HA[self._controller.fan]
+
+ @property
+ def fan_modes(self) -> Optional[List[str]]:
+ """Return the list of available fan modes."""
+ return list(self._fan_to_pizone)
+
+ @property
+ def min_temp(self) -> float:
+ """Return the minimum temperature."""
+ return self._controller.temp_min
+
+ @property
+ def max_temp(self) -> float:
+ """Return the maximum temperature."""
+ return self._controller.temp_max
+
+ async def wrap_and_catch(self, coro):
+ """Catch any connection errors and set unavailable."""
+ try:
+ await coro
+ except ConnectionError as ex:
+ self.set_available(False, ex)
+ else:
+ self.set_available(True)
+
+ async def async_set_temperature(self, **kwargs) -> None:
+ """Set new target temperature."""
+ if not self.supported_features & SUPPORT_TARGET_TEMPERATURE:
+ self.async_schedule_update_ha_state(True)
+ return
+ temp = kwargs.get(ATTR_TEMPERATURE)
+ if temp is not None:
+ await self.wrap_and_catch(self._controller.set_temp_setpoint(temp))
+
+ async def async_set_fan_mode(self, fan_mode: str) -> None:
+ """Set new target fan mode."""
+ fan = self._fan_to_pizone[fan_mode]
+ await self.wrap_and_catch(self._controller.set_fan(fan))
+
+ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
+ """Set new target operation mode."""
+ if hvac_mode == HVAC_MODE_OFF:
+ await self.wrap_and_catch(self._controller.set_on(False))
+ return
+ if not self._controller.is_on:
+ await self.wrap_and_catch(self._controller.set_on(True))
+ if self._controller.free_air:
+ return
+ mode = self._state_to_pizone[hvac_mode]
+ await self.wrap_and_catch(self._controller.set_mode(mode))
+
+ async def async_set_preset_mode(self, preset_mode: str) -> None:
+ """Set the preset mode."""
+ await self.wrap_and_catch(
+ self._controller.set_free_air(preset_mode == PRESET_ECO)
+ )
+
+ async def async_turn_on(self) -> None:
+ """Turn the entity on."""
+ await self.wrap_and_catch(self._controller.set_on(True))
+
+
+class ZoneDevice(ClimateDevice):
+ """Representation of iZone Zone."""
+
+ def __init__(self, controller: ControllerDevice, zone: Zone) -> None:
+ """Initialise ZoneDevice."""
+ self._controller = controller
+ self._zone = zone
+ self._name = zone.name.title()
+
+ self._supported_features = 0
+ if zone.type != Zone.Type.AUTO:
+ self._state_to_pizone = {
+ HVAC_MODE_OFF: Zone.Mode.CLOSE,
+ HVAC_MODE_FAN_ONLY: Zone.Mode.OPEN,
+ }
+ else:
+ self._state_to_pizone = {
+ HVAC_MODE_OFF: Zone.Mode.CLOSE,
+ HVAC_MODE_FAN_ONLY: Zone.Mode.OPEN,
+ HVAC_MODE_HEAT_COOL: Zone.Mode.AUTO,
+ }
+ self._supported_features |= SUPPORT_TARGET_TEMPERATURE
+
+ self._device_info = {
+ "identifiers": {(IZONE, controller.unique_id, zone.index)},
+ "name": self.name,
+ "manufacturer": "IZone",
+ "via_device": (IZONE, controller.unique_id),
+ "model": zone.type.name.title(),
+ }
+
+ async def async_added_to_hass(self):
+ """Call on adding to hass."""
+
+ @callback
+ def zone_update(ctrl: Controller, zone: Zone) -> None:
+ """Handle zone data updates."""
+ if zone is not self._zone:
+ return
+ self._name = zone.name.title()
+ self.async_schedule_update_ha_state()
+
+ self.async_on_remove(
+ async_dispatcher_connect(self.hass, DISPATCH_ZONE_UPDATE, zone_update)
+ )
+
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return self._controller.available
+
+ @property
+ def assumed_state(self) -> bool:
+ """Return True if unable to access real state of the entity."""
+ return self._controller.assumed_state
+
+ @property
+ def device_info(self):
+ """Return the device info for the iZone system."""
+ return self._device_info
+
+ @property
+ def unique_id(self):
+ """Return the ID of the controller device."""
+ return "{}_z{}".format(self._controller.unique_id, self._zone.index + 1)
+
+ @property
+ def name(self) -> str:
+ """Return the name of the entity."""
+ return self._name
+
+ @property
+ def should_poll(self) -> bool:
+ """Return True if entity has to be polled for state.
+
+ False if entity pushes its state to HA.
+ """
+ return False
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ try:
+ if self._zone.mode == Zone.Mode.AUTO:
+ return self._supported_features
+ return self._supported_features & ~SUPPORT_TARGET_TEMPERATURE
+ except ConnectionError:
+ return None
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement which this thermostat uses."""
+ return TEMP_CELSIUS
+
+ @property
+ def precision(self):
+ """Return the precision of the system."""
+ return PRECISION_HALVES
+
+ @property
+ def hvac_mode(self):
+ """Return current operation ie. heat, cool, idle."""
+ mode = self._zone.mode
+ for (key, value) in self._state_to_pizone.items():
+ if value == mode:
+ return key
+ return None
+
+ @property
+ def hvac_modes(self):
+ """Return the list of available operation modes."""
+ return list(self._state_to_pizone.keys())
+
+ @property
+ def current_temperature(self):
+ """Return the current temperature."""
+ return self._zone.temp_current
+
+ @property
+ def target_temperature(self):
+ """Return the temperature we try to reach."""
+ if self._zone.type != Zone.Type.AUTO:
+ return None
+ return self._zone.temp_setpoint
+
+ @property
+ def target_temperature_step(self):
+ """Return the supported step of target temperature."""
+ return 0.5
+
+ @property
+ def min_temp(self):
+ """Return the minimum temperature."""
+ return self._controller.min_temp
+
+ @property
+ def max_temp(self):
+ """Return the maximum temperature."""
+ return self._controller.max_temp
+
+ async def async_set_temperature(self, **kwargs):
+ """Set new target temperature."""
+ if self._zone.mode != Zone.Mode.AUTO:
+ return
+ temp = kwargs.get(ATTR_TEMPERATURE)
+ if temp is not None:
+ await self._controller.wrap_and_catch(self._zone.set_temp_setpoint(temp))
+
+ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
+ """Set new target operation mode."""
+ mode = self._state_to_pizone[hvac_mode]
+ await self._controller.wrap_and_catch(self._zone.set_mode(mode))
+ self.async_schedule_update_ha_state()
+
+ @property
+ def is_on(self):
+ """Return true if on."""
+ return self._zone.mode != Zone.Mode.CLOSE
+
+ async def async_turn_on(self):
+ """Turn device on (open zone)."""
+ if self._zone.type == Zone.Type.AUTO:
+ await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.AUTO))
+ else:
+ await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.OPEN))
+ self.async_schedule_update_ha_state()
+
+ async def async_turn_off(self):
+ """Turn device off (close zone)."""
+ await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.CLOSE))
+ self.async_schedule_update_ha_state()
diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py
new file mode 100644
index 00000000000000..eb57a36a2bb5f7
--- /dev/null
+++ b/homeassistant/components/izone/config_flow.py
@@ -0,0 +1,45 @@
+"""Config flow for izone."""
+
+import logging
+import asyncio
+
+from async_timeout import timeout
+
+from homeassistant import config_entries
+from homeassistant.helpers import config_entry_flow
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from .const import IZONE, TIMEOUT_DISCOVERY, DISPATCH_CONTROLLER_DISCOVERED
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def _async_has_devices(hass):
+ from .discovery import async_start_discovery_service, async_stop_discovery_service
+
+ controller_ready = asyncio.Event()
+ async_dispatcher_connect(
+ hass, DISPATCH_CONTROLLER_DISCOVERED, lambda x: controller_ready.set()
+ )
+
+ disco = await async_start_discovery_service(hass)
+
+ try:
+ async with timeout(TIMEOUT_DISCOVERY):
+ await controller_ready.wait()
+ except asyncio.TimeoutError:
+ pass
+
+ if not disco.pi_disco.controllers:
+ await async_stop_discovery_service(hass)
+ _LOGGER.debug("No controllers found")
+ return False
+
+ _LOGGER.debug("Controllers %s", disco.pi_disco.controllers)
+ return True
+
+
+config_entry_flow.register_discovery_flow(
+ IZONE, "iZone Aircon", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL
+)
diff --git a/homeassistant/components/izone/const.py b/homeassistant/components/izone/const.py
new file mode 100644
index 00000000000000..4da7bc9e4afdb5
--- /dev/null
+++ b/homeassistant/components/izone/const.py
@@ -0,0 +1,14 @@
+"""Constants used by the izone component."""
+
+IZONE = "izone"
+
+DATA_DISCOVERY_SERVICE = "izone_discovery"
+DATA_CONFIG = "izone_config"
+
+DISPATCH_CONTROLLER_DISCOVERED = "izone_controller_discovered"
+DISPATCH_CONTROLLER_DISCONNECTED = "izone_controller_disconnected"
+DISPATCH_CONTROLLER_RECONNECTED = "izone_controller_disconnected"
+DISPATCH_CONTROLLER_UPDATE = "izone_controller_update"
+DISPATCH_ZONE_UPDATE = "izone_zone_update"
+
+TIMEOUT_DISCOVERY = 20
diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py
new file mode 100644
index 00000000000000..3630c28605bb7f
--- /dev/null
+++ b/homeassistant/components/izone/discovery.py
@@ -0,0 +1,87 @@
+"""Internal discovery service for iZone AC."""
+
+import logging
+import pizone
+
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP
+from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+
+from .const import (
+ DATA_DISCOVERY_SERVICE,
+ DISPATCH_CONTROLLER_DISCOVERED,
+ DISPATCH_CONTROLLER_DISCONNECTED,
+ DISPATCH_CONTROLLER_RECONNECTED,
+ DISPATCH_CONTROLLER_UPDATE,
+ DISPATCH_ZONE_UPDATE,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class DiscoveryService(pizone.Listener):
+ """Discovery data and interfacing with pizone library."""
+
+ def __init__(self, hass):
+ """Initialise discovery service."""
+ super().__init__()
+ self.hass = hass
+ self.pi_disco = None
+
+ # Listener interface
+ def controller_discovered(self, ctrl: pizone.Controller) -> None:
+ """Handle new controller discoverery."""
+ async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCOVERED, ctrl)
+
+ def controller_disconnected(self, ctrl: pizone.Controller, ex: Exception) -> None:
+ """On disconnect from controller."""
+ async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCONNECTED, ctrl, ex)
+
+ def controller_reconnected(self, ctrl: pizone.Controller) -> None:
+ """On reconnect to controller."""
+ async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_RECONNECTED, ctrl)
+
+ def controller_update(self, ctrl: pizone.Controller) -> None:
+ """System update message is recieved from the controller."""
+ async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_UPDATE, ctrl)
+
+ def zone_update(self, ctrl: pizone.Controller, zone: pizone.Zone) -> None:
+ """Zone update message is recieved from the controller."""
+ async_dispatcher_send(self.hass, DISPATCH_ZONE_UPDATE, ctrl, zone)
+
+
+async def async_start_discovery_service(hass: HomeAssistantType):
+ """Set up the pizone internal discovery."""
+ disco = hass.data.get(DATA_DISCOVERY_SERVICE)
+ if disco:
+ # Already started
+ return disco
+
+ # discovery local services
+ disco = DiscoveryService(hass)
+ hass.data[DATA_DISCOVERY_SERVICE] = disco
+
+ # Start the pizone discovery service, disco is the listener
+ session = aiohttp_client.async_get_clientsession(hass)
+ loop = hass.loop
+
+ disco.pi_disco = pizone.discovery(disco, loop=loop, session=session)
+ await disco.pi_disco.start_discovery()
+
+ async def shutdown_event(event):
+ await async_stop_discovery_service(hass)
+
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_event)
+
+ return disco
+
+
+async def async_stop_discovery_service(hass: HomeAssistantType):
+ """Stop the discovery service."""
+ disco = hass.data.get(DATA_DISCOVERY_SERVICE)
+ if not disco:
+ return
+
+ await disco.pi_disco.close()
+ del hass.data[DATA_DISCOVERY_SERVICE]
diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json
new file mode 100644
index 00000000000000..2f6747ab4cc51d
--- /dev/null
+++ b/homeassistant/components/izone/manifest.json
@@ -0,0 +1,9 @@
+{
+ "domain": "izone",
+ "name": "izone",
+ "documentation": "https://www.home-assistant.io/components/izone",
+ "requirements": [ "python-izone==1.1.1" ],
+ "dependencies": [],
+ "codeowners": [ "@Swamp-Ig" ],
+ "config_flow": true
+}
diff --git a/homeassistant/components/izone/strings.json b/homeassistant/components/izone/strings.json
new file mode 100644
index 00000000000000..7cb14b03c6c593
--- /dev/null
+++ b/homeassistant/components/izone/strings.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "title": "iZone",
+ "step": {
+ "confirm": {
+ "title": "iZone",
+ "description": "Do you want to set up iZone?"
+ }
+ },
+ "abort": {
+ "single_instance_allowed": "Only a single configuration of iZone is necessary.",
+ "no_devices_found": "No iZone devices found on the network."
+ }
+ }
+}
diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py
index 93a60e363e1aa2..c7bbbdb2d907a9 100644
--- a/homeassistant/components/jewish_calendar/__init__.py
+++ b/homeassistant/components/jewish_calendar/__init__.py
@@ -1 +1,109 @@
"""The jewish_calendar component."""
+import logging
+
+import voluptuous as vol
+import hdate
+
+from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+from homeassistant.helpers.discovery import async_load_platform
+import homeassistant.helpers.config_validation as cv
+
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = "jewish_calendar"
+
+SENSOR_TYPES = {
+ "binary": {
+ "issur_melacha_in_effect": ["Issur Melacha in Effect", "mdi:power-plug-off"]
+ },
+ "data": {
+ "date": ["Date", "mdi:judaism"],
+ "weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"],
+ "holiday_name": ["Holiday name", "mdi:calendar-star"],
+ "holiday_type": ["Holiday type", "mdi:counter"],
+ "omer_count": ["Day of the Omer", "mdi:counter"],
+ },
+ "time": {
+ "first_light": ["Alot Hashachar", "mdi:weather-sunset-up"],
+ "gra_end_shma": ['Latest time for Shm"a GR"A', "mdi:calendar-clock"],
+ "mga_end_shma": ['Latest time for Shm"a MG"A', "mdi:calendar-clock"],
+ "plag_mincha": ["Plag Hamincha", "mdi:weather-sunset-down"],
+ "first_stars": ["T'set Hakochavim", "mdi:weather-night"],
+ "upcoming_shabbat_candle_lighting": [
+ "Upcoming Shabbat Candle Lighting",
+ "mdi:candle",
+ ],
+ "upcoming_shabbat_havdalah": ["Upcoming Shabbat Havdalah", "mdi:weather-night"],
+ "upcoming_candle_lighting": ["Upcoming Candle Lighting", "mdi:candle"],
+ "upcoming_havdalah": ["Upcoming Havdalah", "mdi:weather-night"],
+ },
+}
+
+CONF_DIASPORA = "diaspora"
+CONF_LANGUAGE = "language"
+CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset"
+CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset"
+
+CANDLE_LIGHT_DEFAULT = 18
+
+DEFAULT_NAME = "Jewish Calendar"
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_DIASPORA, default=False): cv.boolean,
+ vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude,
+ vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude,
+ vol.Optional(CONF_LANGUAGE, default="english"): vol.In(
+ ["hebrew", "english"]
+ ),
+ vol.Optional(
+ CONF_CANDLE_LIGHT_MINUTES, default=CANDLE_LIGHT_DEFAULT
+ ): int,
+ # Default of 0 means use 8.5 degrees / 'three_stars' time.
+ vol.Optional(CONF_HAVDALAH_OFFSET_MINUTES, default=0): int,
+ }
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+async def async_setup(hass, config):
+ """Set up the Jewish Calendar component."""
+ name = config[DOMAIN][CONF_NAME]
+ language = config[DOMAIN][CONF_LANGUAGE]
+
+ latitude = config[DOMAIN].get(CONF_LATITUDE, hass.config.latitude)
+ longitude = config[DOMAIN].get(CONF_LONGITUDE, hass.config.longitude)
+ diaspora = config[DOMAIN][CONF_DIASPORA]
+
+ candle_lighting_offset = config[DOMAIN][CONF_CANDLE_LIGHT_MINUTES]
+ havdalah_offset = config[DOMAIN][CONF_HAVDALAH_OFFSET_MINUTES]
+
+ location = hdate.Location(
+ latitude=latitude,
+ longitude=longitude,
+ timezone=hass.config.time_zone,
+ diaspora=diaspora,
+ )
+
+ hass.data[DOMAIN] = {
+ "location": location,
+ "name": name,
+ "language": language,
+ "candle_lighting_offset": candle_lighting_offset,
+ "havdalah_offset": havdalah_offset,
+ "diaspora": diaspora,
+ }
+
+ hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
+
+ hass.async_create_task(
+ async_load_platform(hass, "binary_sensor", DOMAIN, {}, config)
+ )
+
+ return True
diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py
new file mode 100644
index 00000000000000..7362fce3cd0301
--- /dev/null
+++ b/homeassistant/components/jewish_calendar/binary_sensor.py
@@ -0,0 +1,66 @@
+"""Support for Jewish Calendar binary sensors."""
+import logging
+
+import hdate
+
+from homeassistant.components.binary_sensor import BinarySensorDevice
+import homeassistant.util.dt as dt_util
+
+from . import DOMAIN, SENSOR_TYPES
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Set up the Jewish Calendar binary sensor devices."""
+ if discovery_info is None:
+ return
+
+ async_add_entities(
+ [
+ JewishCalendarBinarySensor(hass.data[DOMAIN], sensor, sensor_info)
+ for sensor, sensor_info in SENSOR_TYPES["binary"].items()
+ ]
+ )
+
+
+class JewishCalendarBinarySensor(BinarySensorDevice):
+ """Representation of an Jewish Calendar binary sensor."""
+
+ def __init__(self, data, sensor, sensor_info):
+ """Initialize the binary sensor."""
+ self._location = data["location"]
+ self._type = sensor
+ self._name = f"{data['name']} {sensor_info[0]}"
+ self._icon = sensor_info[1]
+ self._hebrew = data["language"] == "hebrew"
+ self._candle_lighting_offset = data["candle_lighting_offset"]
+ self._havdalah_offset = data["havdalah_offset"]
+ self._state = False
+
+ @property
+ def icon(self):
+ """Return the icon of the entity."""
+ return self._icon
+
+ @property
+ def name(self):
+ """Return the name of the entity."""
+ return self._name
+
+ @property
+ def is_on(self):
+ """Return true if sensor is on."""
+ return self._state
+
+ async def async_update(self):
+ """Update the state of the sensor."""
+ zmanim = hdate.Zmanim(
+ date=dt_util.now(),
+ location=self._location,
+ candle_lighting_offset=self._candle_lighting_offset,
+ havdalah_offset=self._havdalah_offset,
+ hebrew=self._hebrew,
+ )
+
+ self._state = zmanim.issur_melacha_in_effect
diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py
index d298aee91436b4..405838b1fb10f9 100644
--- a/homeassistant/components/jewish_calendar/sensor.py
+++ b/homeassistant/components/jewish_calendar/sensor.py
@@ -1,140 +1,59 @@
"""Platform to retrieve Jewish calendar information for Home Assistant."""
import logging
-import voluptuous as vol
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
- CONF_LATITUDE,
- CONF_LONGITUDE,
- CONF_NAME,
- SUN_EVENT_SUNSET,
-)
-import homeassistant.helpers.config_validation as cv
+import hdate
+
+from homeassistant.const import SUN_EVENT_SUNSET
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.sun import get_astral_event_date
import homeassistant.util.dt as dt_util
-_LOGGER = logging.getLogger(__name__)
+from . import DOMAIN, SENSOR_TYPES
-SENSOR_TYPES = {
- "date": ["Date", "mdi:judaism"],
- "weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"],
- "holiday_name": ["Holiday", "mdi:calendar-star"],
- "holyness": ["Holyness", "mdi:counter"],
- "first_light": ["Alot Hashachar", "mdi:weather-sunset-up"],
- "gra_end_shma": ['Latest time for Shm"a GR"A', "mdi:calendar-clock"],
- "mga_end_shma": ['Latest time for Shm"a MG"A', "mdi:calendar-clock"],
- "plag_mincha": ["Plag Hamincha", "mdi:weather-sunset-down"],
- "first_stars": ["T'set Hakochavim", "mdi:weather-night"],
- "upcoming_shabbat_candle_lighting": [
- "Upcoming Shabbat Candle Lighting",
- "mdi:candle",
- ],
- "upcoming_shabbat_havdalah": ["Upcoming Shabbat Havdalah", "mdi:weather-night"],
- "upcoming_candle_lighting": ["Upcoming Candle Lighting", "mdi:candle"],
- "upcoming_havdalah": ["Upcoming Havdalah", "mdi:weather-night"],
- "issur_melacha_in_effect": ["Issur Melacha in Effect", "mdi:power-plug-off"],
- "omer_count": ["Day of the Omer", "mdi:counter"],
-}
-
-CONF_DIASPORA = "diaspora"
-CONF_LANGUAGE = "language"
-CONF_SENSORS = "sensors"
-CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset"
-CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset"
-
-CANDLE_LIGHT_DEFAULT = 18
-
-DEFAULT_NAME = "Jewish Calendar"
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_DIASPORA, default=False): cv.boolean,
- vol.Optional(CONF_LATITUDE): cv.latitude,
- vol.Optional(CONF_LONGITUDE): cv.longitude,
- vol.Optional(CONF_LANGUAGE, default="english"): vol.In(["hebrew", "english"]),
- vol.Optional(CONF_CANDLE_LIGHT_MINUTES, default=CANDLE_LIGHT_DEFAULT): int,
- # Default of 0 means use 8.5 degrees / 'three_stars' time.
- vol.Optional(CONF_HAVDALAH_OFFSET_MINUTES, default=0): int,
- vol.Optional(CONF_SENSORS, default=["date"]): vol.All(
- cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]
- ),
- }
-)
+_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Jewish calendar sensor platform."""
- language = config.get(CONF_LANGUAGE)
- name = config.get(CONF_NAME)
- latitude = config.get(CONF_LATITUDE, hass.config.latitude)
- longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
- diaspora = config.get(CONF_DIASPORA)
- candle_lighting_offset = config.get(CONF_CANDLE_LIGHT_MINUTES)
- havdalah_offset = config.get(CONF_HAVDALAH_OFFSET_MINUTES)
-
- if None in (latitude, longitude):
- _LOGGER.error("Latitude or longitude not set in Home Assistant config")
+ if discovery_info is None:
return
- dev = []
- for sensor_type in config[CONF_SENSORS]:
- dev.append(
- JewishCalSensor(
- name,
- language,
- sensor_type,
- latitude,
- longitude,
- hass.config.time_zone,
- diaspora,
- candle_lighting_offset,
- havdalah_offset,
- )
- )
- async_add_entities(dev, True)
+ sensors = [
+ JewishCalendarSensor(hass.data[DOMAIN], sensor, sensor_info)
+ for sensor, sensor_info in SENSOR_TYPES["data"].items()
+ ]
+ sensors.extend(
+ JewishCalendarSensor(hass.data[DOMAIN], sensor, sensor_info)
+ for sensor, sensor_info in SENSOR_TYPES["time"].items()
+ )
+
+ async_add_entities(sensors)
-class JewishCalSensor(Entity):
+class JewishCalendarSensor(Entity):
"""Representation of an Jewish calendar sensor."""
- def __init__(
- self,
- name,
- language,
- sensor_type,
- latitude,
- longitude,
- timezone,
- diaspora,
- candle_lighting_offset=CANDLE_LIGHT_DEFAULT,
- havdalah_offset=0,
- ):
+ def __init__(self, data, sensor, sensor_info):
"""Initialize the Jewish calendar sensor."""
- self.client_name = name
- self._name = SENSOR_TYPES[sensor_type][0]
- self.type = sensor_type
- self._hebrew = language == "hebrew"
+ self._location = data["location"]
+ self._type = sensor
+ self._name = f"{data['name']} {sensor_info[0]}"
+ self._icon = sensor_info[1]
+ self._hebrew = data["language"] == "hebrew"
+ self._candle_lighting_offset = data["candle_lighting_offset"]
+ self._havdalah_offset = data["havdalah_offset"]
+ self._diaspora = data["diaspora"]
self._state = None
- self.latitude = latitude
- self.longitude = longitude
- self.timezone = timezone
- self.diaspora = diaspora
- self.candle_lighting_offset = candle_lighting_offset
- self.havdalah_offset = havdalah_offset
- _LOGGER.debug("Sensor %s initialized", self.type)
@property
def name(self):
"""Return the name of the sensor."""
- return f"{self.client_name} {self._name}"
+ return self._name
@property
def icon(self):
"""Icon to display in the front end."""
- return SENSOR_TYPES[self.type][1]
+ return self._icon
@property
def state(self):
@@ -143,9 +62,7 @@ def state(self):
async def async_update(self):
"""Update the state of the sensor."""
- import hdate
-
- now = dt_util.as_local(dt_util.now())
+ now = dt_util.now()
_LOGGER.debug("Now: %s Timezone = %s", now, now.tzinfo)
today = now.date()
@@ -155,66 +72,65 @@ async def async_update(self):
_LOGGER.debug("Now: %s Sunset: %s", now, sunset)
- location = hdate.Location(
- latitude=self.latitude,
- longitude=self.longitude,
- timezone=self.timezone,
- diaspora=self.diaspora,
- )
-
def make_zmanim(date):
"""Create a Zmanim object."""
return hdate.Zmanim(
date=date,
- location=location,
- candle_lighting_offset=self.candle_lighting_offset,
- havdalah_offset=self.havdalah_offset,
+ location=self._location,
+ candle_lighting_offset=self._candle_lighting_offset,
+ havdalah_offset=self._havdalah_offset,
hebrew=self._hebrew,
)
- date = hdate.HDate(today, diaspora=self.diaspora, hebrew=self._hebrew)
- lagging_date = date
+ date = hdate.HDate(today, diaspora=self._diaspora, hebrew=self._hebrew)
- # Advance Hebrew date if sunset has passed.
- # Not all sensors should advance immediately when the Hebrew date
- # officially changes (i.e. after sunset), hence lagging_date.
- if now > sunset:
- date = date.next_day
+ # The Jewish day starts after darkness (called "tzais") and finishes at
+ # sunset ("shkia"). The time in between is a gray area (aka "Bein
+ # Hashmashot" - literally: "in between the sun and the moon").
+
+ # For some sensors, it is more interesting to consider the date to be
+ # tomorrow based on sunset ("shkia"), for others based on "tzais".
+ # Hence the following variables.
+ after_tzais_date = after_shkia_date = date
today_times = make_zmanim(today)
+
+ if now > sunset:
+ after_shkia_date = date.next_day
+
if today_times.havdalah and now > today_times.havdalah:
- lagging_date = lagging_date.next_day
+ after_tzais_date = date.next_day
# Terminology note: by convention in py-libhdate library, "upcoming"
# refers to "current" or "upcoming" dates.
- if self.type == "date":
- self._state = date.hebrew_date
- elif self.type == "weekly_portion":
+ if self._type == "date":
+ self._state = after_shkia_date.hebrew_date
+ elif self._type == "weekly_portion":
# Compute the weekly portion based on the upcoming shabbat.
- self._state = lagging_date.upcoming_shabbat.parasha
- elif self.type == "holiday_name":
- self._state = date.holiday_description
- elif self.type == "holyness":
- self._state = date.holiday_type
- elif self.type == "upcoming_shabbat_candle_lighting":
- times = make_zmanim(lagging_date.upcoming_shabbat.previous_day.gdate)
+ self._state = after_tzais_date.upcoming_shabbat.parasha
+ elif self._type == "holiday_name":
+ self._state = after_shkia_date.holiday_description
+ elif self._type == "holiday_type":
+ self._state = after_shkia_date.holiday_type
+ elif self._type == "upcoming_shabbat_candle_lighting":
+ times = make_zmanim(after_tzais_date.upcoming_shabbat.previous_day.gdate)
self._state = times.candle_lighting
- elif self.type == "upcoming_candle_lighting":
+ elif self._type == "upcoming_candle_lighting":
times = make_zmanim(
- lagging_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate
+ after_tzais_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate
)
self._state = times.candle_lighting
- elif self.type == "upcoming_shabbat_havdalah":
- times = make_zmanim(lagging_date.upcoming_shabbat.gdate)
+ elif self._type == "upcoming_shabbat_havdalah":
+ times = make_zmanim(after_tzais_date.upcoming_shabbat.gdate)
self._state = times.havdalah
- elif self.type == "upcoming_havdalah":
- times = make_zmanim(lagging_date.upcoming_shabbat_or_yom_tov.last_day.gdate)
+ elif self._type == "upcoming_havdalah":
+ times = make_zmanim(
+ after_tzais_date.upcoming_shabbat_or_yom_tov.last_day.gdate
+ )
self._state = times.havdalah
- elif self.type == "issur_melacha_in_effect":
- self._state = make_zmanim(now).issur_melacha_in_effect
- elif self.type == "omer_count":
- self._state = date.omer_day
+ elif self._type == "omer_count":
+ self._state = after_shkia_date.omer_day
else:
times = make_zmanim(today).zmanim
- self._state = times[self.type].time()
+ self._state = times[self._type].time()
_LOGGER.debug("New value: %s", self._state)
diff --git a/homeassistant/components/kaiterra/__init__.py b/homeassistant/components/kaiterra/__init__.py
new file mode 100644
index 00000000000000..8c61ad5418481a
--- /dev/null
+++ b/homeassistant/components/kaiterra/__init__.py
@@ -0,0 +1,92 @@
+"""Support for Kaiterra devices."""
+import voluptuous as vol
+
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.discovery import async_load_platform
+from homeassistant.helpers import config_validation as cv
+
+from homeassistant.const import (
+ CONF_API_KEY,
+ CONF_DEVICES,
+ CONF_DEVICE_ID,
+ CONF_SCAN_INTERVAL,
+ CONF_TYPE,
+ CONF_NAME,
+)
+
+from .const import (
+ AVAILABLE_AQI_STANDARDS,
+ AVAILABLE_UNITS,
+ AVAILABLE_DEVICE_TYPES,
+ CONF_AQI_STANDARD,
+ CONF_PREFERRED_UNITS,
+ DOMAIN,
+ DEFAULT_AQI_STANDARD,
+ DEFAULT_PREFERRED_UNIT,
+ DEFAULT_SCAN_INTERVAL,
+ KAITERRA_COMPONENTS,
+)
+
+from .api_data import KaiterraApiData
+
+KAITERRA_DEVICE_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_DEVICE_ID): cv.string,
+ vol.Required(CONF_TYPE): vol.In(AVAILABLE_DEVICE_TYPES),
+ vol.Optional(CONF_NAME): cv.string,
+ }
+)
+
+KAITERRA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_API_KEY): cv.string,
+ vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [KAITERRA_DEVICE_SCHEMA]),
+ vol.Optional(CONF_AQI_STANDARD, default=DEFAULT_AQI_STANDARD): vol.In(
+ AVAILABLE_AQI_STANDARDS
+ ),
+ vol.Optional(CONF_PREFERRED_UNITS, default=DEFAULT_PREFERRED_UNIT): vol.All(
+ cv.ensure_list, [vol.In(AVAILABLE_UNITS)]
+ ),
+ vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period,
+ }
+)
+
+CONFIG_SCHEMA = vol.Schema({DOMAIN: KAITERRA_SCHEMA}, extra=vol.ALLOW_EXTRA)
+
+
+async def async_setup(hass, config):
+ """Set up the Kaiterra components."""
+
+ conf = config[DOMAIN]
+ scan_interval = conf[CONF_SCAN_INTERVAL]
+ devices = conf[CONF_DEVICES]
+ session = async_get_clientsession(hass)
+ api = hass.data[DOMAIN] = KaiterraApiData(hass, conf, session)
+
+ await api.async_update()
+
+ async def _update(now=None):
+ """Periodic update."""
+ await api.async_update()
+
+ async_track_time_interval(hass, _update, scan_interval)
+
+ # Load platforms for each device
+ for device in devices:
+ device_name, device_id = (
+ device.get(CONF_NAME) or device[CONF_TYPE],
+ device[CONF_DEVICE_ID],
+ )
+ for component in KAITERRA_COMPONENTS:
+ hass.async_create_task(
+ async_load_platform(
+ hass,
+ component,
+ DOMAIN,
+ {CONF_NAME: device_name, CONF_DEVICE_ID: device_id},
+ config,
+ )
+ )
+
+ return True
diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py
new file mode 100644
index 00000000000000..4dfe04f9c2e12d
--- /dev/null
+++ b/homeassistant/components/kaiterra/air_quality.py
@@ -0,0 +1,115 @@
+"""Support for Kaiterra Air Quality Sensors."""
+from homeassistant.components.air_quality import AirQualityEntity
+
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from homeassistant.const import CONF_DEVICE_ID, CONF_NAME
+
+from .const import (
+ DOMAIN,
+ ATTR_VOC,
+ ATTR_AQI_LEVEL,
+ ATTR_AQI_POLLUTANT,
+ DISPATCHER_KAITERRA,
+)
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Set up the air_quality kaiterra sensor."""
+ if discovery_info is None:
+ return
+
+ api = hass.data[DOMAIN]
+ name = discovery_info[CONF_NAME]
+ device_id = discovery_info[CONF_DEVICE_ID]
+
+ async_add_entities([KaiterraAirQuality(api, name, device_id)])
+
+
+class KaiterraAirQuality(AirQualityEntity):
+ """Implementation of a Kaittera air quality sensor."""
+
+ def __init__(self, api, name, device_id):
+ """Initialize the sensor."""
+ self._api = api
+ self._name = f"{name} Air Quality"
+ self._device_id = device_id
+
+ def _data(self, key):
+ return self._device.get(key, {}).get("value")
+
+ @property
+ def _device(self):
+ return self._api.data.get(self._device_id, {})
+
+ @property
+ def should_poll(self):
+ """Return that the sensor should not be polled."""
+ return False
+
+ @property
+ def available(self):
+ """Return the availability of the sensor."""
+ return self._api.data.get(self._device_id) is not None
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def air_quality_index(self):
+ """Return the Air Quality Index (AQI)."""
+ return self._data("aqi")
+
+ @property
+ def air_quality_index_level(self):
+ """Return the Air Quality Index level."""
+ return self._data("aqi_level")
+
+ @property
+ def air_quality_index_pollutant(self):
+ """Return the Air Quality Index level."""
+ return self._data("aqi_pollutant")
+
+ @property
+ def particulate_matter_2_5(self):
+ """Return the particulate matter 2.5 level."""
+ return self._data("rpm25c")
+
+ @property
+ def particulate_matter_10(self):
+ """Return the particulate matter 10 level."""
+ return self._data("rpm10c")
+
+ @property
+ def volatile_organic_compounds(self):
+ """Return the VOC (Volatile Organic Compounds) level."""
+ return self._data("rtvoc")
+
+ @property
+ def unique_id(self):
+ """Return the sensor's unique id."""
+ return f"{self._device_id}_air_quality"
+
+ @property
+ def device_state_attributes(self):
+ """Return the device state attributes."""
+ data = {}
+ attributes = [
+ (ATTR_VOC, self.volatile_organic_compounds),
+ (ATTR_AQI_LEVEL, self.air_quality_index_level),
+ (ATTR_AQI_POLLUTANT, self.air_quality_index_pollutant),
+ ]
+
+ for attr, value in attributes:
+ if value is not None:
+ data[attr] = value
+
+ return data
+
+ async def async_added_to_hass(self):
+ """Register callback."""
+ async_dispatcher_connect(
+ self.hass, DISPATCHER_KAITERRA, self.async_write_ha_state
+ )
diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py
new file mode 100644
index 00000000000000..0c2d6d9366147d
--- /dev/null
+++ b/homeassistant/components/kaiterra/api_data.py
@@ -0,0 +1,109 @@
+"""Data for all Kaiterra devices."""
+from logging import getLogger
+
+import asyncio
+
+import async_timeout
+
+from aiohttp.client_exceptions import ClientResponseError
+
+from kaiterra_async_client import KaiterraAPIClient, AQIStandard, Units
+
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+
+from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_DEVICE_ID, CONF_TYPE
+
+from .const import (
+ AQI_SCALE,
+ AQI_LEVEL,
+ CONF_AQI_STANDARD,
+ CONF_PREFERRED_UNITS,
+ DISPATCHER_KAITERRA,
+)
+
+_LOGGER = getLogger(__name__)
+
+POLLUTANTS = {"rpm25c": "PM2.5", "rpm10c": "PM10", "rtvoc": "TVOC"}
+
+
+class KaiterraApiData:
+ """Get data from Kaiterra API."""
+
+ def __init__(self, hass, config, session):
+ """Initialize the API data object."""
+
+ api_key = config[CONF_API_KEY]
+ aqi_standard = config[CONF_AQI_STANDARD]
+ devices = config[CONF_DEVICES]
+ units = config[CONF_PREFERRED_UNITS]
+
+ self._hass = hass
+ self._api = KaiterraAPIClient(
+ session,
+ api_key=api_key,
+ aqi_standard=AQIStandard.from_str(aqi_standard),
+ preferred_units=[Units.from_str(unit) for unit in units],
+ )
+ self._devices_ids = [device[CONF_DEVICE_ID] for device in devices]
+ self._devices = [
+ f"/{device[CONF_TYPE]}s/{device[CONF_DEVICE_ID]}" for device in devices
+ ]
+ self._scale = AQI_SCALE[aqi_standard]
+ self._level = AQI_LEVEL[aqi_standard]
+ self._update_listeners = []
+ self.data = {}
+
+ async def async_update(self) -> None:
+ """Get the data from Kaiterra API."""
+
+ try:
+ with async_timeout.timeout(10):
+ data = await self._api.get_latest_sensor_readings(self._devices)
+ except (ClientResponseError, asyncio.TimeoutError):
+ _LOGGER.debug("Couldn't fetch data")
+ self.data = {}
+ async_dispatcher_send(self._hass, DISPATCHER_KAITERRA)
+
+ _LOGGER.debug("New data retrieved: %s", data)
+
+ try:
+ self.data = {}
+ for i, device in enumerate(data):
+ if not device:
+ self.data[self._devices_ids[i]] = {}
+ continue
+
+ aqi, main_pollutant = None, None
+ for sensor_name, sensor in device.items():
+ points = sensor.get("points")
+
+ if not points:
+ continue
+
+ point = points[0]
+ sensor["value"] = point.get("value")
+
+ if "aqi" not in point:
+ continue
+
+ sensor["aqi"] = point["aqi"]
+ if not aqi or aqi < point["aqi"]:
+ aqi = point["aqi"]
+ main_pollutant = POLLUTANTS.get(sensor_name)
+
+ level = None
+ for j in range(1, len(self._scale)):
+ if aqi <= self._scale[j]:
+ level = self._level[j - 1]
+ break
+
+ device["aqi"] = {"value": aqi}
+ device["aqi_level"] = {"value": level}
+ device["aqi_pollutant"] = {"value": main_pollutant}
+
+ self.data[self._devices_ids[i]] = device
+
+ async_dispatcher_send(self._hass, DISPATCHER_KAITERRA)
+ except IndexError as err:
+ _LOGGER.error("Parsing error %s", err)
+ async_dispatcher_send(self._hass, DISPATCHER_KAITERRA)
diff --git a/homeassistant/components/kaiterra/const.py b/homeassistant/components/kaiterra/const.py
new file mode 100644
index 00000000000000..7e23edb1259fa9
--- /dev/null
+++ b/homeassistant/components/kaiterra/const.py
@@ -0,0 +1,57 @@
+"""Consts for Kaiterra integration."""
+
+from datetime import timedelta
+
+DOMAIN = "kaiterra"
+
+DISPATCHER_KAITERRA = "kaiterra_update"
+
+AQI_SCALE = {
+ "cn": [0, 50, 100, 150, 200, 300, 400, 500],
+ "in": [0, 50, 100, 200, 300, 400, 500],
+ "us": [0, 50, 100, 150, 200, 300, 500],
+}
+AQI_LEVEL = {
+ "cn": [
+ "Good",
+ "Satisfactory",
+ "Moderate",
+ "Unhealthy for sensitive groups",
+ "Unhealthy",
+ "Very unhealthy",
+ "Hazardous",
+ ],
+ "in": [
+ "Good",
+ "Satisfactory",
+ "Moderately polluted",
+ "Poor",
+ "Very poor",
+ "Severe",
+ ],
+ "us": [
+ "Good",
+ "Moderate",
+ "Unhealthy for sensitive groups",
+ "Unhealthy",
+ "Very unhealthy",
+ "Hazardous",
+ ],
+}
+
+ATTR_VOC = "volatile_organic_compounds"
+ATTR_AQI_LEVEL = "air_quality_index_level"
+ATTR_AQI_POLLUTANT = "air_quality_index_pollutant"
+
+AVAILABLE_AQI_STANDARDS = ["us", "cn", "in"]
+AVAILABLE_UNITS = ["x", "%", "C", "F", "mg/m³", "µg/m³", "ppm", "ppb"]
+AVAILABLE_DEVICE_TYPES = ["laseregg", "sensedge"]
+
+CONF_AQI_STANDARD = "aqi_standard"
+CONF_PREFERRED_UNITS = "preferred_units"
+
+DEFAULT_AQI_STANDARD = "us"
+DEFAULT_PREFERRED_UNIT = []
+DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
+
+KAITERRA_COMPONENTS = ["sensor", "air_quality"]
diff --git a/homeassistant/components/kaiterra/manifest.json b/homeassistant/components/kaiterra/manifest.json
new file mode 100644
index 00000000000000..926f73fa4dbea9
--- /dev/null
+++ b/homeassistant/components/kaiterra/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "kaiterra",
+ "name": "Kaiterra",
+ "documentation": "https://www.home-assistant.io/components/kaiterra",
+ "requirements": ["kaiterra-async-client==0.0.2"],
+ "codeowners": ["@Michsior14"],
+ "dependencies": []
+}
\ No newline at end of file
diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py
new file mode 100644
index 00000000000000..4ff6435b64d8b0
--- /dev/null
+++ b/homeassistant/components/kaiterra/sensor.py
@@ -0,0 +1,95 @@
+"""Support for Kaiterra Temperature ahn Humidity Sensors."""
+from homeassistant.helpers.entity import Entity
+
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT
+
+from .const import DOMAIN, DISPATCHER_KAITERRA
+
+SENSORS = [
+ {"name": "Temperature", "prop": "rtemp", "device_class": "temperature"},
+ {"name": "Humidity", "prop": "rhumid", "device_class": "humidity"},
+]
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Set up the kaiterra temperature and humidity sensor."""
+ if discovery_info is None:
+ return
+
+ api = hass.data[DOMAIN]
+ name = discovery_info[CONF_NAME]
+ device_id = discovery_info[CONF_DEVICE_ID]
+
+ async_add_entities(
+ [KaiterraSensor(api, name, device_id, sensor) for sensor in SENSORS]
+ )
+
+
+class KaiterraSensor(Entity):
+ """Implementation of a Kaittera sensor."""
+
+ def __init__(self, api, name, device_id, sensor):
+ """Initialize the sensor."""
+ self._api = api
+ self._name = f'{name} {sensor["name"]}'
+ self._device_id = device_id
+ self._kind = sensor["name"].lower()
+ self._property = sensor["prop"]
+ self._device_class = sensor["device_class"]
+
+ @property
+ def _sensor(self):
+ """Return the sensor data."""
+ return self._api.data.get(self._device_id, {}).get(self._property, {})
+
+ @property
+ def should_poll(self):
+ """Return that the sensor should not be polled."""
+ return False
+
+ @property
+ def available(self):
+ """Return the availability of the sensor."""
+ return self._api.data.get(self._device_id) is not None
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ return self._device_class
+
+ @property
+ def name(self):
+ """Return the name."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state."""
+ return self._sensor.get("value")
+
+ @property
+ def unique_id(self):
+ """Return the sensor's unique id."""
+ return f"{self._device_id}_{self._kind}"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit the value is expressed in."""
+ if not self._sensor.get("units"):
+ return None
+
+ value = self._sensor["units"].value
+
+ if value == "F":
+ return TEMP_FAHRENHEIT
+ if value == "C":
+ return TEMP_CELSIUS
+ return value
+
+ async def async_added_to_hass(self):
+ """Register callback."""
+ async_dispatcher_connect(
+ self.hass, DISPATCHER_KAITERRA, self.async_write_ha_state
+ )
diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py
index 1a3b41f74bd993..8b901dcc61e930 100644
--- a/homeassistant/components/keyboard_remote/__init__.py
+++ b/homeassistant/components/keyboard_remote/__init__.py
@@ -157,7 +157,7 @@ def run(self):
try:
event = self.dev.read_one()
- except IOError: # Keyboard Disconnected
+ except OSError: # Keyboard Disconnected
self.dev = None
self.hass.bus.fire(
KEYBOARD_REMOTE_DISCONNECTED,
diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py
index c04feed23374fa..71a82c6df2a6eb 100644
--- a/homeassistant/components/knx/light.py
+++ b/homeassistant/components/knx/light.py
@@ -305,7 +305,10 @@ async def async_turn_on(self, **kwargs):
await self.device.set_color_temperature(kelvin)
elif self.device.supports_tunable_white and update_color_temp:
# calculate relative_ct from Kelvin to fit typical KNX devices
- kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds))
+ kelvin = min(
+ self._max_kelvin,
+ int(color_util.color_temperature_mired_to_kelvin(mireds)),
+ )
relative_ct = int(
255
* (kelvin - self._min_kelvin)
diff --git a/homeassistant/components/life360/.translations/es.json b/homeassistant/components/life360/.translations/es.json
index 8fc70a60a052e6..2b185cb1b6c476 100644
--- a/homeassistant/components/life360/.translations/es.json
+++ b/homeassistant/components/life360/.translations/es.json
@@ -9,7 +9,9 @@
},
"error": {
"invalid_credentials": "Credenciales no v\u00e1lidas",
- "invalid_username": "Nombre de usuario no v\u00e1lido"
+ "invalid_username": "Nombre de usuario no v\u00e1lido",
+ "unexpected": "Error inesperado al comunicarse con el servidor Life360",
+ "user_already_configured": "La cuenta ya ha sido configurada"
},
"step": {
"user": {
diff --git a/homeassistant/components/life360/.translations/fr.json b/homeassistant/components/life360/.translations/fr.json
index cb4682fc937103..947425e4807f91 100644
--- a/homeassistant/components/life360/.translations/fr.json
+++ b/homeassistant/components/life360/.translations/fr.json
@@ -10,6 +10,7 @@
"error": {
"invalid_credentials": "Informations d'identification invalides",
"invalid_username": "Nom d'utilisateur invalide",
+ "unexpected": "Erreur inattendue lors de la communication avec le serveur Life360",
"user_already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9"
},
"step": {
diff --git a/homeassistant/components/life360/.translations/it.json b/homeassistant/components/life360/.translations/it.json
index 9c4cb1cc4cb15f..b7d2d6c8f1b211 100644
--- a/homeassistant/components/life360/.translations/it.json
+++ b/homeassistant/components/life360/.translations/it.json
@@ -5,11 +5,12 @@
"user_already_configured": "L'account \u00e8 gi\u00e0 stato configurato"
},
"create_entry": {
- "default": "Per impostare le opzioni avanzate, consultare la [Documentazione Life360] ( {docs_url} )."
+ "default": "Per impostare le opzioni avanzate, consultare la [Documentazione Life360]({docs_url})."
},
"error": {
"invalid_credentials": "Credenziali non valide",
"invalid_username": "Nome utente non valido",
+ "unexpected": "Errore imprevisto durante la comunicazione con il server di Life360",
"user_already_configured": "L'account \u00e8 gi\u00e0 stato configurato"
},
"step": {
@@ -18,6 +19,7 @@
"password": "Password",
"username": "Nome utente"
},
+ "description": "Per impostare le opzioni avanzate, vedere [Documentazione di Life360]({docs_url}).\n\u00c8 consigliabile eseguire questa operazione prima di aggiungere gli account.",
"title": "Informazioni sull'account Life360"
}
},
diff --git a/homeassistant/components/life360/.translations/ko.json b/homeassistant/components/life360/.translations/ko.json
index b81a6fd059f5ce..067b305b80c426 100644
--- a/homeassistant/components/life360/.translations/ko.json
+++ b/homeassistant/components/life360/.translations/ko.json
@@ -10,6 +10,7 @@
"error": {
"invalid_credentials": "\ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"invalid_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unexpected": "Life360 \uc11c\ubc84 \uc5f0\uacb0\uc911 \uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4",
"user_already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
"step": {
diff --git a/homeassistant/components/life360/.translations/lb.json b/homeassistant/components/life360/.translations/lb.json
index bfed5937e24bea..3af9ab00728e5d 100644
--- a/homeassistant/components/life360/.translations/lb.json
+++ b/homeassistant/components/life360/.translations/lb.json
@@ -10,6 +10,7 @@
"error": {
"invalid_credentials": "Ong\u00eblteg Login Informatioune",
"invalid_username": "Ong\u00ebltege Benotzernumm",
+ "unexpected": "Onerwaarte Feeler bei der Kommunikatioun mam Life360 Server",
"user_already_configured": "Kont ass scho konfigur\u00e9iert"
},
"step": {
diff --git a/homeassistant/components/life360/.translations/pl.json b/homeassistant/components/life360/.translations/pl.json
index cd5e61fc123608..e9cd992030442b 100644
--- a/homeassistant/components/life360/.translations/pl.json
+++ b/homeassistant/components/life360/.translations/pl.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce",
- "user_already_configured": "Konto zosta\u0142o ju\u017c skonfigurowane."
+ "user_already_configured": "Konto jest ju\u017c skonfigurowane"
},
"create_entry": {
"default": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url})."
@@ -11,7 +11,7 @@
"invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce",
"invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika",
"unexpected": "Nieoczekiwany b\u0142\u0105d komunikacji z serwerem Life360",
- "user_already_configured": "Konto zosta\u0142o ju\u017c skonfigurowane."
+ "user_already_configured": "Konto jest ju\u017c skonfigurowane"
},
"step": {
"user": {
diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json
index c03ad0f7e1f6a6..1e962142373f89 100644
--- a/homeassistant/components/life360/.translations/ru.json
+++ b/homeassistant/components/life360/.translations/ru.json
@@ -5,7 +5,7 @@
"user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430"
},
"create_entry": {
- "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 Life360]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438."
+ "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438."
},
"error": {
"invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435",
@@ -19,7 +19,7 @@
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"username": "\u041b\u043e\u0433\u0438\u043d"
},
- "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 Life360]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.",
+ "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.",
"title": "Life360"
}
},
diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json
index fd74d9831fca0a..131d1a23b6a5f3 100644
--- a/homeassistant/components/lifx/manifest.json
+++ b/homeassistant/components/lifx/manifest.json
@@ -13,7 +13,5 @@
]
},
"dependencies": [],
- "codeowners": [
- "@amelchio"
- ]
+ "codeowners": []
}
diff --git a/homeassistant/components/lifx_cloud/manifest.json b/homeassistant/components/lifx_cloud/manifest.json
index c2834fbc788b63..83805692e4d246 100644
--- a/homeassistant/components/lifx_cloud/manifest.json
+++ b/homeassistant/components/lifx_cloud/manifest.json
@@ -4,7 +4,5 @@
"documentation": "https://www.home-assistant.io/components/lifx_cloud",
"requirements": [],
"dependencies": [],
- "codeowners": [
- "@amelchio"
- ]
+ "codeowners": []
}
diff --git a/homeassistant/components/lifx_legacy/manifest.json b/homeassistant/components/lifx_legacy/manifest.json
index 4ff59ac17703df..fb38b41f314c4a 100644
--- a/homeassistant/components/lifx_legacy/manifest.json
+++ b/homeassistant/components/lifx_legacy/manifest.json
@@ -6,7 +6,5 @@
"liffylights==0.9.4"
],
"dependencies": [],
- "codeowners": [
- "@amelchio"
- ]
+ "codeowners": []
}
diff --git a/homeassistant/components/light/.translations/bg.json b/homeassistant/components/light/.translations/bg.json
new file mode 100644
index 00000000000000..533ba76b6a7613
--- /dev/null
+++ b/homeassistant/components/light/.translations/bg.json
@@ -0,0 +1,13 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "\u041f\u0440\u0435\u0432\u043a\u043b\u044e\u0447\u0438 {entity_name}",
+ "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438 {entity_name}",
+ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b.",
+ "is_on": "{entity_name} \u0435 \u0432\u043a\u043b."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/ca.json b/homeassistant/components/light/.translations/ca.json
new file mode 100644
index 00000000000000..c9b727088ab180
--- /dev/null
+++ b/homeassistant/components/light/.translations/ca.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Commuta {name}",
+ "turn_off": "Apaga {name}",
+ "turn_on": "Enc\u00e9n {name}"
+ },
+ "condition_type": {
+ "is_off": "{name} est\u00e0 apagat",
+ "is_on": "{name} est\u00e0 enc\u00e8s"
+ },
+ "trigger_type": {
+ "turned_off": "{name} apagat",
+ "turned_on": "{name} enc\u00e8s"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/da.json b/homeassistant/components/light/.translations/da.json
new file mode 100644
index 00000000000000..4ea4a94014ea83
--- /dev/null
+++ b/homeassistant/components/light/.translations/da.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "trigger_type": {
+ "turned_off": "{name} slukket",
+ "turned_on": "{name} t\u00e6ndt"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json
new file mode 100644
index 00000000000000..2fe1c6b42dcd42
--- /dev/null
+++ b/homeassistant/components/light/.translations/de.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "trigger_type": {
+ "turned_off": "{name} ausgeschaltet",
+ "turned_on": "{name} eingeschaltet"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/en.json b/homeassistant/components/light/.translations/en.json
index 9e5d1abddaf486..3f37de5331e3ea 100644
--- a/homeassistant/components/light/.translations/en.json
+++ b/homeassistant/components/light/.translations/en.json
@@ -1,8 +1,17 @@
{
"device_automation": {
+ "action_type": {
+ "toggle": "Toggle {entity_name}",
+ "turn_off": "Turn off {entity_name}",
+ "turn_on": "Turn on {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} is off",
+ "is_on": "{entity_name} is on"
+ },
"trigger_type": {
- "turn_off": "{name} turned off",
- "turn_on": "{name} turned on"
+ "turned_off": "{entity_name} turned off",
+ "turned_on": "{entity_name} turned on"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/es.json b/homeassistant/components/light/.translations/es.json
new file mode 100644
index 00000000000000..6bf91651d2e148
--- /dev/null
+++ b/homeassistant/components/light/.translations/es.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Alternar {entity_name}",
+ "turn_off": "Apagar {entity_name}",
+ "turn_on": "Encender {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} est\u00e1 apagada",
+ "is_on": "{entity_name} est\u00e1 encendida"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} apagada",
+ "turned_on": "{entity_name} encendida"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json
new file mode 100644
index 00000000000000..fd30e9317180ec
--- /dev/null
+++ b/homeassistant/components/light/.translations/fr.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Basculer {entity_name}",
+ "turn_off": "\u00c9teindre {entity_name}",
+ "turn_on": "Allumer {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} est \u00e9teint",
+ "is_on": "{entity_name} est allum\u00e9"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} d\u00e9sactiv\u00e9",
+ "turned_on": "{entity_name} activ\u00e9"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/it.json b/homeassistant/components/light/.translations/it.json
new file mode 100644
index 00000000000000..2f4d2ca121f512
--- /dev/null
+++ b/homeassistant/components/light/.translations/it.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Commuta {entity_name}",
+ "turn_off": "Spegnere {entity_name}",
+ "turn_on": "Accendere {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u00e8 disattivato",
+ "is_on": "{entity_name} \u00e8 attivo"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} disattivato",
+ "turned_on": "{entity_name} attivato"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/ko.json b/homeassistant/components/light/.translations/ko.json
new file mode 100644
index 00000000000000..e055f67421ef53
--- /dev/null
+++ b/homeassistant/components/light/.translations/ko.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "{entity_name} \ud1a0\uae00",
+ "turn_off": "{entity_name} \ub044\uae30",
+ "turn_on": "{entity_name} \ucf1c\uae30"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4",
+ "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4",
+ "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/lb.json b/homeassistant/components/light/.translations/lb.json
new file mode 100644
index 00000000000000..a7f807e8dcda54
--- /dev/null
+++ b/homeassistant/components/light/.translations/lb.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "{entity_name} \u00ebmschalten",
+ "turn_off": "{entity_name} ausschalten",
+ "turn_on": "{entity_name} uschalten"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} ass aus",
+ "is_on": "{entity_name} ass un"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} gouf ausgeschalt",
+ "turned_on": "{entity_name} gouf ugeschalt"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/nl.json b/homeassistant/components/light/.translations/nl.json
new file mode 100644
index 00000000000000..63954ca83a9fa1
--- /dev/null
+++ b/homeassistant/components/light/.translations/nl.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Omschakelen {naam}",
+ "turn_off": "{entity_name} uitschakelen",
+ "turn_on": "{entity_name} inschakelen"
+ },
+ "condition_type": {
+ "is_off": "{name} is uitgeschakeld",
+ "is_on": "{name} is ingeschakeld"
+ },
+ "trigger_type": {
+ "turned_off": "{name} is uitgeschakeld",
+ "turned_on": "{name} is ingeschakeld"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/no.json b/homeassistant/components/light/.translations/no.json
new file mode 100644
index 00000000000000..785e9ca2912e00
--- /dev/null
+++ b/homeassistant/components/light/.translations/no.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Veksle {entity_name}",
+ "turn_off": "Sl\u00e5 av {entity_name}",
+ "turn_on": "Sl\u00e5 p\u00e5 {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} er av",
+ "is_on": "{entity_name} er p\u00e5"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} sl\u00e5tt av",
+ "turned_on": "{entity_name} sl\u00e5tt p\u00e5"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json
new file mode 100644
index 00000000000000..22a93909578608
--- /dev/null
+++ b/homeassistant/components/light/.translations/pl.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Prze\u0142\u0105cz {entity_name}",
+ "turn_off": "Wy\u0142\u0105cz {entity_name}",
+ "turn_on": "W\u0142\u0105cz {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "(entity_name} jest wy\u0142\u0105czony.",
+ "is_on": "(entity_name} jest w\u0142\u0105czony."
+ },
+ "trigger_type": {
+ "turned_off": "{nazwa} wy\u0142\u0105czone",
+ "turned_on": "{name} w\u0142\u0105czone"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json
new file mode 100644
index 00000000000000..a6a7994b7c3603
--- /dev/null
+++ b/homeassistant/components/light/.translations/ru.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}",
+ "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}",
+ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
+ "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
+ "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/sl.json b/homeassistant/components/light/.translations/sl.json
new file mode 100644
index 00000000000000..bef4f1583b6b19
--- /dev/null
+++ b/homeassistant/components/light/.translations/sl.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Preklopite {entity_name}",
+ "turn_off": "Izklopite {entity_name}",
+ "turn_on": "Vklopite {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} je izklopljen",
+ "is_on": "{entity_name} je vklopljen"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} izklopljen",
+ "turned_on": "{entity_name} vklopljen"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/.translations/zh-Hant.json b/homeassistant/components/light/.translations/zh-Hant.json
new file mode 100644
index 00000000000000..5ac06129463b35
--- /dev/null
+++ b/homeassistant/components/light/.translations/zh-Hant.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "\u5207\u63db {entity_name}",
+ "turn_off": "\u95dc\u9589 {entity_name}",
+ "turn_on": "\u958b\u555f {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u5df2\u95dc\u9589",
+ "is_on": "{entity_name} \u5df2\u958b\u555f"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} \u5df2\u95dc\u9589",
+ "turned_on": "{entity_name} \u5df2\u958b\u555f"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/light/device_automation.py b/homeassistant/components/light/device_automation.py
index ed75b5f906f5a8..61292d47449adf 100644
--- a/homeassistant/components/light/device_automation.py
+++ b/homeassistant/components/light/device_automation.py
@@ -1,91 +1,56 @@
"""Provides device automations for lights."""
import voluptuous as vol
-import homeassistant.components.automation.state as state
-from homeassistant.core import split_entity_id
-from homeassistant.const import (
- CONF_DEVICE_ID,
- CONF_DOMAIN,
- CONF_ENTITY_ID,
- CONF_PLATFORM,
- CONF_TYPE,
-)
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity_registry import async_entries_for_device
+from homeassistant.components.device_automation import toggle_entity
+from homeassistant.const import CONF_DOMAIN
from . import DOMAIN
# mypy: allow-untyped-defs, no-check-untyped-defs
-CONF_TURN_OFF = "turn_off"
-CONF_TURN_ON = "turn_on"
-
-ENTITY_TRIGGERS = [
- {
- # Trigger when light is turned on
- CONF_PLATFORM: "device",
- CONF_DOMAIN: DOMAIN,
- CONF_TYPE: CONF_TURN_OFF,
- },
- {
- # Trigger when light is turned off
- CONF_PLATFORM: "device",
- CONF_DOMAIN: DOMAIN,
- CONF_TYPE: CONF_TURN_ON,
- },
-]
-
-TRIGGER_SCHEMA = vol.All(
- vol.Schema(
- {
- vol.Required(CONF_PLATFORM): "device",
- vol.Optional(CONF_DEVICE_ID): str,
- vol.Required(CONF_DOMAIN): DOMAIN,
- vol.Required(CONF_ENTITY_ID): cv.entity_id,
- vol.Required(CONF_TYPE): str,
- }
- )
+ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN})
+
+CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend(
+ {vol.Required(CONF_DOMAIN): DOMAIN}
)
+TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend(
+ {vol.Required(CONF_DOMAIN): DOMAIN}
+)
-def _is_domain(entity, domain):
- return split_entity_id(entity.entity_id)[0] == domain
+async def async_call_action_from_config(hass, config, variables, context):
+ """Change state based on configuration."""
+ config = ACTION_SCHEMA(config)
+ await toggle_entity.async_call_action_from_config(
+ hass, config, variables, context, DOMAIN
+ )
-async def async_attach_trigger(hass, config, action, automation_info):
- """Listen for state changes based on configuration."""
- trigger_type = config.get(CONF_TYPE)
- if trigger_type == CONF_TURN_ON:
- from_state = "off"
- to_state = "on"
- else:
- from_state = "on"
- to_state = "off"
- state_config = {
- state.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
- state.CONF_FROM: from_state,
- state.CONF_TO: to_state,
- }
-
- return await state.async_trigger(hass, state_config, action, automation_info)
+
+def async_condition_from_config(config, config_validation):
+ """Evaluate state based on configuration."""
+ config = CONDITION_SCHEMA(config)
+ return toggle_entity.async_condition_from_config(config, config_validation)
async def async_trigger(hass, config, action, automation_info):
- """Temporary so existing automation framework can be used for testing."""
- return await async_attach_trigger(hass, config, action, automation_info)
+ """Listen for state changes based on configuration."""
+ config = TRIGGER_SCHEMA(config)
+ return await toggle_entity.async_attach_trigger(
+ hass, config, action, automation_info
+ )
+
+
+async def async_get_actions(hass, device_id):
+ """List device actions."""
+ return await toggle_entity.async_get_actions(hass, device_id, DOMAIN)
+
+
+async def async_get_conditions(hass, device_id):
+ """List device conditions."""
+ return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN)
async def async_get_triggers(hass, device_id):
"""List device triggers."""
- triggers = []
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
-
- entities = async_entries_for_device(entity_registry, device_id)
- domain_entities = [x for x in entities if _is_domain(x, DOMAIN)]
- for entity in domain_entities:
- for trigger in ENTITY_TRIGGERS:
- trigger = dict(trigger)
- trigger.update(device_id=device_id, entity_id=entity.entity_id)
- triggers.append(trigger)
-
- return triggers
+ return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN)
diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json
index 94954bb790b198..77b842ba07833a 100644
--- a/homeassistant/components/light/strings.json
+++ b/homeassistant/components/light/strings.json
@@ -1,8 +1,17 @@
{
"device_automation": {
+ "action_type": {
+ "toggle": "Toggle {entity_name}",
+ "turn_on": "Turn on {entity_name}",
+ "turn_off": "Turn off {entity_name}"
+ },
+ "condition_type": {
+ "is_on": "{entity_name} is on",
+ "is_off": "{entity_name} is off"
+ },
"trigger_type": {
- "turn_on": "{name} turned on",
- "turn_off": "{name} turned off"
+ "turned_on": "{entity_name} turned on",
+ "turned_off": "{entity_name} turned off"
}
}
}
diff --git a/homeassistant/components/linky/.translations/bg.json b/homeassistant/components/linky/.translations/bg.json
new file mode 100644
index 00000000000000..6eeb898ee1ffc8
--- /dev/null
+++ b/homeassistant/components/linky/.translations/bg.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "\u0412\u0435\u0447\u0435 \u0438\u043c\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043f\u0440\u043e\u0444\u0438\u043b"
+ },
+ "error": {
+ "access": "\u041d\u044f\u043c\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e Enedis.fr, \u043c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0438",
+ "enedis": "Enedis.fr \u043e\u0442\u0433\u043e\u0432\u043e\u0440\u0438 \u0441 \u0433\u0440\u0435\u0448\u043a\u0430: \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e (\u043e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u043d\u0435 \u043c\u0435\u0436\u0434\u0443 23:00 \u0438 02:00)",
+ "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430: \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e (\u043e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u043d\u0435 \u043c\u0435\u0436\u0434\u0443 23:00 \u0438 02:00)",
+ "username_exists": "\u0412\u0435\u0447\u0435 \u0438\u043c\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043f\u0440\u043e\u0444\u0438\u043b",
+ "wrong_login": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0432\u043b\u0438\u0437\u0430\u043d\u0435: \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0438\u043c\u0435\u0439\u043b\u0430 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u0430",
+ "username": "E-mail"
+ },
+ "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043d\u0434\u0435\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438\u0442\u0435 \u0441\u0438 \u0434\u0430\u043d\u043d\u0438",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/ca.json b/homeassistant/components/linky/.translations/ca.json
new file mode 100644
index 00000000000000..ca437417f590db
--- /dev/null
+++ b/homeassistant/components/linky/.translations/ca.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "El compte ja ha estat configurat"
+ },
+ "error": {
+ "access": "No s'ha pogut accedir a Enedis.fr, comprova la teva connexi\u00f3 a Internet",
+ "enedis": "Enedis.fr ha respost amb un error: torna-ho a provar m\u00e9s tard (millo no entre les 23:00 i les 14:00)",
+ "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard (millor no entre les 23:00 i les 14:00)",
+ "username_exists": "El compte ja ha estat configurat",
+ "wrong_login": "Error d\u2019inici de sessi\u00f3: comprova el teu correu electr\u00f2nic i la contrasenya"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrasenya",
+ "username": "Correu electr\u00f2nic"
+ },
+ "description": "Introdueix les teves credencials",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/da.json b/homeassistant/components/linky/.translations/da.json
new file mode 100644
index 00000000000000..cacad99de584bb
--- /dev/null
+++ b/homeassistant/components/linky/.translations/da.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "Kontoen er allerede konfigureret"
+ },
+ "error": {
+ "access": "Kunne ikke f\u00e5 adgang til Enedis.fr, kontroller din internetforbindelse",
+ "enedis": "Enedis.fr svarede med en fejl: Pr\u00f8v igen senere (normalt ikke mellem 23:00 og 02:00)",
+ "unknown": "Ukendt fejl: Pr\u00f8v igen senere (normalt ikke mellem 23:00 og 02:00)",
+ "username_exists": "Kontoen er allerede konfigureret",
+ "wrong_login": "Loginfejl: Kontroller din e-mail og adgangskode"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Adgangskode",
+ "username": "E-mail"
+ },
+ "description": "Indtast dine legitimationsoplysninger",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/de.json b/homeassistant/components/linky/.translations/de.json
new file mode 100644
index 00000000000000..3fc13126270c66
--- /dev/null
+++ b/homeassistant/components/linky/.translations/de.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "Konto bereits konfiguriert"
+ },
+ "error": {
+ "access": "Konnte nicht auf Enedis.fr zugreifen, \u00fcberpr\u00fcfe bitte die Internetverbindung",
+ "enedis": "Enedis.fr antwortete mit einem Fehler: wiederhole den Vorgang sp\u00e4ter (in der Regel nicht zwischen 23 Uhr und 2 Uhr morgens)",
+ "unknown": "Unbekannter Fehler: Wiederhole den Vorgang sp\u00e4ter (in der Regel nicht zwischen 23 Uhr und 2 Uhr morgens)",
+ "username_exists": "Konto bereits konfiguriert",
+ "wrong_login": "Login-Fehler: Pr\u00fcfe bitte E-Mail & Passwort"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "E-Mail"
+ },
+ "description": "Gib deine Zugangsdaten ein",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/es.json b/homeassistant/components/linky/.translations/es.json
new file mode 100644
index 00000000000000..511f3c9d8e56f8
--- /dev/null
+++ b/homeassistant/components/linky/.translations/es.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "Cuenta ya configurada"
+ },
+ "error": {
+ "access": "No se pudo acceder a Enedis.fr, compruebe su conexi\u00f3n a Internet",
+ "enedis": "Enedis.fr respondi\u00f3 con un error: vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11:00 y las 2 de la ma\u00f1ana)",
+ "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 23:00 y las 02:00 horas).",
+ "username_exists": "Cuenta ya configurada",
+ "wrong_login": "Error de inicio de sesi\u00f3n: compruebe su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "username": "Correo electr\u00f3nico"
+ },
+ "description": "Introduzca sus credenciales",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/fr.json b/homeassistant/components/linky/.translations/fr.json
new file mode 100644
index 00000000000000..af12c2b654d8ff
--- /dev/null
+++ b/homeassistant/components/linky/.translations/fr.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "access": "Impossible d'acc\u00e9der \u00e0 Enedis.fr, merci de v\u00e9rifier votre connexion internet",
+ "enedis": "Erreur d'Enedis.fr: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)",
+ "unknown": "Erreur inconnue: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)",
+ "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9",
+ "wrong_login": "Impossible de vous identifier: merci de v\u00e9rifier vos identifiants"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Email"
+ },
+ "description": "Entrez vos identifiants",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/it.json b/homeassistant/components/linky/.translations/it.json
new file mode 100644
index 00000000000000..09d5f7e2d2bcb4
--- /dev/null
+++ b/homeassistant/components/linky/.translations/it.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "Account gi\u00e0 configurato"
+ },
+ "error": {
+ "access": "Impossibile accedere a Enedis.fr, si prega di controllare la connessione internet",
+ "enedis": "Enedis.fr ha risposto con un errore: si prega di riprovare pi\u00f9 tardi (di solito non tra le 23:00 e le 02:00).",
+ "unknown": "Errore sconosciuto: riprova pi\u00f9 tardi (in genere non tra le 23:00 e le 02:00)",
+ "username_exists": "Account gi\u00e0 configurato",
+ "wrong_login": "Errore di accesso: si prega di controllare la tua E-mail e la password"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "E-mail"
+ },
+ "description": "Inserisci le tue credenziali",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/ko.json b/homeassistant/components/linky/.translations/ko.json
new file mode 100644
index 00000000000000..45172e70097596
--- /dev/null
+++ b/homeassistant/components/linky/.translations/ko.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "access": "Enedis.fr \uc5d0 \uc811\uc18d\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc778\ud130\ub137 \uc5f0\uacb0\uc744 \ud655\uc778\ud574\ubcf4\uc138\uc694",
+ "enedis": "Enedis.fr \uc774 \uc624\ub958\ub85c \uc751\ub2f5\ud588\uc2b5\ub2c8\ub2e4: \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694 (\uc800\ub141 11\uc2dc \ubd80\ud130 \uc0c8\ubcbd 2\uc2dc\ub294 \ud53c\ud574\uc8fc\uc138\uc694)",
+ "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958: \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694 (\uc800\ub141 11\uc2dc \ubd80\ud130 \uc0c8\ubcbd 2\uc2dc\ub294 \ud53c\ud574\uc8fc\uc138\uc694)",
+ "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "wrong_login": "\ub85c\uadf8\uc778 \uc624\ub958: \uc774\uba54\uc77c \ubc0f \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc774\uba54\uc77c"
+ },
+ "description": "\uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/lb.json b/homeassistant/components/linky/.translations/lb.json
new file mode 100644
index 00000000000000..cd3c7152c89e7d
--- /dev/null
+++ b/homeassistant/components/linky/.translations/lb.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "Kont ass scho konfigur\u00e9iert"
+ },
+ "error": {
+ "access": "Keng Verbindung zu Enedis.fr, iwwerpr\u00e9ift d'Internet Verbindung",
+ "enedis": "Enedis.fr huet mat engem Feeler ge\u00e4ntwert: prob\u00e9iert sp\u00e9ider nach emol (normalerweis net t\u00ebscht 23h00 an 2h00)",
+ "unknown": "Onbekannte Feeler: prob\u00e9iert sp\u00e9ider nach emol (normalerweis net t\u00ebscht 23h00 an 2h00)",
+ "username_exists": "Kont ass scho konfigur\u00e9iert",
+ "wrong_login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwuert",
+ "username": "E-Mail"
+ },
+ "description": "F\u00ebllt \u00e4r Login Informatiounen aus",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/nl.json b/homeassistant/components/linky/.translations/nl.json
new file mode 100644
index 00000000000000..89759fdf21630e
--- /dev/null
+++ b/homeassistant/components/linky/.translations/nl.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "Account reeds geconfigureerd"
+ },
+ "error": {
+ "access": "Geen toegang tot Enedis.fr, controleer uw internetverbinding",
+ "enedis": "Enedis.fr antwoordde met een fout: probeer het later opnieuw (meestal niet tussen 23.00 en 02.00 uur)",
+ "unknown": "Onbekende fout: probeer het later opnieuw (meestal niet tussen 23.00 en 02.00 uur)",
+ "username_exists": "Account reeds geconfigureerd",
+ "wrong_login": "Aanmeldingsfout: controleer uw e-mailadres en wachtwoord"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Wachtwoord",
+ "username": "E-mail"
+ },
+ "description": "Voer uw gegevens in",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/no.json b/homeassistant/components/linky/.translations/no.json
new file mode 100644
index 00000000000000..c43f434562c152
--- /dev/null
+++ b/homeassistant/components/linky/.translations/no.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "Kontoen er allerede konfigurert"
+ },
+ "error": {
+ "access": "Kunne ikke f\u00e5 tilgang til Enedis.fr, vennligst sjekk internettforbindelsen din",
+ "enedis": "Enedis.fr svarte med en feil: vennligst pr\u00f8v p\u00e5 nytt senere (vanligvis ikke mellom 23:00 og 02:00)",
+ "unknown": "Ukjent feil: pr\u00f8v p\u00e5 nytt senere (vanligvis ikke mellom 23:00 og 02:00)",
+ "username_exists": "Kontoen er allerede konfigurert",
+ "wrong_login": "Innloggingsfeil: vennligst sjekk e-postadressen og passordet ditt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passord",
+ "username": "E-post"
+ },
+ "description": "Skriv inn legitimasjonen din",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/pl.json b/homeassistant/components/linky/.translations/pl.json
new file mode 100644
index 00000000000000..a4f68fa8687f0a
--- /dev/null
+++ b/homeassistant/components/linky/.translations/pl.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "Konto jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "access": "Nie mo\u017cna uzyska\u0107 dost\u0119pu do Enedis.fr, sprawd\u017a po\u0142\u0105czenie internetowe",
+ "enedis": "Enedis.fr odpowiedzia\u0142 b\u0142\u0119dem: spr\u00f3buj ponownie p\u00f3\u017aniej (zwykle nie mi\u0119dzy 23:00, a 2:00)",
+ "unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej (zwykle nie mi\u0119dzy godzin\u0105 23:00, a 2:00)",
+ "username_exists": "Konto jest ju\u017c skonfigurowane",
+ "wrong_login": "B\u0142\u0105d logowania: sprawd\u017a adres e-mail i has\u0142o"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "E-mail"
+ },
+ "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json
new file mode 100644
index 00000000000000..498b5b2f12f29b
--- /dev/null
+++ b/homeassistant/components/linky/.translations/ru.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430"
+ },
+ "error": {
+ "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443",
+ "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)",
+ "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)",
+ "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430",
+ "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/sl.json b/homeassistant/components/linky/.translations/sl.json
new file mode 100644
index 00000000000000..9e9d6668fcb8fb
--- /dev/null
+++ b/homeassistant/components/linky/.translations/sl.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "Ra\u010dun \u017ee nastavljen"
+ },
+ "error": {
+ "access": "Do Enedis.fr ni bilo mogo\u010de dostopati, preverite internetno povezavo",
+ "enedis": "Enedis.fr je odgovoril z napako: poskusite pozneje (ponavadi med 23. in 2. uro)",
+ "unknown": "Neznana napaka: Prosimo, poskusite pozneje (obi\u010dajno ne med 23. in 2. uro)",
+ "username_exists": "Ra\u010dun \u017ee nastavljen",
+ "wrong_login": "Napaka pri prijavi: preverite svoj e-po\u0161tni naslov in geslo"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Geslo",
+ "username": "E-po\u0161tni naslov"
+ },
+ "description": "Vnesite svoje poverilnice",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/zh-Hans.json b/homeassistant/components/linky/.translations/zh-Hans.json
new file mode 100644
index 00000000000000..b450a3cbdb08c7
--- /dev/null
+++ b/homeassistant/components/linky/.translations/zh-Hans.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "\u8d26\u6237\u5df2\u914d\u7f6e\u5b8c\u6210"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u5bc6\u7801",
+ "username": "\u7535\u5b50\u90ae\u7bb1"
+ },
+ "description": "\u8f93\u5165\u60a8\u7684\u8eab\u4efd\u8ba4\u8bc1"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/.translations/zh-Hant.json b/homeassistant/components/linky/.translations/zh-Hant.json
new file mode 100644
index 00000000000000..bcfac6643c8e6f
--- /dev/null
+++ b/homeassistant/components/linky/.translations/zh-Hant.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "abort": {
+ "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "access": "\u7121\u6cd5\u8a2a\u554f Enedis.fr\uff0c\u8acb\u6aa2\u67e5\u60a8\u7684\u7db2\u969b\u7db2\u8def\u9023\u7dda",
+ "enedis": "Endis.fr \u56de\u5831\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66\uff08\u901a\u5e38\u907f\u958b\u591c\u9593 11 - \u51cc\u6668 2 \u9ede\u4e4b\u9593\uff09",
+ "unknown": "\u672a\u77e5\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66\uff08\u901a\u5e38\u907f\u958b\u591c\u9593 11 - \u51cc\u6668 2 \u9ede\u4e4b\u9593\uff09",
+ "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "wrong_login": "\u767b\u5165\u932f\u8aa4\uff1a\u8acb\u78ba\u8a8d\u96fb\u5b50\u90f5\u4ef6\u8207\u79d8\u5bc6\u6b63\u78ba\u6027"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u5bc6\u78bc",
+ "username": "\u96fb\u5b50\u90f5\u4ef6"
+ },
+ "description": "\u8f38\u5165\u6191\u8b49",
+ "title": "Linky"
+ }
+ },
+ "title": "Linky"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py
index bd2d38735d6192..5ff04c5ee70a38 100644
--- a/homeassistant/components/linky/sensor.py
+++ b/homeassistant/components/linky/sensor.py
@@ -18,6 +18,8 @@
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import HomeAssistantType
+from .const import DOMAIN
+
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(hours=4)
@@ -28,17 +30,6 @@
INDEX_LAST = -2
ATTRIBUTION = "Data provided by Enedis"
-SENSORS = {
- "yesterday": ("Linky yesterday", DAILY, INDEX_LAST),
- "current_month": ("Linky current month", MONTHLY, INDEX_CURRENT),
- "last_month": ("Linky last month", MONTHLY, INDEX_LAST),
- "current_year": ("Linky current year", YEARLY, INDEX_CURRENT),
- "last_year": ("Linky last year", YEARLY, INDEX_LAST),
-}
-SENSORS_INDEX_LABEL = 0
-SENSORS_INDEX_SCALE = 1
-SENSORS_INDEX_WHEN = 2
-
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up the Linky platform."""
@@ -114,6 +105,12 @@ def __init__(self, name, account: LinkyAccount, scale, when):
self._username = account.username
self._time = None
self._consumption = None
+ self._unique_id = f"{self._username}_{scale}_{when}"
+
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._unique_id
@property
def name(self):
@@ -144,6 +141,15 @@ def device_state_attributes(self):
CONF_USERNAME: self._username,
}
+ @property
+ def device_info(self):
+ """Return device information."""
+ return {
+ "identifiers": {(DOMAIN, self.unique_id)},
+ "name": self.name,
+ "manufacturer": "Enedis",
+ }
+
async def async_update(self) -> None:
"""Retrieve the new data for the sensor."""
data = self._account.data[self._scale][self._when]
diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py
index 98418d6be81895..c466d71c4c5fd3 100644
--- a/homeassistant/components/liveboxplaytv/media_player.py
+++ b/homeassistant/components/liveboxplaytv/media_player.py
@@ -70,7 +70,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
try:
device = LiveboxPlayTvDevice(host, port, name)
livebox_devices.append(device)
- except IOError:
+ except OSError:
_LOGGER.error(
"Failed to connect to Livebox Play TV at %s:%s. "
"Please check your configuration",
diff --git a/homeassistant/components/locative/.translations/no.json b/homeassistant/components/locative/.translations/no.json
index 00e3337dfe1eeb..8e9b3272f947b3 100644
--- a/homeassistant/components/locative/.translations/no.json
+++ b/homeassistant/components/locative/.translations/no.json
@@ -10,9 +10,9 @@
"step": {
"user": {
"description": "Er du sikker p\u00e5 at du vil sette opp Locative Webhook?",
- "title": "Sett opp Lokative Webhook"
+ "title": "Sett opp Locative Webhook"
}
},
- "title": "Lokative Webhook"
+ "title": "Locative Webhook"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/logi_circle/.translations/it.json b/homeassistant/components/logi_circle/.translations/it.json
index 568bf79a40d207..d7c1d9ba9de7e4 100644
--- a/homeassistant/components/logi_circle/.translations/it.json
+++ b/homeassistant/components/logi_circle/.translations/it.json
@@ -12,10 +12,11 @@
"error": {
"auth_error": "Autorizzazione API fallita.",
"auth_timeout": "Timeout dell'autorizzazione durante la richiesta del token di accesso.",
- "follow_link": "Segui il link e autenticati prima di premere Invio"
+ "follow_link": "Segui il link e autenticati prima di premere Invia"
},
"step": {
"auth": {
+ "description": "Segui il link qui sotto e Accetta l'accesso al tuo account Logi Circle, quindi torna indietro e premi Invia qui sotto. \n\n [Link]({authorization_url})",
"title": "Autenticarsi con Logi Circle"
},
"user": {
diff --git a/homeassistant/components/logi_circle/.translations/pl.json b/homeassistant/components/logi_circle/.translations/pl.json
index f39df48ae5a483..5d8e6a0607df4a 100644
--- a/homeassistant/components/logi_circle/.translations/pl.json
+++ b/homeassistant/components/logi_circle/.translations/pl.json
@@ -16,7 +16,7 @@
},
"step": {
"auth": {
- "description": "Kliknij poni\u017cszy link i Zaakceptuj dost\u0119p do swojego konta Logi Circle, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n\n [Link]({authorization_url})",
+ "description": "Kliknij poni\u017cszy link i Zaakceptuj dost\u0119p do konta Logi Circle, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n\n [Link]({authorization_url})",
"title": "Uwierzytelnij za pomoc\u0105 Logi Circle"
},
"user": {
diff --git a/homeassistant/components/logi_circle/.translations/ru.json b/homeassistant/components/logi_circle/.translations/ru.json
index 1e9c089828fe92..40c7c8853daeb4 100644
--- a/homeassistant/components/logi_circle/.translations/ru.json
+++ b/homeassistant/components/logi_circle/.translations/ru.json
@@ -4,7 +4,7 @@
"already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
"external_error": "\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u043e \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.",
"external_setup": "Logi Circle \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.",
- "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Logi Circle \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/logi_circle/)."
+ "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Logi Circle \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/logi_circle/)."
},
"create_entry": {
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json
index 153f6b5aea6972..dffb4b52667369 100644
--- a/homeassistant/components/luci/manifest.json
+++ b/homeassistant/components/luci/manifest.json
@@ -3,8 +3,7 @@
"name": "Luci",
"documentation": "https://www.home-assistant.io/components/luci",
"requirements": [
- "openwrt-luci-rpc==1.1.0",
- "packaging==19.1"
+ "openwrt-luci-rpc==1.1.1"
],
"dependencies": [],
"codeowners": ["@fbradyirl"]
diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json
index bece55ae09d819..451a6f3e33d822 100644
--- a/homeassistant/components/lutron/manifest.json
+++ b/homeassistant/components/lutron/manifest.json
@@ -3,7 +3,7 @@
"name": "Lutron",
"documentation": "https://www.home-assistant.io/components/lutron",
"requirements": [
- "pylutron==0.2.2"
+ "pylutron==0.2.5"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json
index 419d4b72864400..4e253741b051f8 100644
--- a/homeassistant/components/media_extractor/manifest.json
+++ b/homeassistant/components/media_extractor/manifest.json
@@ -3,7 +3,7 @@
"name": "Media extractor",
"documentation": "https://www.home-assistant.io/components/media_extractor",
"requirements": [
- "youtube_dl==2019.09.01"
+ "youtube_dl==2019.09.12.1"
],
"dependencies": [
"media_player"
diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py
index 4eba4657d9554d..dac08afe471dea 100644
--- a/homeassistant/components/media_player/reproduce_state.py
+++ b/homeassistant/components/media_player/reproduce_state.py
@@ -36,7 +36,7 @@
)
-# mypy: allow-incomplete-defs, allow-untyped-defs
+# mypy: allow-untyped-defs
async def _async_reproduce_states(
@@ -44,7 +44,7 @@ async def _async_reproduce_states(
) -> None:
"""Reproduce component states."""
- async def call_service(service: str, keys: Iterable):
+ async def call_service(service: str, keys: Iterable) -> None:
"""Call service with set of attributes given."""
data = {}
data["entity_id"] = state.entity_id
diff --git a/homeassistant/components/met/.translations/es.json b/homeassistant/components/met/.translations/es.json
index 7659ab4d2962a8..a475518bd851cb 100644
--- a/homeassistant/components/met/.translations/es.json
+++ b/homeassistant/components/met/.translations/es.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "name_exists": "El nombre ya existe"
+ "name_exists": "La ubicaci\u00f3n ya existe"
},
"step": {
"user": {
@@ -14,6 +14,7 @@
"description": "Instituto de meteorolog\u00eda",
"title": "Ubicaci\u00f3n"
}
- }
+ },
+ "title": "Met.no"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/met/.translations/it.json b/homeassistant/components/met/.translations/it.json
new file mode 100644
index 00000000000000..a1cfd12e8cda7c
--- /dev/null
+++ b/homeassistant/components/met/.translations/it.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "La posizione esiste gi\u00e0"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "elevation": "Altitudine",
+ "latitude": "Latitudine",
+ "longitude": "Longitudine",
+ "name": "Nome"
+ },
+ "description": "Meteorologisk institutt",
+ "title": "Posizione"
+ }
+ },
+ "title": "Met.no"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/met/.translations/ko.json b/homeassistant/components/met/.translations/ko.json
index 6900458ba60d15..81a98b9754fe81 100644
--- a/homeassistant/components/met/.translations/ko.json
+++ b/homeassistant/components/met/.translations/ko.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4"
+ "name_exists": "\uc704\uce58\uac00 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4"
},
"step": {
"user": {
diff --git a/homeassistant/components/met/.translations/lb.json b/homeassistant/components/met/.translations/lb.json
index 660f639d859718..9f91d37c23360c 100644
--- a/homeassistant/components/met/.translations/lb.json
+++ b/homeassistant/components/met/.translations/lb.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "name_exists": "Numm g\u00ebtt et schonn"
+ "name_exists": "Standuert g\u00ebtt et schonn"
},
"step": {
"user": {
diff --git a/homeassistant/components/met/.translations/no.json b/homeassistant/components/met/.translations/no.json
index 6ebaa08457f653..9a3ef350ab1082 100644
--- a/homeassistant/components/met/.translations/no.json
+++ b/homeassistant/components/met/.translations/no.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "name_exists": "Navnet eksisterer allerede"
+ "name_exists": "Lokasjonen finnes allerede"
},
"step": {
"user": {
diff --git a/homeassistant/components/met/.translations/ru.json b/homeassistant/components/met/.translations/ru.json
index d298b1e3b07a5c..d92d28d948419d 100644
--- a/homeassistant/components/met/.translations/ru.json
+++ b/homeassistant/components/met/.translations/ru.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442"
+ "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e"
},
"step": {
"user": {
diff --git a/homeassistant/components/met/.translations/sl.json b/homeassistant/components/met/.translations/sl.json
index 5dffbe133e7db6..71ffdaf8509583 100644
--- a/homeassistant/components/met/.translations/sl.json
+++ b/homeassistant/components/met/.translations/sl.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "name_exists": "Ime \u017ee obstaja"
+ "name_exists": "Lokacija \u017ee obstaja"
},
"step": {
"user": {
diff --git a/homeassistant/components/met/.translations/zh-Hans.json b/homeassistant/components/met/.translations/zh-Hans.json
index 9565bb666181d4..9027347174d3e7 100644
--- a/homeassistant/components/met/.translations/zh-Hans.json
+++ b/homeassistant/components/met/.translations/zh-Hans.json
@@ -1,10 +1,15 @@
{
"config": {
+ "error": {
+ "name_exists": "\u4f4d\u7f6e\u5df2\u5b58\u5728"
+ },
"step": {
"user": {
"data": {
+ "elevation": "\u6d77\u62d4",
"latitude": "\u7eac\u5ea6",
- "longitude": "\u7ecf\u5ea6"
+ "longitude": "\u7ecf\u5ea6",
+ "name": "\u540d\u79f0"
},
"title": "\u4f4d\u7f6e"
}
diff --git a/homeassistant/components/met/.translations/zh-Hant.json b/homeassistant/components/met/.translations/zh-Hant.json
index c49c90ee6e422c..de7c34ffc87969 100644
--- a/homeassistant/components/met/.translations/zh-Hant.json
+++ b/homeassistant/components/met/.translations/zh-Hant.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728"
+ "name_exists": "\u8a72\u5ea7\u6a19\u5df2\u5b58\u5728"
},
"step": {
"user": {
diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py
index d6460fd6e5aa01..cfcd78400bd5e8 100644
--- a/homeassistant/components/meteo_france/__init__.py
+++ b/homeassistant/components/meteo_france/__init__.py
@@ -4,70 +4,17 @@
import voluptuous as vol
-from homeassistant.const import CONF_MONITORED_CONDITIONS, TEMP_CELSIUS
+from homeassistant.const import CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.util import Throttle
-_LOGGER = logging.getLogger(__name__)
-
-ATTRIBUTION = "Data provided by Météo-France"
-
-CONF_CITY = "city"
+from .const import DOMAIN, CONF_CITY, SENSOR_TYPES, DATA_METEO_FRANCE
-DATA_METEO_FRANCE = "data_meteo_france"
-DEFAULT_WEATHER_CARD = True
-DOMAIN = "meteo_france"
+_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = datetime.timedelta(minutes=5)
-SENSOR_TYPES = {
- "rain_chance": ["Rain chance", "%"],
- "freeze_chance": ["Freeze chance", "%"],
- "thunder_chance": ["Thunder chance", "%"],
- "snow_chance": ["Snow chance", "%"],
- "weather": ["Weather", None],
- "wind_speed": ["Wind Speed", "km/h"],
- "next_rain": ["Next rain", "min"],
- "temperature": ["Temperature", TEMP_CELSIUS],
- "uv": ["UV", None],
- "weather_alert": ["Weather Alert", None],
-}
-
-CONDITION_CLASSES = {
- "clear-night": ["Nuit Claire"],
- "cloudy": ["Très nuageux"],
- "fog": ["Brume ou bancs de brouillard", "Brouillard", "Brouillard givrant"],
- "hail": ["Risque de grêle"],
- "lightning": ["Risque d'orages", "Orages"],
- "lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"],
- "partlycloudy": ["Ciel voilé", "Ciel voilé nuit", "Éclaircies"],
- "pouring": ["Pluie forte"],
- "rainy": [
- "Bruine / Pluie faible",
- "Bruine",
- "Pluie faible",
- "Pluies éparses / Rares averses",
- "Pluies éparses",
- "Rares averses",
- "Pluie / Averses",
- "Averses",
- "Pluie",
- ],
- "snowy": [
- "Neige / Averses de neige",
- "Neige",
- "Averses de neige",
- "Neige forte",
- "Quelques flocons",
- ],
- "snowy-rainy": ["Pluie et neige", "Pluie verglaçante"],
- "sunny": ["Ensoleillé"],
- "windy": [],
- "windy-variant": [],
- "exceptional": [],
-}
-
def has_all_unique_cities(value):
"""Validate that all cities are unique."""
diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py
new file mode 100644
index 00000000000000..223aca20bac240
--- /dev/null
+++ b/homeassistant/components/meteo_france/const.py
@@ -0,0 +1,112 @@
+"""Meteo-France component constants."""
+
+from homeassistant.const import TEMP_CELSIUS
+
+DOMAIN = "meteo_france"
+DATA_METEO_FRANCE = "data_meteo_france"
+ATTRIBUTION = "Data provided by Météo-France"
+
+CONF_CITY = "city"
+
+DEFAULT_WEATHER_CARD = True
+
+SENSOR_TYPE_NAME = "name"
+SENSOR_TYPE_UNIT = "unit"
+SENSOR_TYPE_ICON = "icon"
+SENSOR_TYPE_CLASS = "device_class"
+SENSOR_TYPES = {
+ "rain_chance": {
+ SENSOR_TYPE_NAME: "Rain chance",
+ SENSOR_TYPE_UNIT: "%",
+ SENSOR_TYPE_ICON: "mdi:weather-rainy",
+ SENSOR_TYPE_CLASS: None,
+ },
+ "freeze_chance": {
+ SENSOR_TYPE_NAME: "Freeze chance",
+ SENSOR_TYPE_UNIT: "%",
+ SENSOR_TYPE_ICON: "mdi:snowflake",
+ SENSOR_TYPE_CLASS: None,
+ },
+ "thunder_chance": {
+ SENSOR_TYPE_NAME: "Thunder chance",
+ SENSOR_TYPE_UNIT: "%",
+ SENSOR_TYPE_ICON: "mdi:weather-lightning",
+ SENSOR_TYPE_CLASS: None,
+ },
+ "snow_chance": {
+ SENSOR_TYPE_NAME: "Snow chance",
+ SENSOR_TYPE_UNIT: "%",
+ SENSOR_TYPE_ICON: "mdi:weather-snowy",
+ SENSOR_TYPE_CLASS: None,
+ },
+ "weather": {
+ SENSOR_TYPE_NAME: "Weather",
+ SENSOR_TYPE_UNIT: None,
+ SENSOR_TYPE_ICON: "mdi:weather-partly-cloudy",
+ SENSOR_TYPE_CLASS: None,
+ },
+ "wind_speed": {
+ SENSOR_TYPE_NAME: "Wind Speed",
+ SENSOR_TYPE_UNIT: "km/h",
+ SENSOR_TYPE_ICON: "mdi:weather-windy",
+ SENSOR_TYPE_CLASS: None,
+ },
+ "next_rain": {
+ SENSOR_TYPE_NAME: "Next rain",
+ SENSOR_TYPE_UNIT: "min",
+ SENSOR_TYPE_ICON: "mdi:weather-rainy",
+ SENSOR_TYPE_CLASS: None,
+ },
+ "temperature": {
+ SENSOR_TYPE_NAME: "Temperature",
+ SENSOR_TYPE_UNIT: TEMP_CELSIUS,
+ SENSOR_TYPE_ICON: "mdi:thermometer",
+ SENSOR_TYPE_CLASS: "temperature",
+ },
+ "uv": {
+ SENSOR_TYPE_NAME: "UV",
+ SENSOR_TYPE_UNIT: None,
+ SENSOR_TYPE_ICON: "mdi:sunglasses",
+ SENSOR_TYPE_CLASS: None,
+ },
+ "weather_alert": {
+ SENSOR_TYPE_NAME: "Weather Alert",
+ SENSOR_TYPE_UNIT: None,
+ SENSOR_TYPE_ICON: "mdi:weather-cloudy-alert",
+ SENSOR_TYPE_CLASS: None,
+ },
+}
+
+CONDITION_CLASSES = {
+ "clear-night": ["Nuit Claire"],
+ "cloudy": ["Très nuageux"],
+ "fog": ["Brume ou bancs de brouillard", "Brouillard", "Brouillard givrant"],
+ "hail": ["Risque de grêle"],
+ "lightning": ["Risque d'orages", "Orages"],
+ "lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"],
+ "partlycloudy": ["Ciel voilé", "Ciel voilé nuit", "Éclaircies"],
+ "pouring": ["Pluie forte"],
+ "rainy": [
+ "Bruine / Pluie faible",
+ "Bruine",
+ "Pluie faible",
+ "Pluies éparses / Rares averses",
+ "Pluies éparses",
+ "Rares averses",
+ "Pluie / Averses",
+ "Averses",
+ "Pluie",
+ ],
+ "snowy": [
+ "Neige / Averses de neige",
+ "Neige",
+ "Averses de neige",
+ "Neige forte",
+ "Quelques flocons",
+ ],
+ "snowy-rainy": ["Pluie et neige", "Pluie verglaçante"],
+ "sunny": ["Ensoleillé"],
+ "windy": [],
+ "windy-variant": [],
+ "exceptional": [],
+}
diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py
index 95113a60cd38ee..8c2bd32048fc00 100644
--- a/homeassistant/components/meteo_france/sensor.py
+++ b/homeassistant/components/meteo_france/sensor.py
@@ -4,7 +4,16 @@
from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS
from homeassistant.helpers.entity import Entity
-from . import ATTRIBUTION, CONF_CITY, DATA_METEO_FRANCE, SENSOR_TYPES
+from .const import (
+ ATTRIBUTION,
+ CONF_CITY,
+ DATA_METEO_FRANCE,
+ SENSOR_TYPES,
+ SENSOR_TYPE_ICON,
+ SENSOR_TYPE_NAME,
+ SENSOR_TYPE_UNIT,
+ SENSOR_TYPE_CLASS,
+)
_LOGGER = logging.getLogger(__name__)
@@ -44,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
alert_watcher = None
else:
_LOGGER.info(
- "Weather alert watcher added for %s" "in department %s",
+ "Weather alert watcher added for %s in department %s",
city,
datas["dept"],
)
@@ -79,7 +88,7 @@ def __init__(self, condition, client, alert_watcher):
@property
def name(self):
"""Return the name of the sensor."""
- return "{} {}".format(self._data["name"], SENSOR_TYPES[self._condition][0])
+ return f"{self._data['name']} {SENSOR_TYPES[self._condition][SENSOR_TYPE_NAME]}"
@property
def state(self):
@@ -111,7 +120,17 @@ def device_state_attributes(self):
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
- return SENSOR_TYPES[self._condition][1]
+ return SENSOR_TYPES[self._condition][SENSOR_TYPE_UNIT]
+
+ @property
+ def icon(self):
+ """Return the icon."""
+ return SENSOR_TYPES[self._condition][SENSOR_TYPE_ICON]
+
+ @property
+ def device_class(self):
+ """Return the device class of the sensor."""
+ return SENSOR_TYPES[self._condition][SENSOR_TYPE_CLASS]
def update(self):
"""Fetch new state data for the sensor."""
diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py
index 9a861d13c2eab4..00da55809ffb64 100644
--- a/homeassistant/components/meteo_france/weather.py
+++ b/homeassistant/components/meteo_france/weather.py
@@ -12,7 +12,7 @@
import homeassistant.util.dt as dt_util
from homeassistant.const import TEMP_CELSIUS
-from . import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE
+from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py
index 86f1462e2cca55..28020a801750f3 100644
--- a/homeassistant/components/miflora/sensor.py
+++ b/homeassistant/components/miflora/sensor.py
@@ -157,7 +157,7 @@ def update(self):
try:
_LOGGER.debug("Polling data for %s", self.name)
data = self.poller.parameter_value(self.parameter)
- except IOError as ioerr:
+ except OSError as ioerr:
_LOGGER.info("Polling error %s", ioerr)
return
except BluetoothBackendException as bterror:
diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py
index 9cd1f1cebc29be..adeba48dbc8517 100644
--- a/homeassistant/components/mitemp_bt/sensor.py
+++ b/homeassistant/components/mitemp_bt/sensor.py
@@ -157,7 +157,7 @@ def update(self):
try:
_LOGGER.debug("Polling data for %s", self.name)
data = self.poller.parameter_value(self.parameter)
- except IOError as ioerr:
+ except OSError as ioerr:
_LOGGER.warning("Polling error %s", ioerr)
return
except BluetoothBackendException as bterror:
diff --git a/homeassistant/components/mobile_app/.translations/it.json b/homeassistant/components/mobile_app/.translations/it.json
index 049e551d19bdfe..37c0deb9c2d8e3 100644
--- a/homeassistant/components/mobile_app/.translations/it.json
+++ b/homeassistant/components/mobile_app/.translations/it.json
@@ -1,11 +1,11 @@
{
"config": {
"abort": {
- "install_app": "Apri l'app per dispositivi mobili per configurare l'integrazione con Home Assistant. Vedi [i documenti] ( {apps_url} ) per un elenco di app compatibili."
+ "install_app": "Apri l'App per dispositivi mobili per configurare l'integrazione con Home Assistant. Vedi [i documenti]({apps_url}) per un elenco di app compatibili."
},
"step": {
"confirm": {
- "description": "Vuoi configurare il componente Mobile App?",
+ "description": "Si desidera configurare il componente App per dispositivi mobili?",
"title": "App per dispositivi mobili"
}
},
diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py
index d16ed23266a412..1e6a0517026255 100644
--- a/homeassistant/components/mobile_app/notify.py
+++ b/homeassistant/components/mobile_app/notify.py
@@ -1,6 +1,5 @@
"""Support for mobile_app push notifications."""
import asyncio
-from datetime import datetime, timezone
import logging
import async_timeout
@@ -60,7 +59,7 @@ def log_rate_limits(hass, device_name, resp, level=logging.INFO):
rate_limits = resp[ATTR_PUSH_RATE_LIMITS]
resetsAt = rate_limits[ATTR_PUSH_RATE_LIMITS_RESETS_AT]
- resetsAtTime = dt_util.parse_datetime(resetsAt) - datetime.now(timezone.utc)
+ resetsAtTime = dt_util.parse_datetime(resetsAt) - dt_util.utcnow()
rate_limit_msg = (
"mobile_app push notification rate limits for %s: "
"%d sent, %d allowed, %d errors, "
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index 75552d1d14b8d7..8d83cd0cc2ba0f 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -10,7 +10,7 @@
import socket
import ssl
import time
-from typing import Any, Callable, List, Optional, Union, cast # noqa: F401
+from typing import Any, Callable, List, Optional, Union
import attr
import requests.certs
@@ -479,7 +479,7 @@ async def _async_setup_server(hass: HomeAssistantType, config: ConfigType):
This method is a coroutine.
"""
- conf = config.get(DOMAIN, {}) # type: ConfigType
+ conf: ConfigType = config.get(DOMAIN, {})
success, broker_config = await server.async_start(
hass, conf.get(CONF_PASSWORD), conf.get(CONF_EMBEDDED)
@@ -502,16 +502,16 @@ async def _async_setup_discovery(
_LOGGER.error("Unable to load MQTT discovery")
return False
- success = await discovery.async_start(
+ success: bool = await discovery.async_start(
hass, conf[CONF_DISCOVERY_PREFIX], hass_config, config_entry
- ) # type: bool
+ )
return success
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Start the MQTT protocol service."""
- conf = config.get(DOMAIN) # type: Optional[ConfigType]
+ conf: Optional[ConfigType] = config.get(DOMAIN)
# We need this because discovery can cause components to be set up and
# otherwise it will not load the users config.
@@ -621,7 +621,7 @@ async def async_setup_entry(hass, entry):
birth_message = None
# Be able to override versions other than TLSv1.0 under Python3.6
- conf_tls_version = conf.get(CONF_TLS_VERSION) # type: str
+ conf_tls_version: str = conf.get(CONF_TLS_VERSION)
if conf_tls_version == "1.2":
tls_version = ssl.PROTOCOL_TLSv1_2
elif conf_tls_version == "1.1":
@@ -655,7 +655,7 @@ async def async_setup_entry(hass, entry):
tls_version=tls_version,
)
- result = await hass.data[DATA_MQTT].async_connect() # type: str
+ result: str = await hass.data[DATA_MQTT].async_connect()
if result == CONNECTION_FAILED:
return False
@@ -671,11 +671,11 @@ async def async_stop_mqtt(event: Event):
async def async_publish_service(call: ServiceCall):
"""Handle MQTT publish service calls."""
- msg_topic = call.data[ATTR_TOPIC] # type: str
+ msg_topic: str = call.data[ATTR_TOPIC]
payload = call.data.get(ATTR_PAYLOAD)
payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE)
- qos = call.data[ATTR_QOS] # type: int
- retain = call.data[ATTR_RETAIN] # type: bool
+ qos: int = call.data[ATTR_QOS]
+ retain: bool = call.data[ATTR_RETAIN]
if payload_template is not None:
try:
payload = template.Template(payload_template, hass).async_render()
@@ -741,14 +741,14 @@ def __init__(
self.broker = broker
self.port = port
self.keepalive = keepalive
- self.subscriptions = [] # type: List[Subscription]
+ self.subscriptions: List[Subscription] = []
self.birth_message = birth_message
self.connected = False
- self._mqttc = None # type: mqtt.Client
+ self._mqttc: mqtt.Client = None
self._paho_lock = asyncio.Lock()
if protocol == PROTOCOL_31:
- proto = mqtt.MQTTv31 # type: int
+ proto: int = mqtt.MQTTv31
else:
proto = mqtt.MQTTv311
@@ -796,7 +796,7 @@ async def async_connect(self) -> str:
This method is a coroutine.
"""
- result = None # type: int
+ result: int = None
try:
result = await self.hass.async_add_job(
self._mqttc.connect, self.broker, self.port, self.keepalive
@@ -870,7 +870,7 @@ async def _async_unsubscribe(self, topic: str) -> None:
This method is a coroutine.
"""
async with self._paho_lock:
- result = None # type: int
+ result: int = None
result, _ = await self.hass.async_add_job(self._mqttc.unsubscribe, topic)
_raise_on_error(result)
@@ -879,7 +879,7 @@ async def _async_perform_subscription(self, topic: str, qos: int) -> None:
_LOGGER.debug("Subscribing to %s", topic)
async with self._paho_lock:
- result = None # type: int
+ result: int = None
result, _ = await self.hass.async_add_job(self._mqttc.subscribe, topic, qos)
_raise_on_error(result)
@@ -928,7 +928,7 @@ def _mqtt_handle_message(self, msg) -> None:
if not _match_topic(subscription.topic, msg.topic):
continue
- payload = msg.payload # type: SubscribePayloadType
+ payload: SubscribePayloadType = msg.payload
if subscription.encoding is not None:
try:
payload = msg.payload.decode(subscription.encoding)
@@ -1077,7 +1077,7 @@ class MqttAvailability(Entity):
def __init__(self, config: dict) -> None:
"""Initialize the availability mixin."""
self._availability_sub_state = None
- self._available = False # type: bool
+ self._available = False
self._avail_config = config
diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py
index 4617fcf054a232..bcf398464bc7c0 100644
--- a/homeassistant/components/mqtt/binary_sensor.py
+++ b/homeassistant/components/mqtt/binary_sensor.py
@@ -1,4 +1,5 @@
"""Support for MQTT binary sensors."""
+from datetime import timedelta
import logging
import voluptuous as vol
@@ -21,7 +22,9 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.helpers.event as evt
+from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+from homeassistant.util import dt as dt_util
from . import (
ATTR_DISCOVERY_HASH,
@@ -43,12 +46,14 @@
DEFAULT_PAYLOAD_OFF = "OFF"
DEFAULT_PAYLOAD_ON = "ON"
DEFAULT_FORCE_UPDATE = False
+CONF_EXPIRE_AFTER = "expire_after"
PLATFORM_SCHEMA = (
mqtt.MQTT_RO_PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OFF_DELAY): vol.All(vol.Coerce(int), vol.Range(min=0)),
@@ -112,8 +117,9 @@ def __init__(self, config, config_entry, discovery_hash):
self._unique_id = config.get(CONF_UNIQUE_ID)
self._state = None
self._sub_state = None
+ self._expiration_trigger = None
self._delay_listener = None
-
+ self._expired = None
device_config = config.get(CONF_DEVICE)
MqttAttributes.__init__(self, config)
@@ -153,6 +159,26 @@ def off_delay_listener(now):
def state_message_received(msg):
"""Handle a new received MQTT state message."""
payload = msg.payload
+ # auto-expire enabled?
+ expire_after = self._config.get(CONF_EXPIRE_AFTER)
+
+ if expire_after is not None and expire_after > 0:
+
+ # When expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message
+ self._expired = False
+
+ # Reset old trigger
+ if self._expiration_trigger:
+ self._expiration_trigger()
+ self._expiration_trigger = None
+
+ # Set new trigger
+ expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after)
+
+ self._expiration_trigger = async_track_point_in_utc_time(
+ self.hass, self.value_is_expired, expiration_at
+ )
+
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
payload = value_template.async_render_with_possible_json_value(
@@ -202,6 +228,15 @@ async def async_will_remove_from_hass(self):
await MqttAttributes.async_will_remove_from_hass(self)
await MqttAvailability.async_will_remove_from_hass(self)
+ @callback
+ def value_is_expired(self, *_):
+ """Triggered when value is expired."""
+
+ self._expiration_trigger = None
+ self._expired = True
+
+ self.async_write_ha_state()
+
@property
def should_poll(self):
"""Return the polling state."""
@@ -231,3 +266,12 @@ def force_update(self):
def unique_id(self):
"""Return a unique ID."""
return self._unique_id
+
+ @property
+ def available(self) -> bool:
+ """Return true if the device is available and value has not expired."""
+ expire_after = self._config.get(CONF_EXPIRE_AFTER)
+ # pylint: disable=no-member
+ return MqttAvailability.available.fget(self) and (
+ expire_after is None or not self._expired
+ )
diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py
index 1df635bbde4ab3..f3ae36c5746855 100644
--- a/homeassistant/components/mqtt/camera.py
+++ b/homeassistant/components/mqtt/camera.py
@@ -7,13 +7,19 @@
from homeassistant.components import camera, mqtt
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
-from homeassistant.const import CONF_NAME
+from homeassistant.const import CONF_NAME, CONF_DEVICE
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
-from . import ATTR_DISCOVERY_HASH, CONF_UNIQUE_ID, MqttDiscoveryUpdate, subscription
+from . import (
+ ATTR_DISCOVERY_HASH,
+ CONF_UNIQUE_ID,
+ MqttDiscoveryUpdate,
+ MqttEntityDeviceInfo,
+ subscription,
+)
from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash
_LOGGER = logging.getLogger(__name__)
@@ -26,6 +32,7 @@
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_UNIQUE_ID): cv.string,
+ vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
}
)
@@ -45,7 +52,9 @@ async def async_discover(discovery_payload):
try:
discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH)
config = PLATFORM_SCHEMA(discovery_payload)
- await _async_setup_entity(config, async_add_entities, discovery_hash)
+ await _async_setup_entity(
+ config, async_add_entities, config_entry, discovery_hash
+ )
except Exception:
if discovery_hash:
clear_discovery_hash(hass, discovery_hash)
@@ -56,15 +65,17 @@ async def async_discover(discovery_payload):
)
-async def _async_setup_entity(config, async_add_entities, discovery_hash=None):
+async def _async_setup_entity(
+ config, async_add_entities, config_entry=None, discovery_hash=None
+):
"""Set up the MQTT Camera."""
- async_add_entities([MqttCamera(config, discovery_hash)])
+ async_add_entities([MqttCamera(config, config_entry, discovery_hash)])
-class MqttCamera(MqttDiscoveryUpdate, Camera):
+class MqttCamera(MqttDiscoveryUpdate, MqttEntityDeviceInfo, Camera):
"""representation of a MQTT camera."""
- def __init__(self, config, discovery_hash):
+ def __init__(self, config, config_entry, discovery_hash):
"""Initialize the MQTT Camera."""
self._config = config
self._unique_id = config.get(CONF_UNIQUE_ID)
@@ -73,8 +84,11 @@ def __init__(self, config, discovery_hash):
self._qos = 0
self._last_image = None
+ device_config = config.get(CONF_DEVICE)
+
Camera.__init__(self)
MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update)
+ MqttEntityDeviceInfo.__init__(self, device_config, config_entry)
async def async_added_to_hass(self):
"""Subscribe MQTT events."""
@@ -85,6 +99,7 @@ async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload)
self._config = config
+ await self.device_info_discovery_update(config)
await self._subscribe_topics()
self.async_write_ha_state()
diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json
index d63d1707fac2eb..2df50699a9d8d3 100644
--- a/homeassistant/components/mqtt/manifest.json
+++ b/homeassistant/components/mqtt/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/mqtt",
"requirements": [
- "hbmqtt==0.9.4",
+ "hbmqtt==0.9.5",
"paho-mqtt==1.4.0"
],
"dependencies": [
diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py
index d12ecd9d3a61a9..45f603a2cb44f8 100644
--- a/homeassistant/components/mysensors/const.py
+++ b/homeassistant/components/mysensors/const.py
@@ -26,72 +26,72 @@
UPDATE_DELAY = 0.1
BINARY_SENSOR_TYPES = {
- "S_DOOR": "V_TRIPPED",
- "S_MOTION": "V_TRIPPED",
- "S_SMOKE": "V_TRIPPED",
- "S_SPRINKLER": "V_TRIPPED",
- "S_WATER_LEAK": "V_TRIPPED",
- "S_SOUND": "V_TRIPPED",
- "S_VIBRATION": "V_TRIPPED",
- "S_MOISTURE": "V_TRIPPED",
+ "S_DOOR": {"V_TRIPPED"},
+ "S_MOTION": {"V_TRIPPED"},
+ "S_SMOKE": {"V_TRIPPED"},
+ "S_SPRINKLER": {"V_TRIPPED"},
+ "S_WATER_LEAK": {"V_TRIPPED"},
+ "S_SOUND": {"V_TRIPPED"},
+ "S_VIBRATION": {"V_TRIPPED"},
+ "S_MOISTURE": {"V_TRIPPED"},
}
-CLIMATE_TYPES = {"S_HVAC": "V_HVAC_FLOW_STATE"}
+CLIMATE_TYPES = {"S_HVAC": {"V_HVAC_FLOW_STATE"}}
-COVER_TYPES = {"S_COVER": ["V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"]}
+COVER_TYPES = {"S_COVER": {"V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"}}
-DEVICE_TRACKER_TYPES = {"S_GPS": "V_POSITION"}
+DEVICE_TRACKER_TYPES = {"S_GPS": {"V_POSITION"}}
LIGHT_TYPES = {
- "S_DIMMER": ["V_DIMMER", "V_PERCENTAGE"],
- "S_RGB_LIGHT": "V_RGB",
- "S_RGBW_LIGHT": "V_RGBW",
+ "S_DIMMER": {"V_DIMMER", "V_PERCENTAGE"},
+ "S_RGB_LIGHT": {"V_RGB"},
+ "S_RGBW_LIGHT": {"V_RGBW"},
}
-NOTIFY_TYPES = {"S_INFO": "V_TEXT"}
+NOTIFY_TYPES = {"S_INFO": {"V_TEXT"}}
SENSOR_TYPES = {
- "S_SOUND": "V_LEVEL",
- "S_VIBRATION": "V_LEVEL",
- "S_MOISTURE": "V_LEVEL",
- "S_INFO": "V_TEXT",
- "S_GPS": "V_POSITION",
- "S_TEMP": "V_TEMP",
- "S_HUM": "V_HUM",
- "S_BARO": ["V_PRESSURE", "V_FORECAST"],
- "S_WIND": ["V_WIND", "V_GUST", "V_DIRECTION"],
- "S_RAIN": ["V_RAIN", "V_RAINRATE"],
- "S_UV": "V_UV",
- "S_WEIGHT": ["V_WEIGHT", "V_IMPEDANCE"],
- "S_POWER": ["V_WATT", "V_KWH", "V_VAR", "V_VA", "V_POWER_FACTOR"],
- "S_DISTANCE": "V_DISTANCE",
- "S_LIGHT_LEVEL": ["V_LIGHT_LEVEL", "V_LEVEL"],
- "S_IR": "V_IR_RECEIVE",
- "S_WATER": ["V_FLOW", "V_VOLUME"],
- "S_CUSTOM": ["V_VAR1", "V_VAR2", "V_VAR3", "V_VAR4", "V_VAR5", "V_CUSTOM"],
- "S_SCENE_CONTROLLER": ["V_SCENE_ON", "V_SCENE_OFF"],
- "S_COLOR_SENSOR": "V_RGB",
- "S_MULTIMETER": ["V_VOLTAGE", "V_CURRENT", "V_IMPEDANCE"],
- "S_GAS": ["V_FLOW", "V_VOLUME"],
- "S_WATER_QUALITY": ["V_TEMP", "V_PH", "V_ORP", "V_EC"],
- "S_AIR_QUALITY": ["V_DUST_LEVEL", "V_LEVEL"],
- "S_DUST": ["V_DUST_LEVEL", "V_LEVEL"],
+ "S_SOUND": {"V_LEVEL"},
+ "S_VIBRATION": {"V_LEVEL"},
+ "S_MOISTURE": {"V_LEVEL"},
+ "S_INFO": {"V_TEXT"},
+ "S_GPS": {"V_POSITION"},
+ "S_TEMP": {"V_TEMP"},
+ "S_HUM": {"V_HUM"},
+ "S_BARO": {"V_PRESSURE", "V_FORECAST"},
+ "S_WIND": {"V_WIND", "V_GUST", "V_DIRECTION"},
+ "S_RAIN": {"V_RAIN", "V_RAINRATE"},
+ "S_UV": {"V_UV"},
+ "S_WEIGHT": {"V_WEIGHT", "V_IMPEDANCE"},
+ "S_POWER": {"V_WATT", "V_KWH", "V_VAR", "V_VA", "V_POWER_FACTOR"},
+ "S_DISTANCE": {"V_DISTANCE"},
+ "S_LIGHT_LEVEL": {"V_LIGHT_LEVEL", "V_LEVEL"},
+ "S_IR": {"V_IR_RECEIVE"},
+ "S_WATER": {"V_FLOW", "V_VOLUME"},
+ "S_CUSTOM": {"V_VAR1", "V_VAR2", "V_VAR3", "V_VAR4", "V_VAR5", "V_CUSTOM"},
+ "S_SCENE_CONTROLLER": {"V_SCENE_ON", "V_SCENE_OFF"},
+ "S_COLOR_SENSOR": {"V_RGB"},
+ "S_MULTIMETER": {"V_VOLTAGE", "V_CURRENT", "V_IMPEDANCE"},
+ "S_GAS": {"V_FLOW", "V_VOLUME"},
+ "S_WATER_QUALITY": {"V_TEMP", "V_PH", "V_ORP", "V_EC"},
+ "S_AIR_QUALITY": {"V_DUST_LEVEL", "V_LEVEL"},
+ "S_DUST": {"V_DUST_LEVEL", "V_LEVEL"},
}
SWITCH_TYPES = {
- "S_LIGHT": "V_LIGHT",
- "S_BINARY": "V_STATUS",
- "S_DOOR": "V_ARMED",
- "S_MOTION": "V_ARMED",
- "S_SMOKE": "V_ARMED",
- "S_SPRINKLER": "V_STATUS",
- "S_WATER_LEAK": "V_ARMED",
- "S_SOUND": "V_ARMED",
- "S_VIBRATION": "V_ARMED",
- "S_MOISTURE": "V_ARMED",
- "S_IR": "V_IR_SEND",
- "S_LOCK": "V_LOCK_STATUS",
- "S_WATER_QUALITY": "V_STATUS",
+ "S_LIGHT": {"V_LIGHT"},
+ "S_BINARY": {"V_STATUS"},
+ "S_DOOR": {"V_ARMED"},
+ "S_MOTION": {"V_ARMED"},
+ "S_SMOKE": {"V_ARMED"},
+ "S_SPRINKLER": {"V_STATUS"},
+ "S_WATER_LEAK": {"V_ARMED"},
+ "S_SOUND": {"V_ARMED"},
+ "S_VIBRATION": {"V_ARMED"},
+ "S_MOISTURE": {"V_ARMED"},
+ "S_IR": {"V_IR_SEND"},
+ "S_LOCK": {"V_LOCK_STATUS"},
+ "S_WATER_QUALITY": {"V_STATUS"},
}
diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py
index fda89158293bb2..f0e9b06b762a4c 100644
--- a/homeassistant/components/mysensors/helpers.py
+++ b/homeassistant/components/mysensors/helpers.py
@@ -121,20 +121,23 @@ def validate_child(gateway, node_id, child, value_type=None):
child_type_name = next(
(member.name for member in pres if member.value == child.type), None
)
- value_types = [value_type] if value_type else [*child.values]
- value_type_names = [
+ value_types = {value_type} if value_type else {*child.values}
+ value_type_names = {
member.name for member in set_req if member.value in value_types
- ]
+ }
platforms = TYPE_TO_PLATFORMS.get(child_type_name, [])
if not platforms:
_LOGGER.warning("Child type %s is not supported", child.type)
return validated
for platform in platforms:
- v_names = FLAT_PLATFORM_TYPES[platform, child_type_name]
- if not isinstance(v_names, list):
- v_names = [v_names]
- v_names = [v_name for v_name in v_names if v_name in value_type_names]
+ platform_v_names = FLAT_PLATFORM_TYPES[platform, child_type_name]
+ v_names = platform_v_names & value_type_names
+ if not v_names:
+ child_value_names = {
+ member.name for member in set_req if member.value in child.values
+ }
+ v_names = platform_v_names & child_value_names
for v_name in v_names:
child_schema_gen = SCHEMAS.get((platform, v_name), default_schema)
diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json
index 1c24acd96e427a..ac88ed224edb26 100644
--- a/homeassistant/components/nest/.translations/ru.json
+++ b/homeassistant/components/nest/.translations/ru.json
@@ -4,7 +4,7 @@
"already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.",
"authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
"authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
- "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Nest \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)."
+ "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Nest \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)."
},
"error": {
"internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430",
diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json
index 609ea72cc699c3..99ca3cb1ccfb82 100644
--- a/homeassistant/components/netgear_lte/manifest.json
+++ b/homeassistant/components/netgear_lte/manifest.json
@@ -6,7 +6,5 @@
"eternalegypt==0.0.10"
],
"dependencies": [],
- "codeowners": [
- "@amelchio"
- ]
+ "codeowners": []
}
diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json
index 63bdbf8a928e35..5c5a095c8f4cef 100644
--- a/homeassistant/components/nextbus/manifest.json
+++ b/homeassistant/components/nextbus/manifest.json
@@ -4,5 +4,5 @@
"documentation": "https://www.home-assistant.io/components/nextbus",
"dependencies": [],
"codeowners": ["@vividboarder"],
- "requirements": ["py_nextbus==0.1.2"]
+ "requirements": ["py_nextbusnext==0.1.4"]
}
diff --git a/homeassistant/components/notion/.translations/it.json b/homeassistant/components/notion/.translations/it.json
new file mode 100644
index 00000000000000..035c0c38952707
--- /dev/null
+++ b/homeassistant/components/notion/.translations/it.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Nome utente gi\u00e0 registrato",
+ "invalid_credentials": "Nome utente o password non validi",
+ "no_devices": "Nessun dispositivo trovato nell'account"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Nome utente / indirizzo E-mail"
+ },
+ "title": "Inserisci le tue informazioni"
+ }
+ },
+ "title": "Nozione"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/notion/.translations/ko.json b/homeassistant/components/notion/.translations/ko.json
index 32eb4b688558a0..76dc91cf46b0a1 100644
--- a/homeassistant/components/notion/.translations/ko.json
+++ b/homeassistant/components/notion/.translations/ko.json
@@ -3,7 +3,7 @@
"error": {
"identifier_exists": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
- "no_devices": "\uacc4\uc815\uc5d0 \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4"
+ "no_devices": "\uacc4\uc815\uc5d0 \ub4f1\ub85d\ub41c \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
diff --git a/homeassistant/components/notion/.translations/pl.json b/homeassistant/components/notion/.translations/pl.json
index c35de9c535c1d5..380d4ad151e6d6 100644
--- a/homeassistant/components/notion/.translations/pl.json
+++ b/homeassistant/components/notion/.translations/pl.json
@@ -9,9 +9,9 @@
"user": {
"data": {
"password": "Has\u0142o",
- "username": "Nazwa u\u017cytkownika/adres e-mail"
+ "username": "Nazwa u\u017cytkownika / adres e-mail"
},
- "title": "Wprowad\u017a swoje dane"
+ "title": "Wprowad\u017a dane"
}
},
"title": "Poj\u0119cie"
diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json
index f43fbeb58b7b0e..c7e89c368c178c 100644
--- a/homeassistant/components/notion/.translations/ru.json
+++ b/homeassistant/components/notion/.translations/ru.json
@@ -3,7 +3,7 @@
"error": {
"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\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c",
- "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b"
+ "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e"
},
"step": {
"user": {
diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py
index 2e15ac8a68d052..c8b19082585c12 100644
--- a/homeassistant/components/nuki/__init__.py
+++ b/homeassistant/components/nuki/__init__.py
@@ -1 +1,3 @@
"""The nuki component."""
+
+DOMAIN = "nuki"
diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py
index 31a655dfeddd93..7fda26b290041d 100644
--- a/homeassistant/components/nuki/lock.py
+++ b/homeassistant/components/nuki/lock.py
@@ -1,20 +1,18 @@
"""Nuki.io lock platform."""
from datetime import timedelta
import logging
-import requests
+from pynuki import NukiBridge
+from requests.exceptions import RequestException
import voluptuous as vol
-from homeassistant.components.lock import (
- DOMAIN,
- PLATFORM_SCHEMA,
- LockDevice,
- SUPPORT_OPEN,
-)
+from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockDevice
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, CONF_TOKEN
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.service import extract_entity_ids
+from . import DOMAIN
+
_LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 8080
@@ -30,7 +28,8 @@
NUKI_DATA = "nuki"
SERVICE_LOCK_N_GO = "lock_n_go"
-SERVICE_CHECK_CONNECTION = "check_connection"
+
+ERROR_STATES = (0, 254, 255)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -47,48 +46,30 @@
}
)
-CHECK_CONNECTION_SERVICE_SCHEMA = vol.Schema(
- {vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}
-)
-
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Nuki lock platform."""
- from pynuki import NukiBridge
-
bridge = NukiBridge(
config[CONF_HOST], config[CONF_TOKEN], config[CONF_PORT], DEFAULT_TIMEOUT
)
- add_entities([NukiLock(lock) for lock in bridge.locks])
+ devices = [NukiLock(lock) for lock in bridge.locks]
def service_handler(service):
"""Service handler for nuki services."""
entity_ids = extract_entity_ids(hass, service)
- all_locks = hass.data[NUKI_DATA][DOMAIN]
- target_locks = []
- if not entity_ids:
- target_locks = all_locks
- else:
- for lock in all_locks:
- if lock.entity_id in entity_ids:
- target_locks.append(lock)
- for lock in target_locks:
- if service.service == SERVICE_LOCK_N_GO:
- unlatch = service.data[ATTR_UNLATCH]
- lock.lock_n_go(unlatch=unlatch)
- elif service.service == SERVICE_CHECK_CONNECTION:
- lock.check_connection()
+ unlatch = service.data[ATTR_UNLATCH]
+
+ for lock in devices:
+ if lock.entity_id not in entity_ids:
+ continue
+ lock.lock_n_go(unlatch=unlatch)
hass.services.register(
- "nuki", SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA
- )
- hass.services.register(
- "nuki",
- SERVICE_CHECK_CONNECTION,
- service_handler,
- schema=CHECK_CONNECTION_SERVICE_SCHEMA,
+ DOMAIN, SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA
)
+ add_entities(devices)
+
class NukiLock(LockDevice):
"""Representation of a Nuki lock."""
@@ -99,15 +80,7 @@ def __init__(self, nuki_lock):
self._locked = nuki_lock.is_locked
self._name = nuki_lock.name
self._battery_critical = nuki_lock.battery_critical
- self._available = nuki_lock.state != 255
-
- async def async_added_to_hass(self):
- """Call when entity is added to hass."""
- if NUKI_DATA not in self.hass.data:
- self.hass.data[NUKI_DATA] = {}
- if DOMAIN not in self.hass.data[NUKI_DATA]:
- self.hass.data[NUKI_DATA][DOMAIN] = []
- self.hass.data[NUKI_DATA][DOMAIN].append(self)
+ self._available = nuki_lock.state not in ERROR_STATES
@property
def name(self):
@@ -140,13 +113,19 @@ def available(self) -> bool:
def update(self):
"""Update the nuki lock properties."""
- try:
- self._nuki_lock.update(aggressive=False)
- except requests.exceptions.RequestException:
- self._available = False
- return
+ for level in (False, True):
+ try:
+ self._nuki_lock.update(aggressive=level)
+ except RequestException:
+ _LOGGER.warning("Network issues detect with %s", self.name)
+ self._available = False
+ return
+
+ # If in error state, we force an update and repoll data
+ self._available = self._nuki_lock.state not in ERROR_STATES
+ if self._available:
+ break
- self._available = self._nuki_lock.state != 255
self._name = self._nuki_lock.name
self._locked = self._nuki_lock.is_locked
self._battery_critical = self._nuki_lock.battery_critical
@@ -170,12 +149,3 @@ def lock_n_go(self, unlatch=False, **kwargs):
amount of time depending on the lock settings) and relock.
"""
self._nuki_lock.lock_n_go(unlatch, kwargs)
-
- def check_connection(self, **kwargs):
- """Update the nuki lock properties."""
- try:
- self._nuki_lock.update(aggressive=True)
- except requests.exceptions.RequestException:
- self._available = False
- else:
- self._available = self._nuki_lock.state != 255
diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json
index 932b80690c4d2c..e7f078a1a0594a 100644
--- a/homeassistant/components/nuki/manifest.json
+++ b/homeassistant/components/nuki/manifest.json
@@ -2,11 +2,7 @@
"domain": "nuki",
"name": "Nuki",
"documentation": "https://www.home-assistant.io/components/nuki",
- "requirements": [
- "pynuki==1.3.3"
- ],
+ "requirements": ["pynuki==1.3.3"],
"dependencies": [],
- "codeowners": [
- "@pschmitt"
- ]
+ "codeowners": ["@pvizeli"]
}
diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json
index b0e5fdb208844a..bad90d9e827d77 100644
--- a/homeassistant/components/nws/manifest.json
+++ b/homeassistant/components/nws/manifest.json
@@ -4,5 +4,5 @@
"documentation": "https://www.home-assistant.io/components/nws",
"dependencies": [],
"codeowners": ["@MatthewFlamm"],
- "requirements": ["pynws==0.7.4"]
+ "requirements": ["pynws==0.8.1"]
}
diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py
index 2480daf2ead090..37744dce180342 100644
--- a/homeassistant/components/nzbget/__init__.py
+++ b/homeassistant/components/nzbget/__init__.py
@@ -1 +1,105 @@
"""The nzbget component."""
+from datetime import timedelta
+import logging
+
+import pynzbgetapi
+import voluptuous as vol
+
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_NAME,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_SCAN_INTERVAL,
+ CONF_SSL,
+ CONF_USERNAME,
+)
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.dispatcher import dispatcher_send
+from homeassistant.helpers.event import track_time_interval
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = "nzbget"
+DATA_NZBGET = "data_nzbget"
+DATA_UPDATED = "nzbget_data_updated"
+
+DEFAULT_NAME = "NZBGet"
+DEFAULT_PORT = 6789
+
+DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_USERNAME): cv.string,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(
+ CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
+ ): cv.time_period,
+ vol.Optional(CONF_SSL, default=False): cv.boolean,
+ }
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+def setup(hass, config):
+ """Set up the NZBGet sensors."""
+ host = config[DOMAIN][CONF_HOST]
+ port = config[DOMAIN][CONF_PORT]
+ ssl = "s" if config[DOMAIN][CONF_SSL] else ""
+ name = config[DOMAIN][CONF_NAME]
+ username = config[DOMAIN].get(CONF_USERNAME)
+ password = config[DOMAIN].get(CONF_PASSWORD)
+ scan_interval = config[DOMAIN][CONF_SCAN_INTERVAL]
+
+ try:
+ nzbget_api = pynzbgetapi.NZBGetAPI(host, username, password, ssl, ssl, port)
+ nzbget_api.version()
+ except pynzbgetapi.NZBGetAPIException as conn_err:
+ _LOGGER.error("Error setting up NZBGet API: %s", conn_err)
+ return False
+
+ _LOGGER.debug("Successfully validated NZBGet API connection")
+
+ nzbget_data = hass.data[DATA_NZBGET] = NZBGetData(hass, nzbget_api)
+ nzbget_data.update()
+
+ def refresh(event_time):
+ """Get the latest data from NZBGet."""
+ nzbget_data.update()
+
+ track_time_interval(hass, refresh, scan_interval)
+
+ sensorconfig = {"client_name": name}
+
+ hass.helpers.discovery.load_platform("sensor", DOMAIN, sensorconfig, config)
+
+ return True
+
+
+class NZBGetData:
+ """Get the latest data and update the states."""
+
+ def __init__(self, hass, api):
+ """Initialize the NZBGet RPC API."""
+ self.hass = hass
+ self.status = None
+ self.available = True
+ self._api = api
+
+ def update(self):
+ """Get the latest data from NZBGet instance."""
+ try:
+ self.status = self._api.status()
+ self.available = True
+ dispatcher_send(self.hass, DATA_UPDATED)
+ except pynzbgetapi.NZBGetAPIException:
+ self.available = False
+ _LOGGER.error("Unable to refresh NZBGet data")
diff --git a/homeassistant/components/nzbget/manifest.json b/homeassistant/components/nzbget/manifest.json
index 69293ede516aee..17b11d6aef9fdc 100644
--- a/homeassistant/components/nzbget/manifest.json
+++ b/homeassistant/components/nzbget/manifest.json
@@ -2,7 +2,7 @@
"domain": "nzbget",
"name": "Nzbget",
"documentation": "https://www.home-assistant.io/components/nzbget",
- "requirements": [],
+ "requirements": ["pynzbgetapi==0.2.0"],
"dependencies": [],
- "codeowners": []
+ "codeowners": ["@chriscla"]
}
diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py
index 73643a5383cea1..ce1fda0839e10b 100644
--- a/homeassistant/components/nzbget/sensor.py
+++ b/homeassistant/components/nzbget/sensor.py
@@ -1,32 +1,15 @@
-"""Support for monitoring NZBGet NZB client."""
-from datetime import timedelta
+"""Monitor the NZBGet API."""
import logging
-from aiohttp.hdrs import CONTENT_TYPE
-import requests
-import voluptuous as vol
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
- CONF_SSL,
- CONF_HOST,
- CONF_NAME,
- CONF_PORT,
- CONF_PASSWORD,
- CONF_USERNAME,
- CONTENT_TYPE_JSON,
- CONF_MONITORED_VARIABLES,
-)
-import homeassistant.helpers.config_validation as cv
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle
+
+from . import DATA_NZBGET, DATA_UPDATED
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "NZBGet"
-DEFAULT_PORT = 6789
-
-MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
SENSOR_TYPES = {
"article_cache": ["ArticleCacheMB", "Article Cache", "MB"],
@@ -40,66 +23,39 @@
"uptime": ["UpTimeSec", "Uptime", "min"],
}
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_MONITORED_VARIABLES, default=["download_rate"]): vol.All(
- cv.ensure_list, [vol.In(SENSOR_TYPES)]
- ),
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
- vol.Optional(CONF_SSL, default=False): cv.boolean,
- vol.Optional(CONF_USERNAME): cv.string,
- }
-)
-
def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the NZBGet sensors."""
- host = config.get(CONF_HOST)
- port = config.get(CONF_PORT)
- ssl = "s" if config.get(CONF_SSL) else ""
- name = config.get(CONF_NAME)
- username = config.get(CONF_USERNAME)
- password = config.get(CONF_PASSWORD)
- monitored_types = config.get(CONF_MONITORED_VARIABLES)
-
- url = f"http{ssl}://{host}:{port}/jsonrpc"
-
- try:
- nzbgetapi = NZBGetAPI(api_url=url, username=username, password=password)
- nzbgetapi.update()
- except (
- requests.exceptions.ConnectionError,
- requests.exceptions.HTTPError,
- ) as conn_err:
- _LOGGER.error("Error setting up NZBGet API: %s", conn_err)
- return False
+ """Create NZBGet sensors."""
+
+ if discovery_info is None:
+ return
+
+ nzbget_data = hass.data[DATA_NZBGET]
+ name = discovery_info["client_name"]
devices = []
- for ng_type in monitored_types:
+ for sensor_type, sensor_config in SENSOR_TYPES.items():
new_sensor = NZBGetSensor(
- api=nzbgetapi, sensor_type=SENSOR_TYPES.get(ng_type), client_name=name
+ nzbget_data, sensor_type, name, sensor_config[0], sensor_config[1]
)
devices.append(new_sensor)
- add_entities(devices)
+ add_entities(devices, True)
class NZBGetSensor(Entity):
"""Representation of a NZBGet sensor."""
- def __init__(self, api, sensor_type, client_name):
+ def __init__(
+ self, nzbget_data, sensor_type, client_name, sensor_name, unit_of_measurement
+ ):
"""Initialize a new NZBGet sensor."""
- self._name = "{} {}".format(client_name, sensor_type[1])
- self.type = sensor_type[0]
+ self._name = f"{client_name} {sensor_type}"
+ self.type = sensor_name
self.client_name = client_name
- self.api = api
+ self.nzbget_data = nzbget_data
self._state = None
- self._unit_of_measurement = sensor_type[2]
- self.update()
- _LOGGER.debug("Created NZBGet sensor: %s", self.type)
+ self._unit_of_measurement = unit_of_measurement
@property
def name(self):
@@ -116,21 +72,31 @@ def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
+ @property
+ def available(self):
+ """Return whether the sensor is available."""
+ return self.nzbget_data.available
+
+ async def async_added_to_hass(self):
+ """Handle entity which will be added."""
+ async_dispatcher_connect(
+ self.hass, DATA_UPDATED, self._schedule_immediate_update
+ )
+
+ @callback
+ def _schedule_immediate_update(self):
+ self.async_schedule_update_ha_state(True)
+
def update(self):
"""Update state of sensor."""
- try:
- self.api.update()
- except requests.exceptions.ConnectionError:
- # Error calling the API, already logged in api.update()
- return
- if self.api.status is None:
+ if self.nzbget_data.status is None:
_LOGGER.debug(
"Update of %s requested, but no status is available", self._name
)
return
- value = self.api.status.get(self.type)
+ value = self.nzbget_data.status.get(self.type)
if value is None:
_LOGGER.warning("Unable to locate value for %s", self.type)
return
@@ -143,48 +109,3 @@ def update(self):
self._state = round(value / 60, 2)
else:
self._state = value
-
-
-class NZBGetAPI:
- """Simple JSON-RPC wrapper for NZBGet's API."""
-
- def __init__(self, api_url, username=None, password=None):
- """Initialize NZBGet API and set headers needed later."""
- self.api_url = api_url
- self.status = None
- self.headers = {CONTENT_TYPE: CONTENT_TYPE_JSON}
-
- if username is not None and password is not None:
- self.auth = (username, password)
- else:
- self.auth = None
- self.update()
-
- def post(self, method, params=None):
- """Send a POST request and return the response as a dict."""
- payload = {"method": method}
-
- if params:
- payload["params"] = params
- try:
- response = requests.post(
- self.api_url,
- json=payload,
- auth=self.auth,
- headers=self.headers,
- timeout=5,
- )
- response.raise_for_status()
- return response.json()
- except requests.exceptions.ConnectionError as conn_exc:
- _LOGGER.error(
- "Failed to update NZBGet status from %s. Error: %s",
- self.api_url,
- conn_exc,
- )
- raise
-
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- def update(self):
- """Update cached response."""
- self.status = self.post("status")["result"]
diff --git a/homeassistant/components/obihai/__init__.py b/homeassistant/components/obihai/__init__.py
new file mode 100644
index 00000000000000..8e65423b73bb3c
--- /dev/null
+++ b/homeassistant/components/obihai/__init__.py
@@ -0,0 +1 @@
+"""The Obihai integration."""
diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json
new file mode 100644
index 00000000000000..e7706b0435ceec
--- /dev/null
+++ b/homeassistant/components/obihai/manifest.json
@@ -0,0 +1,11 @@
+{
+ "domain": "obihai",
+ "name": "Obihai",
+ "documentation": "https://www.home-assistant.io/components/obihai",
+ "requirements": [
+ "pyobihai==1.1.0"
+ ],
+ "dependencies": [],
+ "codeowners": ["@dshokouhi"]
+ }
+
\ No newline at end of file
diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py
new file mode 100644
index 00000000000000..4eb3881e95bf37
--- /dev/null
+++ b/homeassistant/components/obihai/sensor.py
@@ -0,0 +1,104 @@
+"""Support for Obihai Sensors."""
+import logging
+
+from datetime import timedelta
+import voluptuous as vol
+
+from pyobihai import PyObihai
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_USERNAME,
+ DEVICE_CLASS_TIMESTAMP,
+)
+
+from homeassistant.helpers.entity import Entity
+import homeassistant.helpers.config_validation as cv
+
+
+_LOGGER = logging.getLogger(__name__)
+
+SCAN_INTERVAL = timedelta(seconds=5)
+
+OBIHAI = "Obihai"
+DEFAULT_USERNAME = "admin"
+DEFAULT_PASSWORD = "admin"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
+ vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
+ }
+)
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the Obihai sensor platform."""
+
+ username = config[CONF_USERNAME]
+ password = config[CONF_PASSWORD]
+ host = config[CONF_HOST]
+
+ sensors = []
+
+ pyobihai = PyObihai()
+
+ services = pyobihai.get_state(host, username, password)
+
+ line_services = pyobihai.get_line_state(host, username, password)
+
+ for key in services:
+ sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key))
+
+ for key in line_services:
+ sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key))
+
+ add_entities(sensors)
+
+
+class ObihaiServiceSensors(Entity):
+ """Get the status of each Obihai Lines."""
+
+ def __init__(self, pyobihai, host, username, password, service_name):
+ """Initialize monitor sensor."""
+ self._host = host
+ self._username = username
+ self._password = password
+ self._service_name = service_name
+ self._state = None
+ self._name = f"{OBIHAI} {self._service_name}"
+ self._pyobihai = pyobihai
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def device_class(self):
+ """Return the device class for uptime sensor."""
+ if self._service_name == "Last Reboot":
+ return DEVICE_CLASS_TIMESTAMP
+ return None
+
+ def update(self):
+ """Update the sensor."""
+ services = self._pyobihai.get_state(self._host, self._username, self._password)
+
+ if self._service_name in services:
+ self._state = services.get(self._service_name)
+
+ services = self._pyobihai.get_line_state(
+ self._host, self._username, self._password
+ )
+
+ if self._service_name in services:
+ self._state = services.get(self._service_name)
diff --git a/homeassistant/components/ombi/__init__.py b/homeassistant/components/ombi/__init__.py
new file mode 100644
index 00000000000000..860c7d4dcb4df0
--- /dev/null
+++ b/homeassistant/components/ombi/__init__.py
@@ -0,0 +1,149 @@
+"""Support for Ombi."""
+import logging
+
+import pyombi
+import voluptuous as vol
+
+from homeassistant.const import (
+ CONF_API_KEY,
+ CONF_HOST,
+ CONF_PORT,
+ CONF_SSL,
+ CONF_USERNAME,
+)
+import homeassistant.helpers.config_validation as cv
+
+from .const import (
+ ATTR_NAME,
+ ATTR_SEASON,
+ CONF_URLBASE,
+ DEFAULT_PORT,
+ DEFAULT_SEASON,
+ DEFAULT_SSL,
+ DEFAULT_URLBASE,
+ DOMAIN,
+ SERVICE_MOVIE_REQUEST,
+ SERVICE_MUSIC_REQUEST,
+ SERVICE_TV_REQUEST,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def urlbase(value) -> str:
+ """Validate and transform urlbase."""
+ if value is None:
+ raise vol.Invalid("string value is None")
+ value = str(value).strip("/")
+ if not value:
+ return value
+ return value + "/"
+
+
+SUBMIT_MOVIE_REQUEST_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): cv.string})
+
+SUBMIT_MUSIC_REQUEST_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): cv.string})
+
+SUBMIT_TV_REQUEST_SERVICE_SCHEMA = vol.Schema(
+ {
+ vol.Required(ATTR_NAME): cv.string,
+ vol.Optional(ATTR_SEASON, default=DEFAULT_SEASON): vol.In(
+ ["first", "latest", "all"]
+ ),
+ }
+)
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_API_KEY): cv.string,
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+ vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): urlbase,
+ vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
+ }
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+def setup(hass, config):
+ """Set up the Ombi component platform."""
+
+ ombi = pyombi.Ombi(
+ ssl=config[DOMAIN][CONF_SSL],
+ host=config[DOMAIN][CONF_HOST],
+ port=config[DOMAIN][CONF_PORT],
+ api_key=config[DOMAIN][CONF_API_KEY],
+ username=config[DOMAIN][CONF_USERNAME],
+ urlbase=config[DOMAIN][CONF_URLBASE],
+ )
+
+ try:
+ ombi.test_connection()
+ except pyombi.OmbiError as err:
+ _LOGGER.warning("Unable to setup Ombi: %s", err)
+ return False
+
+ hass.data[DOMAIN] = {"instance": ombi}
+
+ def submit_movie_request(call):
+ """Submit request for movie."""
+ name = call.data[ATTR_NAME]
+ movies = ombi.search_movie(name)
+ if movies:
+ movie = movies[0]
+ ombi.request_movie(movie["theMovieDbId"])
+ else:
+ raise Warning("No movie found.")
+
+ def submit_tv_request(call):
+ """Submit request for TV show."""
+ name = call.data[ATTR_NAME]
+ tv_shows = ombi.search_tv(name)
+
+ if tv_shows:
+ season = call.data[ATTR_SEASON]
+ show = tv_shows[0]["id"]
+ if season == "first":
+ ombi.request_tv(show, request_first=True)
+ elif season == "latest":
+ ombi.request_tv(show, request_latest=True)
+ elif season == "all":
+ ombi.request_tv(show, request_all=True)
+ else:
+ raise Warning("No TV show found.")
+
+ def submit_music_request(call):
+ """Submit request for music album."""
+ name = call.data[ATTR_NAME]
+ music = ombi.search_music_album(name)
+ if music:
+ ombi.request_music(music[0]["foreignAlbumId"])
+ else:
+ raise Warning("No music album found.")
+
+ hass.services.register(
+ DOMAIN,
+ SERVICE_MOVIE_REQUEST,
+ submit_movie_request,
+ schema=SUBMIT_MOVIE_REQUEST_SERVICE_SCHEMA,
+ )
+ hass.services.register(
+ DOMAIN,
+ SERVICE_MUSIC_REQUEST,
+ submit_music_request,
+ schema=SUBMIT_MUSIC_REQUEST_SERVICE_SCHEMA,
+ )
+ hass.services.register(
+ DOMAIN,
+ SERVICE_TV_REQUEST,
+ submit_tv_request,
+ schema=SUBMIT_TV_REQUEST_SERVICE_SCHEMA,
+ )
+ hass.helpers.discovery.load_platform("sensor", DOMAIN, {}, config)
+
+ return True
diff --git a/homeassistant/components/ombi/const.py b/homeassistant/components/ombi/const.py
new file mode 100644
index 00000000000000..42b58e7f50d631
--- /dev/null
+++ b/homeassistant/components/ombi/const.py
@@ -0,0 +1,24 @@
+"""Support for Ombi."""
+ATTR_NAME = "name"
+ATTR_SEASON = "season"
+
+CONF_URLBASE = "urlbase"
+
+DEFAULT_NAME = DOMAIN = "ombi"
+DEFAULT_PORT = 5000
+DEFAULT_SEASON = "latest"
+DEFAULT_SSL = False
+DEFAULT_URLBASE = ""
+
+SERVICE_MOVIE_REQUEST = "submit_movie_request"
+SERVICE_MUSIC_REQUEST = "submit_music_request"
+SERVICE_TV_REQUEST = "submit_tv_request"
+
+SENSOR_TYPES = {
+ "movies": {"type": "Movie requests", "icon": "mdi:movie"},
+ "tv": {"type": "TV show requests", "icon": "mdi:television-classic"},
+ "music": {"type": "Music album requests", "icon": "mdi:album"},
+ "pending": {"type": "Pending requests", "icon": "mdi:clock-alert-outline"},
+ "approved": {"type": "Approved requests", "icon": "mdi:check"},
+ "available": {"type": "Available requests", "icon": "mdi:download"},
+}
diff --git a/homeassistant/components/ombi/manifest.json b/homeassistant/components/ombi/manifest.json
new file mode 100644
index 00000000000000..066f3270ccdd5b
--- /dev/null
+++ b/homeassistant/components/ombi/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "ombi",
+ "name": "Ombi",
+ "documentation": "https://www.home-assistant.io/components/ombi/",
+ "dependencies": [],
+ "codeowners": ["@larssont"],
+ "requirements": ["pyombi==0.1.5"]
+}
diff --git a/homeassistant/components/ombi/sensor.py b/homeassistant/components/ombi/sensor.py
new file mode 100644
index 00000000000000..2a2f50532b4e23
--- /dev/null
+++ b/homeassistant/components/ombi/sensor.py
@@ -0,0 +1,77 @@
+"""Support for Ombi."""
+from datetime import timedelta
+import logging
+
+from pyombi import OmbiError
+
+from homeassistant.helpers.entity import Entity
+
+from .const import DOMAIN, SENSOR_TYPES
+
+_LOGGER = logging.getLogger(__name__)
+
+SCAN_INTERVAL = timedelta(seconds=60)
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the Ombi sensor platform."""
+ if discovery_info is None:
+ return
+
+ sensors = []
+
+ ombi = hass.data[DOMAIN]["instance"]
+
+ for sensor in SENSOR_TYPES:
+ sensor_label = sensor
+ sensor_type = SENSOR_TYPES[sensor]["type"]
+ sensor_icon = SENSOR_TYPES[sensor]["icon"]
+ sensors.append(OmbiSensor(sensor_label, sensor_type, ombi, sensor_icon))
+
+ add_entities(sensors, True)
+
+
+class OmbiSensor(Entity):
+ """Representation of an Ombi sensor."""
+
+ def __init__(self, label, sensor_type, ombi, icon):
+ """Initialize the sensor."""
+ self._state = None
+ self._label = label
+ self._type = sensor_type
+ self._ombi = ombi
+ self._icon = icon
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return f"Ombi {self._type}"
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend."""
+ return self._icon
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ def update(self):
+ """Update the sensor."""
+ try:
+ if self._label == "movies":
+ self._state = self._ombi.movie_requests
+ elif self._label == "tv":
+ self._state = self._ombi.tv_requests
+ elif self._label == "music":
+ self._state = self._ombi.music_requests
+ elif self._label == "pending":
+ self._state = self._ombi.total_requests["pending"]
+ elif self._label == "approved":
+ self._state = self._ombi.total_requests["approved"]
+ elif self._label == "available":
+ self._state = self._ombi.total_requests["available"]
+ except OmbiError as err:
+ _LOGGER.warning("Unable to update Ombi sensor: %s", err)
+ self._state = None
diff --git a/homeassistant/components/ombi/services.yaml b/homeassistant/components/ombi/services.yaml
new file mode 100644
index 00000000000000..5f4f2defe32097
--- /dev/null
+++ b/homeassistant/components/ombi/services.yaml
@@ -0,0 +1,27 @@
+# Ombi services.yaml entries
+
+submit_movie_request:
+ description: Searches for a movie and requests the first result.
+ fields:
+ name:
+ description: Search parameter
+ example: "beverly hills cop"
+
+
+submit_tv_request:
+ description: Searches for a TV show and requests the first result.
+ fields:
+ name:
+ description: Search parameter
+ example: "breaking bad"
+ season:
+ description: Which season(s) to request (first, latest or all)
+ example: "latest"
+
+
+submit_music_request:
+ description: Searches for a music album and requests the first result.
+ fields:
+ name:
+ description: Search parameter
+ example: "nevermind"
\ No newline at end of file
diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py
index b5001a1f983801..023fb32e6e40da 100644
--- a/homeassistant/components/onkyo/media_player.py
+++ b/homeassistant/components/onkyo/media_player.py
@@ -1,8 +1,6 @@
"""Support for Onkyo Receivers."""
import logging
-
-# pylint: disable=unused-import
-from typing import List # noqa: F401
+from typing import List
import voluptuous as vol
@@ -54,7 +52,7 @@
| SUPPORT_PLAY_MEDIA
)
-KNOWN_HOSTS = [] # type: List[str]
+KNOWN_HOSTS: List[str] = []
DEFAULT_SOURCES = {
"tv": "TV",
"bd": "Bluray",
diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py
index 0635a2d1f11bb1..4fdd513f840f28 100644
--- a/homeassistant/components/onvif/camera.py
+++ b/homeassistant/components/onvif/camera.py
@@ -282,7 +282,7 @@ def setup_ptz(self):
"""Set up PTZ if available."""
_LOGGER.debug("Setting up the ONVIF PTZ service")
if self._camera.get_service("ptz", create=False) is None:
- _LOGGER.warning("PTZ is not available on this camera")
+ _LOGGER.debug("PTZ is not available")
else:
self._ptz_service = self._camera.create_ptz_service()
_LOGGER.debug("Completed set up of the ONVIF camera component")
diff --git a/homeassistant/components/openuv/.translations/pl.json b/homeassistant/components/openuv/.translations/pl.json
index 2c4c47e8da44ed..ee3875c2903c42 100644
--- a/homeassistant/components/openuv/.translations/pl.json
+++ b/homeassistant/components/openuv/.translations/pl.json
@@ -12,7 +12,7 @@
"latitude": "Szeroko\u015b\u0107 geograficzna",
"longitude": "D\u0142ugo\u015b\u0107 geograficzna"
},
- "title": "Wprowad\u017a swoje dane"
+ "title": "Wprowad\u017a dane"
}
},
"title": "OpenUV"
diff --git a/homeassistant/components/owntracks/.translations/it.json b/homeassistant/components/owntracks/.translations/it.json
index 9b66b693c333a1..03b0c84744f75d 100644
--- a/homeassistant/components/owntracks/.translations/it.json
+++ b/homeassistant/components/owntracks/.translations/it.json
@@ -3,6 +3,9 @@
"abort": {
"one_instance_allowed": "\u00c8 necessaria una sola istanza."
},
+ "create_entry": {
+ "default": "\n\nSu Android, apri l'[app OwnTracks]({android_url}), vai su preferenze -> connessione. Modifica le seguenti impostazioni: \n - Modalit\u00e0: HTTP privato \n - Host: {webhook_url} \n - Identificazione: \n - Nome utente: `` \n - ID dispositivo: ``\n\nSu iOS, apri l'[app OwnTracks]({ios_url}), tocca l'icona (i) in alto a sinistra -> impostazioni. Modifica le seguenti impostazioni: \n - Modalit\u00e0: HTTP \n - URL: {webhook_url} \n - Attiva autenticazione \n - UserID: `` \n\n {secret} \n \n Vedi [la documentazione]({docs_url}) per maggiori informazioni."
+ },
"step": {
"user": {
"description": "Sei sicuro di voler configurare OwnTracks?",
diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py
index c6a2f91bab3977..832853c670d827 100644
--- a/homeassistant/components/person/__init__.py
+++ b/homeassistant/components/person/__init__.py
@@ -441,7 +441,7 @@ def ws_list_person(
hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg
):
"""List persons."""
- manager = hass.data[DOMAIN] # type: PersonManager
+ manager: PersonManager = hass.data[DOMAIN]
connection.send_result(
msg["id"],
{"storage": manager.storage_persons, "config": manager.config_persons},
@@ -464,7 +464,7 @@ async def ws_create_person(
hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg
):
"""Create a person."""
- manager = hass.data[DOMAIN] # type: PersonManager
+ manager: PersonManager = hass.data[DOMAIN]
try:
person = await manager.async_create_person(
name=msg["name"],
@@ -495,7 +495,7 @@ async def ws_update_person(
hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg
):
"""Update a person."""
- manager = hass.data[DOMAIN] # type: PersonManager
+ manager: PersonManager = hass.data[DOMAIN]
changes = {}
for key in ("name", "user_id", "device_trackers"):
if key in msg:
@@ -519,7 +519,7 @@ async def ws_delete_person(
hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg
):
"""Delete a person."""
- manager = hass.data[DOMAIN] # type: PersonManager
+ manager: PersonManager = hass.data[DOMAIN]
await manager.async_delete_person(msg["person_id"])
connection.send_result(msg["id"])
diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json
index 2d19ab25fe78dd..7fe8bba6873913 100644
--- a/homeassistant/components/pi_hole/manifest.json
+++ b/homeassistant/components/pi_hole/manifest.json
@@ -7,6 +7,7 @@
],
"dependencies": [],
"codeowners": [
- "@fabaff"
+ "@fabaff",
+ "@johnluetke"
]
}
diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py
index 5d4f5dd25b52f8..2688b15e837c1d 100644
--- a/homeassistant/components/pilight/__init__.py
+++ b/homeassistant/components/pilight/__init__.py
@@ -92,7 +92,7 @@ def send_code(call):
try:
pilight_client.send_code(message_data)
- except IOError:
+ except OSError:
_LOGGER.error("Pilight send failed for %s", str(message_data))
hass.services.register(DOMAIN, SERVICE_NAME, send_code, schema=RF_CODE_SCHEMA)
diff --git a/homeassistant/components/plaato/.translations/es.json b/homeassistant/components/plaato/.translations/es.json
index e52a80be986370..ecb061e91c9c44 100644
--- a/homeassistant/components/plaato/.translations/es.json
+++ b/homeassistant/components/plaato/.translations/es.json
@@ -12,6 +12,7 @@
"description": "\u00bfEst\u00e1s seguro de que quieres configurar el Airlock de Plaato?",
"title": "Configurar el webhook de Plaato"
}
- }
+ },
+ "title": "Plaato Airlock"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/plaato/.translations/it.json b/homeassistant/components/plaato/.translations/it.json
new file mode 100644
index 00000000000000..7e7697a339bc20
--- /dev/null
+++ b/homeassistant/components/plaato/.translations/it.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Plaato Airlook.",
+ "one_instance_allowed": "\u00c8 necessaria solo una singola istanza."
+ },
+ "create_entry": {
+ "default": "Per inviare eventi a Home Assistant, dovrai impostare la funzione webhook in Plaato Airlock. \n\n Inserisci le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Metodo: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli."
+ },
+ "step": {
+ "user": {
+ "description": "Sei sicuro di voler configurare Plaato Airlock?",
+ "title": "Configura il webhook di Plaato"
+ }
+ },
+ "title": "Plaato Airlock"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plaato/.translations/zh-Hans.json b/homeassistant/components/plaato/.translations/zh-Hans.json
new file mode 100644
index 00000000000000..8d5c25babfab46
--- /dev/null
+++ b/homeassistant/components/plaato/.translations/zh-Hans.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u53ef\u4ece\u4e92\u8054\u7f51\u8bbf\u95ee\u4ee5\u63a5\u6536Plaato Airlock\u6d88\u606f\u3002"
+ },
+ "step": {
+ "user": {
+ "description": "\u4f60\u786e\u5b9a\u8981\u8bbe\u7f6ePlaato Airlock\u5417\uff1f",
+ "title": "\u8bbe\u7f6ePlaato Webhook"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json
new file mode 100644
index 00000000000000..eb4f6459f4dcbe
--- /dev/null
+++ b/homeassistant/components/plex/.translations/ca.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Tots els servidors enlla\u00e7ats ja estan configurats",
+ "already_configured": "Aquest servidor Plex ja est\u00e0 configurat",
+ "already_in_progress": "S\u2019est\u00e0 configurant Plex",
+ "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida",
+ "unknown": "Ha fallat per motiu desconegut"
+ },
+ "error": {
+ "faulty_credentials": "Ha fallat l'autoritzaci\u00f3",
+ "no_servers": "No hi ha servidors enlla\u00e7ats amb el compte",
+ "not_found": "No s'ha trobat el servidor Plex"
+ },
+ "step": {
+ "select_server": {
+ "data": {
+ "server": "Servidor"
+ },
+ "description": "Hi ha diversos servidors disponibles, selecciona'n un:",
+ "title": "Selecciona servidor Plex"
+ },
+ "user": {
+ "data": {
+ "token": "Testimoni d'autenticaci\u00f3 Plex"
+ },
+ "description": "Introdueix un testimoni d'autenticaci\u00f3 Plex per configurar-ho autom\u00e0ticament.",
+ "title": "Connexi\u00f3 amb el servidor Plex"
+ }
+ },
+ "title": "Plex"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json
new file mode 100644
index 00000000000000..7fa9f62be07763
--- /dev/null
+++ b/homeassistant/components/plex/.translations/en.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "All linked servers already configured",
+ "already_configured": "This Plex server is already configured",
+ "already_in_progress": "Plex is being configured",
+ "invalid_import": "Imported configuration is invalid",
+ "unknown": "Failed for unknown reason"
+ },
+ "error": {
+ "faulty_credentials": "Authorization failed",
+ "no_servers": "No servers linked to account",
+ "no_token": "Provide a token or select manual setup",
+ "not_found": "Plex server not found"
+ },
+ "step": {
+ "manual_setup": {
+ "data": {
+ "host": "Host",
+ "port": "Port",
+ "ssl": "Use SSL",
+ "token": "Token (if required)",
+ "verify_ssl": "Verify SSL certificate"
+ },
+ "title": "Plex server"
+ },
+ "select_server": {
+ "data": {
+ "server": "Server"
+ },
+ "description": "Multiple servers available, select one:",
+ "title": "Select Plex server"
+ },
+ "user": {
+ "data": {
+ "manual_setup": "Manual setup",
+ "token": "Plex token"
+ },
+ "description": "Enter a Plex token for automatic setup or manually configure a server.",
+ "title": "Connect Plex server"
+ }
+ },
+ "title": "Plex"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json
new file mode 100644
index 00000000000000..58a5169ac02701
--- /dev/null
+++ b/homeassistant/components/plex/.translations/fr.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Tous les serveurs li\u00e9s sont d\u00e9j\u00e0 configur\u00e9s",
+ "already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9",
+ "already_in_progress": "Plex en cours de configuration",
+ "invalid_import": "La configuration import\u00e9e est invalide",
+ "unknown": "\u00c9chec pour une raison inconnue"
+ },
+ "error": {
+ "faulty_credentials": "L'autorisation \u00e0 \u00e9chou\u00e9e",
+ "no_servers": "Aucun serveur li\u00e9 au compte",
+ "not_found": "Serveur Plex introuvable"
+ },
+ "step": {
+ "select_server": {
+ "data": {
+ "server": "Serveur"
+ },
+ "description": "Plusieurs serveurs disponibles, s\u00e9lectionnez-en un:",
+ "title": "S\u00e9lectionnez le serveur Plex"
+ },
+ "user": {
+ "data": {
+ "token": "Jeton plex"
+ },
+ "description": "Entrez un jeton Plex pour la configuration automatique.",
+ "title": "Connecter un serveur Plex"
+ }
+ },
+ "title": "Plex"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json
new file mode 100644
index 00000000000000..2e77b4ba9768b5
--- /dev/null
+++ b/homeassistant/components/plex/.translations/it.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Tutti i server collegati sono gi\u00e0 configurati",
+ "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Plex \u00e8 in fase di configurazione",
+ "invalid_import": "La configurazione importata non \u00e8 valida",
+ "unknown": "Non riuscito per motivo sconosciuto"
+ },
+ "error": {
+ "faulty_credentials": "Autorizzazione non riuscita",
+ "no_servers": "Nessun server collegato all'account",
+ "not_found": "Server Plex non trovato"
+ },
+ "step": {
+ "select_server": {
+ "data": {
+ "server": "Server"
+ },
+ "description": "Sono disponibili pi\u00f9 server, selezionarne uno:",
+ "title": "Selezionare il server Plex"
+ },
+ "user": {
+ "data": {
+ "token": "Token Plex"
+ },
+ "description": "Immettere un token Plex per la configurazione automatica.",
+ "title": "Collegare il server Plex"
+ }
+ },
+ "title": "Plex"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json
new file mode 100644
index 00000000000000..d2610c68aed74f
--- /dev/null
+++ b/homeassistant/components/plex/.translations/ko.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "\uc774\ubbf8 \uad6c\uc131\ub41c \ubaa8\ub4e0 \uc5f0\uacb0\ub41c \uc11c\ubc84",
+ "already_configured": "\uc774 Plex \uc11c\ubc84\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4",
+ "invalid_import": "\uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc774\uc720\ub85c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "faulty_credentials": "\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4",
+ "no_servers": "\uacc4\uc815\uc5d0 \uc5f0\uacb0\ub41c \uc11c\ubc84\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "not_found": "Plex \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "select_server": {
+ "data": {
+ "server": "\uc11c\ubc84"
+ },
+ "description": "\uc5ec\ub7ec \uc11c\ubc84\uac00 \uc0ac\uc6a9 \uac00\ub2a5\ud569\ub2c8\ub2e4. \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:",
+ "title": "Plex \uc11c\ubc84 \uc120\ud0dd"
+ },
+ "user": {
+ "data": {
+ "token": "Plex \ud1a0\ud070"
+ },
+ "description": "\uc790\ub3d9 \uc124\uc815\uc744 \uc704\ud574 Plex \ud1a0\ud070\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.",
+ "title": "Plex \uc11c\ubc84 \uc5f0\uacb0"
+ }
+ },
+ "title": "Plex"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json
new file mode 100644
index 00000000000000..130cf2067abe72
--- /dev/null
+++ b/homeassistant/components/plex/.translations/lb.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "All verbonne Server sinn scho konfigur\u00e9iert",
+ "already_configured": "D\u00ebse Plex Server ass scho konfigur\u00e9iert",
+ "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert",
+ "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg",
+ "unknown": "Onbekannte Feeler opgetrueden"
+ },
+ "error": {
+ "faulty_credentials": "Feeler beider Autorisatioun",
+ "no_servers": "Kee Server as mam Kont verbonnen",
+ "not_found": "Kee Plex Server fonnt"
+ },
+ "step": {
+ "select_server": {
+ "data": {
+ "server": "Server"
+ },
+ "description": "M\u00e9i Server disponibel, wielt een aus:",
+ "title": "Plex Server auswielen"
+ },
+ "user": {
+ "data": {
+ "token": "Jeton fir de Plex"
+ },
+ "description": "Gitt een Jeton fir de Plex un fir eng automatesch Konfiguratioun",
+ "title": "Plex Server verbannen"
+ }
+ },
+ "title": "Plex"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json
new file mode 100644
index 00000000000000..8ac90efe3d1474
--- /dev/null
+++ b/homeassistant/components/plex/.translations/no.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Alle knyttet servere som allerede er konfigurert",
+ "already_configured": "Denne Plex-serveren er allerede konfigurert",
+ "already_in_progress": "Plex blir konfigurert",
+ "invalid_import": "Den importerte konfigurasjonen er ugyldig",
+ "unknown": "Mislyktes av ukjent \u00e5rsak"
+ },
+ "error": {
+ "faulty_credentials": "Autorisasjonen mislyktes",
+ "no_servers": "Ingen servere koblet til kontoen",
+ "not_found": "Plex-server ikke funnet"
+ },
+ "step": {
+ "select_server": {
+ "data": {
+ "server": "Server"
+ },
+ "description": "Flere servere tilgjengelig, velg en:",
+ "title": "Velg Plex-server"
+ },
+ "user": {
+ "data": {
+ "token": "Plex token"
+ },
+ "description": "Legg inn et Plex-token for automatisk oppsett.",
+ "title": "Koble til Plex-server"
+ }
+ },
+ "title": "Plex"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json
new file mode 100644
index 00000000000000..606f97d6965c60
--- /dev/null
+++ b/homeassistant/components/plex/.translations/pl.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.",
+ "already_configured": "Serwer Plex jest ju\u017c skonfigurowany",
+ "already_in_progress": "Plex jest konfigurowany",
+ "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa",
+ "unknown": "Nieznany b\u0142\u0105d"
+ },
+ "error": {
+ "faulty_credentials": "Autoryzacja nie powiod\u0142a si\u0119",
+ "no_servers": "Brak serwer\u00f3w po\u0142\u0105czonych z kontem",
+ "not_found": "Nie znaleziono serwera Plex"
+ },
+ "step": {
+ "select_server": {
+ "data": {
+ "server": "Serwer"
+ },
+ "description": "Dost\u0119pnych jest wiele serwer\u00f3w, wybierz jeden:",
+ "title": "Wybierz serwer Plex"
+ },
+ "user": {
+ "data": {
+ "token": "Token Plex"
+ },
+ "description": "Wprowad\u017a token Plex do automatycznej konfiguracji.",
+ "title": "Po\u0142\u0105cz z serwerem Plex"
+ }
+ },
+ "title": "Plex"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json
new file mode 100644
index 00000000000000..46cd613df4ac70
--- /dev/null
+++ b/homeassistant/components/plex/.translations/ru.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b",
+ "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d",
+ "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430",
+ "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430",
+ "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435"
+ },
+ "error": {
+ "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438",
+ "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e",
+ "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d"
+ },
+ "step": {
+ "select_server": {
+ "data": {
+ "server": "\u0421\u0435\u0440\u0432\u0435\u0440"
+ },
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u0438\u043d \u0438\u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432:",
+ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex"
+ },
+ "user": {
+ "data": {
+ "token": "\u0422\u043e\u043a\u0435\u043d"
+ },
+ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d Plex \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.",
+ "title": "Plex"
+ }
+ },
+ "title": "Plex"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json
new file mode 100644
index 00000000000000..c79a49470e000d
--- /dev/null
+++ b/homeassistant/components/plex/.translations/zh-Hant.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "\u6240\u6709\u7d81\u5b9a\u4f3a\u670d\u5668\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210",
+ "already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a",
+ "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548",
+ "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557"
+ },
+ "error": {
+ "faulty_credentials": "\u9a57\u8b49\u5931\u6557",
+ "no_servers": "\u6b64\u5e33\u865f\u672a\u7d81\u5b9a\u4f3a\u670d\u5668",
+ "not_found": "\u627e\u4e0d\u5230 Plex \u4f3a\u670d\u5668"
+ },
+ "step": {
+ "select_server": {
+ "data": {
+ "server": "\u4f3a\u670d\u5668"
+ },
+ "description": "\u627e\u5230\u591a\u500b\u4f3a\u670d\u5668\uff0c\u8acb\u9078\u64c7\u4e00\u7d44\uff1a",
+ "title": "\u9078\u64c7 Plex \u4f3a\u670d\u5668"
+ },
+ "user": {
+ "data": {
+ "token": "Plex \u5bc6\u9470"
+ },
+ "description": "\u8acb\u8f38\u5165 Plex \u5bc6\u9470\u4ee5\u9032\u884c\u81ea\u52d5\u8a2d\u5b9a\u3002",
+ "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668"
+ }
+ },
+ "title": "Plex"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py
index 6e4e02026abff8..dd458dda07880a 100644
--- a/homeassistant/components/plex/__init__.py
+++ b/homeassistant/components/plex/__init__.py
@@ -1 +1,152 @@
-"""The plex component."""
+"""Support to embed Plex."""
+import asyncio
+import logging
+
+import plexapi.exceptions
+import requests.exceptions
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PORT,
+ CONF_SSL,
+ CONF_TOKEN,
+ CONF_URL,
+ CONF_VERIFY_SSL,
+)
+from homeassistant.helpers import config_validation as cv
+
+from .const import (
+ CONF_USE_EPISODE_ART,
+ CONF_SHOW_ALL_CONTROLS,
+ CONF_SERVER,
+ CONF_SERVER_IDENTIFIER,
+ DEFAULT_PORT,
+ DEFAULT_SSL,
+ DEFAULT_VERIFY_SSL,
+ DOMAIN as PLEX_DOMAIN,
+ PLATFORMS,
+ PLEX_MEDIA_PLAYER_OPTIONS,
+ PLEX_SERVER_CONFIG,
+ REFRESH_LISTENERS,
+ SERVERS,
+)
+from .server import PlexServer
+
+MEDIA_PLAYER_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean,
+ vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean,
+ }
+)
+
+SERVER_CONFIG_SCHEMA = vol.Schema(
+ vol.All(
+ {
+ vol.Optional(CONF_HOST): cv.string,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+ vol.Optional(CONF_TOKEN): cv.string,
+ vol.Optional(CONF_SERVER): cv.string,
+ vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
+ vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
+ vol.Optional(MP_DOMAIN, default={}): MEDIA_PLAYER_SCHEMA,
+ },
+ cv.has_at_least_one_key(CONF_HOST, CONF_TOKEN),
+ )
+)
+
+CONFIG_SCHEMA = vol.Schema({PLEX_DOMAIN: SERVER_CONFIG_SCHEMA}, extra=vol.ALLOW_EXTRA)
+
+_LOGGER = logging.getLogger(__package__)
+
+
+def setup(hass, config):
+ """Set up the Plex component."""
+ hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}})
+
+ plex_config = config.get(PLEX_DOMAIN, {})
+ if plex_config:
+ _setup_plex(hass, plex_config)
+
+ return True
+
+
+def _setup_plex(hass, config):
+ """Pass configuration to a config flow."""
+ server_config = dict(config)
+ if MP_DOMAIN in server_config:
+ hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = server_config.pop(MP_DOMAIN)
+ if CONF_HOST in server_config:
+ prefix = "https" if server_config.pop(CONF_SSL) else "http"
+ server_config[
+ CONF_URL
+ ] = f"{prefix}://{server_config.pop(CONF_HOST)}:{server_config.pop(CONF_PORT)}"
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ PLEX_DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=server_config,
+ )
+ )
+
+
+async def async_setup_entry(hass, entry):
+ """Set up Plex from a config entry."""
+ server_config = entry.data[PLEX_SERVER_CONFIG]
+
+ plex_server = PlexServer(server_config)
+ try:
+ await hass.async_add_executor_job(plex_server.connect)
+ except requests.exceptions.ConnectionError as error:
+ _LOGGER.error(
+ "Plex server (%s) could not be reached: [%s]",
+ server_config[CONF_URL],
+ error,
+ )
+ return False
+ except (
+ plexapi.exceptions.BadRequest,
+ plexapi.exceptions.Unauthorized,
+ plexapi.exceptions.NotFound,
+ ) as error:
+ _LOGGER.error(
+ "Login to %s failed, verify token and SSL settings: [%s]",
+ server_config[CONF_SERVER],
+ error,
+ )
+ return False
+
+ _LOGGER.debug(
+ "Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use
+ )
+ hass.data[PLEX_DOMAIN][SERVERS][plex_server.machine_identifier] = plex_server
+
+ if not hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS):
+ hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = MEDIA_PLAYER_SCHEMA({})
+
+ for platform in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
+ server_id = entry.data[CONF_SERVER_IDENTIFIER]
+
+ cancel = hass.data[PLEX_DOMAIN][REFRESH_LISTENERS].pop(server_id)
+ await hass.async_add_executor_job(cancel)
+
+ tasks = [
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
+ await asyncio.gather(*tasks)
+
+ hass.data[PLEX_DOMAIN][SERVERS].pop(server_id)
+
+ return True
diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py
new file mode 100644
index 00000000000000..e620e4869e5083
--- /dev/null
+++ b/homeassistant/components/plex/config_flow.py
@@ -0,0 +1,216 @@
+"""Config flow for Plex."""
+import logging
+
+import plexapi.exceptions
+import requests.exceptions
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PORT,
+ CONF_URL,
+ CONF_TOKEN,
+ CONF_SSL,
+ CONF_VERIFY_SSL,
+)
+from homeassistant.core import callback
+from homeassistant.util.json import load_json
+
+from .const import ( # pylint: disable=unused-import
+ CONF_SERVER,
+ CONF_SERVER_IDENTIFIER,
+ DEFAULT_PORT,
+ DEFAULT_SSL,
+ DEFAULT_VERIFY_SSL,
+ DOMAIN,
+ PLEX_CONFIG_FILE,
+ PLEX_SERVER_CONFIG,
+)
+from .errors import NoServersFound, ServerNotSpecified
+from .server import PlexServer
+
+USER_SCHEMA = vol.Schema(
+ {vol.Optional(CONF_TOKEN): str, vol.Optional("manual_setup"): bool}
+)
+
+_LOGGER = logging.getLogger(__package__)
+
+
+@callback
+def configured_servers(hass):
+ """Return a set of the configured Plex servers."""
+ return set(
+ entry.data[CONF_SERVER_IDENTIFIER]
+ for entry in hass.config_entries.async_entries(DOMAIN)
+ )
+
+
+class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a Plex config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ def __init__(self):
+ """Initialize the Plex flow."""
+ self.current_login = {}
+ self.discovery_info = {}
+ self.available_servers = None
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ errors = {}
+ if user_input is not None:
+ if user_input.pop("manual_setup", False):
+ return await self.async_step_manual_setup(user_input)
+ if CONF_TOKEN in user_input:
+ return await self.async_step_server_validate(user_input)
+ errors[CONF_TOKEN] = "no_token"
+
+ return self.async_show_form(
+ step_id="user", data_schema=USER_SCHEMA, errors=errors
+ )
+
+ async def async_step_server_validate(self, server_config):
+ """Validate a provided configuration."""
+ errors = {}
+ self.current_login = server_config
+
+ plex_server = PlexServer(server_config)
+ try:
+ await self.hass.async_add_executor_job(plex_server.connect)
+
+ except NoServersFound:
+ errors["base"] = "no_servers"
+ except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized):
+ _LOGGER.error("Invalid credentials provided, config not created")
+ errors["base"] = "faulty_credentials"
+ except (plexapi.exceptions.NotFound, requests.exceptions.ConnectionError):
+ _LOGGER.error(
+ "Plex server could not be reached: %s", server_config[CONF_URL]
+ )
+ errors["base"] = "not_found"
+
+ except ServerNotSpecified as available_servers:
+ self.available_servers = available_servers.args[0]
+ return await self.async_step_select_server()
+
+ except Exception as error: # pylint: disable=broad-except
+ _LOGGER.error("Unknown error connecting to Plex server: %s", error)
+ return self.async_abort(reason="unknown")
+
+ if errors:
+ return self.async_show_form(
+ step_id="user", data_schema=USER_SCHEMA, errors=errors
+ )
+
+ server_id = plex_server.machine_identifier
+
+ for entry in self._async_current_entries():
+ if entry.data[CONF_SERVER_IDENTIFIER] == server_id:
+ return self.async_abort(reason="already_configured")
+
+ url = plex_server.url_in_use
+ token = server_config.get(CONF_TOKEN)
+
+ entry_config = {CONF_URL: url}
+ if token:
+ entry_config[CONF_TOKEN] = token
+ if url.startswith("https"):
+ entry_config[CONF_VERIFY_SSL] = server_config.get(
+ CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL
+ )
+
+ _LOGGER.debug("Valid config created for %s", plex_server.friendly_name)
+
+ return self.async_create_entry(
+ title=plex_server.friendly_name,
+ data={
+ CONF_SERVER: plex_server.friendly_name,
+ CONF_SERVER_IDENTIFIER: server_id,
+ PLEX_SERVER_CONFIG: entry_config,
+ },
+ )
+
+ async def async_step_manual_setup(self, user_input=None):
+ """Begin manual configuration."""
+ if len(user_input) > 1:
+ host = user_input.pop(CONF_HOST)
+ port = user_input.pop(CONF_PORT)
+ prefix = "https" if user_input.pop(CONF_SSL) else "http"
+ user_input[CONF_URL] = f"{prefix}://{host}:{port}"
+ return await self.async_step_server_validate(user_input)
+
+ data_schema = vol.Schema(
+ {
+ vol.Required(
+ CONF_HOST, default=self.discovery_info.get(CONF_HOST)
+ ): str,
+ vol.Required(
+ CONF_PORT, default=self.discovery_info.get(CONF_PORT, DEFAULT_PORT)
+ ): int,
+ vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
+ vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool,
+ vol.Optional(CONF_TOKEN, default=user_input.get(CONF_TOKEN, "")): str,
+ }
+ )
+ return self.async_show_form(step_id="manual_setup", data_schema=data_schema)
+
+ async def async_step_select_server(self, user_input=None):
+ """Use selected Plex server."""
+ config = dict(self.current_login)
+ if user_input is not None:
+ config[CONF_SERVER] = user_input[CONF_SERVER]
+ return await self.async_step_server_validate(config)
+
+ configured = configured_servers(self.hass)
+ available_servers = [
+ name
+ for (name, server_id) in self.available_servers
+ if server_id not in configured
+ ]
+
+ if not available_servers:
+ return self.async_abort(reason="all_configured")
+ if len(available_servers) == 1:
+ config[CONF_SERVER] = available_servers[0]
+ return await self.async_step_server_validate(config)
+
+ return self.async_show_form(
+ step_id="select_server",
+ data_schema=vol.Schema(
+ {vol.Required(CONF_SERVER): vol.In(available_servers)}
+ ),
+ errors={},
+ )
+
+ async def async_step_discovery(self, discovery_info):
+ """Set default host and port from discovery."""
+ if self._async_current_entries() or self._async_in_progress():
+ # Skip discovery if a config already exists or is in progress.
+ return self.async_abort(reason="already_configured")
+
+ discovery_info[CONF_PORT] = int(discovery_info[CONF_PORT])
+ self.discovery_info = discovery_info
+ json_file = self.hass.config.path(PLEX_CONFIG_FILE)
+ file_config = await self.hass.async_add_executor_job(load_json, json_file)
+
+ if file_config:
+ host_and_port, host_config = file_config.popitem()
+ prefix = "https" if host_config[CONF_SSL] else "http"
+
+ server_config = {
+ CONF_URL: f"{prefix}://{host_and_port}",
+ CONF_TOKEN: host_config[CONF_TOKEN],
+ CONF_VERIFY_SSL: host_config["verify"],
+ }
+ _LOGGER.info("Imported legacy config, file can be removed: %s", json_file)
+ return await self.async_step_server_validate(server_config)
+
+ return await self.async_step_user()
+
+ async def async_step_import(self, import_config):
+ """Import from Plex configuration."""
+ _LOGGER.debug("Imported Plex configuration")
+ return await self.async_step_server_validate(import_config)
diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py
new file mode 100644
index 00000000000000..478dd3754e7d8f
--- /dev/null
+++ b/homeassistant/components/plex/const.py
@@ -0,0 +1,20 @@
+"""Constants for the Plex component."""
+DOMAIN = "plex"
+NAME_FORMAT = "Plex {}"
+
+DEFAULT_PORT = 32400
+DEFAULT_SSL = False
+DEFAULT_VERIFY_SSL = True
+
+PLATFORMS = ["media_player", "sensor"]
+REFRESH_LISTENERS = "refresh_listeners"
+SERVERS = "servers"
+
+PLEX_CONFIG_FILE = "plex.conf"
+PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options"
+PLEX_SERVER_CONFIG = "server_config"
+
+CONF_SERVER = "server"
+CONF_SERVER_IDENTIFIER = "server_id"
+CONF_USE_EPISODE_ART = "use_episode_art"
+CONF_SHOW_ALL_CONTROLS = "show_all_controls"
diff --git a/homeassistant/components/plex/errors.py b/homeassistant/components/plex/errors.py
new file mode 100644
index 00000000000000..11c15404f4505c
--- /dev/null
+++ b/homeassistant/components/plex/errors.py
@@ -0,0 +1,14 @@
+"""Errors for the Plex component."""
+from homeassistant.exceptions import HomeAssistantError
+
+
+class PlexException(HomeAssistantError):
+ """Base class for Plex exceptions."""
+
+
+class NoServersFound(PlexException):
+ """No servers found on Plex account."""
+
+
+class ServerNotSpecified(PlexException):
+ """Multiple servers linked to account without choice provided."""
diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json
index 32ddb83476c81e..94d990952a684e 100644
--- a/homeassistant/components/plex/manifest.json
+++ b/homeassistant/components/plex/manifest.json
@@ -1,10 +1,13 @@
{
"domain": "plex",
"name": "Plex",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/components/plex",
"requirements": [
"plexapi==3.0.6"
],
- "dependencies": ["configurator"],
- "codeowners": []
+ "dependencies": [],
+ "codeowners": [
+ "@jjlawren"
+ ]
}
diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py
index 39694a061c4839..4d097253ea1a93 100644
--- a/homeassistant/components/plex/media_player.py
+++ b/homeassistant/components/plex/media_player.py
@@ -3,10 +3,12 @@
import json
import logging
-import requests
-import voluptuous as vol
+import plexapi.exceptions
+import plexapi.playlist
+import plexapi.playqueue
+import requests.exceptions
-from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
+from homeassistant.components.media_player import MediaPlayerDevice
from homeassistant.components.media_player.const import (
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC,
@@ -27,123 +29,52 @@
STATE_PAUSED,
STATE_PLAYING,
)
-from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_time_interval
from homeassistant.util import dt as dt_util
-from homeassistant.util.json import load_json, save_json
-_CONFIGURING = {}
+from .const import (
+ CONF_USE_EPISODE_ART,
+ CONF_SHOW_ALL_CONTROLS,
+ CONF_SERVER_IDENTIFIER,
+ DOMAIN as PLEX_DOMAIN,
+ NAME_FORMAT,
+ PLEX_MEDIA_PLAYER_OPTIONS,
+ REFRESH_LISTENERS,
+ SERVERS,
+)
+
_LOGGER = logging.getLogger(__name__)
-NAME_FORMAT = "Plex {}"
-PLEX_CONFIG_FILE = "plex.conf"
-PLEX_DATA = "plex"
-
-CONF_USE_EPISODE_ART = "use_episode_art"
-CONF_SHOW_ALL_CONTROLS = "show_all_controls"
-CONF_REMOVE_UNAVAILABLE_CLIENTS = "remove_unavailable_clients"
-CONF_CLIENT_REMOVE_INTERVAL = "client_remove_interval"
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean,
- vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean,
- vol.Optional(CONF_REMOVE_UNAVAILABLE_CLIENTS, default=True): cv.boolean,
- vol.Optional(
- CONF_CLIENT_REMOVE_INTERVAL, default=timedelta(seconds=600)
- ): vol.All(cv.time_period, cv.positive_timedelta),
- }
-)
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Set up the Plex media_player platform.
-def setup_platform(hass, config, add_entities_callback, discovery_info=None):
- """Set up the Plex platform."""
- if PLEX_DATA not in hass.data:
- hass.data[PLEX_DATA] = {}
+ Deprecated.
+ """
+ pass
- # get config from plex.conf
- file_config = load_json(hass.config.path(PLEX_CONFIG_FILE))
- if file_config:
- # Setup a configured PlexServer
- host, host_config = file_config.popitem()
- token = host_config["token"]
- try:
- has_ssl = host_config["ssl"]
- except KeyError:
- has_ssl = False
- try:
- verify_ssl = host_config["verify"]
- except KeyError:
- verify_ssl = True
-
- # Via discovery
- elif discovery_info is not None:
- # Parse discovery data
- host = discovery_info.get("host")
- port = discovery_info.get("port")
- host = f"{host}:{port}"
- _LOGGER.info("Discovered PLEX server: %s", host)
-
- if host in _CONFIGURING:
- return
- token = None
- has_ssl = False
- verify_ssl = True
- else:
- return
-
- setup_plexserver(
- host, token, has_ssl, verify_ssl, hass, config, add_entities_callback
- )
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Plex media_player from a config entry."""
+ def add_entities(entities, update_before_add=False):
+ """Sync version of async add entities."""
+ hass.add_job(async_add_entities, entities, update_before_add)
+
+ hass.async_add_executor_job(_setup_platform, hass, config_entry, add_entities)
-def setup_plexserver(
- host, token, has_ssl, verify_ssl, hass, config, add_entities_callback
-):
- """Set up a plexserver based on host parameter."""
- import plexapi.server
- import plexapi.exceptions
-
- cert_session = None
- http_prefix = "https" if has_ssl else "http"
- if has_ssl and (verify_ssl is False):
- _LOGGER.info("Ignoring SSL verification")
- cert_session = requests.Session()
- cert_session.verify = False
- try:
- plexserver = plexapi.server.PlexServer(
- f"{http_prefix}://{host}", token, cert_session
- )
- _LOGGER.info("Discovery configuration done (no token needed)")
- except (
- plexapi.exceptions.BadRequest,
- plexapi.exceptions.Unauthorized,
- plexapi.exceptions.NotFound,
- ) as error:
- _LOGGER.info(error)
- # No token or wrong token
- request_configuration(host, hass, config, add_entities_callback)
- return
-
- # If we came here and configuring this host, mark as done
- if host in _CONFIGURING:
- request_id = _CONFIGURING.pop(host)
- configurator = hass.components.configurator
- configurator.request_done(request_id)
- _LOGGER.info("Discovery configuration done")
-
- # Save config
- save_json(
- hass.config.path(PLEX_CONFIG_FILE),
- {host: {"token": token, "ssl": has_ssl, "verify": verify_ssl}},
- )
- _LOGGER.info("Connected to: %s://%s", http_prefix, host)
+def _setup_platform(hass, config_entry, add_entities_callback):
+ """Set up the Plex media_player platform."""
+ server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
+ config = hass.data[PLEX_MEDIA_PLAYER_OPTIONS]
- plex_clients = hass.data[PLEX_DATA]
+ plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id]
+ plex_clients = {}
plex_sessions = {}
- track_time_interval(hass, lambda now: update_devices(), timedelta(seconds=10))
+ hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = track_time_interval(
+ hass, lambda now: update_devices(), timedelta(seconds=10)
+ )
def update_devices():
"""Update the devices objects."""
@@ -154,7 +85,9 @@ def update_devices():
return
except requests.exceptions.RequestException as ex:
_LOGGER.warning(
- "Could not connect to plex server at http://%s (%s)", host, ex
+ "Could not connect to Plex server: %s (%s)",
+ plexserver.friendly_name,
+ ex,
)
return
@@ -186,7 +119,9 @@ def update_devices():
return
except requests.exceptions.RequestException as ex:
_LOGGER.warning(
- "Could not connect to plex server at http://%s (%s)", host, ex
+ "Could not connect to Plex server: %s (%s)",
+ plexserver.friendly_name,
+ ex,
)
return
@@ -215,7 +150,6 @@ def update_devices():
_LOGGER.debug("Refreshing session: %s", machine_identifier)
plex_clients[machine_identifier].refresh(None, session)
- clients_to_remove = []
for client in plex_clients.values():
# force devices to idle that do not have a valid session
if client.session is None:
@@ -229,59 +163,10 @@ def update_devices():
if client not in new_plex_clients:
client.schedule_update_ha_state()
- if not config.get(CONF_REMOVE_UNAVAILABLE_CLIENTS) or client.available:
- continue
-
- if (dt_util.utcnow() - client.marked_unavailable) >= (
- config.get(CONF_CLIENT_REMOVE_INTERVAL)
- ):
- hass.add_job(client.async_remove())
- clients_to_remove.append(client.machine_identifier)
-
- while clients_to_remove:
- del plex_clients[clients_to_remove.pop()]
-
if new_plex_clients:
add_entities_callback(new_plex_clients)
-def request_configuration(host, hass, config, add_entities_callback):
- """Request configuration steps from the user."""
- configurator = hass.components.configurator
- # We got an error if this method is called while we are configuring
- if host in _CONFIGURING:
- configurator.notify_errors(
- _CONFIGURING[host], "Failed to register, please try again."
- )
-
- return
-
- def plex_configuration_callback(data):
- """Handle configuration changes."""
- setup_plexserver(
- host,
- data.get("token"),
- cv.boolean(data.get("has_ssl")),
- cv.boolean(data.get("do_not_verify_ssl")),
- hass,
- config,
- add_entities_callback,
- )
-
- _CONFIGURING[host] = configurator.request_config(
- "Plex Media Server",
- plex_configuration_callback,
- description="Enter the X-Plex-Token",
- entity_picture="/static/images/logo_plex_mediaserver.png",
- submit_caption="Confirm",
- fields=[
- {"id": "token", "name": "X-Plex-Token", "type": ""},
- {"id": "has_ssl", "name": "Use SSL", "type": ""},
- {"id": "do_not_verify_ssl", "name": "Do not verify SSL", "type": ""},
- ],
- )
-
-
class PlexClient(MediaPlayerDevice):
"""Representation of a Plex device."""
@@ -354,9 +239,6 @@ def _clear_media_details(self):
def refresh(self, device, session):
"""Refresh key device data."""
- import plexapi.exceptions
-
- # new data refresh
self._clear_media_details()
if session: # Not being triggered by Chrome or FireTablet Plex App
@@ -827,8 +709,6 @@ def play_media(self, media_type, media_id, **kwargs):
src["video_name"]
)
- import plexapi.playlist
-
if (
media
and media_type == "EPISODE"
@@ -894,8 +774,6 @@ def _client_play_media(self, media, delete=False, **params):
_LOGGER.error("Client cannot play media: %s", self.entity_id)
return
- import plexapi.playqueue
-
playqueue = plexapi.playqueue.PlayQueue.create(
self.device.server, media, **params
)
diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py
index d900b4de87c1d7..7d5b54356a0c82 100644
--- a/homeassistant/components/plex/sensor.py
+++ b/homeassistant/components/plex/sensor.py
@@ -1,132 +1,56 @@
"""Support for Plex media server monitoring."""
from datetime import timedelta
import logging
-import voluptuous as vol
-
-from homeassistant.components.switch import PLATFORM_SCHEMA
-from homeassistant.const import (
- CONF_NAME,
- CONF_USERNAME,
- CONF_PASSWORD,
- CONF_HOST,
- CONF_PORT,
- CONF_TOKEN,
- CONF_SSL,
- CONF_VERIFY_SSL,
-)
+
+import plexapi.exceptions
+import requests.exceptions
+
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
-import homeassistant.helpers.config_validation as cv
+
+from .const import CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, SERVERS
_LOGGER = logging.getLogger(__name__)
-CONF_SERVER = "server"
+MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
-DEFAULT_HOST = "localhost"
-DEFAULT_NAME = "Plex"
-DEFAULT_PORT = 32400
-DEFAULT_SSL = False
-DEFAULT_VERIFY_SSL = True
-MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Set up the Plex sensor platform.
+
+ Deprecated.
+ """
+ pass
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_TOKEN): cv.string,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
- vol.Optional(CONF_SERVER): cv.string,
- vol.Optional(CONF_USERNAME): cv.string,
- vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
- vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
- }
-)
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the Plex sensor."""
- name = config.get(CONF_NAME)
- plex_user = config.get(CONF_USERNAME)
- plex_password = config.get(CONF_PASSWORD)
- plex_server = config.get(CONF_SERVER)
- plex_host = config.get(CONF_HOST)
- plex_port = config.get(CONF_PORT)
- plex_token = config.get(CONF_TOKEN)
-
- plex_url = "{}://{}:{}".format(
- "https" if config.get(CONF_SSL) else "http", plex_host, plex_port
- )
-
- import plexapi.exceptions
-
- try:
- add_entities(
- [
- PlexSensor(
- name,
- plex_url,
- plex_user,
- plex_password,
- plex_server,
- plex_token,
- config.get(CONF_VERIFY_SSL),
- )
- ],
- True,
- )
- except (
- plexapi.exceptions.BadRequest,
- plexapi.exceptions.Unauthorized,
- plexapi.exceptions.NotFound,
- ) as error:
- _LOGGER.error(error)
- return
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Plex sensor from a config entry."""
+ server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
+ sensor = PlexSensor(hass.data[PLEX_DOMAIN][SERVERS][server_id])
+ async_add_entities([sensor], True)
class PlexSensor(Entity):
"""Representation of a Plex now playing sensor."""
- def __init__(
- self,
- name,
- plex_url,
- plex_user,
- plex_password,
- plex_server,
- plex_token,
- verify_ssl,
- ):
+ def __init__(self, plex_server):
"""Initialize the sensor."""
- from plexapi.myplex import MyPlexAccount
- from plexapi.server import PlexServer
- from requests import Session
-
- self._name = name
- self._state = 0
+ self._state = None
self._now_playing = []
-
- cert_session = None
- if not verify_ssl:
- _LOGGER.info("Ignoring SSL verification")
- cert_session = Session()
- cert_session.verify = False
-
- if plex_token:
- self._server = PlexServer(plex_url, plex_token, cert_session)
- elif plex_user and plex_password:
- user = MyPlexAccount(plex_user, plex_password)
- server = plex_server if plex_server else user.resources()[0].name
- self._server = user.resource(server).connect()
- else:
- self._server = PlexServer(plex_url, None, cert_session)
+ self._server = plex_server
+ self._name = f"Plex ({plex_server.friendly_name})"
+ self._unique_id = f"sensor-{plex_server.machine_identifier}"
@property
def name(self):
"""Return the name of the sensor."""
return self._name
+ @property
+ def unique_id(self):
+ """Return the id of this plex client."""
+ return self._unique_id
+
@property
def state(self):
"""Return the state of the sensor."""
@@ -145,7 +69,19 @@ def device_state_attributes(self):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update method for Plex sensor."""
- sessions = self._server.sessions()
+ try:
+ sessions = self._server.sessions()
+ except plexapi.exceptions.BadRequest:
+ _LOGGER.error(
+ "Error listing current Plex sessions on %s", self._server.friendly_name
+ )
+ return
+ except requests.exceptions.RequestException as ex:
+ _LOGGER.warning(
+ "Temporary error connecting to %s (%s)", self._server.friendly_name, ex
+ )
+ return
+
now_playing = []
for sess in sessions:
user = sess.usernames[0]
diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py
new file mode 100644
index 00000000000000..f41a9bdabae183
--- /dev/null
+++ b/homeassistant/components/plex/server.py
@@ -0,0 +1,82 @@
+"""Shared class to maintain Plex server instances."""
+import plexapi.myplex
+import plexapi.server
+from requests import Session
+
+from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
+
+from .const import CONF_SERVER, DEFAULT_VERIFY_SSL
+from .errors import NoServersFound, ServerNotSpecified
+
+
+class PlexServer:
+ """Manages a single Plex server connection."""
+
+ def __init__(self, server_config):
+ """Initialize a Plex server instance."""
+ self._plex_server = None
+ self._url = server_config.get(CONF_URL)
+ self._token = server_config.get(CONF_TOKEN)
+ self._server_name = server_config.get(CONF_SERVER)
+ self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
+
+ def connect(self):
+ """Connect to a Plex server directly, obtaining direct URL if necessary."""
+
+ def _set_missing_url():
+ account = plexapi.myplex.MyPlexAccount(token=self._token)
+ available_servers = [
+ (x.name, x.clientIdentifier)
+ for x in account.resources()
+ if "server" in x.provides
+ ]
+
+ if not available_servers:
+ raise NoServersFound
+ if not self._server_name and len(available_servers) > 1:
+ raise ServerNotSpecified(available_servers)
+
+ server_choice = (
+ self._server_name if self._server_name else available_servers[0]
+ )
+ connections = account.resource(server_choice).connections
+ local_url = [x.httpuri for x in connections if x.local]
+ remote_url = [x.uri for x in connections if not x.local]
+ self._url = local_url[0] if local_url else remote_url[0]
+
+ def _connect_with_url():
+ session = None
+ if self._url.startswith("https") and not self._verify_ssl:
+ session = Session()
+ session.verify = False
+ self._plex_server = plexapi.server.PlexServer(
+ self._url, self._token, session
+ )
+
+ if self._token and not self._url:
+ _set_missing_url()
+
+ _connect_with_url()
+
+ def clients(self):
+ """Pass through clients call to plexapi."""
+ return self._plex_server.clients()
+
+ def sessions(self):
+ """Pass through sessions call to plexapi."""
+ return self._plex_server.sessions()
+
+ @property
+ def friendly_name(self):
+ """Return name of connected Plex server."""
+ return self._plex_server.friendlyName
+
+ @property
+ def machine_identifier(self):
+ """Return unique identifier of connected Plex server."""
+ return self._plex_server.machineIdentifier
+
+ @property
+ def url_in_use(self):
+ """Return URL used for connected Plex server."""
+ return self._plex_server._baseurl # pylint: disable=W0212
diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json
new file mode 100644
index 00000000000000..c093d4fe0cec1e
--- /dev/null
+++ b/homeassistant/components/plex/strings.json
@@ -0,0 +1,45 @@
+{
+ "config": {
+ "title": "Plex",
+ "step": {
+ "manual_setup": {
+ "title": "Plex server",
+ "data": {
+ "host": "Host",
+ "port": "Port",
+ "ssl": "Use SSL",
+ "verify_ssl": "Verify SSL certificate",
+ "token": "Token (if required)"
+ }
+ },
+ "select_server": {
+ "title": "Select Plex server",
+ "description": "Multiple servers available, select one:",
+ "data": {
+ "server": "Server"
+ }
+ },
+ "user": {
+ "title": "Connect Plex server",
+ "description": "Enter a Plex token for automatic setup or manually configure a server.",
+ "data": {
+ "token": "Plex token",
+ "manual_setup": "Manual setup"
+ }
+ }
+ },
+ "error": {
+ "faulty_credentials": "Authorization failed",
+ "no_servers": "No servers linked to account",
+ "not_found": "Plex server not found",
+ "no_token": "Provide a token or select manual setup"
+ },
+ "abort": {
+ "all_configured": "All linked servers already configured",
+ "already_configured": "This Plex server is already configured",
+ "already_in_progress": "Plex is being configured",
+ "invalid_import": "Imported configuration is invalid",
+ "unknown": "Failed for unknown reason"
+ }
+ }
+}
diff --git a/homeassistant/components/point/.translations/es.json b/homeassistant/components/point/.translations/es.json
index 33b6b1d38271a9..9a94e54dd5fa30 100644
--- a/homeassistant/components/point/.translations/es.json
+++ b/homeassistant/components/point/.translations/es.json
@@ -27,6 +27,6 @@
"title": "Proveedor de autenticaci\u00f3n"
}
},
- "title": "Point de Minut"
+ "title": "Minut Point"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/point/.translations/it.json b/homeassistant/components/point/.translations/it.json
index 324801009ca5aa..3c0ef8306e0cb6 100644
--- a/homeassistant/components/point/.translations/it.json
+++ b/homeassistant/components/point/.translations/it.json
@@ -16,6 +16,7 @@
},
"step": {
"auth": {
+ "description": "Segui il link qui sotto e Accetta l'accesso al tuo account Minut, quindi torna indietro e premi Invia qui sotto. \n\n [Link] ( {authorization_url} )",
"title": "Autenticare Point"
},
"user": {
diff --git a/homeassistant/components/point/.translations/ko.json b/homeassistant/components/point/.translations/ko.json
index d70859c8bde0d6..0dd9cd43adadc1 100644
--- a/homeassistant/components/point/.translations/ko.json
+++ b/homeassistant/components/point/.translations/ko.json
@@ -8,7 +8,7 @@
"no_flows": "Point \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Point \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/point/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694."
},
"create_entry": {
- "default": "Point \uae30\uae30\ub294 Minut \ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ "default": "Point \uae30\uae30\ub85c Minut \uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
"follow_link": "Submit \ubc84\ud2bc\uc744 \ub204\ub974\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694",
diff --git a/homeassistant/components/point/.translations/no.json b/homeassistant/components/point/.translations/no.json
index 58b6e1e63fd311..c87c1a702c8d39 100644
--- a/homeassistant/components/point/.translations/no.json
+++ b/homeassistant/components/point/.translations/no.json
@@ -8,11 +8,11 @@
"no_flows": "Du m\u00e5 konfigurere Point f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/point/)."
},
"create_entry": {
- "default": "Vellykket godkjenning med Minut for din(e) Point enhet(er)"
+ "default": "Vellykket autentisering med Minut for din(e) Point enhet(er)"
},
"error": {
- "follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker p\u00e5 Send",
- "no_token": "Ikke godkjent med Minut"
+ "follow_link": "Vennligst f\u00f8lg lenken og autentiser f\u00f8r du trykker p\u00e5 Send",
+ "no_token": "Ikke autentisert med Minut"
},
"step": {
"auth": {
diff --git a/homeassistant/components/point/.translations/pl.json b/homeassistant/components/point/.translations/pl.json
index 66b454e47ff1b7..ca36001cc1ade8 100644
--- a/homeassistant/components/point/.translations/pl.json
+++ b/homeassistant/components/point/.translations/pl.json
@@ -16,7 +16,7 @@
},
"step": {
"auth": {
- "description": "Kliknij poni\u017cszy link i Zaakceptuj dost\u0119p do swojego konta Minut, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n\n [Link]({authorization_url})",
+ "description": "Kliknij poni\u017cszy link i Zaakceptuj dost\u0119p do konta Minut, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n\n [Link]({authorization_url})",
"title": "Uwierzytelnienie Point"
},
"user": {
diff --git a/homeassistant/components/point/.translations/ru.json b/homeassistant/components/point/.translations/ru.json
index 2a10b234e99e21..487510969481f6 100644
--- a/homeassistant/components/point/.translations/ru.json
+++ b/homeassistant/components/point/.translations/ru.json
@@ -5,7 +5,7 @@
"authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
"authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
"external_setup": "Point \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.",
- "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Point \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/point/)."
+ "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Point \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/point/)."
},
"create_entry": {
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py
index 1ba2c4809b6618..82db5f6725f512 100644
--- a/homeassistant/components/prometheus/__init__.py
+++ b/homeassistant/components/prometheus/__init__.py
@@ -1,5 +1,6 @@
"""Support for Prometheus metrics export."""
import logging
+import string
from aiohttp import web
import voluptuous as vol
@@ -159,10 +160,23 @@ def _metric(self, metric, factory, documentation, labels=None):
try:
return self._metrics[metric]
except KeyError:
- full_metric_name = f"{self.metrics_prefix}{metric}"
+ full_metric_name = self._sanitize_metric_name(
+ f"{self.metrics_prefix}{metric}"
+ )
self._metrics[metric] = factory(full_metric_name, documentation, labels)
return self._metrics[metric]
+ @staticmethod
+ def _sanitize_metric_name(metric: str) -> str:
+ return "".join(
+ [
+ c
+ if c in string.ascii_letters or c.isdigit() or c == "_" or c == ":"
+ else f"u{hex(ord(c))}"
+ for c in metric
+ ]
+ )
+
@staticmethod
def state_as_number(state):
"""Return a state casted to a float."""
diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py
index b5856b7f78e921..1f86958d08e065 100644
--- a/homeassistant/components/proximity/__init__.py
+++ b/homeassistant/components/proximity/__init__.py
@@ -211,8 +211,8 @@ def check_proximity_state_change(self, entity, old_state, new_state):
# Loop through each of the distances collected and work out the
# closest.
- closest_device = None # type: str
- dist_to_zone = None # type: float
+ closest_device: str = None
+ dist_to_zone: float = None
for device in distances_to_zone:
if not dist_to_zone or distances_to_zone[device] < dist_to_zone:
diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py
index 7d145315748417..53a4f620dcc3d4 100644
--- a/homeassistant/components/proxy/camera.py
+++ b/homeassistant/components/proxy/camera.py
@@ -66,7 +66,7 @@ def _precheck_image(image, opts):
raise ValueError()
try:
img = Image.open(io.BytesIO(image))
- except IOError:
+ except OSError:
_LOGGER.warning("Failed to open image")
raise ValueError()
imgfmt = str(img.format)
diff --git a/homeassistant/components/ps4/.translations/fr.json b/homeassistant/components/ps4/.translations/fr.json
index 03baf0c032e3a3..991222d45be78b 100644
--- a/homeassistant/components/ps4/.translations/fr.json
+++ b/homeassistant/components/ps4/.translations/fr.json
@@ -4,8 +4,8 @@
"credential_error": "Erreur lors de l'extraction des informations d'identification.",
"devices_configured": "Tous les p\u00e9riph\u00e9riques trouv\u00e9s sont d\u00e9j\u00e0 configur\u00e9s.",
"no_devices_found": "Aucun appareil PlayStation 4 trouv\u00e9 sur le r\u00e9seau.",
- "port_987_bind_error": "Impossible de se connecter au port 997.",
- "port_997_bind_error": "Impossible de se connecter au port 997."
+ "port_987_bind_error": "Impossible de se connecter au port 997. Reportez-vous \u00e0 la [documentation] (https://www.home-assistant.io/components/ps4/) pour plus d'informations.",
+ "port_997_bind_error": "Impossible de se connecter au port 997. Reportez-vous \u00e0 la [documentation] (https://www.home-assistant.io/components/ps4/) pour plus d'informations."
},
"error": {
"credential_timeout": "Le service d'informations d'identification a expir\u00e9. Appuyez sur soumettre pour red\u00e9marrer.",
diff --git a/homeassistant/components/ps4/.translations/it.json b/homeassistant/components/ps4/.translations/it.json
index afa32056757ccb..de5eb4e5e6f30f 100644
--- a/homeassistant/components/ps4/.translations/it.json
+++ b/homeassistant/components/ps4/.translations/it.json
@@ -4,11 +4,12 @@
"credential_error": "Errore nel recupero delle credenziali.",
"devices_configured": "Tutti i dispositivi trovati sono gi\u00e0 configurati.",
"no_devices_found": "Nessun dispositivo PlayStation 4 trovato in rete.",
- "port_987_bind_error": "Impossibile connettersi alla porta 987.",
- "port_997_bind_error": "Impossibile connettersi alla porta 997."
+ "port_987_bind_error": "Impossibile collegarsi alla porta 987. Per ulteriori informazioni, consultare la [documentazione] (https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.",
+ "port_997_bind_error": "Impossibile collegarsi alla porta 997. Consultare la [documentazione] (https://www.home-assistant.io/components/ps4/) per ulteriori informazioni."
},
"error": {
- "login_failed": "Accoppiamento alla PlayStation 4 fallito. Verifica che il PIN sia corretto.",
+ "credential_timeout": "Servizio credenziali scaduto. Premi Invia per riavviare.",
+ "login_failed": "Impossibile eseguire l'associazione a PlayStation 4. Verificare che il PIN sia corretto.",
"no_ipaddress": "Inserisci l'indirizzo IP della PlayStation 4 che desideri configurare.",
"not_ready": "La PlayStation 4 non \u00e8 accesa o non \u00e8 collegata alla rete."
},
@@ -24,7 +25,7 @@
"name": "Nome",
"region": "Area geografica"
},
- "description": "Inserisci le informazioni della tua PlayStation 4. Per il \"PIN\", vai su \"Impostazioni\" sulla tua console PlayStation 4. Quindi accedi a \"Impostazioni connessione app mobile\" e seleziona \"Aggiungi dispositivo\". Inserisci il PIN che viene visualizzato.",
+ "description": "Inserisci le tue informazioni su PlayStation 4. Per \"PIN\", vai a \"Impostazioni\" sulla console PlayStation 4. Quindi vai a 'Impostazioni di connessione app mobile' e seleziona 'Aggiungi dispositivo'. Immettere il PIN visualizzato. Fare riferimento alla [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.",
"title": "PlayStation 4"
},
"mode": {
diff --git a/homeassistant/components/ps4/.translations/ko.json b/homeassistant/components/ps4/.translations/ko.json
index f13a66d5e8a039..25f64cd21e9db2 100644
--- a/homeassistant/components/ps4/.translations/ko.json
+++ b/homeassistant/components/ps4/.translations/ko.json
@@ -3,7 +3,7 @@
"abort": {
"credential_error": "\uc790\uaca9 \uc99d\uba85\uc744 \uac00\uc838\uc624\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
"devices_configured": "\ubc1c\uacac \ub41c \ubaa8\ub4e0 \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
- "no_devices_found": "PlayStation 4 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
+ "no_devices_found": "PlayStation 4 \uae30\uae30\ub97c \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
"port_987_bind_error": "\ud3ec\ud2b8 987 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.",
"port_997_bind_error": "\ud3ec\ud2b8 997 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
},
diff --git a/homeassistant/components/ps4/.translations/lb.json b/homeassistant/components/ps4/.translations/lb.json
index 17757cb9d20317..0986b0e0240afb 100644
--- a/homeassistant/components/ps4/.translations/lb.json
+++ b/homeassistant/components/ps4/.translations/lb.json
@@ -4,8 +4,8 @@
"credential_error": "Feeler beim ausliesen vun den Umeldungs Informatiounen.",
"devices_configured": "All Apparater sinn schonn konfigur\u00e9iert",
"no_devices_found": "Keng Playstation 4 am Netzwierk fonnt.",
- "port_987_bind_error": "Konnt sech net mam Port 987 verbannen.",
- "port_997_bind_error": "Konnt sech net mam Port 997 verbannen."
+ "port_987_bind_error": "Konnt sech net mam Port 987 verbannen. Liest [Dokumentatioun](https://www.home-assistant.io/components/ps4/) fir w\u00e9ider Informatiounen.",
+ "port_997_bind_error": "Konnt sech net mam Port 997 verbannen. Liest [Dokumentatioun](https://www.home-assistant.io/components/ps4/) fir w\u00e9ider Informatiounen."
},
"error": {
"credential_timeout": "Z\u00e4it Iwwerschreidung beim Service vun den Umeldungsinformatiounen. Dr\u00e9ck op ofsch\u00e9cke fir nach emol ze starten.",
@@ -25,7 +25,7 @@
"name": "Numm",
"region": "Regioun"
},
- "description": "Gitt \u00e4r Playstation 4 Informatiounen an. Fir 'PIN', gitt an d'Astellunge vun der Playstation 4 Konsole. Dann op 'Mobile App Verbindungs Astellungen' a wielt \"Apparat dob\u00e4isetzen' aus. Gitt de PIN an deen ugewise g\u00ebtt.",
+ "description": "Gitt \u00e4r Playstation 4 Informatiounen an. Fir 'PIN', gitt an d'Astellunge vun der Playstation 4 Konsole. Dann op 'Mobile App Verbindungs Astellungen' a wielt \"Apparat dob\u00e4isetzen' aus. Gitt de PIN an deen ugewise g\u00ebtt. Liest [Dokumentatioun](https://www.home-assistant.io/components/ps4/) fir w\u00e9ider Informatiounen.",
"title": "PlayStation 4"
},
"mode": {
diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json
index 610ec92a2b3939..83d70830b11a43 100644
--- a/homeassistant/components/python_script/manifest.json
+++ b/homeassistant/components/python_script/manifest.json
@@ -3,8 +3,8 @@
"name": "Python script",
"documentation": "https://www.home-assistant.io/components/python_script",
"requirements": [
- "restrictedpython==4.0"
+ "restrictedpython==5.0"
],
"dependencies": [],
"codeowners": []
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py
index e8d32c036d561f..8ae80ca9027a43 100644
--- a/homeassistant/components/qld_bushfire/geo_location.py
+++ b/homeassistant/components/qld_bushfire/geo_location.py
@@ -198,6 +198,11 @@ def _update_from_feed(self, feed_entry):
self._updated_date = feed_entry.updated
self._status = feed_entry.status
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend."""
+ return "mdi:fire"
+
@property
def source(self) -> str:
"""Return source value of this external event."""
diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py
index f2c3e229c9586a..a007dd673ace3a 100644
--- a/homeassistant/components/radiotherm/climate.py
+++ b/homeassistant/components/radiotherm/climate.py
@@ -1,5 +1,4 @@
"""Support for Radio Thermostat wifi-enabled home thermostats."""
-import datetime
import logging
import voluptuous as vol
@@ -11,6 +10,11 @@
HVAC_MODE_COOL,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
+ FAN_ON,
+ FAN_OFF,
+ CURRENT_HVAC_IDLE,
+ CURRENT_HVAC_HEAT,
+ CURRENT_HVAC_COOL,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_FAN_MODE,
)
@@ -21,12 +25,12 @@
TEMP_FAHRENHEIT,
STATE_ON,
)
+from homeassistant.util import dt as dt_util
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
-ATTR_FAN = "fan"
-ATTR_MODE = "mode"
+ATTR_FAN_ACTION = "fan_action"
CONF_HOLD_TEMP = "hold_temp"
@@ -55,11 +59,11 @@
# Active thermostat state (is it heating or cooling?). In the future
# this should probably made into heat and cool binary sensors.
-CODE_TO_TEMP_STATE = {0: HVAC_MODE_OFF, 1: HVAC_MODE_HEAT, 2: HVAC_MODE_COOL}
+CODE_TO_TEMP_STATE = {0: CURRENT_HVAC_IDLE, 1: CURRENT_HVAC_HEAT, 2: CURRENT_HVAC_COOL}
# Active fan state. This is if the fan is actually on or not. In the
# future this should probably made into a binary sensor for the fan.
-CODE_TO_FAN_STATE = {0: HVAC_MODE_OFF, 1: STATE_ON}
+CODE_TO_FAN_STATE = {0: FAN_OFF, 1: FAN_ON}
def round_temp(temperature):
@@ -160,7 +164,7 @@ def precision(self):
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
- return {ATTR_FAN: self._fstate, ATTR_MODE: self._tstate}
+ return {ATTR_FAN_ACTION: self._fstate}
@property
def fan_modes(self):
@@ -200,6 +204,13 @@ def hvac_modes(self):
"""Return the operation modes list."""
return OPERATION_LIST
+ @property
+ def hvac_action(self):
+ """Return the current running hvac operation if supported."""
+ if self.hvac_mode == HVAC_MODE_OFF:
+ return None
+ return self._tstate
+
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
@@ -261,9 +272,9 @@ def update(self):
# This doesn't really work - tstate is only set if the HVAC is
# active. If it's idle, we don't know what to do with the target
# temperature.
- if self._tstate == HVAC_MODE_COOL:
+ if self._tstate == CURRENT_HVAC_COOL:
self._target_temperature = data["t_cool"]
- elif self._tstate == HVAC_MODE_HEAT:
+ elif self._tstate == CURRENT_HVAC_HEAT:
self._target_temperature = data["t_heat"]
else:
self._current_operation = HVAC_MODE_OFF
@@ -281,9 +292,9 @@ def set_temperature(self, **kwargs):
elif self._current_operation == HVAC_MODE_HEAT:
self.device.t_heat = temperature
elif self._current_operation == HVAC_MODE_AUTO:
- if self._tstate == HVAC_MODE_COOL:
+ if self._tstate == CURRENT_HVAC_COOL:
self.device.t_cool = temperature
- elif self._tstate == HVAC_MODE_HEAT:
+ elif self._tstate == CURRENT_HVAC_HEAT:
self.device.t_heat = temperature
# Only change the hold if requested or if hold mode was turned
@@ -299,7 +310,7 @@ def set_time(self):
"""Set device time."""
# Calling this clears any local temperature override and
# reverts to the scheduled temperature.
- now = datetime.datetime.now()
+ now = dt_util.now()
self.device.time = {
"day": now.weekday(),
"hour": now.hour,
diff --git a/homeassistant/components/rainmachine/.translations/pl.json b/homeassistant/components/rainmachine/.translations/pl.json
index 9891ac50f4811f..9ab6156549d5a1 100644
--- a/homeassistant/components/rainmachine/.translations/pl.json
+++ b/homeassistant/components/rainmachine/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane",
+ "identifier_exists": "Konto jest ju\u017c zarejestrowane",
"invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia"
},
"step": {
@@ -11,7 +11,7 @@
"password": "Has\u0142o",
"port": "Port"
},
- "title": "Wprowad\u017a swoje dane"
+ "title": "Wprowad\u017a dane"
}
},
"title": "RainMachine"
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index 0d814a5d74baac..9d34cc6fb79f27 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -7,7 +7,7 @@
import queue
import threading
import time
-from typing import Any, Dict, Optional # noqa: F401
+from typing import Any, Dict, Optional
import voluptuous as vol
@@ -177,12 +177,12 @@ def __init__(
self.hass = hass
self.keep_days = keep_days
self.purge_interval = purge_interval
- self.queue = queue.Queue() # type: Any
+ self.queue: Any = queue.Queue()
self.recording_start = dt_util.utcnow()
self.db_url = uri
self.async_db_ready = asyncio.Future()
- self.engine = None # type: Any
- self.run_info = None # type: Any
+ self.engine: Any = None
+ self.run_info: Any = None
self.entity_filter = generate_filter(
include.get(CONF_DOMAINS, []),
diff --git a/homeassistant/components/remote_rpi_gpio/__init__.py b/homeassistant/components/remote_rpi_gpio/__init__.py
index ccefd00c723ac3..33356d0e3b82cc 100644
--- a/homeassistant/components/remote_rpi_gpio/__init__.py
+++ b/homeassistant/components/remote_rpi_gpio/__init__.py
@@ -47,7 +47,7 @@ def setup_input(address, port, pull_mode, bouncetime):
bounce_time=bouncetime,
pin_factory=PiGPIOFactory(address),
)
- except (ValueError, IndexError, KeyError, IOError):
+ except (ValueError, IndexError, KeyError, OSError):
return None
diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py
index 8c7d7b7d023b1d..e12d83324fd756 100644
--- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py
+++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py
@@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
button = remote_rpi_gpio.setup_input(
address, port_num, pull_mode, bouncetime
)
- except (ValueError, IndexError, KeyError, IOError):
+ except (ValueError, IndexError, KeyError, OSError):
return
new_sensor = RemoteRPiGPIOBinarySensor(port_name, button, invert_logic)
devices.append(new_sensor)
diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py
index aa20a2909d2ffd..8240de7951d710 100644
--- a/homeassistant/components/remote_rpi_gpio/switch.py
+++ b/homeassistant/components/remote_rpi_gpio/switch.py
@@ -36,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for port, name in ports.items():
try:
led = remote_rpi_gpio.setup_output(address, port, invert_logic)
- except (ValueError, IndexError, KeyError, IOError):
+ except (ValueError, IndexError, KeyError, OSError):
return
new_switch = RemoteRPiGPIOSwitch(name, led, invert_logic)
devices.append(new_switch)
diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py
index 5805114252e09a..697be4d1579220 100644
--- a/homeassistant/components/ring/light.py
+++ b/homeassistant/components/ring/light.py
@@ -1,9 +1,10 @@
"""This component provides HA switch support for Ring Door Bell/Chimes."""
import logging
-from datetime import datetime, timedelta
+from datetime import timedelta
from homeassistant.components.light import Light
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.core import callback
+import homeassistant.util.dt as dt_util
from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING
@@ -41,7 +42,7 @@ def __init__(self, device):
self._device = device
self._unique_id = self._device.id
self._light_on = False
- self._no_updates_until = datetime.now()
+ self._no_updates_until = dt_util.utcnow()
async def async_added_to_hass(self):
"""Register callbacks."""
@@ -77,7 +78,7 @@ def _set_light(self, new_state):
"""Update light state, and causes HASS to correctly update."""
self._device.lights = new_state
self._light_on = new_state == ON_STATE
- self._no_updates_until = datetime.now() + SKIP_UPDATES_DELAY
+ self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY
self.async_schedule_update_ha_state(True)
def turn_on(self, **kwargs):
@@ -90,7 +91,7 @@ def turn_off(self, **kwargs):
def update(self):
"""Update current state of the light."""
- if self._no_updates_until > datetime.now():
+ if self._no_updates_until > dt_util.utcnow():
_LOGGER.debug("Skipping update...")
return
diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py
index cbbecb1a40398b..413d2a70aae2ba 100644
--- a/homeassistant/components/ring/switch.py
+++ b/homeassistant/components/ring/switch.py
@@ -1,9 +1,10 @@
"""This component provides HA switch support for Ring Door Bell/Chimes."""
import logging
-from datetime import datetime, timedelta
+from datetime import timedelta
from homeassistant.components.switch import SwitchDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.core import callback
+import homeassistant.util.dt as dt_util
from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING
@@ -72,14 +73,14 @@ class SirenSwitch(BaseRingSwitch):
def __init__(self, device):
"""Initialize the switch for a device with a siren."""
super().__init__(device, "siren")
- self._no_updates_until = datetime.now()
+ self._no_updates_until = dt_util.utcnow()
self._siren_on = False
def _set_switch(self, new_state):
"""Update switch state, and causes HASS to correctly update."""
self._device.siren = new_state
self._siren_on = new_state > 0
- self._no_updates_until = datetime.now() + SKIP_UPDATES_DELAY
+ self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY
self.schedule_update_ha_state()
@property
@@ -102,7 +103,7 @@ def icon(self):
def update(self):
"""Update state of the siren."""
- if self._no_updates_until > datetime.now():
+ if self._no_updates_until > dt_util.utcnow():
_LOGGER.debug("Skipping update...")
return
self._siren_on = self._device.siren > 0
diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py
index f2e0ccac2b0008..cdd6af57617e46 100644
--- a/homeassistant/components/season/sensor.py
+++ b/homeassistant/components/season/sensor.py
@@ -8,6 +8,7 @@
from homeassistant.const import CONF_TYPE
from homeassistant.helpers.entity import Entity
from homeassistant import util
+import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -104,7 +105,7 @@ def __init__(self, hass, hemisphere, season_tracking_type):
"""Initialize the season."""
self.hass = hass
self.hemisphere = hemisphere
- self.datetime = datetime.now()
+ self.datetime = dt_util.utcnow().replace(tzinfo=None)
self.type = season_tracking_type
self.season = get_season(self.datetime, self.hemisphere, self.type)
@@ -125,5 +126,5 @@ def icon(self):
def update(self):
"""Update season."""
- self.datetime = datetime.utcnow()
+ self.datetime = dt_util.utcnow().replace(tzinfo=None)
self.season = get_season(self.datetime, self.hemisphere, self.type)
diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json
index eb006f408bcbd8..1ffbe69888f329 100644
--- a/homeassistant/components/sendgrid/manifest.json
+++ b/homeassistant/components/sendgrid/manifest.json
@@ -3,7 +3,7 @@
"name": "Sendgrid",
"documentation": "https://www.home-assistant.io/components/sendgrid",
"requirements": [
- "sendgrid==6.0.5"
+ "sendgrid==6.1.0"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py
index ac334587b89035..f16758a53559c2 100644
--- a/homeassistant/components/sendgrid/notify.py
+++ b/homeassistant/components/sendgrid/notify.py
@@ -3,6 +3,8 @@
import voluptuous as vol
+from sendgrid import SendGridAPIClient
+
from homeassistant.const import (
CONF_API_KEY,
CONF_RECIPIENT,
@@ -45,8 +47,6 @@ class SendgridNotificationService(BaseNotificationService):
def __init__(self, config):
"""Initialize the service."""
- from sendgrid import SendGridAPIClient
-
self.api_key = config[CONF_API_KEY]
self.sender = config[CONF_SENDER]
self.sender_name = config[CONF_SENDER_NAME]
diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json
index 7ecc298e3f6f17..be7f0a524dcd0b 100644
--- a/homeassistant/components/shodan/manifest.json
+++ b/homeassistant/components/shodan/manifest.json
@@ -3,7 +3,7 @@
"name": "Shodan",
"documentation": "https://www.home-assistant.io/components/shodan",
"requirements": [
- "shodan==1.15.0"
+ "shodan==1.17.0"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/simplisafe/.translations/it.json b/homeassistant/components/simplisafe/.translations/it.json
index 134bfae366821c..6f0e403a356548 100644
--- a/homeassistant/components/simplisafe/.translations/it.json
+++ b/homeassistant/components/simplisafe/.translations/it.json
@@ -9,7 +9,7 @@
"data": {
"code": "Codice (Home Assistant)",
"password": "Password",
- "username": "Indirizzo email"
+ "username": "Indirizzo E-mail"
},
"title": "Inserisci i tuoi dati"
}
diff --git a/homeassistant/components/simplisafe/.translations/pl.json b/homeassistant/components/simplisafe/.translations/pl.json
index 0b83ba8cbedd10..c4d616600f56a7 100644
--- a/homeassistant/components/simplisafe/.translations/pl.json
+++ b/homeassistant/components/simplisafe/.translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"error": {
- "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane",
+ "identifier_exists": "Konto jest ju\u017c zarejestrowane",
"invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia"
},
"step": {
@@ -11,7 +11,7 @@
"password": "Has\u0142o",
"username": "Adres e-mail"
},
- "title": "Wprowad\u017a swoje dane"
+ "title": "Wprowad\u017a dane"
}
},
"title": "SimpliSafe"
diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json
index 8a03ac47402bae..cf26955b207b5e 100644
--- a/homeassistant/components/simplisafe/manifest.json
+++ b/homeassistant/components/simplisafe/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/simplisafe",
"requirements": [
- "simplisafe-python==4.3.0"
+ "simplisafe-python==5.0.1"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py
index c8969add244a19..109c410c16d0b8 100644
--- a/homeassistant/components/sky_hub/device_tracker.py
+++ b/homeassistant/components/sky_hub/device_tracker.py
@@ -94,7 +94,7 @@ def _parse_skyhub_response(data_str):
"""Parse the Sky Hub data format."""
pattmatch = re.search("attach_dev = '(.*)'", data_str)
if pattmatch is None:
- raise IOError(
+ raise OSError(
"Error: Impossible to fetch data from"
+ " Sky Hub. Try to reboot the router."
)
diff --git a/homeassistant/components/smartthings/.translations/it.json b/homeassistant/components/smartthings/.translations/it.json
index 486a61847a71a0..c2b17eed04d3fe 100644
--- a/homeassistant/components/smartthings/.translations/it.json
+++ b/homeassistant/components/smartthings/.translations/it.json
@@ -5,6 +5,7 @@
"app_setup_error": "Impossibile configurare SmartApp. Riprovare.",
"base_url_not_https": "Il `base_url` per il componente `http` deve essere configurato e deve iniziare con `https://`.",
"token_already_setup": "Il token \u00e8 gi\u00e0 stato configurato.",
+ "token_forbidden": "Il token non dispone degli ambiti OAuth necessari.",
"token_invalid_format": "Il token deve essere nel formato UID/GUID",
"token_unauthorized": "Il token non \u00e8 valido o non \u00e8 pi\u00f9 autorizzato.",
"webhook_error": "SmartThings non ha potuto convalidare l'endpoint configurato in `base_url`. Si prega di rivedere i requisiti del componente."
@@ -18,6 +19,7 @@
"title": "Inserisci il Token di Accesso Personale"
},
"wait_install": {
+ "description": "Si prega di installare l'Home Assistant SmartApp in almeno una posizione e fare clic su Invia.",
"title": "Installa SmartApp"
}
},
diff --git a/homeassistant/components/solaredge/.translations/bg.json b/homeassistant/components/solaredge/.translations/bg.json
new file mode 100644
index 00000000000000..72f1ad2a4c758f
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/bg.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "\u0422\u043e\u0432\u0430 site_id \u0432\u0435\u0447\u0435 \u0435 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u043e"
+ },
+ "error": {
+ "site_exists": "\u0422\u043e\u0432\u0430 site_id \u0432\u0435\u0447\u0435 \u0435 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u043e"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \u043a\u043b\u044e\u0447\u0430 \u0437\u0430 \u0442\u043e\u0437\u0438 \u0441\u0430\u0439\u0442",
+ "name": "\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435\u0442\u043e \u043d\u0430 \u0442\u0430\u0437\u0438 \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f",
+ "site_id": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u044a\u0440\u044a\u0442 site-id \u043d\u0430 SolarEdge"
+ },
+ "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 (API) \u0437\u0430 \u0442\u0430\u0437\u0438 \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/ca.json b/homeassistant/components/solaredge/.translations/ca.json
new file mode 100644
index 00000000000000..fd3707af3ddd88
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/ca.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "Aquest site_id ja est\u00e0 configurat"
+ },
+ "error": {
+ "site_exists": "Aquest site_id ja est\u00e0 configurat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Clau API d\u2019aquest lloc",
+ "name": "Nom d\u2019aquesta instal\u00b7laci\u00f3",
+ "site_id": "SolarEdge site_id"
+ },
+ "title": "Configuraci\u00f3 dels par\u00e0metres de l'API per aquesta instal\u00b7laci\u00f3"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/da.json b/homeassistant/components/solaredge/.translations/da.json
new file mode 100644
index 00000000000000..7ed64f51083d16
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/da.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "Dette site_id er allerede konfigureret"
+ },
+ "error": {
+ "site_exists": "Dette site_id er allerede konfigureret"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-n\u00f8glen til dette websted",
+ "name": "Navnet p\u00e5 denne installation",
+ "site_id": "SolarEdge site-id"
+ },
+ "title": "Definer API-parametre til denne installation"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/en.json b/homeassistant/components/solaredge/.translations/en.json
new file mode 100644
index 00000000000000..7b06c110397f04
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/en.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "This site_id is already configured"
+ },
+ "error": {
+ "site_exists": "This site_id is already configured"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "The API key for this site",
+ "name": "The name of this installation",
+ "site_id": "The SolarEdge site-id"
+ },
+ "title": "Define the API parameters for this installation"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/es.json b/homeassistant/components/solaredge/.translations/es.json
new file mode 100644
index 00000000000000..8708729bf4aebd
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/es.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "Este site_id ya est\u00e1 configurado"
+ },
+ "error": {
+ "site_exists": "Este site_id ya est\u00e1 configurado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "La clave de la API para este sitio",
+ "name": "El nombre de esta instalaci\u00f3n",
+ "site_id": "La identificaci\u00f3n del sitio de SolarEdge"
+ },
+ "title": "Definir los par\u00e1metros de la API para esta instalaci\u00f3n"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/fr.json b/homeassistant/components/solaredge/.translations/fr.json
new file mode 100644
index 00000000000000..201e3ff49c6105
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/fr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "La cl\u00e9 API pour ce site",
+ "name": "Le nom de cette installation",
+ "site_id": "L'identifiant de site SolarEdge"
+ },
+ "title": "D\u00e9finir les param\u00e8tres de l'API pour cette installation"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/it.json b/homeassistant/components/solaredge/.translations/it.json
new file mode 100644
index 00000000000000..6523f393628f27
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/it.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "Questo site_id \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "site_exists": "Questo site_id \u00e8 gi\u00e0 configurato"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "La chiave API per questo sito",
+ "name": "Il nome di questa installazione",
+ "site_id": "Il sito-id di SolarEdge"
+ },
+ "title": "Definire i parametri API per questa installazione"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/ko.json b/homeassistant/components/solaredge/.translations/ko.json
new file mode 100644
index 00000000000000..3d4b344825289a
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/ko.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\uc774 \uc0ac\uc774\ud2b8\uc758 API \ud0a4",
+ "name": "\uc774 \uc124\uce58\uc758 \uc774\ub984",
+ "site_id": "SolarEdge site-id"
+ },
+ "title": "\uc774 \uc124\uce58\uc5d0 \ub300\ud55c API \ub9e4\uac1c\ubcc0\uc218\ub97c \uc815\uc758\ud574\uc8fc\uc138\uc694"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/lb.json b/homeassistant/components/solaredge/.translations/lb.json
new file mode 100644
index 00000000000000..afc558ca80cd84
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/lb.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert"
+ },
+ "error": {
+ "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Schl\u00ebssel fir d\u00ebsen Site",
+ "name": "Numm vun d\u00ebser Installatioun",
+ "site_id": "SolarEdge site-ID"
+ },
+ "title": "API Parameter fir d\u00ebs Installatioun d\u00e9fin\u00e9ieren"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/nl.json b/homeassistant/components/solaredge/.translations/nl.json
new file mode 100644
index 00000000000000..3cc52b43a63126
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/nl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "Deze site_id is al geconfigureerd"
+ },
+ "error": {
+ "site_exists": "Deze site_id is al geconfigureerd"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "De API-sleutel voor deze site",
+ "name": "De naam van deze installatie",
+ "site_id": "De SolarEdge site-id"
+ },
+ "title": "Definieer de API-parameters voor deze installatie"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/no.json b/homeassistant/components/solaredge/.translations/no.json
new file mode 100644
index 00000000000000..ad7cb55316b696
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/no.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "Denne site_id er allerede konfigurert"
+ },
+ "error": {
+ "site_exists": "Denne site_id er allerede konfigurert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-n\u00f8kkelen for dette nettstedet",
+ "name": "Navnet p\u00e5 denne installasjonen",
+ "site_id": "SolarEdge nettsted-id"
+ },
+ "title": "Definer API-parametrene for denne installasjonen"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/pl.json b/homeassistant/components/solaredge/.translations/pl.json
new file mode 100644
index 00000000000000..376a81219b0c81
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/pl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "Ten site_id jest ju\u017c skonfigurowany"
+ },
+ "error": {
+ "site_exists": "Ten site_id jest ju\u017c skonfigurowany"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Klucz API dla tej strony",
+ "name": "Nazwa tej instalacji",
+ "site_id": "SolarEdge site-id"
+ },
+ "title": "Zdefiniuj parametry API dla tej instalacji"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/ru.json b/homeassistant/components/solaredge/.translations/ru.json
new file mode 100644
index 00000000000000..fe36e4296feb92
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/ru.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d"
+ },
+ "error": {
+ "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u0430\u0439\u0442\u0430",
+ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
+ "site_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0430\u0439\u0442\u0430 SolarEdge"
+ },
+ "title": "SolarEdge"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/sl.json b/homeassistant/components/solaredge/.translations/sl.json
new file mode 100644
index 00000000000000..ebfefe40b0e54d
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/sl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "Ta site_id je \u017ee nastavljen"
+ },
+ "error": {
+ "site_exists": "Ta site_id je \u017ee nastavljen"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API klju\u010d za to stran",
+ "name": "Ime te namestitve",
+ "site_id": "SolarEdge site-ID"
+ },
+ "title": "Dolo\u010dite parametre API za to namestitev"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/.translations/zh-Hant.json b/homeassistant/components/solaredge/.translations/zh-Hant.json
new file mode 100644
index 00000000000000..698c28d99bf72a
--- /dev/null
+++ b/homeassistant/components/solaredge/.translations/zh-Hant.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \u91d1\u9470",
+ "name": "\u5b89\u88dd\u540d\u7a31",
+ "site_id": "SolarEdge site-id"
+ },
+ "title": "\u8a2d\u5b9a API \u53c3\u6578"
+ }
+ },
+ "title": "SolarEdge"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py
index b675126c5fd69b..8909b970aafd2b 100644
--- a/homeassistant/components/solaredge/__init__.py
+++ b/homeassistant/components/solaredge/__init__.py
@@ -1 +1,43 @@
"""The solaredge component."""
+import voluptuous as vol
+
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
+from homeassistant.const import CONF_API_KEY, CONF_NAME
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import DEFAULT_NAME, DOMAIN, CONF_SITE_ID
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Required(CONF_API_KEY): cv.string,
+ vol.Required(CONF_SITE_ID): cv.string,
+ }
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+async def async_setup(hass, config):
+ """Platform setup, do nothing."""
+ if DOMAIN not in config:
+ return True
+
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config[DOMAIN])
+ )
+ )
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
+ """Load the saved entities."""
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, "sensor")
+ )
+ return True
diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py
new file mode 100644
index 00000000000000..67f05d83aa0f17
--- /dev/null
+++ b/homeassistant/components/solaredge/config_flow.py
@@ -0,0 +1,98 @@
+"""Config flow for the SolarEdge platform."""
+import solaredge
+import voluptuous as vol
+from requests.exceptions import HTTPError, ConnectTimeout
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_API_KEY, CONF_NAME
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.util import slugify
+
+from .const import DOMAIN, DEFAULT_NAME, CONF_SITE_ID
+
+
+@callback
+def solaredge_entries(hass: HomeAssistant):
+ """Return the site_ids for the domain."""
+ return set(
+ (entry.data[CONF_SITE_ID])
+ for entry in hass.config_entries.async_entries(DOMAIN)
+ )
+
+
+class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ def __init__(self) -> None:
+ """Initialize the config flow."""
+ self._errors = {}
+
+ def _site_in_configuration_exists(self, site_id) -> bool:
+ """Return True if site_id exists in configuration."""
+ if site_id in solaredge_entries(self.hass):
+ return True
+ return False
+
+ def _check_site(self, site_id, api_key) -> bool:
+ """Check if we can connect to the soleredge api service."""
+ api = solaredge.Solaredge(api_key)
+ try:
+ response = api.get_details(site_id)
+ except (ConnectTimeout, HTTPError):
+ self._errors[CONF_SITE_ID] = "could_not_connect"
+ return False
+ try:
+ if response["details"]["status"].lower() != "active":
+ self._errors[CONF_SITE_ID] = "site_not_active"
+ return False
+ except KeyError:
+ self._errors[CONF_SITE_ID] = "api_failure"
+ return False
+ return True
+
+ async def async_step_user(self, user_input=None):
+ """Step when user intializes a integration."""
+ self._errors = {}
+ if user_input is not None:
+ name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME))
+ if self._site_in_configuration_exists(user_input[CONF_SITE_ID]):
+ self._errors[CONF_SITE_ID] = "site_exists"
+ else:
+ site = user_input[CONF_SITE_ID]
+ api = user_input[CONF_API_KEY]
+ can_connect = await self.hass.async_add_executor_job(
+ self._check_site, site, api
+ )
+ if can_connect:
+ return self.async_create_entry(
+ title=name, data={CONF_SITE_ID: site, CONF_API_KEY: api}
+ )
+
+ else:
+ user_input = {}
+ user_input[CONF_NAME] = DEFAULT_NAME
+ user_input[CONF_SITE_ID] = ""
+ user_input[CONF_API_KEY] = ""
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(
+ CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME)
+ ): str,
+ vol.Required(CONF_SITE_ID, default=user_input[CONF_SITE_ID]): str,
+ vol.Required(CONF_API_KEY, default=user_input[CONF_API_KEY]): str,
+ }
+ ),
+ errors=self._errors,
+ )
+
+ async def async_step_import(self, user_input=None):
+ """Import a config entry."""
+ if self._site_in_configuration_exists(user_input[CONF_SITE_ID]):
+ return self.async_abort(reason="site_exists")
+ return await self.async_step_user(user_input)
diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py
new file mode 100644
index 00000000000000..0d3d1a0cb5fff7
--- /dev/null
+++ b/homeassistant/components/solaredge/const.py
@@ -0,0 +1,68 @@
+"""Constants for the SolarEdge Monitoring API."""
+from datetime import timedelta
+
+from homeassistant.const import POWER_WATT, ENERGY_WATT_HOUR
+
+DOMAIN = "solaredge"
+
+# Config for solaredge monitoring api requests.
+CONF_SITE_ID = "site_id"
+
+DEFAULT_NAME = "SolarEdge"
+
+OVERVIEW_UPDATE_DELAY = timedelta(minutes=10)
+DETAILS_UPDATE_DELAY = timedelta(hours=12)
+INVENTORY_UPDATE_DELAY = timedelta(hours=12)
+POWER_FLOW_UPDATE_DELAY = timedelta(minutes=10)
+
+SCAN_INTERVAL = timedelta(minutes=10)
+
+# Supported overview sensor types:
+# Key: ['json_key', 'name', unit, icon, default]
+SENSOR_TYPES = {
+ "lifetime_energy": [
+ "lifeTimeData",
+ "Lifetime energy",
+ ENERGY_WATT_HOUR,
+ "mdi:solar-power",
+ False,
+ ],
+ "energy_this_year": [
+ "lastYearData",
+ "Energy this year",
+ ENERGY_WATT_HOUR,
+ "mdi:solar-power",
+ False,
+ ],
+ "energy_this_month": [
+ "lastMonthData",
+ "Energy this month",
+ ENERGY_WATT_HOUR,
+ "mdi:solar-power",
+ False,
+ ],
+ "energy_today": [
+ "lastDayData",
+ "Energy today",
+ ENERGY_WATT_HOUR,
+ "mdi:solar-power",
+ False,
+ ],
+ "current_power": [
+ "currentPower",
+ "Current Power",
+ POWER_WATT,
+ "mdi:solar-power",
+ True,
+ ],
+ "site_details": [None, "Site details", None, None, False],
+ "meters": ["meters", "Meters", None, None, False],
+ "sensors": ["sensors", "Sensors", None, None, False],
+ "gateways": ["gateways", "Gateways", None, None, False],
+ "batteries": ["batteries", "Batteries", None, None, False],
+ "inverters": ["inverters", "Inverters", None, None, False],
+ "power_consumption": ["LOAD", "Power Consumption", None, "mdi:flash", False],
+ "solar_power": ["PV", "Solar Power", None, "mdi:solar-power", False],
+ "grid_power": ["GRID", "Grid Power", None, "mdi:power-plug", False],
+ "storage_power": ["STORAGE", "Storage Power", None, "mdi:car-battery", False],
+}
diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json
index b2707a0a9372de..7452790cd6043f 100644
--- a/homeassistant/components/solaredge/manifest.json
+++ b/homeassistant/components/solaredge/manifest.json
@@ -6,6 +6,7 @@
"solaredge==0.0.2",
"stringcase==1.2.0"
],
+ "config_flow": true,
"dependencies": [],
"codeowners": []
}
diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py
index cad81c3c3381eb..896596a2a34d01 100644
--- a/homeassistant/components/solaredge/sensor.py
+++ b/homeassistant/components/solaredge/sensor.py
@@ -1,102 +1,39 @@
"""Support for SolarEdge Monitoring API."""
-
-from datetime import timedelta
import logging
-
-import voluptuous as vol
+import solaredge
from requests.exceptions import HTTPError, ConnectTimeout
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
- CONF_API_KEY,
- CONF_MONITORED_CONDITIONS,
- CONF_NAME,
- POWER_WATT,
- ENERGY_WATT_HOUR,
-)
-import homeassistant.helpers.config_validation as cv
+from homeassistant.const import CONF_API_KEY
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
-# Config for solaredge monitoring api requests.
-CONF_SITE_ID = "site_id"
-
-OVERVIEW_UPDATE_DELAY = timedelta(minutes=10)
-DETAILS_UPDATE_DELAY = timedelta(hours=12)
-INVENTORY_UPDATE_DELAY = timedelta(hours=12)
-POWER_FLOW_UPDATE_DELAY = timedelta(minutes=10)
-
-SCAN_INTERVAL = timedelta(minutes=10)
-
-# Supported overview sensor types:
-# Key: ['json_key', 'name', unit, icon]
-SENSOR_TYPES = {
- "lifetime_energy": [
- "lifeTimeData",
- "Lifetime energy",
- ENERGY_WATT_HOUR,
- "mdi:solar-power",
- ],
- "energy_this_year": [
- "lastYearData",
- "Energy this year",
- ENERGY_WATT_HOUR,
- "mdi:solar-power",
- ],
- "energy_this_month": [
- "lastMonthData",
- "Energy this month",
- ENERGY_WATT_HOUR,
- "mdi:solar-power",
- ],
- "energy_today": [
- "lastDayData",
- "Energy today",
- ENERGY_WATT_HOUR,
- "mdi:solar-power",
- ],
- "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"],
- "site_details": [None, "Site details", None, None],
- "meters": ["meters", "Meters", None, None],
- "sensors": ["sensors", "Sensors", None, None],
- "gateways": ["gateways", "Gateways", None, None],
- "batteries": ["batteries", "Batteries", None, None],
- "inverters": ["inverters", "Inverters", None, None],
- "power_consumption": ["LOAD", "Power Consumption", None, "mdi:flash"],
- "solar_power": ["PV", "Solar Power", None, "mdi:solar-power"],
- "grid_power": ["GRID", "Grid Power", None, "mdi:power-plug"],
- "storage_power": ["STORAGE", "Storage Power", None, "mdi:car-battery"],
-}
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_API_KEY): cv.string,
- vol.Required(CONF_SITE_ID): cv.string,
- vol.Optional(CONF_NAME, default="SolarEdge"): cv.string,
- vol.Optional(CONF_MONITORED_CONDITIONS, default=["current_power"]): vol.All(
- cv.ensure_list, [vol.In(SENSOR_TYPES)]
- ),
- }
+from .const import (
+ CONF_SITE_ID,
+ OVERVIEW_UPDATE_DELAY,
+ DETAILS_UPDATE_DELAY,
+ INVENTORY_UPDATE_DELAY,
+ POWER_FLOW_UPDATE_DELAY,
+ SENSOR_TYPES,
)
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Create the SolarEdge Monitoring API sensor."""
- import solaredge
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Old configuration."""
+ pass
- api_key = config[CONF_API_KEY]
- site_id = config[CONF_SITE_ID]
- platform_name = config[CONF_NAME]
- # Create new SolarEdge object to retrieve data
- api = solaredge.Solaredge(api_key)
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Add an solarEdge entry."""
+ # Add the needed sensors to hass
+ api = solaredge.Solaredge(entry.data[CONF_API_KEY])
# Check if api can be reached and site is active
try:
- response = api.get_details(site_id)
-
+ response = await hass.async_add_executor_job(
+ api.get_details, entry.data[CONF_SITE_ID]
+ )
if response["details"]["status"].lower() != "active":
_LOGGER.error("SolarEdge site is not active")
return
@@ -108,17 +45,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.error("Could not retrieve details from SolarEdge API")
return
- # Create sensor factory that will create sensors based on sensor_key.
- sensor_factory = SolarEdgeSensorFactory(platform_name, site_id, api)
-
- # Create a new sensor for each sensor type.
+ sensor_factory = SolarEdgeSensorFactory(entry.title, entry.data[CONF_SITE_ID], api)
entities = []
- for sensor_key in config[CONF_MONITORED_CONDITIONS]:
+ for sensor_key in SENSOR_TYPES:
sensor = sensor_factory.create_sensor(sensor_key)
if sensor is not None:
entities.append(sensor)
-
- add_entities(entities, True)
+ async_add_entities(entities)
class SolarEdgeSensorFactory:
diff --git a/homeassistant/components/solaredge/strings.json b/homeassistant/components/solaredge/strings.json
new file mode 100644
index 00000000000000..3265e3bb1b0a86
--- /dev/null
+++ b/homeassistant/components/solaredge/strings.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "title": "SolarEdge",
+ "step": {
+ "user": {
+ "title": "Define the API parameters for this installation",
+ "data": {
+ "name": "The name of this installation",
+ "site_id": "The SolarEdge site-id",
+ "api_key": "The API key for this site"
+ }
+ }
+ },
+ "error": {
+ "site_exists": "This site_id is already configured"
+ },
+ "abort": {
+ "site_exists": "This site_id is already configured"
+ }
+ }
+}
diff --git a/homeassistant/components/solaredge_local/manifest.json b/homeassistant/components/solaredge_local/manifest.json
index 5fb07011983edd..291c774c383d56 100644
--- a/homeassistant/components/solaredge_local/manifest.json
+++ b/homeassistant/components/solaredge_local/manifest.json
@@ -1,8 +1,13 @@
{
- "domain": "solaredge_local",
- "name": "Solar Edge Local",
- "documentation": "",
- "dependencies": [],
- "codeowners": ["@drobtravels"],
- "requirements": ["solaredge-local==0.1.4"]
- }
\ No newline at end of file
+ "domain": "solaredge_local",
+ "name": "Solar Edge Local",
+ "documentation": "https://www.home-assistant.io/components/solaredge_local",
+ "requirements": [
+ "solaredge-local==0.2.0"
+ ],
+ "dependencies": [],
+ "codeowners": [
+ "@drobtravels",
+ "@scheric"
+ ]
+}
diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py
index 8586d950e39cdb..4fc62e44921448 100644
--- a/homeassistant/components/solaredge_local/sensor.py
+++ b/homeassistant/components/solaredge_local/sensor.py
@@ -1,19 +1,20 @@
-"""
-Support for SolarEdge Monitoring API.
-
-For more details about this platform, please refer to the documentation at
-https://home-assistant.io/components/sensor.solaredge_local/
-"""
+"""Support for SolarEdge-local Monitoring API."""
import logging
from datetime import timedelta
+import statistics
from requests.exceptions import HTTPError, ConnectTimeout
from solaredge_local import SolarEdge
import voluptuous as vol
-
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME, POWER_WATT, ENERGY_WATT_HOUR
+from homeassistant.const import (
+ CONF_IP_ADDRESS,
+ CONF_NAME,
+ POWER_WATT,
+ ENERGY_WATT_HOUR,
+ TEMP_CELSIUS,
+)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
@@ -24,9 +25,10 @@
# Supported sensor types:
# Key: ['json_key', 'name', unit, icon]
SENSOR_TYPES = {
- "lifetime_energy": [
- "energyTotal",
- "Lifetime energy",
+ "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"],
+ "energy_this_month": [
+ "energyThisMonth",
+ "Energy this month",
ENERGY_WATT_HOUR,
"mdi:solar-power",
],
@@ -36,19 +38,48 @@
ENERGY_WATT_HOUR,
"mdi:solar-power",
],
- "energy_this_month": [
- "energyThisMonth",
- "Energy this month",
- ENERGY_WATT_HOUR,
- "mdi:solar-power",
- ],
"energy_today": [
"energyToday",
"Energy today",
ENERGY_WATT_HOUR,
"mdi:solar-power",
],
- "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"],
+ "inverter_temperature": [
+ "invertertemperature",
+ "Inverter Temperature",
+ TEMP_CELSIUS,
+ "mdi:thermometer",
+ ],
+ "lifetime_energy": [
+ "energyTotal",
+ "Lifetime energy",
+ ENERGY_WATT_HOUR,
+ "mdi:solar-power",
+ ],
+ "optimizer_current": [
+ "optimizercurrent",
+ "Avrage Optimizer Current",
+ "A",
+ "mdi:solar-panel",
+ ],
+ "optimizer_power": [
+ "optimizerpower",
+ "Avrage Optimizer Power",
+ POWER_WATT,
+ "mdi:solar-panel",
+ ],
+ "optimizer_temperature": [
+ "optimizertemperature",
+ "Avrage Optimizer Temperature",
+ TEMP_CELSIUS,
+ "mdi:solar-panel",
+ ],
+ "optimizer_voltage": [
+ "optimizervoltage",
+ "Avrage Optimizer Voltage",
+ "V",
+ "mdi:solar-panel",
+ ],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
@@ -66,18 +97,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
ip_address = config[CONF_IP_ADDRESS]
platform_name = config[CONF_NAME]
- # Create new SolarEdge object to retrieve data
+ # Create new SolarEdge object to retrieve data.
api = SolarEdge(f"http://{ip_address}/")
- # Check if api can be reached and site is active
+ # Check if api can be reached and site is active.
try:
status = api.get_status()
-
- status.energy # pylint: disable=pointless-statement
_LOGGER.debug("Credentials correct and site is active")
except AttributeError:
- _LOGGER.error("Missing details data in solaredge response")
- _LOGGER.debug("Response is: %s", status)
+ _LOGGER.error("Missing details data in solaredge status")
+ _LOGGER.debug("Status is: %s", status)
return
except (ConnectTimeout, HTTPError):
_LOGGER.error("Could not retrieve details from SolarEdge API")
@@ -111,7 +140,7 @@ def __init__(self, platform_name, sensor_key, data):
@property
def name(self):
"""Return the name."""
- return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1])
+ return f"{self.platform_name} ({SENSOR_TYPES[self.sensor_key][1]})"
@property
def unit_of_measurement(self):
@@ -147,21 +176,55 @@ def __init__(self, hass, api):
def update(self):
"""Update the data from the SolarEdge Monitoring API."""
try:
- response = self.api.get_status()
- _LOGGER.debug("response from SolarEdge: %s", response)
- except (ConnectTimeout):
+ status = self.api.get_status()
+ _LOGGER.debug("Status from SolarEdge: %s", status)
+ except ConnectTimeout:
_LOGGER.error("Connection timeout, skipping update")
return
- except (HTTPError):
- _LOGGER.error("Could not retrieve data, skipping update")
+ except HTTPError:
+ _LOGGER.error("Could not retrieve status, skipping update")
return
try:
- self.data["energyTotal"] = response.energy.total
- self.data["energyThisYear"] = response.energy.thisYear
- self.data["energyThisMonth"] = response.energy.thisMonth
- self.data["energyToday"] = response.energy.today
- self.data["currentPower"] = response.powerWatt
- _LOGGER.debug("Updated SolarEdge overview data: %s", self.data)
- except AttributeError:
- _LOGGER.error("Missing details data in SolarEdge response")
+ maintenance = self.api.get_maintenance()
+ _LOGGER.debug("Maintenance from SolarEdge: %s", maintenance)
+ except ConnectTimeout:
+ _LOGGER.error("Connection timeout, skipping update")
+ return
+ except HTTPError:
+ _LOGGER.error("Could not retrieve maintenance, skipping update")
+ return
+
+ temperature = []
+ voltage = []
+ current = []
+ power = 0
+
+ for optimizer in maintenance.diagnostics.inverters.primary.optimizer:
+ if not optimizer.online:
+ continue
+ temperature.append(optimizer.temperature.value)
+ voltage.append(optimizer.inputV)
+ current.append(optimizer.inputC)
+
+ if not voltage:
+ temperature.append(0)
+ voltage.append(0)
+ current.append(0)
+ else:
+ power = statistics.mean(voltage) * statistics.mean(current)
+
+ if status.sn:
+ self.data["energyTotal"] = round(status.energy.total, 2)
+ self.data["energyThisYear"] = round(status.energy.thisYear, 2)
+ self.data["energyThisMonth"] = round(status.energy.thisMonth, 2)
+ self.data["energyToday"] = round(status.energy.today, 2)
+ self.data["currentPower"] = round(status.powerWatt, 2)
+ self.data[
+ "invertertemperature"
+ ] = status.inverters.primary.temperature.value
+ if maintenance.system.name:
+ self.data["optimizertemperature"] = statistics.mean(temperature)
+ self.data["optimizervoltage"] = statistics.mean(voltage)
+ self.data["optimizercurrent"] = statistics.mean(current)
+ self.data["optimizerpower"] = power
diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json
index 52e50ab47998a5..3a154b857fe878 100644
--- a/homeassistant/components/solax/manifest.json
+++ b/homeassistant/components/solax/manifest.json
@@ -3,7 +3,7 @@
"name": "Solax Inverter",
"documentation": "https://www.home-assistant.io/components/solax",
"requirements": [
- "solax==0.1.2"
+ "solax==0.2.2"
],
"dependencies": [],
"codeowners": ["@squishykid"]
diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py
index 0c1cfcf21da32a..a5b4547b344894 100644
--- a/homeassistant/components/solax/sensor.py
+++ b/homeassistant/components/solax/sensor.py
@@ -4,9 +4,11 @@
from datetime import timedelta
import logging
+from solax import real_time_api
+from solax.inverter import InverterError
import voluptuous as vol
-from homeassistant.const import TEMP_CELSIUS, CONF_IP_ADDRESS
+from homeassistant.const import TEMP_CELSIUS, CONF_IP_ADDRESS, CONF_PORT
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -15,24 +17,28 @@
_LOGGER = logging.getLogger(__name__)
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_IP_ADDRESS): cv.string})
+DEFAULT_PORT = 80
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_IP_ADDRESS): cv.string,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+ }
+)
SCAN_INTERVAL = timedelta(seconds=30)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Platform setup."""
- import solax
-
- api = solax.RealTimeAPI(config[CONF_IP_ADDRESS])
+ api = await real_time_api(config[CONF_IP_ADDRESS], config[CONF_PORT])
endpoint = RealTimeDataEndpoint(hass, api)
resp = await api.get_data()
serial = resp.serial_number
hass.async_add_job(endpoint.async_refresh)
async_track_time_interval(hass, endpoint.async_refresh, SCAN_INTERVAL)
devices = []
- for sensor in solax.INVERTER_SENSORS:
- idx, unit = solax.INVERTER_SENSORS[sensor]
+ for sensor, (idx, unit) in api.inverter.sensor_map().items():
if unit == "C":
unit = TEMP_CELSIUS
uid = f"{serial}-{idx}"
@@ -56,16 +62,14 @@ async def async_refresh(self, now=None):
This is the only method that should fetch new data for Home Assistant.
"""
- from solax import SolaxRequestError
-
try:
api_response = await self.api.get_data()
self.ready.set()
- except SolaxRequestError:
+ except InverterError:
if now is not None:
self.ready.clear()
- else:
- raise PlatformNotReady
+ return
+ raise PlatformNotReady
data = api_response.data
for sensor in self.sensors:
if sensor.key in data:
diff --git a/homeassistant/components/somfy/.translations/it.json b/homeassistant/components/somfy/.translations/it.json
new file mode 100644
index 00000000000000..06fc8bed40f409
--- /dev/null
+++ b/homeassistant/components/somfy/.translations/it.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\u00c8 possibile configurare un solo account Somfy.",
+ "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione",
+ "missing_configuration": "Il componente Somfy non \u00e8 configurato. Si prega di seguire la documentazione."
+ },
+ "create_entry": {
+ "default": "Autenticato con successo con Somfy."
+ },
+ "title": "Somfy"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json
index 8c231ec63e0914..a08c0a59c07fd6 100644
--- a/homeassistant/components/sonos/manifest.json
+++ b/homeassistant/components/sonos/manifest.json
@@ -12,7 +12,5 @@
"urn:schemas-upnp-org:device:ZonePlayer:1"
]
},
- "codeowners": [
- "@amelchio"
- ]
+ "codeowners": []
}
diff --git a/homeassistant/components/spaceapi/__init__.py b/homeassistant/components/spaceapi/__init__.py
index 607d9c45538142..ea5a64d97e7cf2 100644
--- a/homeassistant/components/spaceapi/__init__.py
+++ b/homeassistant/components/spaceapi/__init__.py
@@ -7,9 +7,7 @@
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_ICON,
- ATTR_LATITUDE,
ATTR_LOCATION,
- ATTR_LONGITUDE,
ATTR_STATE,
ATTR_UNIT_OF_MEASUREMENT,
CONF_ADDRESS,
@@ -26,6 +24,15 @@
_LOGGER = logging.getLogger(__name__)
ATTR_ADDRESS = "address"
+ATTR_SPACEFED = "spacefed"
+ATTR_CAM = "cam"
+ATTR_STREAM = "stream"
+ATTR_FEEDS = "feeds"
+ATTR_CACHE = "cache"
+ATTR_PROJECTS = "projects"
+ATTR_RADIO_SHOW = "radio_show"
+ATTR_LAT = "lat"
+ATTR_LON = "lon"
ATTR_API = "api"
ATTR_CLOSE = "close"
ATTR_CONTACT = "contact"
@@ -49,32 +56,135 @@
CONF_IRC = "irc"
CONF_ISSUE_REPORT_CHANNELS = "issue_report_channels"
CONF_LOCATION = "location"
+CONF_SPACEFED = "spacefed"
+CONF_SPACENET = "spacenet"
+CONF_SPACESAML = "spacesaml"
+CONF_SPACEPHONE = "spacephone"
+CONF_CAM = "cam"
+CONF_STREAM = "stream"
+CONF_M4 = "m4"
+CONF_MJPEG = "mjpeg"
+CONF_USTREAM = "ustream"
+CONF_FEEDS = "feeds"
+CONF_FEED_BLOG = "blog"
+CONF_FEED_WIKI = "wiki"
+CONF_FEED_CALENDAR = "calendar"
+CONF_FEED_FLICKER = "flicker"
+CONF_FEED_TYPE = "type"
+CONF_FEED_URL = "url"
+CONF_CACHE = "cache"
+CONF_CACHE_SCHEDULE = "schedule"
+CONF_PROJECTS = "projects"
+CONF_RADIO_SHOW = "radio_show"
+CONF_RADIO_SHOW_NAME = "name"
+CONF_RADIO_SHOW_URL = "url"
+CONF_RADIO_SHOW_TYPE = "type"
+CONF_RADIO_SHOW_START = "start"
+CONF_RADIO_SHOW_END = "end"
CONF_LOGO = "logo"
-CONF_MAILING_LIST = "mailing_list"
CONF_PHONE = "phone"
+CONF_SIP = "sip"
+CONF_KEYMASTERS = "keymasters"
+CONF_KEYMASTER_NAME = "name"
+CONF_KEYMASTER_IRC_NICK = "irc_nick"
+CONF_KEYMASTER_PHONE = "phone"
+CONF_KEYMASTER_EMAIL = "email"
+CONF_KEYMASTER_TWITTER = "twitter"
+CONF_TWITTER = "twitter"
+CONF_FACEBOOK = "facebook"
+CONF_IDENTICA = "identica"
+CONF_FOURSQUARE = "foursquare"
+CONF_ML = "ml"
+CONF_JABBER = "jabber"
+CONF_ISSUE_MAIL = "issue_mail"
CONF_SPACE = "space"
CONF_TEMPERATURE = "temperature"
-CONF_TWITTER = "twitter"
DATA_SPACEAPI = "data_spaceapi"
DOMAIN = "spaceapi"
-ISSUE_REPORT_CHANNELS = [CONF_EMAIL, CONF_IRC, CONF_MAILING_LIST, CONF_TWITTER]
+ISSUE_REPORT_CHANNELS = [CONF_EMAIL, CONF_ISSUE_MAIL, CONF_ML, CONF_TWITTER]
SENSOR_TYPES = [CONF_HUMIDITY, CONF_TEMPERATURE]
-SPACEAPI_VERSION = 0.13
+SPACEAPI_VERSION = "0.13"
URL_API_SPACEAPI = "/api/spaceapi"
-LOCATION_SCHEMA = vol.Schema({vol.Optional(CONF_ADDRESS): cv.string}, required=True)
+LOCATION_SCHEMA = vol.Schema({vol.Optional(CONF_ADDRESS): cv.string})
+
+SPACEFED_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_SPACENET): cv.boolean,
+ vol.Optional(CONF_SPACESAML): cv.boolean,
+ vol.Optional(CONF_SPACEPHONE): cv.boolean,
+ }
+)
+
+STREAM_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_M4): cv.url,
+ vol.Optional(CONF_MJPEG): cv.url,
+ vol.Optional(CONF_USTREAM): cv.url,
+ }
+)
+
+FEED_SCHEMA = vol.Schema(
+ {vol.Optional(CONF_FEED_TYPE): cv.string, vol.Required(CONF_FEED_URL): cv.url}
+)
+
+FEEDS_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_FEED_BLOG): FEED_SCHEMA,
+ vol.Optional(CONF_FEED_WIKI): FEED_SCHEMA,
+ vol.Optional(CONF_FEED_CALENDAR): FEED_SCHEMA,
+ vol.Optional(CONF_FEED_FLICKER): FEED_SCHEMA,
+ }
+)
+
+CACHE_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_CACHE_SCHEDULE): cv.matches_regex(
+ r"(m.02|m.05|m.10|m.15|m.30|h.01|h.02|h.04|h.08|h.12|d.01)"
+ )
+ }
+)
+
+RADIO_SHOW_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_RADIO_SHOW_NAME): cv.string,
+ vol.Required(CONF_RADIO_SHOW_URL): cv.url,
+ vol.Required(CONF_RADIO_SHOW_TYPE): cv.matches_regex(r"(mp3|ogg)"),
+ vol.Required(CONF_RADIO_SHOW_START): cv.string,
+ vol.Required(CONF_RADIO_SHOW_END): cv.string,
+ }
+)
+
+KEYMASTER_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_KEYMASTER_NAME): cv.string,
+ vol.Optional(CONF_KEYMASTER_IRC_NICK): cv.string,
+ vol.Optional(CONF_KEYMASTER_PHONE): cv.string,
+ vol.Optional(CONF_KEYMASTER_EMAIL): cv.string,
+ vol.Optional(CONF_KEYMASTER_TWITTER): cv.string,
+ }
+)
CONTACT_SCHEMA = vol.Schema(
{
vol.Optional(CONF_EMAIL): cv.string,
vol.Optional(CONF_IRC): cv.string,
- vol.Optional(CONF_MAILING_LIST): cv.string,
+ vol.Optional(CONF_ML): cv.string,
vol.Optional(CONF_PHONE): cv.string,
vol.Optional(CONF_TWITTER): cv.string,
+ vol.Optional(CONF_SIP): cv.string,
+ vol.Optional(CONF_FACEBOOK): cv.string,
+ vol.Optional(CONF_IDENTICA): cv.string,
+ vol.Optional(CONF_FOURSQUARE): cv.string,
+ vol.Optional(CONF_JABBER): cv.string,
+ vol.Optional(CONF_ISSUE_MAIL): cv.string,
+ vol.Optional(CONF_KEYMASTERS): vol.All(
+ cv.ensure_list, [KEYMASTER_SCHEMA], vol.Length(min=1)
+ ),
},
required=False,
)
@@ -100,12 +210,23 @@
vol.Required(CONF_ISSUE_REPORT_CHANNELS): vol.All(
cv.ensure_list, [vol.In(ISSUE_REPORT_CHANNELS)]
),
- vol.Required(CONF_LOCATION): LOCATION_SCHEMA,
+ vol.Optional(CONF_LOCATION): LOCATION_SCHEMA,
vol.Required(CONF_LOGO): cv.url,
vol.Required(CONF_SPACE): cv.string,
vol.Required(CONF_STATE): STATE_SCHEMA,
vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_SENSORS): SENSOR_SCHEMA,
+ vol.Optional(CONF_SPACEFED): SPACEFED_SCHEMA,
+ vol.Optional(CONF_CAM): vol.All(
+ cv.ensure_list, [cv.url], vol.Length(min=1)
+ ),
+ vol.Optional(CONF_STREAM): STREAM_SCHEMA,
+ vol.Optional(CONF_FEEDS): FEEDS_SCHEMA,
+ vol.Optional(CONF_CACHE): CACHE_SCHEMA,
+ vol.Optional(CONF_PROJECTS): vol.All(cv.ensure_list, [cv.url]),
+ vol.Optional(CONF_RADIO_SHOW): vol.All(
+ cv.ensure_list, [RADIO_SHOW_SCHEMA]
+ ),
}
)
},
@@ -150,11 +271,14 @@ def get(self, request):
spaceapi = dict(hass.data[DATA_SPACEAPI])
is_sensors = spaceapi.get("sensors")
- location = {
- ATTR_ADDRESS: spaceapi[ATTR_LOCATION][CONF_ADDRESS],
- ATTR_LATITUDE: hass.config.latitude,
- ATTR_LONGITUDE: hass.config.longitude,
- }
+ location = {ATTR_LAT: hass.config.latitude, ATTR_LON: hass.config.longitude}
+
+ try:
+ location[ATTR_ADDRESS] = spaceapi[ATTR_LOCATION][CONF_ADDRESS]
+ except KeyError:
+ pass
+ except TypeError:
+ pass
state_entity = spaceapi["state"][ATTR_ENTITY_ID]
space_state = hass.states.get(state_entity)
@@ -186,6 +310,41 @@ def get(self, request):
ATTR_URL: spaceapi[CONF_URL],
}
+ try:
+ data[ATTR_CAM] = spaceapi[CONF_CAM]
+ except KeyError:
+ pass
+
+ try:
+ data[ATTR_SPACEFED] = spaceapi[CONF_SPACEFED]
+ except KeyError:
+ pass
+
+ try:
+ data[ATTR_STREAM] = spaceapi[CONF_STREAM]
+ except KeyError:
+ pass
+
+ try:
+ data[ATTR_FEEDS] = spaceapi[CONF_FEEDS]
+ except KeyError:
+ pass
+
+ try:
+ data[ATTR_CACHE] = spaceapi[CONF_CACHE]
+ except KeyError:
+ pass
+
+ try:
+ data[ATTR_PROJECTS] = spaceapi[CONF_PROJECTS]
+ except KeyError:
+ pass
+
+ try:
+ data[ATTR_RADIO_SHOW] = spaceapi[CONF_RADIO_SHOW]
+ except KeyError:
+ pass
+
if is_sensors is not None:
sensors = {}
for sensor_type in is_sensors:
diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py
deleted file mode 100644
index 71e04d7b8c99aa..00000000000000
--- a/homeassistant/components/srp_energy/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The srp_energy component."""
diff --git a/homeassistant/components/srp_energy/manifest.json b/homeassistant/components/srp_energy/manifest.json
deleted file mode 100644
index 050a78223c17f5..00000000000000
--- a/homeassistant/components/srp_energy/manifest.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "domain": "srp_energy",
- "name": "Srp energy",
- "documentation": "https://www.home-assistant.io/components/srp_energy",
- "requirements": [
- "srpenergy==1.0.6"
- ],
- "dependencies": [],
- "codeowners": []
-}
diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py
deleted file mode 100644
index f1d1787b7b48c5..00000000000000
--- a/homeassistant/components/srp_energy/sensor.py
+++ /dev/null
@@ -1,156 +0,0 @@
-"""Platform for retrieving energy data from SRP."""
-from datetime import datetime, timedelta
-import logging
-
-from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout
-import voluptuous as vol
-
-from homeassistant.const import (
- CONF_NAME,
- CONF_PASSWORD,
- ENERGY_KILO_WATT_HOUR,
- CONF_USERNAME,
- CONF_ID,
-)
-import homeassistant.helpers.config_validation as cv
-from homeassistant.util import Throttle
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.helpers.entity import Entity
-
-_LOGGER = logging.getLogger(__name__)
-
-ATTRIBUTION = "Powered by SRP Energy"
-
-DEFAULT_NAME = "SRP Energy"
-MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1440)
-ENERGY_KWH = ENERGY_KILO_WATT_HOUR
-
-ATTR_READING_COST = "reading_cost"
-ATTR_READING_TIME = "datetime"
-ATTR_READING_USAGE = "reading_usage"
-ATTR_DAILY_USAGE = "daily_usage"
-ATTR_USAGE_HISTORY = "usage_history"
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Required(CONF_ID): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- }
-)
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the SRP energy."""
- _LOGGER.warning(
- "The srp_energy integration is deprecated and will be removed "
- "in Home Assistant 0.100.0. For more information see ADR-0004:"
- "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md"
- )
-
- name = config[CONF_NAME]
- username = config[CONF_USERNAME]
- password = config[CONF_PASSWORD]
- account_id = config[CONF_ID]
-
- from srpenergy.client import SrpEnergyClient
-
- srp_client = SrpEnergyClient(account_id, username, password)
-
- if not srp_client.validate():
- _LOGGER.error("Couldn't connect to %s. Check credentials", name)
- return
-
- add_entities([SrpEnergy(name, srp_client)], True)
-
-
-class SrpEnergy(Entity):
- """Representation of an srp usage."""
-
- def __init__(self, name, client):
- """Initialize SRP Usage."""
- self._state = None
- self._name = name
- self._client = client
- self._history = None
- self._usage = None
-
- @property
- def attribution(self):
- """Return the attribution."""
- return ATTRIBUTION
-
- @property
- def state(self):
- """Return the current state."""
- if self._state is None:
- return None
-
- return f"{self._state:.2f}"
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return ENERGY_KWH
-
- @property
- def history(self):
- """Return the energy usage history of this entity, if any."""
- if self._usage is None:
- return None
-
- history = [
- {
- ATTR_READING_TIME: isodate,
- ATTR_READING_USAGE: kwh,
- ATTR_READING_COST: cost,
- }
- for _, _, isodate, kwh, cost in self._usage
- ]
-
- return history
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- attributes = {ATTR_USAGE_HISTORY: self.history}
-
- return attributes
-
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- def update(self):
- """Get the latest usage from SRP Energy."""
- start_date = datetime.now() + timedelta(days=-1)
- end_date = datetime.now()
-
- try:
-
- usage = self._client.usage(start_date, end_date)
-
- daily_usage = 0.0
- for _, _, _, kwh, _ in usage:
- daily_usage += float(kwh)
-
- if usage:
-
- self._state = daily_usage
- self._usage = usage
-
- else:
- _LOGGER.error("Unable to fetch data from SRP. No data")
-
- except (ConnectError, HTTPError, Timeout) as error:
- _LOGGER.error("Unable to connect to SRP. %s", error)
- except ValueError as error:
- _LOGGER.error("Value error connecting to SRP. %s", error)
- except TypeError as error:
- _LOGGER.error(
- "Type error connecting to SRP. " "Check username and password. %s",
- error,
- )
diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py
index 5e370ed7b63290..1b567c58b45b87 100644
--- a/homeassistant/components/startca/sensor.py
+++ b/homeassistant/components/startca/sensor.py
@@ -18,8 +18,8 @@
DEFAULT_NAME = "Start.ca"
CONF_TOTAL_BANDWIDTH = "total_bandwidth"
-GIGABYTES = "GB" # type: str
-PERCENT = "%" # type: str
+GIGABYTES = "GB"
+PERCENT = "%"
MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1)
REQUEST_TIMEOUT = 5 # seconds
diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py
index 9eef9d989cb093..86e763142e6191 100644
--- a/homeassistant/components/supla/__init__.py
+++ b/homeassistant/components/supla/__init__.py
@@ -65,7 +65,7 @@ def setup(hass, base_config):
srv_info,
)
return False
- except IOError:
+ except OSError:
_LOGGER.exception(
"Server: %s not configured. Error on Supla API access: ", server_address
)
diff --git a/homeassistant/components/switch/.translations/bg.json b/homeassistant/components/switch/.translations/bg.json
new file mode 100644
index 00000000000000..efccc652d5beb7
--- /dev/null
+++ b/homeassistant/components/switch/.translations/bg.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "\u041f\u0440\u0435\u0432\u043a\u043b\u044e\u0447\u0438 {entity_name}",
+ "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438 {entity_name}",
+ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}"
+ },
+ "condition_type": {
+ "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}",
+ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}"
+ },
+ "trigger_type": {
+ "turned_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}",
+ "turned_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/ca.json b/homeassistant/components/switch/.translations/ca.json
new file mode 100644
index 00000000000000..dbf5e152656497
--- /dev/null
+++ b/homeassistant/components/switch/.translations/ca.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Commuta {entity_name}",
+ "turn_off": "Desactiva {entity_name}",
+ "turn_on": "Activa {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} est\u00e0 apagat",
+ "is_on": "{entity_name} est\u00e0 enc\u00e8s",
+ "turn_off": "{entity_name} desactivat",
+ "turn_on": "{entity_name} activat"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} desactivat",
+ "turned_on": "{entity_name} activat"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/en.json b/homeassistant/components/switch/.translations/en.json
new file mode 100644
index 00000000000000..391a071cb8f4fb
--- /dev/null
+++ b/homeassistant/components/switch/.translations/en.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Toggle {entity_name}",
+ "turn_off": "Turn off {entity_name}",
+ "turn_on": "Turn on {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} is off",
+ "is_on": "{entity_name} is on",
+ "turn_off": "{entity_name} turned off",
+ "turn_on": "{entity_name} turned on"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} turned off",
+ "turned_on": "{entity_name} turned on"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/es.json b/homeassistant/components/switch/.translations/es.json
new file mode 100644
index 00000000000000..24dbc2cdc1fff0
--- /dev/null
+++ b/homeassistant/components/switch/.translations/es.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Alternar {entity_name}",
+ "turn_off": "Apagar {entity_name}",
+ "turn_on": "Encender {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} est\u00e1 apagada",
+ "is_on": "{entity_name} est\u00e1 encendida",
+ "turn_off": "{entity_name} apagado",
+ "turn_on": "{entity_name} encendido"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} apagado",
+ "turned_on": "{entity_name} encendido"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/fr.json b/homeassistant/components/switch/.translations/fr.json
new file mode 100644
index 00000000000000..807b85c5fb56d6
--- /dev/null
+++ b/homeassistant/components/switch/.translations/fr.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Basculer {entity_name}",
+ "turn_off": "\u00c9teindre {entity_name}",
+ "turn_on": "Allumer {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} est \u00e9teint",
+ "is_on": "{entity_name} est allum\u00e9",
+ "turn_off": "{entity_name} \u00e9teint",
+ "turn_on": "{entity_name} allum\u00e9"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} \u00e9teint",
+ "turned_on": "{entity_name} allum\u00e9"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/it.json b/homeassistant/components/switch/.translations/it.json
new file mode 100644
index 00000000000000..ec742e4113bd7c
--- /dev/null
+++ b/homeassistant/components/switch/.translations/it.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Attivare / Disattivare {entity_name}",
+ "turn_off": "Disattivare {entity_name}",
+ "turn_on": "Attivare {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u00e8 disattivato",
+ "is_on": "{entity_name} \u00e8 attivo",
+ "turn_off": "{entity_name} disattivato",
+ "turn_on": "{entity_name} attivato"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} disattivato",
+ "turned_on": "{entity_name} attivato"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/ko.json b/homeassistant/components/switch/.translations/ko.json
new file mode 100644
index 00000000000000..02c303f932987b
--- /dev/null
+++ b/homeassistant/components/switch/.translations/ko.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "{entity_name} \ud1a0\uae00",
+ "turn_off": "{entity_name} \ub044\uae30",
+ "turn_on": "{entity_name} \ucf1c\uae30"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4",
+ "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4",
+ "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4",
+ "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4",
+ "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/lb.json b/homeassistant/components/switch/.translations/lb.json
new file mode 100644
index 00000000000000..8e974a0a8de137
--- /dev/null
+++ b/homeassistant/components/switch/.translations/lb.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "{entity_name} \u00ebmschalten",
+ "turn_off": "{entity_name} ausschalten",
+ "turn_on": "{entity_name} uschalten"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} ass aus",
+ "is_on": "{entity_name} ass un",
+ "turn_off": "{entity_name} gouf ausgeschalt",
+ "turn_on": "{entity_name} gouf ugeschalt"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} gouf ausgeschalt",
+ "turned_on": "{entity_name} gouf ugeschalt"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/nl.json b/homeassistant/components/switch/.translations/nl.json
new file mode 100644
index 00000000000000..5e2aa6747a4c32
--- /dev/null
+++ b/homeassistant/components/switch/.translations/nl.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Omschakelen {entity_name}",
+ "turn_off": "Zet {entity_name} uit.",
+ "turn_on": "Zet {entity_name} aan."
+ },
+ "condition_type": {
+ "is_off": "{entity_name} is uitgeschakeld",
+ "is_on": "{entity_name} is ingeschakeld",
+ "turn_off": "{entity_name} uitgeschakeld",
+ "turn_on": "{entity_name} ingeschakeld"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} uitgeschakeld",
+ "turned_on": "{entity_name} ingeschakeld"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/no.json b/homeassistant/components/switch/.translations/no.json
new file mode 100644
index 00000000000000..3469079f230b43
--- /dev/null
+++ b/homeassistant/components/switch/.translations/no.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Veksle {entity_name}",
+ "turn_off": "Sl\u00e5 av {entity_name}",
+ "turn_on": "Sl\u00e5 p\u00e5 {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} er av",
+ "is_on": "{entity_name} er p\u00e5",
+ "turn_off": "{entity_name} sl\u00e5tt av",
+ "turn_on": "{entity_name} sl\u00e5tt p\u00e5"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} sl\u00e5tt av",
+ "turned_on": "{entity_name} sl\u00e5tt p\u00e5"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json
new file mode 100644
index 00000000000000..199b150f68ee6d
--- /dev/null
+++ b/homeassistant/components/switch/.translations/pl.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Prze\u0142\u0105cz {entity_name}",
+ "turn_off": "Wy\u0142\u0105cz {entity_name}",
+ "turn_on": "W\u0142\u0105cz {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} jest wy\u0142\u0105czony.",
+ "is_on": "{entity_name} jest w\u0142\u0105czony",
+ "turn_off": "{entity_name} wy\u0142\u0105czone",
+ "turn_on": "{entity_name} w\u0142\u0105czone"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} wy\u0142\u0105czone",
+ "turned_on": "{entity_name} w\u0142\u0105czone"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json
new file mode 100644
index 00000000000000..cd5cbc0d6a174b
--- /dev/null
+++ b/homeassistant/components/switch/.translations/ru.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}",
+ "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}",
+ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
+ "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
+ "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
+ "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
+ "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/sl.json b/homeassistant/components/switch/.translations/sl.json
new file mode 100644
index 00000000000000..f1b851b05b6f4b
--- /dev/null
+++ b/homeassistant/components/switch/.translations/sl.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Preklopite {entity_name}",
+ "turn_off": "Izklopite {entity_name}",
+ "turn_on": "Vklopite {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} je izklopljen",
+ "is_on": "{entity_name} je vklopljen",
+ "turn_off": "{entity_name} izklopljen",
+ "turn_on": "{entity_name} vklopljen"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} izklopljen",
+ "turned_on": "{entity_name} vklopljen"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/.translations/zh-Hant.json b/homeassistant/components/switch/.translations/zh-Hant.json
new file mode 100644
index 00000000000000..517d48354dcc2d
--- /dev/null
+++ b/homeassistant/components/switch/.translations/zh-Hant.json
@@ -0,0 +1,19 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "\u5207\u63db {entity_name}",
+ "turn_off": "\u95dc\u9589 {entity_name}",
+ "turn_on": "\u958b\u555f {entity_name}"
+ },
+ "condition_type": {
+ "is_off": "{entity_name} \u5df2\u95dc\u9589",
+ "is_on": "{entity_name} \u5df2\u958b\u555f",
+ "turn_off": "{entity_name} \u5df2\u95dc\u9589",
+ "turn_on": "{entity_name} \u5df2\u958b\u555f"
+ },
+ "trigger_type": {
+ "turned_off": "{entity_name} \u5df2\u95dc\u9589",
+ "turned_on": "{entity_name} \u5df2\u958b\u555f"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/switch/device_automation.py b/homeassistant/components/switch/device_automation.py
new file mode 100644
index 00000000000000..61292d47449adf
--- /dev/null
+++ b/homeassistant/components/switch/device_automation.py
@@ -0,0 +1,56 @@
+"""Provides device automations for lights."""
+import voluptuous as vol
+
+from homeassistant.components.device_automation import toggle_entity
+from homeassistant.const import CONF_DOMAIN
+from . import DOMAIN
+
+
+# mypy: allow-untyped-defs, no-check-untyped-defs
+
+ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN})
+
+CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend(
+ {vol.Required(CONF_DOMAIN): DOMAIN}
+)
+
+TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend(
+ {vol.Required(CONF_DOMAIN): DOMAIN}
+)
+
+
+async def async_call_action_from_config(hass, config, variables, context):
+ """Change state based on configuration."""
+ config = ACTION_SCHEMA(config)
+ await toggle_entity.async_call_action_from_config(
+ hass, config, variables, context, DOMAIN
+ )
+
+
+def async_condition_from_config(config, config_validation):
+ """Evaluate state based on configuration."""
+ config = CONDITION_SCHEMA(config)
+ return toggle_entity.async_condition_from_config(config, config_validation)
+
+
+async def async_trigger(hass, config, action, automation_info):
+ """Listen for state changes based on configuration."""
+ config = TRIGGER_SCHEMA(config)
+ return await toggle_entity.async_attach_trigger(
+ hass, config, action, automation_info
+ )
+
+
+async def async_get_actions(hass, device_id):
+ """List device actions."""
+ return await toggle_entity.async_get_actions(hass, device_id, DOMAIN)
+
+
+async def async_get_conditions(hass, device_id):
+ """List device conditions."""
+ return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN)
+
+
+async def async_get_triggers(hass, device_id):
+ """List device triggers."""
+ return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN)
diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py
index 0b1094c0dd9d71..8f3b5d87f8c202 100644
--- a/homeassistant/components/switch/light.py
+++ b/homeassistant/components/switch/light.py
@@ -1,6 +1,6 @@
"""Light support for switch entities."""
import logging
-from typing import cast
+from typing import cast, Callable, Dict, Optional, Sequence
import voluptuous as vol
@@ -14,13 +14,14 @@
)
from homeassistant.core import State, callback
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.components.light import PLATFORM_SCHEMA, Light
-# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
+# mypy: allow-untyped-calls, allow-untyped-defs
_LOGGER = logging.getLogger(__name__)
@@ -35,7 +36,10 @@
async def async_setup_platform(
- hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None
+ hass: HomeAssistantType,
+ config: ConfigType,
+ async_add_entities: Callable[[Sequence[Entity], bool], None],
+ discovery_info: Optional[Dict] = None,
) -> None:
"""Initialize Light Switch platform."""
async_add_entities(
@@ -48,10 +52,10 @@ class LightSwitch(Light):
def __init__(self, name: str, switch_entity_id: str) -> None:
"""Initialize Light Switch."""
- self._name = name # type: str
- self._switch_entity_id = switch_entity_id # type: str
- self._is_on = False # type: bool
- self._available = False # type: bool
+ self._name = name
+ self._switch_entity_id = switch_entity_id
+ self._is_on = False
+ self._available = False
self._async_unsub_state_changed = None
@property
@@ -105,7 +109,7 @@ async def async_added_to_hass(self) -> None:
@callback
def async_state_changed_listener(
entity_id: str, old_state: State, new_state: State
- ):
+ ) -> None:
"""Handle child updates."""
self.async_schedule_update_ha_state(True)
diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json
new file mode 100644
index 00000000000000..77b842ba07833a
--- /dev/null
+++ b/homeassistant/components/switch/strings.json
@@ -0,0 +1,17 @@
+{
+ "device_automation": {
+ "action_type": {
+ "toggle": "Toggle {entity_name}",
+ "turn_on": "Turn on {entity_name}",
+ "turn_off": "Turn off {entity_name}"
+ },
+ "condition_type": {
+ "is_on": "{entity_name} is on",
+ "is_off": "{entity_name} is off"
+ },
+ "trigger_type": {
+ "turned_on": "{entity_name} turned on",
+ "turned_off": "{entity_name} turned off"
+ }
+ }
+}
diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py
index a758a5843472cb..454baca4eef849 100644
--- a/homeassistant/components/switcher_kis/switch.py
+++ b/homeassistant/components/switcher_kis/switch.py
@@ -143,7 +143,7 @@ async def _control_device(self, send_on: bool) -> None:
STATE_ON as SWITCHER_STATE_ON,
)
- response = None # type: SwitcherV2ControlResponseMSG
+ response: "SwitcherV2ControlResponseMSG" = None
async with SwitcherV2Api(
self.hass.loop,
self._device_data.ip_addr,
diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py
index 446a36ec350f88..ad2072baaa5230 100644
--- a/homeassistant/components/systemmonitor/sensor.py
+++ b/homeassistant/components/systemmonitor/sensor.py
@@ -1,5 +1,4 @@
"""Support for monitoring the local system."""
-from datetime import datetime
import logging
import os
import socket
@@ -193,7 +192,7 @@ def update(self):
counters = psutil.net_io_counters(pernic=True)
if self.argument in counters:
counter = counters[self.argument][IO_COUNTER[self.type]]
- now = datetime.now()
+ now = dt_util.utcnow()
if self._last_value and self._last_value < counter:
self._state = round(
(counter - self._last_value)
diff --git a/homeassistant/components/sytadin/__init__.py b/homeassistant/components/sytadin/__init__.py
deleted file mode 100644
index 5243fe379a774e..00000000000000
--- a/homeassistant/components/sytadin/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The sytadin component."""
diff --git a/homeassistant/components/sytadin/manifest.json b/homeassistant/components/sytadin/manifest.json
deleted file mode 100644
index c1453d88d81448..00000000000000
--- a/homeassistant/components/sytadin/manifest.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "domain": "sytadin",
- "name": "Sytadin",
- "documentation": "https://www.home-assistant.io/components/sytadin",
- "requirements": [
- "beautifulsoup4==4.8.0"
- ],
- "dependencies": [],
- "codeowners": [
- "@gautric"
- ]
-}
diff --git a/homeassistant/components/sytadin/sensor.py b/homeassistant/components/sytadin/sensor.py
deleted file mode 100644
index b7c94933a39974..00000000000000
--- a/homeassistant/components/sytadin/sensor.py
+++ /dev/null
@@ -1,151 +0,0 @@
-"""Support for Sytadin Traffic, French Traffic Supervision."""
-import logging
-import re
-from datetime import timedelta
-
-import requests
-import voluptuous as vol
-
-import homeassistant.helpers.config_validation as cv
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
- LENGTH_KILOMETERS,
- CONF_MONITORED_CONDITIONS,
- CONF_NAME,
- ATTR_ATTRIBUTION,
-)
-from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle
-
-_LOGGER = logging.getLogger(__name__)
-
-URL = "http://www.sytadin.fr/sys/barometres_de_la_circulation.jsp.html"
-
-ATTRIBUTION = "Data provided by Direction des routes Île-de-France (DiRIF)"
-
-DEFAULT_NAME = "Sytadin"
-REGEX = r"(\d*\.\d+|\d+)"
-
-OPTION_TRAFFIC_JAM = "traffic_jam"
-OPTION_MEAN_VELOCITY = "mean_velocity"
-OPTION_CONGESTION = "congestion"
-
-SENSOR_TYPES = {
- OPTION_CONGESTION: ["Congestion", ""],
- OPTION_MEAN_VELOCITY: ["Mean Velocity", LENGTH_KILOMETERS + "/h"],
- OPTION_TRAFFIC_JAM: ["Traffic Jam", LENGTH_KILOMETERS],
-}
-
-MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_MONITORED_CONDITIONS, default=[OPTION_TRAFFIC_JAM]): vol.All(
- cv.ensure_list, [vol.In(SENSOR_TYPES)]
- ),
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- }
-)
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up of the Sytadin Traffic sensor platform."""
- _LOGGER.warning(
- "The sytadin integration is deprecated and will be removed "
- "in Home Assistant 0.100.0. For more information see ADR-0004:"
- "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md"
- )
-
- name = config.get(CONF_NAME)
-
- sytadin = SytadinData(URL)
-
- dev = []
- for option in config.get(CONF_MONITORED_CONDITIONS):
- _LOGGER.debug("Sensor device - %s", option)
- dev.append(
- SytadinSensor(
- sytadin, name, option, SENSOR_TYPES[option][0], SENSOR_TYPES[option][1]
- )
- )
- add_entities(dev, True)
-
-
-class SytadinSensor(Entity):
- """Representation of a Sytadin Sensor."""
-
- def __init__(self, data, name, sensor_type, option, unit):
- """Initialize the sensor."""
- self.data = data
- self._state = None
- self._name = name
- self._option = option
- self._type = sensor_type
- self._unit = unit
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return f"{self._name} {self._option}"
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return self._unit
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- return {ATTR_ATTRIBUTION: ATTRIBUTION}
-
- def update(self):
- """Fetch new state data for the sensor."""
- self.data.update()
-
- if self.data is None:
- return
-
- if self._type == OPTION_TRAFFIC_JAM:
- self._state = self.data.traffic_jam
- elif self._type == OPTION_MEAN_VELOCITY:
- self._state = self.data.mean_velocity
- elif self._type == OPTION_CONGESTION:
- self._state = self.data.congestion
-
-
-class SytadinData:
- """The class for handling the data retrieval."""
-
- def __init__(self, resource):
- """Initialize the data object."""
- self._resource = resource
- self.data = None
- self.traffic_jam = self.mean_velocity = self.congestion = None
-
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- def update(self):
- """Get the latest data from the Sytadin."""
- from bs4 import BeautifulSoup
-
- try:
- raw_html = requests.get(self._resource, timeout=10).text
- data = BeautifulSoup(raw_html, "html.parser")
-
- values = data.select(".barometre_valeur")
- parse_traffic_jam = re.search(REGEX, values[0].text)
- if parse_traffic_jam:
- self.traffic_jam = parse_traffic_jam.group()
- parse_mean_velocity = re.search(REGEX, values[1].text)
- if parse_mean_velocity:
- self.mean_velocity = parse_mean_velocity.group()
- parse_congestion = re.search(REGEX, values[2].text)
- if parse_congestion:
- self.congestion = parse_congestion.group()
- except requests.exceptions.ConnectionError:
- _LOGGER.error("Connection error")
- self.data = None
diff --git a/homeassistant/components/teksavvy/sensor.py b/homeassistant/components/teksavvy/sensor.py
index 74c39a221ba0cf..dc8b16b8ce18fc 100644
--- a/homeassistant/components/teksavvy/sensor.py
+++ b/homeassistant/components/teksavvy/sensor.py
@@ -17,8 +17,8 @@
DEFAULT_NAME = "TekSavvy"
CONF_TOTAL_BANDWIDTH = "total_bandwidth"
-GIGABYTES = "GB" # type: str
-PERCENT = "%" # type: str
+GIGABYTES = "GB"
+PERCENT = "%"
MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1)
REQUEST_TIMEOUT = 5 # seconds
diff --git a/homeassistant/components/tellduslive/.translations/es.json b/homeassistant/components/tellduslive/.translations/es.json
index b0313a1eee357a..677e0389d45b71 100644
--- a/homeassistant/components/tellduslive/.translations/es.json
+++ b/homeassistant/components/tellduslive/.translations/es.json
@@ -18,6 +18,7 @@
"data": {
"host": "Host"
},
+ "description": "Vac\u00edo",
"title": "Elige el punto final."
}
},
diff --git a/homeassistant/components/tellduslive/.translations/it.json b/homeassistant/components/tellduslive/.translations/it.json
index 3baa307de51f7b..ce152285e757d3 100644
--- a/homeassistant/components/tellduslive/.translations/it.json
+++ b/homeassistant/components/tellduslive/.translations/it.json
@@ -11,12 +11,14 @@
},
"step": {
"auth": {
+ "description": "Per collegare il tuo account TelldusLive:\n 1. Clicca sul link sottostante\n 2. Accedi a Telldus Live\n 3. Autorizzare **{app_name}**** (cliccare **S\u00ec**).\n 4. Torna qui e clicca su **SUBMIT**.\n\n [Collega account TelldusLive]({auth_url})",
"title": "Autenticati con TelldusLive"
},
"user": {
"data": {
"host": "Host"
},
+ "description": "Vuoto",
"title": "Scegli l'endpoint."
}
},
diff --git a/homeassistant/components/tellduslive/.translations/no.json b/homeassistant/components/tellduslive/.translations/no.json
index d311b3b0d38068..090de51703654f 100644
--- a/homeassistant/components/tellduslive/.translations/no.json
+++ b/homeassistant/components/tellduslive/.translations/no.json
@@ -12,7 +12,7 @@
"step": {
"auth": {
"description": "For \u00e5 koble TelldusLive-kontoen din:\n 1. Klikk p\u00e5 linken under\n 2. Logg inn p\u00e5 Telldus Live \n 3. Tillat **{app_name}** (klikk**Ja**). \n 4. Kom tilbake hit og klikk **SUBMIT**. \n\n [Link TelldusLive-konto]({auth_url})",
- "title": "Godkjen mot TelldusLive"
+ "title": "Godkjenn mot TelldusLive"
},
"user": {
"data": {
diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py
index 83b56c2cf394d8..98d162d6d81b5d 100644
--- a/homeassistant/components/tellstick/sensor.py
+++ b/homeassistant/components/tellstick/sensor.py
@@ -5,7 +5,7 @@
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import TEMP_CELSIUS, CONF_ID, CONF_NAME
+from homeassistant.const import TEMP_CELSIUS, CONF_ID, CONF_NAME, CONF_PROTOCOL
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
@@ -16,6 +16,7 @@
CONF_DATATYPE_MASK = "datatype_mask"
CONF_ONLY_NAMED = "only_named"
CONF_TEMPERATURE_SCALE = "temperature_scale"
+CONF_MODEL = "model"
DEFAULT_DATATYPE_MASK = 127
DEFAULT_TEMPERATURE_SCALE = TEMP_CELSIUS
@@ -35,6 +36,8 @@
{
vol.Required(CONF_ID): cv.positive_int,
vol.Required(CONF_NAME): cv.string,
+ vol.Optional(CONF_PROTOCOL): cv.string,
+ vol.Optional(CONF_MODEL): cv.string,
}
)
],
@@ -74,18 +77,36 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
datatype_mask = config.get(CONF_DATATYPE_MASK)
if config[CONF_ONLY_NAMED]:
- named_sensors = {
- named_sensor[CONF_ID]: named_sensor[CONF_NAME]
- for named_sensor in config[CONF_ONLY_NAMED]
- }
+ named_sensors = {}
+ for named_sensor in config[CONF_ONLY_NAMED]:
+ name = named_sensor[CONF_NAME]
+ proto = named_sensor.get(CONF_PROTOCOL)
+ model = named_sensor.get(CONF_MODEL)
+ id_ = named_sensor[CONF_ID]
+ if proto is not None:
+ if model is not None:
+ named_sensors["{}{}{}".format(proto, model, id_)] = name
+ else:
+ named_sensors["{}{}".format(proto, id_)] = name
+ else:
+ named_sensors[id_] = name
for tellcore_sensor in tellcore_lib.sensors():
if not config[CONF_ONLY_NAMED]:
sensor_name = str(tellcore_sensor.id)
else:
- if tellcore_sensor.id not in named_sensors:
+ proto_id = "{}{}".format(tellcore_sensor.protocol, tellcore_sensor.id)
+ proto_model_id = "{}{}{}".format(
+ tellcore_sensor.protocol, tellcore_sensor.model, tellcore_sensor.id
+ )
+ if tellcore_sensor.id in named_sensors:
+ sensor_name = named_sensors[tellcore_sensor.id]
+ elif proto_id in named_sensors:
+ sensor_name = named_sensors[proto_id]
+ elif proto_model_id in named_sensors:
+ sensor_name = named_sensors[proto_model_id]
+ else:
continue
- sensor_name = named_sensors[tellcore_sensor.id]
for datatype in sensor_value_descriptions:
if datatype & datatype_mask and tellcore_sensor.has_value(datatype):
diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py
index a4777af54578bb..87fb70bb8886a6 100644
--- a/homeassistant/components/telnet/switch.py
+++ b/homeassistant/components/telnet/switch.py
@@ -117,7 +117,7 @@ def _telnet_command(self, command):
response = telnet.read_until(b"\r", timeout=self._timeout)
_LOGGER.debug("telnet response: %s", response.decode("ASCII").strip())
return response.decode("ASCII").strip()
- except IOError as error:
+ except OSError as error:
_LOGGER.error(
'Command "%s" failed with exception: %s', command, repr(error)
)
diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py
index c5e5c4af978773..a32de3da10fb62 100644
--- a/homeassistant/components/temper/sensor.py
+++ b/homeassistant/components/temper/sensor.py
@@ -96,7 +96,7 @@ def update(self):
)
sensor_value = self.temper_device.get_temperature(format_str)
self.current_value = round(sensor_value, 1)
- except IOError:
+ except OSError:
_LOGGER.error(
"Failed to get temperature. The device address may"
"have changed. Attempting to reset device"
diff --git a/homeassistant/components/tfiac/manifest.json b/homeassistant/components/tfiac/manifest.json
index 9997ae00f0a4f4..d7282317d95dfb 100644
--- a/homeassistant/components/tfiac/manifest.json
+++ b/homeassistant/components/tfiac/manifest.json
@@ -3,7 +3,7 @@
"name": "Tfiac",
"documentation": "https://www.home-assistant.io/components/tfiac",
"requirements": [
- "pytfiac==0.3"
+ "pytfiac==0.4"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py
index 3dfe0265bdeef5..a5a7f320d93315 100644
--- a/homeassistant/components/tibber/sensor.py
+++ b/homeassistant/components/tibber/sensor.py
@@ -44,8 +44,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(dev, True)
-class TibberSensorElPrice(Entity):
- """Representation of an Tibber sensor for el price."""
+class TibberSensor(Entity):
+ """Representation of a generic Tibber sensor."""
def __init__(self, tibber_home):
"""Initialize the sensor."""
@@ -54,10 +54,25 @@ def __init__(self, tibber_home):
self._state = None
self._is_available = False
self._device_state_attributes = {}
- self._unit_of_measurement = self._tibber_home.price_unit
- self._name = "Electricity price {}".format(
- tibber_home.info["viewer"]["home"]["appNickname"]
- )
+ self._name = tibber_home.info["viewer"]["home"]["appNickname"]
+ if self._name is None:
+ self._name = tibber_home.info["viewer"]["home"]["address"].get(
+ "address1", ""
+ )
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ return self._device_state_attributes
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ return self._state
+
+
+class TibberSensorElPrice(TibberSensor):
+ """Representation of a Tibber sensor for el price."""
async def async_update(self):
"""Get the latest data and updates the states."""
@@ -86,11 +101,6 @@ async def async_update(self):
self._device_state_attributes.update(attrs)
self._is_available = self._state is not None
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- return self._device_state_attributes
-
@property
def available(self):
"""Return True if entity is available."""
@@ -99,12 +109,7 @@ def available(self):
@property
def name(self):
"""Return the name of the sensor."""
- return self._name
-
- @property
- def state(self):
- """Return the state of the device."""
- return self._state
+ return "Electricity price {}".format(self._name)
@property
def icon(self):
@@ -114,7 +119,7 @@ def icon(self):
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
- return self._unit_of_measurement
+ return self._tibber_home.price_unit
@property
def unique_id(self):
@@ -139,17 +144,8 @@ async def _fetch_data(self):
]["estimatedAnnualConsumption"]
-class TibberSensorRT(Entity):
- """Representation of an Tibber sensor for real time consumption."""
-
- def __init__(self, tibber_home):
- """Initialize the sensor."""
- self._tibber_home = tibber_home
- self._state = None
- self._device_state_attributes = {}
- self._unit_of_measurement = "W"
- nickname = tibber_home.info["viewer"]["home"]["appNickname"]
- self._name = f"Real time consumption {nickname}"
+class TibberSensorRT(TibberSensor):
+ """Representation of a Tibber sensor for real time consumption."""
async def async_added_to_hass(self):
"""Start unavailability tracking."""
@@ -175,11 +171,6 @@ async def _async_callback(self, payload):
self.async_schedule_update_ha_state()
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- return self._device_state_attributes
-
@property
def available(self):
"""Return True if entity is available."""
@@ -188,18 +179,13 @@ def available(self):
@property
def name(self):
"""Return the name of the sensor."""
- return self._name
+ return "Real time consumption {}".format(self._name)
@property
def should_poll(self):
"""Return the polling state."""
return False
- @property
- def state(self):
- """Return the state of the device."""
- return self._state
-
@property
def icon(self):
"""Return the icon to use in the frontend."""
@@ -208,7 +194,7 @@ def icon(self):
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
- return self._unit_of_measurement
+ return "W"
@property
def unique_id(self):
diff --git a/homeassistant/components/toon/.translations/it.json b/homeassistant/components/toon/.translations/it.json
index 696c770f130952..7934913558114f 100644
--- a/homeassistant/components/toon/.translations/it.json
+++ b/homeassistant/components/toon/.translations/it.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"client_id": "L'ID client dalla configurazione non \u00e8 valido.",
+ "client_secret": "Il client segreto della configurazione non \u00e8 valido.",
"no_agreements": "Questo account non ha display Toon.",
"no_app": "\u00c8 necessario configurare Toon prima di poter eseguire l'autenticazione con esso. [Si prega di leggere le istruzioni] (https://www.home-assistant.io/components/toon/).",
"unknown_auth_fail": "Si \u00e8 verificato un errore imprevisto durante l'autenticazione."
@@ -14,6 +15,7 @@
"authenticate": {
"data": {
"password": "Password",
+ "tenant": "Inquilino",
"username": "Nome utente"
},
"description": "Autenticati con il tuo account Eneco Toon (non l'account sviluppatore).",
diff --git a/homeassistant/components/toon/.translations/no.json b/homeassistant/components/toon/.translations/no.json
index 37dcd8ac22f9f5..a033d2954d98d1 100644
--- a/homeassistant/components/toon/.translations/no.json
+++ b/homeassistant/components/toon/.translations/no.json
@@ -8,7 +8,7 @@
"unknown_auth_fail": "Uventet feil oppstod under autentisering."
},
"error": {
- "credentials": "De oppgitte legitimasjonene er ugyldige.",
+ "credentials": "Den oppgitte kontoinformasjonen er ugyldig.",
"display_exists": "Den valgte skjermen er allerede konfigurert."
},
"step": {
@@ -18,7 +18,7 @@
"tenant": "Leietaker",
"username": "Brukernavn"
},
- "description": "Godkjen med Eneco Toon kontoen din (ikke utviklerkontoen).",
+ "description": "Godkjenn med Eneco Toon kontoen din (ikke utviklerkontoen).",
"title": "Linken din Toon konto"
},
"display": {
diff --git a/homeassistant/components/toon/.translations/pl.json b/homeassistant/components/toon/.translations/pl.json
index 26627389ddd592..403be9bc067a54 100644
--- a/homeassistant/components/toon/.translations/pl.json
+++ b/homeassistant/components/toon/.translations/pl.json
@@ -18,8 +18,8 @@
"tenant": "Najemca",
"username": "Nazwa u\u017cytkownika"
},
- "description": "Uwierzytelnij swoje konto Eneco Toon (nie konto programisty).",
- "title": "Po\u0142\u0105cz swoje konto Toon"
+ "description": "Uwierzytelnij konto Eneco Toon (nie konto programisty).",
+ "title": "Po\u0142\u0105cz konto Toon"
},
"display": {
"data": {
diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json
index 012aa65187c28b..0eddbe2a151d79 100644
--- a/homeassistant/components/toon/.translations/ru.json
+++ b/homeassistant/components/toon/.translations/ru.json
@@ -4,7 +4,7 @@
"client_id": "Client ID \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.",
"client_secret": "Client secret \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.",
"no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.",
- "no_app": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Toon \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/toon/).",
+ "no_app": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Toon \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/toon/).",
"unknown_auth_fail": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"error": {
diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py
index 0806ba0799c452..10161856a47a71 100644
--- a/homeassistant/components/torque/sensor.py
+++ b/homeassistant/components/torque/sensor.py
@@ -88,7 +88,12 @@ def get(self, request):
names[pid] = data[key]
elif is_unit:
pid = convert_pid(is_unit.group(1))
- units[pid] = data[key]
+
+ temp_unit = data[key]
+ if "\\xC2\\xB0" in temp_unit:
+ temp_unit = temp_unit.replace("\\xC2\\xB0", "°")
+
+ units[pid] = temp_unit
elif is_value:
pid = convert_pid(is_value.group(1))
if pid in self.sensors:
diff --git a/homeassistant/components/traccar/.translations/de.json b/homeassistant/components/traccar/.translations/de.json
new file mode 100644
index 00000000000000..c835ddf76b2481
--- /dev/null
+++ b/homeassistant/components/traccar/.translations/de.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Ihre Home Assistant-Instanz muss \u00fcber das Internet zug\u00e4nglich sein, um Nachrichten von Traccar zu empfangen.",
+ "one_instance_allowed": "Es ist nur eine einzelne Instanz erforderlich."
+ },
+ "create_entry": {
+ "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}}`\n\nSiehe [die Dokumentation]({docs_url}) f\u00fcr weitere Details."
+ },
+ "step": {
+ "user": {
+ "description": "M\u00f6chten Sie Traccar wirklich einrichten?",
+ "title": "Traccar einrichten"
+ }
+ },
+ "title": "Traccar"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/.translations/es.json b/homeassistant/components/traccar/.translations/es.json
new file mode 100644
index 00000000000000..dedaf02971c79f
--- /dev/null
+++ b/homeassistant/components/traccar/.translations/es.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Traccar.",
+ "one_instance_allowed": "S\u00f3lo se necesita una \u00fanica instancia."
+ },
+ "create_entry": {
+ "default": "Para enviar eventos a Home Assistant, necesitar\u00e1 configurar la funci\u00f3n de webhook en Traccar.\n\nUtilice la siguiente url: ``{webhook_url}``\n\nConsulte la [documentaci\u00f3n]({docs_url}) para m\u00e1s detalles."
+ },
+ "step": {
+ "user": {
+ "description": "\u00bfEst\u00e1 seguro de querer configurar Traccar?",
+ "title": "Configurar Traccar"
+ }
+ },
+ "title": "Traccar"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/.translations/fr.json b/homeassistant/components/traccar/.translations/fr.json
new file mode 100644
index 00000000000000..0948a31739fcea
--- /dev/null
+++ b/homeassistant/components/traccar/.translations/fr.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Votre instance de Home Assistant doit \u00eatre accessible depuis Internet pour recevoir les messages de Traccar.",
+ "one_instance_allowed": "Une seule instance est n\u00e9cessaire."
+ },
+ "create_entry": {
+ "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans Traccar. \n\n Utilisez l'URL suivante: ` {webhook_url} ` \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails."
+ },
+ "step": {
+ "user": {
+ "description": "\u00cates-vous s\u00fbr de vouloir configurer Traccar?",
+ "title": "Configurer Traccar"
+ }
+ },
+ "title": "Traccar"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/.translations/it.json b/homeassistant/components/traccar/.translations/it.json
new file mode 100644
index 00000000000000..a0980644a71a27
--- /dev/null
+++ b/homeassistant/components/traccar/.translations/it.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Traccar.",
+ "one_instance_allowed": "\u00c8 necessaria solo una singola istanza."
+ },
+ "create_entry": {
+ "default": "Per inviare eventi a Home Assistant, \u00e8 necessario configurare la funzionalit\u00e0 webhook in Traccar.\n\nUtilizzare l'URL seguente: `{webhook_url}`\n\nPer ulteriori dettagli, vedere [la documentazione]({docs_url}) ."
+ },
+ "step": {
+ "user": {
+ "description": "Sei sicuro di voler configurare Traccar?",
+ "title": "Imposta Traccar"
+ }
+ },
+ "title": "Traccar"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/.translations/ko.json b/homeassistant/components/traccar/.translations/ko.json
new file mode 100644
index 00000000000000..d9f31967e68b9e
--- /dev/null
+++ b/homeassistant/components/traccar/.translations/ko.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Traccar \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.",
+ "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
+ },
+ "create_entry": {
+ "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Traccar \uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c URL \uc815\ubcf4\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4: `{webhook_url}`\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694."
+ },
+ "step": {
+ "user": {
+ "description": "Traccar \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "Traccar \uc124\uc815"
+ }
+ },
+ "title": "Traccar"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/.translations/lb.json b/homeassistant/components/traccar/.translations/lb.json
new file mode 100644
index 00000000000000..8808d85a1d6db2
--- /dev/null
+++ b/homeassistant/components/traccar/.translations/lb.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Traccar Noriichten z'empf\u00e4nken.",
+ "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
+ },
+ "create_entry": {
+ "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am Traccar ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider Informatiounen."
+ },
+ "step": {
+ "user": {
+ "description": "S\u00e9cher fir Traccar anzeriichten?",
+ "title": "Traccar ariichten"
+ }
+ },
+ "title": "Traccar"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/.translations/no.json b/homeassistant/components/traccar/.translations/no.json
new file mode 100644
index 00000000000000..dea146b649aace
--- /dev/null
+++ b/homeassistant/components/traccar/.translations/no.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Din Home Assistant-forekomst m\u00e5 v\u00e6re tilgjengelig fra Internett for \u00e5 motta meldinger fra Traccar.",
+ "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig."
+ },
+ "create_entry": {
+ "default": "Hvis du vil sende hendelser til Home Assistant, m\u00e5 du konfigurere webhook-funksjonen i Traccar.\n\nBruk f\u00f8lgende URL-adresse: ' {webhook_url} '\n\nSe [dokumentasjonen] ({docs_url}) for mer informasjon."
+ },
+ "step": {
+ "user": {
+ "description": "Er du sikker p\u00e5 at du vil sette opp Traccar?",
+ "title": "Sett opp Traccar"
+ }
+ },
+ "title": "Traccar"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/.translations/sl.json b/homeassistant/components/traccar/.translations/sl.json
new file mode 100644
index 00000000000000..95aaca7e67df2f
--- /dev/null
+++ b/homeassistant/components/traccar/.translations/sl.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "Va\u0161 Home Assistant mora biti dostopen prek interneta, da boste lahko prejemali Traccar sporo\u010dila.",
+ "one_instance_allowed": "Potrebna je samo ena instanca."
+ },
+ "create_entry": {
+ "default": "\u010ce \u017eelite poslati dogodke v Home Assistant, boste morali nastaviti funkcijo \"webhook\" v traccar.\n\nUporabite naslednji URL: ' {webhook_url} '\n\nZa podrobnej\u0161e informacije glejte [dokumentacijo] ({docs_url})."
+ },
+ "step": {
+ "user": {
+ "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti Traccar?",
+ "title": "Nastavite Traccar"
+ }
+ },
+ "title": "Traccar"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/.translations/zh-Hans.json b/homeassistant/components/traccar/.translations/zh-Hans.json
new file mode 100644
index 00000000000000..248e8f9f44ed89
--- /dev/null
+++ b/homeassistant/components/traccar/.translations/zh-Hans.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u53ef\u4ece\u4e92\u8054\u7f51\u8bbf\u95ee\u4ee5\u63a5\u6536Traccar\u6d88\u606f\u3002",
+ "one_instance_allowed": "\u53ea\u6709\u4e00\u4e2a\u5b9e\u4f8b\u662f\u5fc5\u9700\u7684\u3002"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tradfri/.translations/it.json b/homeassistant/components/tradfri/.translations/it.json
index 4c114492336492..99ba9053d79792 100644
--- a/homeassistant/components/tradfri/.translations/it.json
+++ b/homeassistant/components/tradfri/.translations/it.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured": "Il bridge \u00e8 gi\u00e0 configurato"
+ "already_configured": "Bridge gi\u00e0 configurato.",
+ "already_in_progress": "La configurazione del Bridge \u00e8 gi\u00e0 in corso."
},
"error": {
"cannot_connect": "Impossibile connettersi al gateway.",
diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json
index e3fcfc89c5bd4b..3a1798e66d995b 100644
--- a/homeassistant/components/tradfri/.translations/pl.json
+++ b/homeassistant/components/tradfri/.translations/pl.json
@@ -15,7 +15,7 @@
"host": "Host",
"security_code": "Kod bezpiecze\u0144stwa"
},
- "description": "Mo\u017cesz znale\u017a\u0107 kod bezpiecze\u0144stwa z ty\u0142u bramy.",
+ "description": "Mo\u017cesz znale\u017a\u0107 kod bezpiecze\u0144stwa z ty\u0142u bramki.",
"title": "Wprowad\u017a kod bezpiecze\u0144stwa"
}
},
diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py
index 87b073db052001..bca91134bedf47 100644
--- a/homeassistant/components/tradfri/__init__.py
+++ b/homeassistant/components/tradfri/__init__.py
@@ -131,6 +131,9 @@ async def on_hass_stop(event):
sw_version=gateway_info.firmware_version,
)
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, "cover")
+ )
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "light")
)
diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py
new file mode 100644
index 00000000000000..3dea978044fcae
--- /dev/null
+++ b/homeassistant/components/tradfri/cover.py
@@ -0,0 +1,149 @@
+"""Support for IKEA Tradfri covers."""
+import logging
+
+from pytradfri.error import PytradfriError
+
+from homeassistant.components.cover import (
+ CoverDevice,
+ ATTR_POSITION,
+ SUPPORT_OPEN,
+ SUPPORT_CLOSE,
+ SUPPORT_SET_POSITION,
+)
+from homeassistant.core import callback
+from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY
+from .const import CONF_GATEWAY_ID
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Load Tradfri covers based on a config entry."""
+ gateway_id = config_entry.data[CONF_GATEWAY_ID]
+ api = hass.data[KEY_API][config_entry.entry_id]
+ gateway = hass.data[KEY_GATEWAY][config_entry.entry_id]
+
+ devices_commands = await api(gateway.get_devices())
+ devices = await api(devices_commands)
+ covers = [dev for dev in devices if dev.has_blind_control]
+ if covers:
+ async_add_entities(TradfriCover(cover, api, gateway_id) for cover in covers)
+
+
+class TradfriCover(CoverDevice):
+ """The platform class required by Home Assistant."""
+
+ def __init__(self, cover, api, gateway_id):
+ """Initialize a cover."""
+ self._api = api
+ self._unique_id = f"{gateway_id}-{cover.id}"
+ self._cover = None
+ self._cover_control = None
+ self._cover_data = None
+ self._name = None
+ self._available = True
+ self._gateway_id = gateway_id
+
+ self._refresh(cover)
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
+
+ @property
+ def unique_id(self):
+ """Return unique ID for cover."""
+ return self._unique_id
+
+ @property
+ def device_info(self):
+ """Return the device info."""
+ info = self._cover.device_info
+
+ return {
+ "identifiers": {(TRADFRI_DOMAIN, self._cover.id)},
+ "name": self._name,
+ "manufacturer": info.manufacturer,
+ "model": info.model_number,
+ "sw_version": info.firmware_version,
+ "via_device": (TRADFRI_DOMAIN, self._gateway_id),
+ }
+
+ async def async_added_to_hass(self):
+ """Start thread when added to hass."""
+ self._async_start_observe()
+
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return self._available
+
+ @property
+ def should_poll(self):
+ """No polling needed for tradfri cover."""
+ return False
+
+ @property
+ def name(self):
+ """Return the display name of this cover."""
+ return self._name
+
+ @property
+ def current_cover_position(self):
+ """Return current position of cover.
+
+ None is unknown, 0 is closed, 100 is fully open.
+ """
+ return 100 - self._cover_data.current_cover_position
+
+ async def async_set_cover_position(self, **kwargs):
+ """Move the cover to a specific position."""
+ await self._api(self._cover_control.set_state(100 - kwargs[ATTR_POSITION]))
+
+ async def async_open_cover(self, **kwargs):
+ """Open the cover."""
+ await self._api(self._cover_control.set_state(0))
+
+ async def async_close_cover(self, **kwargs):
+ """Close cover."""
+ await self._api(self._cover_control.set_state(100))
+
+ @property
+ def is_closed(self):
+ """Return if the cover is closed or not."""
+ return self.current_cover_position == 0
+
+ @callback
+ def _async_start_observe(self, exc=None):
+ """Start observation of cover."""
+ if exc:
+ self._available = False
+ self.async_schedule_update_ha_state()
+ _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc)
+ try:
+ cmd = self._cover.observe(
+ callback=self._observe_update,
+ err_callback=self._async_start_observe,
+ duration=0,
+ )
+ self.hass.async_create_task(self._api(cmd))
+ except PytradfriError as err:
+ _LOGGER.warning("Observation failed, trying again", exc_info=err)
+ self._async_start_observe()
+
+ def _refresh(self, cover):
+ """Refresh the cover data."""
+ self._cover = cover
+
+ # Caching of BlindControl and cover object
+ self._available = cover.reachable
+ self._cover_control = cover.blind_control
+ self._cover_data = cover.blind_control.blinds[0]
+ self._name = cover.name
+
+ @callback
+ def _observe_update(self, tradfri_device):
+ """Receive new state data for this cover."""
+ self._refresh(tradfri_device)
+ self.async_schedule_update_ha_state()
diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py
index 97fdfd9d36d885..615899a98c8d22 100644
--- a/homeassistant/components/tradfri/light.py
+++ b/homeassistant/components/tradfri/light.py
@@ -1,6 +1,9 @@
"""Support for IKEA Tradfri lights."""
import logging
+from pytradfri.error import PytradfriError
+
+import homeassistant.util.color as color_util
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
@@ -14,8 +17,6 @@
Light,
)
from homeassistant.core import callback
-import homeassistant.util.color as color_util
-
from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY
from .const import CONF_GATEWAY_ID, CONF_IMPORT_GROUPS
@@ -26,7 +27,6 @@
ATTR_SAT = "saturation"
ATTR_TRANSITION_TIME = "transition_time"
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA
-IKEA = "IKEA of Sweden"
TRADFRI_LIGHT_MANAGER = "Tradfri Light Manager"
SUPPORTED_FEATURES = SUPPORT_TRANSITION
SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
@@ -113,9 +113,6 @@ async def async_turn_on(self, **kwargs):
@callback
def _async_start_observe(self, exc=None):
"""Start observation of light."""
- # pylint: disable=import-error
- from pytradfri.error import PytradfriError
-
if exc:
_LOGGER.warning("Observation failed for %s", self._name, exc_info=exc)
@@ -339,8 +336,6 @@ async def async_turn_on(self, **kwargs):
@callback
def _async_start_observe(self, exc=None):
"""Start observation of light."""
- # pylint: disable=import-error
- from pytradfri.error import PytradfriError
if exc:
self._available = False
diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json
index ba6b21e00283ab..d847c6df24f7cb 100644
--- a/homeassistant/components/tradfri/manifest.json
+++ b/homeassistant/components/tradfri/manifest.json
@@ -3,17 +3,11 @@
"name": "Tradfri",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/tradfri",
- "requirements": [
- "pytradfri[async]==6.0.1"
- ],
+ "requirements": ["pytradfri[async]==6.3.1"],
"homekit": {
- "models": [
- "TRADFRI"
- ]
+ "models": ["TRADFRI"]
},
"dependencies": [],
"zeroconf": ["_coap._udp.local."],
- "codeowners": [
- "@ggravlingen"
- ]
+ "codeowners": ["@ggravlingen"]
}
diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py
index 627a98821549ce..4877dbbb541f1a 100644
--- a/homeassistant/components/tradfri/sensor.py
+++ b/homeassistant/components/tradfri/sensor.py
@@ -1,10 +1,11 @@
"""Support for IKEA Tradfri sensors."""
-from datetime import timedelta
import logging
+from datetime import timedelta
+
+from pytradfri.error import PytradfriError
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
-
from . import KEY_API, KEY_GATEWAY
_LOGGER = logging.getLogger(__name__)
@@ -79,9 +80,6 @@ def state(self):
@callback
def _async_start_observe(self, exc=None):
"""Start observation of light."""
- # pylint: disable=import-error
- from pytradfri.error import PytradfriError
-
if exc:
_LOGGER.warning("Observation failed for %s", self._name, exc_info=exc)
diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py
index 4be72eb7359e64..545c1ad93cec17 100644
--- a/homeassistant/components/tradfri/switch.py
+++ b/homeassistant/components/tradfri/switch.py
@@ -1,15 +1,15 @@
"""Support for IKEA Tradfri switches."""
import logging
+from pytradfri.error import PytradfriError
+
from homeassistant.components.switch import SwitchDevice
from homeassistant.core import callback
-
from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY
from .const import CONF_GATEWAY_ID
_LOGGER = logging.getLogger(__name__)
-IKEA = "IKEA of Sweden"
TRADFRI_SWITCH_MANAGER = "Tradfri Switch Manager"
@@ -98,8 +98,6 @@ async def async_turn_on(self, **kwargs):
@callback
def _async_start_observe(self, exc=None):
"""Start observation of switch."""
- from pytradfri.error import PytradfriError
-
if exc:
self._available = False
self.async_schedule_update_ha_state()
diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py
index 2c72dd60490899..9ac72419612aa3 100644
--- a/homeassistant/components/tuya/light.py
+++ b/homeassistant/components/tuya/light.py
@@ -40,6 +40,8 @@ def __init__(self, tuya):
@property
def brightness(self):
"""Return the brightness of the light."""
+ if self.tuya.brightness() is None:
+ return None
return int(self.tuya.brightness())
@property
diff --git a/homeassistant/components/twentemilieu/.translations/de.json b/homeassistant/components/twentemilieu/.translations/de.json
new file mode 100644
index 00000000000000..502a54a8a3d7ee
--- /dev/null
+++ b/homeassistant/components/twentemilieu/.translations/de.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "address_exists": "Adresse bereits eingerichtet."
+ },
+ "error": {
+ "connection_error": "Fehler beim Herstellen einer Verbindung.",
+ "invalid_address": "Adresse nicht im Einzugsgebiet von Twente Milieu gefunden."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "house_letter": "Hausbrief/zusatz",
+ "house_number": "Hausnummer",
+ "post_code": "Postleitzahl"
+ },
+ "description": "Richten Sie Twente Milieu mit Informationen zur Abfallsammlung unter Ihrer Adresse ein.",
+ "title": "Twente Milieu"
+ }
+ },
+ "title": "Twente Milieu"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twentemilieu/.translations/es.json b/homeassistant/components/twentemilieu/.translations/es.json
new file mode 100644
index 00000000000000..60a412684f775b
--- /dev/null
+++ b/homeassistant/components/twentemilieu/.translations/es.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "address_exists": "Direcci\u00f3n ya configurada."
+ },
+ "error": {
+ "connection_error": "No se conect\u00f3.",
+ "invalid_address": "Direcci\u00f3n no encontrada en el \u00e1rea de servicio de Twente Milieu."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "house_letter": "Letra de la casa/adicional",
+ "house_number": "N\u00famero de casa",
+ "post_code": "C\u00f3digo postal"
+ },
+ "description": "Configure Twente Milieu proporcionando informaci\u00f3n sobre la recolecci\u00f3n de residuos en su direcci\u00f3n.",
+ "title": "Twente Milieu"
+ }
+ },
+ "title": "Twente Milieu"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twentemilieu/.translations/fr.json b/homeassistant/components/twentemilieu/.translations/fr.json
new file mode 100644
index 00000000000000..0321a6b73cec35
--- /dev/null
+++ b/homeassistant/components/twentemilieu/.translations/fr.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "address_exists": "Adresse d\u00e9j\u00e0 configur\u00e9e."
+ },
+ "error": {
+ "connection_error": "\u00c9chec de connexion.",
+ "invalid_address": "Adresse introuvable dans la zone de service de Twente Milieu."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "house_letter": "Lettre de la maison / suppl\u00e9mentaire",
+ "house_number": "Num\u00e9ro de maison",
+ "post_code": "Code postal"
+ },
+ "description": "Configurez Twente Milieu en fournissant des informations sur la collecte des d\u00e9chets sur votre adresse.",
+ "title": "Twente Milieu"
+ }
+ },
+ "title": "Twente Milieu"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twentemilieu/.translations/it.json b/homeassistant/components/twentemilieu/.translations/it.json
new file mode 100644
index 00000000000000..27850d207b0591
--- /dev/null
+++ b/homeassistant/components/twentemilieu/.translations/it.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "address_exists": "Indirizzo gi\u00e0 impostato."
+ },
+ "error": {
+ "connection_error": "Impossibile connettersi.",
+ "invalid_address": "Indirizzo non trovato nell'area di servizio di Twente Milieu."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "house_letter": "Edificio, Scala, Interno, ecc. / Informazioni aggiuntive",
+ "house_number": "Numero civico",
+ "post_code": "Codice di Avviamento Postale"
+ },
+ "description": "Imposta Twente Milieu fornendo le informazioni sulla raccolta dei rifiuti al tuo indirizzo.",
+ "title": "Twente Milieu"
+ }
+ },
+ "title": "Twente Milieu"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twentemilieu/.translations/ko.json b/homeassistant/components/twentemilieu/.translations/ko.json
new file mode 100644
index 00000000000000..a78867d86a8ba2
--- /dev/null
+++ b/homeassistant/components/twentemilieu/.translations/ko.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "address_exists": "\uc8fc\uc18c\uac00 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "error": {
+ "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.",
+ "invalid_address": "Twente Milieu \uc11c\ube44\uc2a4 \uc9c0\uc5ed\uc5d0\uc11c \uc8fc\uc18c\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "house_letter": "\uc9d1 \uc8fc\uc18c/\ucd94\uac00\uc815\ubcf4",
+ "house_number": "\uc9d1 \ubc88\ud638",
+ "post_code": "\uc6b0\ud3b8\ubc88\ud638"
+ },
+ "description": "\uc8fc\uc18c\uc5d0 \uc4f0\ub808\uae30 \uc218\uac70 \uc815\ubcf4\ub97c \ub123\uc5b4 Twente Milieu \ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.",
+ "title": "Twente Milieu"
+ }
+ },
+ "title": "Twente Milieu"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twentemilieu/.translations/lb.json b/homeassistant/components/twentemilieu/.translations/lb.json
new file mode 100644
index 00000000000000..b6f10842b4d1c7
--- /dev/null
+++ b/homeassistant/components/twentemilieu/.translations/lb.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "address_exists": "Adresse ass scho ageriicht."
+ },
+ "error": {
+ "connection_error": "Feeler beim verbannen.",
+ "invalid_address": "Adresse net am Twente Milieu Service Ber\u00e4ich fonnt"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "house_letter": "Haus Buschtaf/zous\u00e4tzlech",
+ "house_number": "Haus Nummer",
+ "post_code": "Postleitzuel"
+ },
+ "description": "Offallsammlung Informatiounen vun Twente Milieu zu \u00e4erer Adresse ariichten.",
+ "title": "Twente Milieu"
+ }
+ },
+ "title": "Twente Milieu"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twentemilieu/.translations/no.json b/homeassistant/components/twentemilieu/.translations/no.json
new file mode 100644
index 00000000000000..1d4395bb2c80fd
--- /dev/null
+++ b/homeassistant/components/twentemilieu/.translations/no.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "address_exists": "Adressen er allerede konfigurert."
+ },
+ "error": {
+ "connection_error": "Tilkobling mislyktes.",
+ "invalid_address": "Adresse ble ikke funnet i Twente Milieu tjenesteomr\u00e5de."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "house_letter": "Hus brev/ekstra",
+ "house_number": "Husnummer",
+ "post_code": "Postnummer"
+ },
+ "description": "Sett opp Twente Milieu som gir informasjon om innsamling av avfall p\u00e5 adressen din.",
+ "title": "Twente Milieu"
+ }
+ },
+ "title": "Twente Milieu"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twentemilieu/.translations/sl.json b/homeassistant/components/twentemilieu/.translations/sl.json
new file mode 100644
index 00000000000000..7b74b96d0574a6
--- /dev/null
+++ b/homeassistant/components/twentemilieu/.translations/sl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "address_exists": "Naslov je \u017ee nastavljen."
+ },
+ "error": {
+ "connection_error": "Povezava ni uspela.",
+ "invalid_address": "V storitvenem obmo\u010dju Twente Milieu ni mogo\u010de najti naslova."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "house_letter": "Hi\u0161na \u0161tevilka -\u010drka/dodatno",
+ "house_number": "Hi\u0161na \u0161tevilka",
+ "post_code": "Po\u0161tna \u0161tevilka"
+ },
+ "description": "Nastavite Twente milieu, ki zagotavlja informacije o zbiranju odpadkov na va\u0161em naslovu.",
+ "title": "Twente Milieu"
+ }
+ },
+ "title": "Twente Milieu"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twentemilieu/.translations/zh-Hans.json b/homeassistant/components/twentemilieu/.translations/zh-Hans.json
new file mode 100644
index 00000000000000..80301cfd57b808
--- /dev/null
+++ b/homeassistant/components/twentemilieu/.translations/zh-Hans.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "address_exists": "\u5730\u5740\u5df2\u7ecf\u8bbe\u7f6e\u597d\u4e86\u3002"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py
index 8e6e46531e2f41..eb325d32212e11 100644
--- a/homeassistant/components/uk_transport/sensor.py
+++ b/homeassistant/components/uk_transport/sensor.py
@@ -11,6 +11,7 @@
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
+import homeassistant.util.dt as dt_util
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_MODE
from homeassistant.helpers.entity import Entity
@@ -277,12 +278,11 @@ def device_state_attributes(self):
def _delta_mins(hhmm_time_str):
"""Calculate time delta in minutes to a time in hh:mm format."""
- now = datetime.now()
+ now = dt_util.now()
hhmm_time = datetime.strptime(hhmm_time_str, "%H:%M")
- hhmm_datetime = datetime(
- now.year, now.month, now.day, hour=hhmm_time.hour, minute=hhmm_time.minute
- )
+ hhmm_datetime = now.replace(hour=hhmm_time.hour, minute=hhmm_time.minute)
+
if hhmm_datetime < now:
hhmm_datetime += timedelta(days=1)
diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json
index 8a8d8b11f57661..3741b035d7a9d6 100644
--- a/homeassistant/components/unifi/.translations/ca.json
+++ b/homeassistant/components/unifi/.translations/ca.json
@@ -32,6 +32,12 @@
"track_devices": "Segueix dispositius de la xarxa (dispositius Ubiquiti)",
"track_wired_clients": "Inclou clients de xarxa per cable"
}
+ },
+ "init": {
+ "data": {
+ "one": "un",
+ "other": "altre"
+ }
}
}
}
diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json
index 0c44871f583c97..e447e89644f5b5 100644
--- a/homeassistant/components/unifi/.translations/de.json
+++ b/homeassistant/components/unifi/.translations/de.json
@@ -32,6 +32,12 @@
"track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)",
"track_wired_clients": "Einbinden von kabelgebundenen Netzwerk-Clients"
}
+ },
+ "init": {
+ "data": {
+ "one": "eins",
+ "other": "andere"
+ }
}
}
}
diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json
index 8b0eb56203700d..0539f5607b4c36 100644
--- a/homeassistant/components/unifi/.translations/es.json
+++ b/homeassistant/components/unifi/.translations/es.json
@@ -29,8 +29,15 @@
"data": {
"detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta considerarlo desconectado",
"track_clients": "Seguimiento de los clientes de red",
+ "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)",
"track_wired_clients": "Incluir clientes de red cableada"
}
+ },
+ "init": {
+ "data": {
+ "one": "uno",
+ "other": "otro"
+ }
}
}
}
diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json
index 9e567fcc394a75..8c2526f8a1565a 100644
--- a/homeassistant/components/unifi/.translations/fr.json
+++ b/homeassistant/components/unifi/.translations/fr.json
@@ -22,5 +22,17 @@
}
},
"title": "Contr\u00f4leur UniFi"
+ },
+ "options": {
+ "step": {
+ "device_tracker": {
+ "data": {
+ "detection_time": "Temps en secondes depuis la derni\u00e8re vue avant de consid\u00e9rer comme absent",
+ "track_clients": "Suivre les clients du r\u00e9seau",
+ "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)",
+ "track_wired_clients": "Inclure les clients du r\u00e9seau filaire"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json
index 407371bf89f198..5285ed21873529 100644
--- a/homeassistant/components/unifi/.translations/it.json
+++ b/homeassistant/components/unifi/.translations/it.json
@@ -22,5 +22,23 @@
}
},
"title": "UniFi Controller"
+ },
+ "options": {
+ "step": {
+ "device_tracker": {
+ "data": {
+ "detection_time": "Tempo in secondi dall'ultima volta che viene visto fino a quando non \u00e8 considerato lontano",
+ "track_clients": "Traccia i client di rete",
+ "track_devices": "Tracciare i dispositivi di rete (dispositivi Ubiquiti)",
+ "track_wired_clients": "Includi i client di rete cablata"
+ }
+ },
+ "init": {
+ "data": {
+ "one": "uno",
+ "other": "altro"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/.translations/ko.json b/homeassistant/components/unifi/.translations/ko.json
index 431d6bbf5e6d73..1fff9887906b53 100644
--- a/homeassistant/components/unifi/.translations/ko.json
+++ b/homeassistant/components/unifi/.translations/ko.json
@@ -22,5 +22,17 @@
}
},
"title": "UniFi \ucee8\ud2b8\ub864\ub7ec"
+ },
+ "options": {
+ "step": {
+ "device_tracker": {
+ "data": {
+ "detection_time": "\ub9c8\uc9c0\ub9c9\uc73c\ub85c \ud655\uc778\ub41c \uc2dc\uac04\ubd80\ud130 \uc678\ucd9c \uc0c1\ud0dc\ub85c \uac04\uc8fc\ub418\ub294 \uc2dc\uac04 (\ucd08)",
+ "track_clients": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uc801 \ub300\uc0c1",
+ "track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)",
+ "track_wired_clients": "\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ud3ec\ud568"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/.translations/lb.json b/homeassistant/components/unifi/.translations/lb.json
index 3bef273b83e53f..05b0ffc0c44cdf 100644
--- a/homeassistant/components/unifi/.translations/lb.json
+++ b/homeassistant/components/unifi/.translations/lb.json
@@ -22,5 +22,23 @@
}
},
"title": "Unifi Kontroller"
+ },
+ "options": {
+ "step": {
+ "device_tracker": {
+ "data": {
+ "detection_time": "Z\u00e4it a Sekonne vum leschten Z\u00e4itpunkt un bis den Apparat als \u00ebnnerwee consider\u00e9iert g\u00ebtt",
+ "track_clients": "Netzwierk Cliente verfollegen",
+ "track_devices": "Netzwierk Apparater (Ubiquiti Apparater) verfollegen",
+ "track_wired_clients": "Kabel Netzwierk Cliente abez\u00e9ien"
+ }
+ },
+ "init": {
+ "data": {
+ "one": "Een",
+ "other": "M\u00e9i"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json
index f907364327c958..518f0066534411 100644
--- a/homeassistant/components/unifi/.translations/nl.json
+++ b/homeassistant/components/unifi/.translations/nl.json
@@ -32,6 +32,12 @@
"track_devices": "Netwerkapparaten volgen (Ubiquiti-apparaten)",
"track_wired_clients": "Inclusief bedrade netwerkcli\u00ebnten"
}
+ },
+ "init": {
+ "data": {
+ "one": "Leeg",
+ "other": "Leeg"
+ }
}
}
}
diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json
index 068f4341544900..c21a47c7ea2c8b 100644
--- a/homeassistant/components/unifi/.translations/no.json
+++ b/homeassistant/components/unifi/.translations/no.json
@@ -32,6 +32,12 @@
"track_devices": "Spore nettverksenheter (Ubiquiti-enheter)",
"track_wired_clients": "Inkluder kablede nettverksklienter"
}
+ },
+ "init": {
+ "data": {
+ "one": "en",
+ "other": "andre"
+ }
}
}
}
diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py
index 00b003746a2568..fdb75d09194ef9 100644
--- a/homeassistant/components/unifi/config_flow.py
+++ b/homeassistant/components/unifi/config_flow.py
@@ -1,4 +1,4 @@
-"""Config flow for Unifi."""
+"""Config flow for UniFi."""
import voluptuous as vol
from homeassistant import config_entries
@@ -164,20 +164,6 @@ async def async_step_site(self, user_input=None):
errors=errors,
)
- async def async_step_import(self, import_config):
- """Import from UniFi device tracker config."""
- config = {
- CONF_HOST: import_config[CONF_HOST],
- CONF_USERNAME: import_config[CONF_USERNAME],
- CONF_PASSWORD: import_config[CONF_PASSWORD],
- CONF_PORT: import_config.get(CONF_PORT),
- CONF_VERIFY_SSL: import_config.get(CONF_VERIFY_SSL),
- }
-
- self.desc = import_config[CONF_SITE_ID]
-
- return await self.async_step_user(user_input=config)
-
class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Unifi options."""
diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py
index ca6ddb6820660c..b3982e7327d42d 100644
--- a/homeassistant/components/unifi/device_tracker.py
+++ b/homeassistant/components/unifi/device_tracker.py
@@ -1,36 +1,20 @@
-"""Support for Unifi WAP controllers."""
-from datetime import timedelta
-
+"""Track devices using UniFi controllers."""
import logging
import voluptuous as vol
-from homeassistant import config_entries
from homeassistant.components.unifi.config_flow import get_controller_from_config_entry
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
from homeassistant.components.device_tracker.config_entry import ScannerEntity
from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER
from homeassistant.core import callback
-from homeassistant.const import (
- CONF_HOST,
- CONF_USERNAME,
- CONF_PASSWORD,
- CONF_PORT,
- CONF_VERIFY_SSL,
-)
from homeassistant.helpers import entity_registry
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY
-import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
-from .const import (
- ATTR_MANUFACTURER,
- CONF_CONTROLLER,
- CONF_SITE_ID,
- DOMAIN as UNIFI_DOMAIN,
-)
+from .const import ATTR_MANUFACTURER
LOGGER = logging.getLogger(__name__)
@@ -55,51 +39,11 @@
"vlan",
]
-CONF_DT_SITE_ID = "site_id"
-
-DEFAULT_HOST = "localhost"
-DEFAULT_PORT = 8443
-DEFAULT_VERIFY_SSL = True
-DEFAULT_DETECTION_TIME = timedelta(seconds=300)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
- vol.Optional(CONF_DT_SITE_ID, default="default"): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
- vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): vol.Any(
- cv.boolean, cv.isfile
- ),
- },
- extra=vol.ALLOW_EXTRA,
-)
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
async def async_setup_scanner(hass, config, sync_see, discovery_info):
"""Set up the Unifi integration."""
- config[CONF_SITE_ID] = config.pop(CONF_DT_SITE_ID) # Current from legacy
-
- exist = False
-
- for entry in hass.config_entries.async_entries(UNIFI_DOMAIN):
- if (
- config[CONF_HOST] == entry.data[CONF_CONTROLLER][CONF_HOST]
- and config[CONF_SITE_ID] == entry.data[CONF_CONTROLLER][CONF_SITE_ID]
- ):
- exist = True
- break
-
- if not exist:
- hass.async_create_task(
- hass.config_entries.flow.async_init(
- UNIFI_DOMAIN,
- context={"source": config_entries.SOURCE_IMPORT},
- data=config,
- )
- )
-
return True
diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py
index 384b82d139564c..fc9225c6ef4256 100644
--- a/homeassistant/components/upc_connect/device_tracker.py
+++ b/homeassistant/components/upc_connect/device_tracker.py
@@ -1,10 +1,9 @@
"""Support for UPC ConnectBox router."""
-import asyncio
import logging
+from typing import List, Optional
-import aiohttp
-from aiohttp.hdrs import REFERER, USER_AGENT
-import async_timeout
+from connect_box import ConnectBox
+from connect_box.exceptions import ConnectBoxError, ConnectBoxLoginError
import voluptuous as vol
from homeassistant.components.device_tracker import (
@@ -12,118 +11,66 @@
PLATFORM_SCHEMA,
DeviceScanner,
)
-from homeassistant.const import CONF_HOST, HTTP_HEADER_X_REQUESTED_WITH
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
-CMD_DEVICES = 123
-
DEFAULT_IP = "192.168.0.1"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string}
+ {
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string,
+ }
)
async def async_get_scanner(hass, config):
"""Return the UPC device scanner."""
- scanner = UPCDeviceScanner(hass, config[DOMAIN])
- success_init = await scanner.async_initialize_token()
+ conf = config[DOMAIN]
+ session = hass.helpers.aiohttp_client.async_get_clientsession()
+ connect_box = ConnectBox(session, conf[CONF_PASSWORD], host=conf[CONF_HOST])
+
+ # Check login data
+ try:
+ await connect_box.async_initialize_token()
+ except ConnectBoxLoginError:
+ _LOGGER.error("ConnectBox login data error!")
+ return None
+ except ConnectBoxError:
+ pass
+
+ async def _shutdown(event):
+ """Shutdown event."""
+ await connect_box.async_close_session()
- return scanner if success_init else None
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
+
+ return UPCDeviceScanner(connect_box)
class UPCDeviceScanner(DeviceScanner):
"""This class queries a router running UPC ConnectBox firmware."""
- def __init__(self, hass, config):
+ def __init__(self, connect_box: ConnectBox):
"""Initialize the scanner."""
- self.hass = hass
- self.host = config[CONF_HOST]
-
- self.data = {}
- self.token = None
+ self.connect_box: ConnectBox = connect_box
- self.headers = {
- HTTP_HEADER_X_REQUESTED_WITH: "XMLHttpRequest",
- REFERER: f"http://{self.host}/index.html",
- USER_AGENT: (
- "Mozilla/5.0 (Windows NT 10.0; WOW64) "
- "AppleWebKit/537.36 (KHTML, like Gecko) "
- "Chrome/47.0.2526.106 Safari/537.36"
- ),
- }
-
- self.websession = async_get_clientsession(hass)
-
- async def async_scan_devices(self):
+ async def async_scan_devices(self) -> List[str]:
"""Scan for new devices and return a list with found device IDs."""
- import defusedxml.ElementTree as ET
-
- if self.token is None:
- token_initialized = await self.async_initialize_token()
- if not token_initialized:
- _LOGGER.error("Not connected to %s", self.host)
- return []
-
- raw = await self._async_ws_function(CMD_DEVICES)
-
try:
- xml_root = ET.fromstring(raw)
- return [mac.text for mac in xml_root.iter("MACAddr")]
- except (ET.ParseError, TypeError):
- _LOGGER.warning("Can't read device from %s", self.host)
- self.token = None
+ await self.connect_box.async_get_devices()
+ except ConnectBoxError:
return []
- async def async_get_device_name(self, device):
- """Get the device name (the name of the wireless device not used)."""
- return None
-
- async def async_initialize_token(self):
- """Get first token."""
- try:
- # get first token
- with async_timeout.timeout(10):
- response = await self.websession.get(
- f"http://{self.host}/common_page/login.html", headers=self.headers
- )
-
- await response.text()
+ return [device.mac for device in self.connect_box.devices]
- self.token = response.cookies["sessionToken"].value
-
- return True
-
- except (asyncio.TimeoutError, aiohttp.ClientError):
- _LOGGER.error("Can not load login page from %s", self.host)
- return False
+ async def async_get_device_name(self, device: str) -> Optional[str]:
+ """Get the device name (the name of the wireless device not used)."""
+ for connected_device in self.connect_box.devices:
+ if connected_device != device:
+ continue
+ return connected_device.hostname
- async def _async_ws_function(self, function):
- """Execute a command on UPC firmware webservice."""
- try:
- with async_timeout.timeout(10):
- # The 'token' parameter has to be first, and 'fun' second
- # or the UPC firmware will return an error
- response = await self.websession.post(
- f"http://{self.host}/xml/getter.xml",
- data=f"token={self.token}&fun={function}",
- headers=self.headers,
- allow_redirects=False,
- )
-
- # Error?
- if response.status != 200:
- _LOGGER.warning("Receive http code %d", response.status)
- self.token = None
- return
-
- # Load data, store token for next request
- self.token = response.cookies["sessionToken"].value
- return await response.text()
-
- except (asyncio.TimeoutError, aiohttp.ClientError):
- _LOGGER.error("Error on %s", function)
- self.token = None
+ return None
diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json
index 36a06ac3204223..efa38286e7e2f6 100644
--- a/homeassistant/components/upc_connect/manifest.json
+++ b/homeassistant/components/upc_connect/manifest.json
@@ -2,9 +2,7 @@
"domain": "upc_connect",
"name": "Upc connect",
"documentation": "https://www.home-assistant.io/components/upc_connect",
- "requirements": [
- "defusedxml==0.6.0"
- ],
+ "requirements": ["connect-box==0.2.4"],
"dependencies": [],
- "codeowners": []
+ "codeowners": ["@pvizeli"]
}
diff --git a/homeassistant/components/upnp/.translations/ca.json b/homeassistant/components/upnp/.translations/ca.json
index 161b5d85599147..28ad9ce954d995 100644
--- a/homeassistant/components/upnp/.translations/ca.json
+++ b/homeassistant/components/upnp/.translations/ca.json
@@ -8,6 +8,10 @@
"no_sensors_or_port_mapping": "Activa, com a m\u00ednim, els sensors o l'assignaci\u00f3 de ports",
"single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de UPnP/IGD."
},
+ "error": {
+ "one": "un",
+ "other": "altre"
+ },
"step": {
"confirm": {
"description": "Vols configurar UPnP/IGD?",
diff --git a/homeassistant/components/upnp/.translations/it.json b/homeassistant/components/upnp/.translations/it.json
index 798f6578093950..e822895a6cfaab 100644
--- a/homeassistant/components/upnp/.translations/it.json
+++ b/homeassistant/components/upnp/.translations/it.json
@@ -8,6 +8,10 @@
"no_sensors_or_port_mapping": "Abilita almeno i sensori o la mappatura delle porte",
"single_instance_allowed": "\u00c8 necessaria una sola configurazione di UPnP/IGD."
},
+ "error": {
+ "one": "Vuoto",
+ "other": "Vuoto"
+ },
"step": {
"confirm": {
"description": "Vuoi configurare UPnP/IGD?",
diff --git a/homeassistant/components/upnp/.translations/ko.json b/homeassistant/components/upnp/.translations/ko.json
index 9fa37e1236dd32..d846a5e38ce342 100644
--- a/homeassistant/components/upnp/.translations/ko.json
+++ b/homeassistant/components/upnp/.translations/ko.json
@@ -2,9 +2,9 @@
"config": {
"abort": {
"already_configured": "UPnP/IGD \uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4",
- "incomplete_device": "\ubd88\uc644\uc804\ud55c UPnP \uc7a5\uce58 \ubb34\uc2dc\ud558\uae30",
+ "incomplete_device": "\ubd88\uc644\uc804\ud55c UPnP \uae30\uae30 \ubb34\uc2dc\ud558\uae30",
"no_devices_discovered": "\ubc1c\uacac\ub41c UPnP/IGD \uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
- "no_devices_found": "UPnP/IGD \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
+ "no_devices_found": "UPnP/IGD \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
"no_sensors_or_port_mapping": "\ucd5c\uc18c\ud55c \uc13c\uc11c \ud639\uc740 \ud3ec\ud2b8 \ub9e4\ud551\uc744 \ud65c\uc131\ud654 \ud574\uc57c \ud569\ub2c8\ub2e4",
"single_instance_allowed": "\ud558\ub098\uc758 UPnP/IGD \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4."
},
diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json
index 6120b6b3ca6d9e..9aec23a687ce0d 100644
--- a/homeassistant/components/upnp/manifest.json
+++ b/homeassistant/components/upnp/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/upnp",
"requirements": [
- "async-upnp-client==0.14.10"
+ "async-upnp-client==0.14.11"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py
index e5746e088f866b..b721fa29cdd860 100644
--- a/homeassistant/components/upnp/sensor.py
+++ b/homeassistant/components/upnp/sensor.py
@@ -1,11 +1,11 @@
"""Support for UPnP/IGD Sensors."""
-from datetime import datetime
import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
+import homeassistant.util.dt as dt_util
from .const import DOMAIN as DOMAIN_UPNP, SIGNAL_REMOVE_SENSOR
@@ -199,10 +199,10 @@ async def async_update(self):
if self._last_value is None:
self._last_value = new_value
- self._last_update_time = datetime.now()
+ self._last_update_time = dt_util.utcnow()
return
- now = datetime.now()
+ now = dt_util.utcnow()
if self._is_overflowed(new_value):
self._state = None # temporarily report nothing
else:
diff --git a/homeassistant/components/ups/__init__.py b/homeassistant/components/ups/__init__.py
deleted file mode 100644
index 690d3102f9c5bf..00000000000000
--- a/homeassistant/components/ups/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The ups component."""
diff --git a/homeassistant/components/ups/manifest.json b/homeassistant/components/ups/manifest.json
deleted file mode 100644
index 98db00c30948e1..00000000000000
--- a/homeassistant/components/ups/manifest.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "domain": "ups",
- "name": "Ups",
- "documentation": "https://www.home-assistant.io/components/ups",
- "requirements": [
- "upsmychoice==1.0.6"
- ],
- "dependencies": [],
- "codeowners": []
-}
diff --git a/homeassistant/components/ups/sensor.py b/homeassistant/components/ups/sensor.py
deleted file mode 100644
index cfe35a9a63fc0c..00000000000000
--- a/homeassistant/components/ups/sensor.py
+++ /dev/null
@@ -1,126 +0,0 @@
-"""Sensor for UPS packages."""
-import logging
-from collections import defaultdict
-from datetime import timedelta
-
-import voluptuous as vol
-
-import homeassistant.helpers.config_validation as cv
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
- ATTR_ATTRIBUTION,
- CONF_NAME,
- CONF_PASSWORD,
- CONF_SCAN_INTERVAL,
- CONF_USERNAME,
-)
-from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle, slugify
-from homeassistant.util.dt import now, parse_date
-
-_LOGGER = logging.getLogger(__name__)
-
-DOMAIN = "ups"
-COOKIE = "upsmychoice_cookies.pickle"
-ICON = "mdi:package-variant-closed"
-STATUS_DELIVERED = "delivered"
-
-SCAN_INTERVAL = timedelta(seconds=1800)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_NAME): cv.string,
- }
-)
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the UPS platform."""
- import upsmychoice
-
- _LOGGER.warning(
- "The ups integration is deprecated and will be removed "
- "in Home Assistant 0.100.0. For more information see ADR-0004:"
- "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md"
- )
-
- try:
- cookie = hass.config.path(COOKIE)
- session = upsmychoice.get_session(
- config.get(CONF_USERNAME), config.get(CONF_PASSWORD), cookie_path=cookie
- )
- except upsmychoice.UPSError:
- _LOGGER.exception("Could not connect to UPS My Choice")
- return False
-
- add_entities(
- [
- UPSSensor(
- session,
- config.get(CONF_NAME),
- config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL),
- )
- ],
- True,
- )
-
-
-class UPSSensor(Entity):
- """UPS Sensor."""
-
- def __init__(self, session, name, interval):
- """Initialize the sensor."""
- self._session = session
- self._name = name
- self._attributes = None
- self._state = None
- self.update = Throttle(interval)(self._update)
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name or DOMAIN
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return "packages"
-
- def _update(self):
- """Update device state."""
- import upsmychoice
-
- status_counts = defaultdict(int)
- try:
- for package in upsmychoice.get_packages(self._session):
- status = slugify(package["status"])
- skip = (
- status == STATUS_DELIVERED
- and parse_date(package["delivery_date"]) < now().date()
- )
- if skip:
- continue
- status_counts[status] += 1
- except upsmychoice.UPSError:
- _LOGGER.error("Could not connect to UPS My Choice account")
-
- self._attributes = {ATTR_ATTRIBUTION: upsmychoice.ATTRIBUTION}
- self._attributes.update(status_counts)
- self._state = sum(status_counts.values())
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- return self._attributes
-
- @property
- def icon(self):
- """Icon to use in the frontend."""
- return ICON
diff --git a/homeassistant/components/usps/__init__.py b/homeassistant/components/usps/__init__.py
deleted file mode 100644
index 61da78fa6d7194..00000000000000
--- a/homeassistant/components/usps/__init__.py
+++ /dev/null
@@ -1,96 +0,0 @@
-"""Support for USPS packages and mail."""
-from datetime import timedelta
-import logging
-
-import voluptuous as vol
-
-from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD
-from homeassistant.helpers import config_validation as cv, discovery
-from homeassistant.util import Throttle
-from homeassistant.util.dt import now
-
-_LOGGER = logging.getLogger(__name__)
-
-DOMAIN = "usps"
-DATA_USPS = "data_usps"
-MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30)
-COOKIE = "usps_cookies.pickle"
-CACHE = "usps_cache"
-CONF_DRIVER = "driver"
-
-USPS_TYPE = ["sensor", "camera"]
-
-CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.Schema(
- {
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_NAME, default=DOMAIN): cv.string,
- vol.Optional(CONF_DRIVER): cv.string,
- }
- )
- },
- extra=vol.ALLOW_EXTRA,
-)
-
-
-def setup(hass, config):
- """Use config values to set up a function enabling status retrieval."""
- _LOGGER.warning(
- "The usps integration is deprecated and will be removed "
- "in Home Assistant 0.100.0. For more information see ADR-0004:"
- "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md"
- )
-
- conf = config[DOMAIN]
- username = conf.get(CONF_USERNAME)
- password = conf.get(CONF_PASSWORD)
- name = conf.get(CONF_NAME)
- driver = conf.get(CONF_DRIVER)
-
- import myusps
-
- try:
- cookie = hass.config.path(COOKIE)
- cache = hass.config.path(CACHE)
- session = myusps.get_session(
- username, password, cookie_path=cookie, cache_path=cache, driver=driver
- )
- except myusps.USPSError:
- _LOGGER.exception("Could not connect to My USPS")
- return False
-
- hass.data[DATA_USPS] = USPSData(session, name)
-
- for component in USPS_TYPE:
- discovery.load_platform(hass, component, DOMAIN, {}, config)
-
- return True
-
-
-class USPSData:
- """Stores the data retrieved from USPS.
-
- For each entity to use, acts as the single point responsible for fetching
- updates from the server.
- """
-
- def __init__(self, session, name):
- """Initialize the data object."""
- self.session = session
- self.name = name
- self.packages = []
- self.mail = []
- self.attribution = None
-
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- def update(self, **kwargs):
- """Fetch the latest info from USPS."""
- import myusps
-
- self.packages = myusps.get_packages(self.session)
- self.mail = myusps.get_mail(self.session, now().date())
- self.attribution = myusps.ATTRIBUTION
- _LOGGER.debug("Mail, request date: %s, list: %s", now().date(), self.mail)
- _LOGGER.debug("Package list: %s", self.packages)
diff --git a/homeassistant/components/usps/camera.py b/homeassistant/components/usps/camera.py
deleted file mode 100644
index 3141314b049cb5..00000000000000
--- a/homeassistant/components/usps/camera.py
+++ /dev/null
@@ -1,88 +0,0 @@
-"""Support for a camera made up of USPS mail images."""
-from datetime import timedelta
-import logging
-
-from homeassistant.components.camera import Camera
-
-from . import DATA_USPS
-
-_LOGGER = logging.getLogger(__name__)
-
-SCAN_INTERVAL = timedelta(seconds=10)
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up USPS mail camera."""
- if discovery_info is None:
- return
-
- usps = hass.data[DATA_USPS]
- add_entities([USPSCamera(usps)])
-
-
-class USPSCamera(Camera):
- """Representation of the images available from USPS."""
-
- def __init__(self, usps):
- """Initialize the USPS camera images."""
- super().__init__()
-
- self._usps = usps
- self._name = self._usps.name
- self._session = self._usps.session
-
- self._mail_img = []
- self._last_mail = None
- self._mail_index = 0
- self._mail_count = 0
-
- self._timer = None
-
- def camera_image(self):
- """Update the camera's image if it has changed."""
- self._usps.update()
- try:
- self._mail_count = len(self._usps.mail)
- except TypeError:
- # No mail
- return None
-
- if self._usps.mail != self._last_mail:
- # Mail items must have changed
- self._mail_img = []
- if len(self._usps.mail) >= 1:
- self._last_mail = self._usps.mail
- for article in self._usps.mail:
- _LOGGER.debug("Fetching article image: %s", article)
- img = self._session.get(article["image"]).content
- self._mail_img.append(img)
-
- try:
- return self._mail_img[self._mail_index]
- except IndexError:
- return None
-
- @property
- def name(self):
- """Return the name of this camera."""
- return f"{self._name} mail"
-
- @property
- def model(self):
- """Return date of mail as model."""
- try:
- return "Date: {}".format(str(self._usps.mail[0]["date"]))
- except IndexError:
- return None
-
- @property
- def should_poll(self):
- """Update the mail image index periodically."""
- return True
-
- def update(self):
- """Update mail image index."""
- if self._mail_index < (self._mail_count - 1):
- self._mail_index += 1
- else:
- self._mail_index = 0
diff --git a/homeassistant/components/usps/manifest.json b/homeassistant/components/usps/manifest.json
deleted file mode 100644
index 9e2f8886d3acbd..00000000000000
--- a/homeassistant/components/usps/manifest.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "domain": "usps",
- "name": "Usps",
- "documentation": "https://www.home-assistant.io/components/usps",
- "requirements": [
- "myusps==1.3.2"
- ],
- "dependencies": [],
- "codeowners": []
-}
diff --git a/homeassistant/components/usps/sensor.py b/homeassistant/components/usps/sensor.py
deleted file mode 100644
index 7e26e6c9e5c793..00000000000000
--- a/homeassistant/components/usps/sensor.py
+++ /dev/null
@@ -1,122 +0,0 @@
-"""Sensor for USPS packages."""
-from collections import defaultdict
-import logging
-
-from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DATE
-from homeassistant.helpers.entity import Entity
-from homeassistant.util import slugify
-from homeassistant.util.dt import now
-
-from . import DATA_USPS
-
-_LOGGER = logging.getLogger(__name__)
-
-STATUS_DELIVERED = "delivered"
-
-
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the USPS platform."""
- if discovery_info is None:
- return
-
- usps = hass.data[DATA_USPS]
- add_entities([USPSPackageSensor(usps), USPSMailSensor(usps)], True)
-
-
-class USPSPackageSensor(Entity):
- """USPS Package Sensor."""
-
- def __init__(self, usps):
- """Initialize the sensor."""
- self._usps = usps
- self._name = self._usps.name
- self._attributes = None
- self._state = None
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return f"{self._name} packages"
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- def update(self):
- """Update device state."""
- self._usps.update()
- status_counts = defaultdict(int)
- for package in self._usps.packages:
- status = slugify(package["primary_status"])
- if status == STATUS_DELIVERED and package["delivery_date"] < now().date():
- continue
- status_counts[status] += 1
- self._attributes = {ATTR_ATTRIBUTION: self._usps.attribution}
- self._attributes.update(status_counts)
- self._state = sum(status_counts.values())
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- return self._attributes
-
- @property
- def icon(self):
- """Return the icon to use in the frontend."""
- return "mdi:package-variant-closed"
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return "packages"
-
-
-class USPSMailSensor(Entity):
- """USPS Mail Sensor."""
-
- def __init__(self, usps):
- """Initialize the sensor."""
- self._usps = usps
- self._name = self._usps.name
- self._attributes = None
- self._state = None
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return f"{self._name} mail"
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- def update(self):
- """Update device state."""
- self._usps.update()
- if self._usps.mail is not None:
- self._state = len(self._usps.mail)
- else:
- self._state = 0
-
- @property
- def device_state_attributes(self):
- """Return the state attributes."""
- attr = {}
- attr[ATTR_ATTRIBUTION] = self._usps.attribution
- try:
- attr[ATTR_DATE] = str(self._usps.mail[0]["date"])
- except IndexError:
- pass
- return attr
-
- @property
- def icon(self):
- """Icon to use in the frontend."""
- return "mdi:mailbox"
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement of this entity, if any."""
- return "pieces"
diff --git a/homeassistant/components/velbus/.translations/de.json b/homeassistant/components/velbus/.translations/de.json
new file mode 100644
index 00000000000000..72af917e12ed29
--- /dev/null
+++ b/homeassistant/components/velbus/.translations/de.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "port_exists": "Dieser Port ist bereits konfiguriert"
+ },
+ "error": {
+ "connection_failed": "Die Velbus-Verbindung ist fehlgeschlagen",
+ "port_exists": "Dieser Port ist bereits konfiguriert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Der Name f\u00fcr diese Velbus-Verbindung",
+ "port": "Verbindungs details"
+ },
+ "title": "Definieren des Velbus-Verbindungstyps"
+ }
+ },
+ "title": "Velbus-Schnittstelle"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/.translations/es.json b/homeassistant/components/velbus/.translations/es.json
new file mode 100644
index 00000000000000..1e1e8897c30155
--- /dev/null
+++ b/homeassistant/components/velbus/.translations/es.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "port_exists": "Este puerto ya est\u00e1 configurado"
+ },
+ "error": {
+ "connection_failed": "La conexi\u00f3n velbus fall\u00f3",
+ "port_exists": "Este puerto ya est\u00e1 configurado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "El nombre de esta conexi\u00f3n velbus",
+ "port": "Cadena de conexi\u00f3n"
+ },
+ "title": "Definir el tipo de conexi\u00f3n velbus"
+ }
+ },
+ "title": "Interfaz Velbus"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/.translations/fr.json b/homeassistant/components/velbus/.translations/fr.json
new file mode 100644
index 00000000000000..8d93adbf4a92dd
--- /dev/null
+++ b/homeassistant/components/velbus/.translations/fr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "port_exists": "Ce port est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "error": {
+ "connection_failed": "La connexion velbus a \u00e9chou\u00e9",
+ "port_exists": "Ce port est d\u00e9j\u00e0 configur\u00e9"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Le nom pour cette connexion velbus",
+ "port": "Cha\u00eene de connexion"
+ },
+ "title": "D\u00e9finir le type de connexion velbus"
+ }
+ },
+ "title": "Interface Velbus"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/.translations/it.json b/homeassistant/components/velbus/.translations/it.json
new file mode 100644
index 00000000000000..e4f1fbf9c6b7b3
--- /dev/null
+++ b/homeassistant/components/velbus/.translations/it.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "port_exists": "Questa porta \u00e8 gi\u00e0 configurata"
+ },
+ "error": {
+ "connection_failed": "La connessione Velbus non \u00e8 riuscita",
+ "port_exists": "Questa porta \u00e8 gi\u00e0 configurata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Il nome per questa connessione Velbus",
+ "port": "Stringa di connessione"
+ },
+ "title": "Definire il tipo di connessione Velbus"
+ }
+ },
+ "title": "Interfaccia Velbus"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/.translations/ko.json b/homeassistant/components/velbus/.translations/ko.json
new file mode 100644
index 00000000000000..6e218afc97c10a
--- /dev/null
+++ b/homeassistant/components/velbus/.translations/ko.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "port_exists": "\ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "error": {
+ "connection_failed": "Velbus \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
+ "port_exists": "\ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Velbus \uc5f0\uacb0 \uc774\ub984",
+ "port": "\uc5f0\uacb0 \ubb38\uc790\uc5f4"
+ },
+ "title": "Velbus \uc5f0\uacb0 \uc720\ud615 \uc815\uc758"
+ }
+ },
+ "title": "Velbus \uc778\ud130\ud398\uc774\uc2a4"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/.translations/lb.json b/homeassistant/components/velbus/.translations/lb.json
new file mode 100644
index 00000000000000..f38a74e5c1fd19
--- /dev/null
+++ b/homeassistant/components/velbus/.translations/lb.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "port_exists": "D\u00ebse Port ass scho konfigur\u00e9iert"
+ },
+ "error": {
+ "connection_failed": "Feeler bei der velbus Verbindung",
+ "port_exists": "D\u00ebse Port ass scho konfigur\u00e9iert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Numm fir d\u00ebs velbus Verbindung",
+ "port": "Verbindungs zeeche-folleg"
+ },
+ "title": "D\u00e9fin\u00e9iert den Typ vun der Velbus Verbindung"
+ }
+ },
+ "title": "Velbus Interface"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/.translations/no.json b/homeassistant/components/velbus/.translations/no.json
new file mode 100644
index 00000000000000..c6b16170877edf
--- /dev/null
+++ b/homeassistant/components/velbus/.translations/no.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "port_exists": "Denne porten er allerede konfigurert"
+ },
+ "error": {
+ "connection_failed": "Velbus-tilkoblingen mislyktes",
+ "port_exists": "Denne porten er allerede konfigurert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Navnet p\u00e5 denne velbus tilkoblingen",
+ "port": "Tilkoblingsstreng"
+ },
+ "title": "Definer tilkoblingstype for velbus"
+ }
+ },
+ "title": "Velbus-grensesnitt"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/.translations/sl.json b/homeassistant/components/velbus/.translations/sl.json
new file mode 100644
index 00000000000000..2fa1ccadcea616
--- /dev/null
+++ b/homeassistant/components/velbus/.translations/sl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "port_exists": "Ta vrata so \u017ee nastavljena"
+ },
+ "error": {
+ "connection_failed": "Povezava z velbusom ni uspela",
+ "port_exists": "Ta vrata so \u017ee nastavljena"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Ime za to velbus povezavo",
+ "port": "Povezovalni niz"
+ },
+ "title": "Dolo\u010dite vrsto povezave z velbusom"
+ }
+ },
+ "title": "Velbus vmesnik"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velbus/.translations/zh-Hans.json b/homeassistant/components/velbus/.translations/zh-Hans.json
new file mode 100644
index 00000000000000..7b2bc3b028b78d
--- /dev/null
+++ b/homeassistant/components/velbus/.translations/zh-Hans.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "error": {
+ "port_exists": "\u6b64\u7aef\u53e3\u5df2\u914d\u7f6e\u5b8c\u6210"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "\u8fd9\u4e2avelbus\u8fde\u63a5\u7684\u540d\u79f0"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py
index 4c21bb7fdefbf5..51f615e68aaf5a 100644
--- a/homeassistant/components/velux/__init__.py
+++ b/homeassistant/components/velux/__init__.py
@@ -1,11 +1,12 @@
"""Support for VELUX KLF 200 devices."""
import logging
-
import voluptuous as vol
+from pyvlx import PyVLX
+from pyvlx import PyVLXException
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
-from homeassistant.const import CONF_HOST, CONF_PASSWORD
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP
DOMAIN = "velux"
DATA_VELUX = "data_velux"
@@ -24,10 +25,9 @@
async def async_setup(hass, config):
"""Set up the velux component."""
- from pyvlx import PyVLXException
-
try:
- hass.data[DATA_VELUX] = VeluxModule(hass, config)
+ hass.data[DATA_VELUX] = VeluxModule(hass, config[DOMAIN])
+ hass.data[DATA_VELUX].setup()
await hass.data[DATA_VELUX].async_start()
except PyVLXException as ex:
@@ -44,15 +44,27 @@ async def async_setup(hass, config):
class VeluxModule:
"""Abstraction for velux component."""
- def __init__(self, hass, config):
+ def __init__(self, hass, domain_config):
"""Initialize for velux component."""
- from pyvlx import PyVLX
+ self.pyvlx = None
+ self._hass = hass
+ self._domain_config = domain_config
+
+ def setup(self):
+ """Velux component setup."""
+
+ async def on_hass_stop(event):
+ """Close connection when hass stops."""
+ _LOGGER.debug("Velux interface terminated")
+ await self.pyvlx.disconnect()
- host = config[DOMAIN].get(CONF_HOST)
- password = config[DOMAIN].get(CONF_PASSWORD)
+ self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
+ host = self._domain_config.get(CONF_HOST)
+ password = self._domain_config.get(CONF_PASSWORD)
self.pyvlx = PyVLX(host=host, password=password)
async def async_start(self):
"""Start velux component."""
+ _LOGGER.debug("Velux interface started")
await self.pyvlx.load_scenes()
await self.pyvlx.load_nodes()
diff --git a/homeassistant/components/vesync/.translations/de.json b/homeassistant/components/vesync/.translations/de.json
new file mode 100644
index 00000000000000..44b3ea86c5509d
--- /dev/null
+++ b/homeassistant/components/vesync/.translations/de.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Nur eine Vesync-Instanz ist zul\u00e4ssig"
+ },
+ "error": {
+ "invalid_login": "Ung\u00fcltiger Benutzername oder Kennwort"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwort",
+ "username": "E-Mail-Adresse"
+ },
+ "title": "Benutzername und Passwort eingeben"
+ }
+ },
+ "title": "VeSync"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vesync/.translations/es.json b/homeassistant/components/vesync/.translations/es.json
new file mode 100644
index 00000000000000..856dc77a52c48d
--- /dev/null
+++ b/homeassistant/components/vesync/.translations/es.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Solo se permite una instancia de Vesync"
+ },
+ "error": {
+ "invalid_login": "Nombre de usuario o contrase\u00f1a no v\u00e1lidos"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "username": "Direcci\u00f3n de correo electr\u00f3nico"
+ },
+ "title": "Introduzca el nombre de usuario y la contrase\u00f1a"
+ }
+ },
+ "title": "VeSync"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vesync/.translations/fr.json b/homeassistant/components/vesync/.translations/fr.json
new file mode 100644
index 00000000000000..4928ea4f0be508
--- /dev/null
+++ b/homeassistant/components/vesync/.translations/fr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Une seule instance de Vesync est autoris\u00e9e"
+ },
+ "error": {
+ "invalid_login": "Nom d'utilisateur ou mot de passe invalide"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Mot de passe",
+ "username": "Adresse e-mail"
+ },
+ "title": "Entrez vos identifiants"
+ }
+ },
+ "title": "VeSync"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vesync/.translations/it.json b/homeassistant/components/vesync/.translations/it.json
new file mode 100644
index 00000000000000..d3e53547559a7e
--- /dev/null
+++ b/homeassistant/components/vesync/.translations/it.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\u00c8 consentita una sola istanza di Vesync"
+ },
+ "error": {
+ "invalid_login": "Nome utente o password non validi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Password",
+ "username": "Indirizzo E-mail"
+ },
+ "title": "Immettere nome utente e password"
+ }
+ },
+ "title": "VeSync"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vesync/.translations/ko.json b/homeassistant/components/vesync/.translations/ko.json
new file mode 100644
index 00000000000000..ca43b90acc9632
--- /dev/null
+++ b/homeassistant/components/vesync/.translations/ko.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\ud558\ub098\uc758 Vesync \uc778\uc2a4\ud134\uc2a4\ub9cc \ud5c8\uc6a9\ub429\ub2c8\ub2e4"
+ },
+ "error": {
+ "invalid_login": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\ube44\ubc00\ubc88\ud638",
+ "username": "\uc774\uba54\uc77c \uc8fc\uc18c"
+ },
+ "title": "\uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694"
+ }
+ },
+ "title": "VeSync"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vesync/.translations/lb.json b/homeassistant/components/vesync/.translations/lb.json
new file mode 100644
index 00000000000000..cfccd8b1dbb49a
--- /dev/null
+++ b/homeassistant/components/vesync/.translations/lb.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "N\u00ebmmen eng eenzeg Instanz vu Vesync ass erlaabt."
+ },
+ "error": {
+ "invalid_login": "Ong\u00ebltege Benotzernumm oder Passwuert"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passwuert",
+ "username": "E-Mail Adresse"
+ },
+ "title": "Benotznumm a Passwuert aginn"
+ }
+ },
+ "title": "VeSync"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vesync/.translations/no.json b/homeassistant/components/vesync/.translations/no.json
new file mode 100644
index 00000000000000..be5f27b7a0f0e0
--- /dev/null
+++ b/homeassistant/components/vesync/.translations/no.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Bare en Vesync-forekomst er tillatt"
+ },
+ "error": {
+ "invalid_login": "Ugyldig brukernavn eller passord"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Passord",
+ "username": "E-postadresse"
+ },
+ "title": "Skriv inn brukernavn og passord"
+ }
+ },
+ "title": "VeSync"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vesync/.translations/sl.json b/homeassistant/components/vesync/.translations/sl.json
new file mode 100644
index 00000000000000..636237dcfc1946
--- /dev/null
+++ b/homeassistant/components/vesync/.translations/sl.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Dovoljen je samo ena instanca Vesync"
+ },
+ "error": {
+ "invalid_login": "Neveljavno uporabni\u0161ko ime ali geslo"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Geslo",
+ "username": "E-po\u0161tni naslov"
+ },
+ "title": "Vnesite uporabni\u0161ko Ime in Geslo"
+ }
+ },
+ "title": "VeSync"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vesync/.translations/zh-Hans.json b/homeassistant/components/vesync/.translations/zh-Hans.json
new file mode 100644
index 00000000000000..caa00f36c89435
--- /dev/null
+++ b/homeassistant/components/vesync/.translations/zh-Hans.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\u53ea\u5141\u8bb8\u4e00\u4e2aVesync\u5b9e\u4f8b"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py
new file mode 100644
index 00000000000000..9fec04f23283f3
--- /dev/null
+++ b/homeassistant/components/vicare/__init__.py
@@ -0,0 +1,58 @@
+"""The ViCare integration."""
+import logging
+
+import voluptuous as vol
+from PyViCare.PyViCareDevice import Device
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME
+from homeassistant.helpers import discovery
+
+_LOGGER = logging.getLogger(__name__)
+
+VICARE_PLATFORMS = ["climate", "water_heater"]
+
+DOMAIN = "vicare"
+VICARE_API = "api"
+VICARE_NAME = "name"
+
+CONF_CIRCUIT = "circuit"
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ DOMAIN: vol.Schema(
+ {
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_CIRCUIT): int,
+ vol.Optional(CONF_NAME, default="ViCare"): cv.string,
+ }
+ )
+ },
+ extra=vol.ALLOW_EXTRA,
+)
+
+
+def setup(hass, config):
+ """Create the ViCare component."""
+ conf = config[DOMAIN]
+ params = {"token_file": "/tmp/vicare_token.save"}
+ if conf.get(CONF_CIRCUIT) is not None:
+ params["circuit"] = conf[CONF_CIRCUIT]
+
+ try:
+ vicare_api = Device(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params)
+ except AttributeError:
+ _LOGGER.error(
+ "Failed to create PyViCare API client. Please check your credentials."
+ )
+ return False
+
+ hass.data[DOMAIN] = {}
+ hass.data[DOMAIN][VICARE_API] = vicare_api
+ hass.data[DOMAIN][VICARE_NAME] = conf[CONF_NAME]
+
+ for platform in VICARE_PLATFORMS:
+ discovery.load_platform(hass, platform, DOMAIN, {}, config)
+
+ return True
diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py
new file mode 100644
index 00000000000000..7010f943707303
--- /dev/null
+++ b/homeassistant/components/vicare/climate.py
@@ -0,0 +1,235 @@
+"""Viessmann ViCare climate device."""
+import logging
+
+from homeassistant.components.climate import ClimateDevice
+from homeassistant.components.climate.const import (
+ SUPPORT_PRESET_MODE,
+ SUPPORT_TARGET_TEMPERATURE,
+ PRESET_ECO,
+ PRESET_COMFORT,
+ HVAC_MODE_OFF,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_AUTO,
+)
+from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE
+
+from . import DOMAIN as VICARE_DOMAIN
+from . import VICARE_API
+from . import VICARE_NAME
+
+_LOGGER = logging.getLogger(__name__)
+
+VICARE_MODE_DHW = "dhw"
+VICARE_MODE_DHWANDHEATING = "dhwAndHeating"
+VICARE_MODE_FORCEDREDUCED = "forcedReduced"
+VICARE_MODE_FORCEDNORMAL = "forcedNormal"
+VICARE_MODE_OFF = "standby"
+
+VICARE_PROGRAM_ACTIVE = "active"
+VICARE_PROGRAM_COMFORT = "comfort"
+VICARE_PROGRAM_ECO = "eco"
+VICARE_PROGRAM_EXTERNAL = "external"
+VICARE_PROGRAM_HOLIDAY = "holiday"
+VICARE_PROGRAM_NORMAL = "normal"
+VICARE_PROGRAM_REDUCED = "reduced"
+VICARE_PROGRAM_STANDBY = "standby"
+
+VICARE_HOLD_MODE_AWAY = "away"
+VICARE_HOLD_MODE_HOME = "home"
+VICARE_HOLD_MODE_OFF = "off"
+
+VICARE_TEMP_HEATING_MIN = 3
+VICARE_TEMP_HEATING_MAX = 37
+
+SUPPORT_FLAGS_HEATING = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
+
+VICARE_TO_HA_HVAC_HEATING = {
+ VICARE_MODE_DHW: HVAC_MODE_OFF,
+ VICARE_MODE_DHWANDHEATING: HVAC_MODE_AUTO,
+ VICARE_MODE_FORCEDREDUCED: HVAC_MODE_OFF,
+ VICARE_MODE_FORCEDNORMAL: HVAC_MODE_HEAT,
+ VICARE_MODE_OFF: HVAC_MODE_OFF,
+}
+
+HA_TO_VICARE_HVAC_HEATING = {
+ HVAC_MODE_HEAT: VICARE_MODE_FORCEDNORMAL,
+ HVAC_MODE_OFF: VICARE_MODE_FORCEDREDUCED,
+ HVAC_MODE_AUTO: VICARE_MODE_DHWANDHEATING,
+}
+
+VICARE_TO_HA_PRESET_HEATING = {
+ VICARE_PROGRAM_COMFORT: PRESET_COMFORT,
+ VICARE_PROGRAM_ECO: PRESET_ECO,
+}
+
+HA_TO_VICARE_PRESET_HEATING = {
+ PRESET_COMFORT: VICARE_PROGRAM_COMFORT,
+ PRESET_ECO: VICARE_PROGRAM_ECO,
+}
+
+PYVICARE_ERROR = "error"
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Create the ViCare climate devices."""
+ if discovery_info is None:
+ return
+ vicare_api = hass.data[VICARE_DOMAIN][VICARE_API]
+ add_entities(
+ [ViCareClimate(f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", vicare_api)]
+ )
+
+
+class ViCareClimate(ClimateDevice):
+ """Representation of the ViCare heating climate device."""
+
+ def __init__(self, name, api):
+ """Initialize the climate device."""
+ self._name = name
+ self._state = None
+ self._api = api
+ self._attributes = {}
+ self._target_temperature = None
+ self._current_mode = None
+ self._current_temperature = None
+ self._current_program = None
+
+ def update(self):
+ """Let HA know there has been an update from the ViCare API."""
+ _room_temperature = self._api.getRoomTemperature()
+ _supply_temperature = self._api.getSupplyTemperature()
+ if _room_temperature is not None and _room_temperature != PYVICARE_ERROR:
+ self._current_temperature = _room_temperature
+ elif _supply_temperature != PYVICARE_ERROR:
+ self._current_temperature = _supply_temperature
+ else:
+ self._current_temperature = None
+ self._current_program = self._api.getActiveProgram()
+
+ # The getCurrentDesiredTemperature call can yield 'error' (str) when the system is in standby
+ desired_temperature = self._api.getCurrentDesiredTemperature()
+ if desired_temperature == PYVICARE_ERROR:
+ desired_temperature = None
+
+ self._target_temperature = desired_temperature
+
+ self._current_mode = self._api.getActiveMode()
+
+ # Update the device attributes
+ self._attributes = {}
+ self._attributes["room_temperature"] = _room_temperature
+ self._attributes["supply_temperature"] = _supply_temperature
+ self._attributes["outside_temperature"] = self._api.getOutsideTemperature()
+ self._attributes["active_vicare_program"] = self._current_program
+ self._attributes["active_vicare_mode"] = self._current_mode
+ self._attributes["heating_curve_slope"] = self._api.getHeatingCurveSlope()
+ self._attributes["heating_curve_shift"] = self._api.getHeatingCurveShift()
+ self._attributes[
+ "month_since_last_service"
+ ] = self._api.getMonthSinceLastService()
+ self._attributes["date_last_service"] = self._api.getLastServiceDate()
+ self._attributes["error_history"] = self._api.getErrorHistory()
+ self._attributes["active_error"] = self._api.getActiveError()
+ self._attributes[
+ "circulationpump_active"
+ ] = self._api.getCirculationPumpActive()
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ return SUPPORT_FLAGS_HEATING
+
+ @property
+ def name(self):
+ """Return the name of the climate device."""
+ return self._name
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement."""
+ return TEMP_CELSIUS
+
+ @property
+ def current_temperature(self):
+ """Return the current temperature."""
+ return self._current_temperature
+
+ @property
+ def target_temperature(self):
+ """Return the temperature we try to reach."""
+ return self._target_temperature
+
+ @property
+ def hvac_mode(self):
+ """Return current hvac mode."""
+ return VICARE_TO_HA_HVAC_HEATING.get(self._current_mode)
+
+ def set_hvac_mode(self, hvac_mode):
+ """Set a new hvac mode on the ViCare API."""
+ vicare_mode = HA_TO_VICARE_HVAC_HEATING.get(hvac_mode)
+ if vicare_mode is None:
+ _LOGGER.error(
+ "Cannot set invalid vicare mode: %s / %s", hvac_mode, vicare_mode
+ )
+ return
+
+ _LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode)
+ self._api.setMode(vicare_mode)
+
+ @property
+ def hvac_modes(self):
+ """Return the list of available hvac modes."""
+ return list(HA_TO_VICARE_HVAC_HEATING)
+
+ @property
+ def min_temp(self):
+ """Return the minimum temperature."""
+ return VICARE_TEMP_HEATING_MIN
+
+ @property
+ def max_temp(self):
+ """Return the maximum temperature."""
+ return VICARE_TEMP_HEATING_MAX
+
+ @property
+ def precision(self):
+ """Return the precision of the system."""
+ return PRECISION_WHOLE
+
+ def set_temperature(self, **kwargs):
+ """Set new target temperatures."""
+ temp = kwargs.get(ATTR_TEMPERATURE)
+ if temp is not None:
+ self._api.setProgramTemperature(
+ self._current_program, self._target_temperature
+ )
+
+ @property
+ def preset_mode(self):
+ """Return the current preset mode, e.g., home, away, temp."""
+ return VICARE_TO_HA_PRESET_HEATING.get(self._current_program)
+
+ @property
+ def preset_modes(self):
+ """Return the available preset mode."""
+ return list(VICARE_TO_HA_PRESET_HEATING)
+
+ def set_preset_mode(self, preset_mode):
+ """Set new preset mode and deactivate any existing programs."""
+ vicare_program = HA_TO_VICARE_PRESET_HEATING.get(preset_mode)
+ if vicare_program is None:
+ _LOGGER.error(
+ "Cannot set invalid vicare program: %s / %s",
+ preset_mode,
+ vicare_program,
+ )
+ return
+
+ _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program)
+ self._api.deactivateProgram(self._current_program)
+ self._api.activateProgram(vicare_program)
+
+ @property
+ def device_state_attributes(self):
+ """Show Device Attributes."""
+ return self._attributes
diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json
new file mode 100644
index 00000000000000..e5f55b20ddaf8d
--- /dev/null
+++ b/homeassistant/components/vicare/manifest.json
@@ -0,0 +1,9 @@
+{
+ "domain": "vicare",
+ "name": "Viessmann ViCare",
+ "documentation": "https://www.home-assistant.io/components/vicare",
+ "dependencies": [],
+ "codeowners": ["@oischinger"],
+ "requirements": ["PyViCare==0.1.1"]
+}
+
diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py
new file mode 100644
index 00000000000000..71c0f6c2aefe7a
--- /dev/null
+++ b/homeassistant/components/vicare/water_heater.py
@@ -0,0 +1,132 @@
+"""Viessmann ViCare water_heater device."""
+import logging
+
+from homeassistant.components.water_heater import (
+ SUPPORT_TARGET_TEMPERATURE,
+ WaterHeaterDevice,
+)
+from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE
+
+from . import DOMAIN as VICARE_DOMAIN
+from . import VICARE_API
+from . import VICARE_NAME
+
+_LOGGER = logging.getLogger(__name__)
+
+VICARE_MODE_DHW = "dhw"
+VICARE_MODE_DHWANDHEATING = "dhwAndHeating"
+VICARE_MODE_FORCEDREDUCED = "forcedReduced"
+VICARE_MODE_FORCEDNORMAL = "forcedNormal"
+VICARE_MODE_OFF = "standby"
+
+VICARE_TEMP_WATER_MIN = 10
+VICARE_TEMP_WATER_MAX = 60
+
+OPERATION_MODE_ON = "on"
+OPERATION_MODE_OFF = "off"
+
+SUPPORT_FLAGS_HEATER = SUPPORT_TARGET_TEMPERATURE
+
+VICARE_TO_HA_HVAC_DHW = {
+ VICARE_MODE_DHW: OPERATION_MODE_ON,
+ VICARE_MODE_DHWANDHEATING: OPERATION_MODE_ON,
+ VICARE_MODE_FORCEDREDUCED: OPERATION_MODE_OFF,
+ VICARE_MODE_FORCEDNORMAL: OPERATION_MODE_ON,
+ VICARE_MODE_OFF: OPERATION_MODE_OFF,
+}
+
+HA_TO_VICARE_HVAC_DHW = {
+ OPERATION_MODE_OFF: VICARE_MODE_OFF,
+ OPERATION_MODE_ON: VICARE_MODE_DHW,
+}
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Create the ViCare water_heater devices."""
+ if discovery_info is None:
+ return
+ vicare_api = hass.data[VICARE_DOMAIN][VICARE_API]
+ add_entities(
+ [ViCareWater(f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Water", vicare_api)]
+ )
+
+
+class ViCareWater(WaterHeaterDevice):
+ """Representation of the ViCare domestic hot water device."""
+
+ def __init__(self, name, api):
+ """Initialize the DHW water_heater device."""
+ self._name = name
+ self._state = None
+ self._api = api
+ self._target_temperature = None
+ self._current_temperature = None
+ self._current_mode = None
+
+ def update(self):
+ """Let HA know there has been an update from the ViCare API."""
+ current_temperature = self._api.getDomesticHotWaterStorageTemperature()
+ if current_temperature is not None and current_temperature != "error":
+ self._current_temperature = current_temperature
+ else:
+ self._current_temperature = None
+
+ self._target_temperature = self._api.getDomesticHotWaterConfiguredTemperature()
+
+ self._current_mode = self._api.getActiveMode()
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ return SUPPORT_FLAGS_HEATER
+
+ @property
+ def name(self):
+ """Return the name of the water_heater device."""
+ return self._name
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement."""
+ return TEMP_CELSIUS
+
+ @property
+ def current_temperature(self):
+ """Return the current temperature."""
+ return self._current_temperature
+
+ @property
+ def target_temperature(self):
+ """Return the temperature we try to reach."""
+ return self._target_temperature
+
+ def set_temperature(self, **kwargs):
+ """Set new target temperatures."""
+ temp = kwargs.get(ATTR_TEMPERATURE)
+ if temp is not None:
+ self._api.setDomesticHotWaterTemperature(self._target_temperature)
+
+ @property
+ def min_temp(self):
+ """Return the minimum temperature."""
+ return VICARE_TEMP_WATER_MIN
+
+ @property
+ def max_temp(self):
+ """Return the maximum temperature."""
+ return VICARE_TEMP_WATER_MAX
+
+ @property
+ def precision(self):
+ """Return the precision of the system."""
+ return PRECISION_WHOLE
+
+ @property
+ def current_operation(self):
+ """Return current operation ie. heat, cool, idle."""
+ return VICARE_TO_HA_HVAC_DHW.get(self._current_mode)
+
+ @property
+ def operation_list(self):
+ """Return the list of available operation modes."""
+ return list(HA_TO_VICARE_HVAC_DHW)
diff --git a/homeassistant/components/vivotek/__init__.py b/homeassistant/components/vivotek/__init__.py
new file mode 100644
index 00000000000000..b5220b12a9b68a
--- /dev/null
+++ b/homeassistant/components/vivotek/__init__.py
@@ -0,0 +1 @@
+"""The Vivotek camera component."""
diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py
new file mode 100644
index 00000000000000..012c1e1df34755
--- /dev/null
+++ b/homeassistant/components/vivotek/camera.py
@@ -0,0 +1,125 @@
+"""Support for Vivotek IP Cameras."""
+
+import logging
+
+import voluptuous as vol
+from libpyvivotek import VivotekCamera
+
+from homeassistant.const import (
+ CONF_IP_ADDRESS,
+ CONF_NAME,
+ CONF_PASSWORD,
+ CONF_SSL,
+ CONF_USERNAME,
+ CONF_VERIFY_SSL,
+)
+from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
+from homeassistant.helpers import config_validation as cv
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_FRAMERATE = "framerate"
+
+DEFAULT_CAMERA_BRAND = "Vivotek"
+DEFAULT_NAME = "Vivotek Camera"
+DEFAULT_EVENT_0_KEY = "event_i0_enable"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_IP_ADDRESS): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Optional(CONF_SSL, default=False): cv.boolean,
+ vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
+ vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int,
+ }
+)
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up a Vivotek IP Camera."""
+ args = dict(
+ config=config,
+ cam=VivotekCamera(
+ host=config[CONF_IP_ADDRESS],
+ port=(443 if config[CONF_SSL] else 80),
+ verify_ssl=config[CONF_VERIFY_SSL],
+ usr=config[CONF_USERNAME],
+ pwd=config[CONF_PASSWORD],
+ ),
+ stream_source=(
+ "rtsp://%s:%s@%s:554/live.sdp",
+ config[CONF_USERNAME],
+ config[CONF_PASSWORD],
+ config[CONF_IP_ADDRESS],
+ ),
+ )
+ add_entities([VivotekCam(**args)], True)
+
+
+class VivotekCam(Camera):
+ """A Vivotek IP camera."""
+
+ def __init__(self, config, cam, stream_source):
+ """Initialize a Vivotek camera."""
+ super().__init__()
+
+ self._cam = cam
+ self._frame_interval = 1 / config[CONF_FRAMERATE]
+ self._motion_detection_enabled = False
+ self._model_name = None
+ self._name = config[CONF_NAME]
+ self._stream_source = stream_source
+
+ @property
+ def supported_features(self):
+ """Return supported features for this camera."""
+ return SUPPORT_STREAM
+
+ @property
+ def frame_interval(self):
+ """Return the interval between frames of the mjpeg stream."""
+ return self._frame_interval
+
+ def camera_image(self):
+ """Return bytes of camera image."""
+ return self._cam.snapshot()
+
+ @property
+ def name(self):
+ """Return the name of this device."""
+ return self._name
+
+ async def stream_source(self):
+ """Return the source of the stream."""
+ return self._stream_source
+
+ @property
+ def motion_detection_enabled(self):
+ """Return the camera motion detection status."""
+ return self._motion_detection_enabled
+
+ def disable_motion_detection(self):
+ """Disable motion detection in camera."""
+ response = self._cam.set_param(DEFAULT_EVENT_0_KEY, 0)
+ self._motion_detection_enabled = int(response) == 1
+
+ def enable_motion_detection(self):
+ """Enable motion detection in camera."""
+ response = self._cam.set_param(DEFAULT_EVENT_0_KEY, 1)
+ self._motion_detection_enabled = int(response) == 1
+
+ @property
+ def brand(self):
+ """Return the camera brand."""
+ return DEFAULT_CAMERA_BRAND
+
+ @property
+ def model(self):
+ """Return the camera model."""
+ return self._model_name
+
+ def update(self):
+ """Update entity status."""
+ self._model_name = self._cam.model_name
diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json
new file mode 100644
index 00000000000000..cce2307bc4b554
--- /dev/null
+++ b/homeassistant/components/vivotek/manifest.json
@@ -0,0 +1,10 @@
+{
+ "domain": "vivotek",
+ "name": "Vivotek",
+ "documentation": "https://www.home-assistant.io/components/vivotek",
+ "requirements": [
+ "libpyvivotek==0.2.2"
+ ],
+ "dependencies": [],
+ "codeowners": []
+}
diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py
index 8bd1952a6507d0..7c13488c3f573e 100644
--- a/homeassistant/components/volumio/media_player.py
+++ b/homeassistant/components/volumio/media_player.py
@@ -306,7 +306,7 @@ def async_mute_volume(self, mute):
def async_set_shuffle(self, shuffle):
"""Enable/disable shuffle mode."""
return self.send_volumio_msg(
- "commands", params={"cmd": "random", "value": str(shuffle)}
+ "commands", params={"cmd": "random", "value": str(shuffle).lower()}
)
def async_select_source(self, source):
diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py
index 0b7228fb568bcf..a30d08f31f3d9e 100644
--- a/homeassistant/components/watson_tts/tts.py
+++ b/homeassistant/components/watson_tts/tts.py
@@ -99,6 +99,7 @@ def get_engine(hass, config):
supported_languages = list({s[:5] for s in SUPPORTED_VOICES})
default_voice = config[CONF_VOICE]
output_format = config[CONF_OUTPUT_FORMAT]
+ service.set_default_headers({"x-watson-learning-opt-out": "true"})
return WatsonTTSProvider(service, supported_languages, default_voice, output_format)
diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py
index 0b5696709fd212..1da70bc60ec255 100644
--- a/homeassistant/components/webostv/media_player.py
+++ b/homeassistant/components/webostv/media_player.py
@@ -3,7 +3,7 @@
from datetime import timedelta
import logging
from urllib.parse import urlparse
-from typing import Dict # noqa: F401 pylint: disable=unused-import
+from typing import Dict
import voluptuous as vol
@@ -36,7 +36,7 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.script import Script
-_CONFIGURING = {} # type: Dict[str, str]
+_CONFIGURING: Dict[str, str] = {}
_LOGGER = logging.getLogger(__name__)
CONF_SOURCES = "sources"
diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py
index a1614eb1ce30c2..21c911a66ce944 100644
--- a/homeassistant/components/wemo/config_flow.py
+++ b/homeassistant/components/wemo/config_flow.py
@@ -1,14 +1,16 @@
"""Config flow for Wemo."""
+
+import pywemo
+
from homeassistant.helpers import config_entry_flow
from homeassistant import config_entries
+
from . import DOMAIN
async def _async_has_devices(hass):
"""Return if there are devices that can be discovered."""
- import pywemo
-
- return bool(pywemo.discover_devices())
+ return bool(await hass.async_add_executor_job(pywemo.discover_devices))
config_entry_flow.register_discovery_flow(
diff --git a/homeassistant/components/whois/manifest.json b/homeassistant/components/whois/manifest.json
index dec3e78a50362b..6040c8655b9f25 100644
--- a/homeassistant/components/whois/manifest.json
+++ b/homeassistant/components/whois/manifest.json
@@ -3,7 +3,7 @@
"name": "Whois",
"documentation": "https://www.home-assistant.io/components/whois",
"requirements": [
- "python-whois==0.7.1"
+ "python-whois==0.7.2"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py
index 313a6337a11b5b..09cf40f193fe87 100644
--- a/homeassistant/components/whois/sensor.py
+++ b/homeassistant/components/whois/sensor.py
@@ -3,6 +3,7 @@
import logging
import voluptuous as vol
+import whois
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
@@ -32,8 +33,6 @@
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the WHOIS sensor."""
- import whois
-
domain = config.get(CONF_DOMAIN)
name = config.get(CONF_NAME)
@@ -41,7 +40,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if "expiration_date" in whois.whois(domain):
add_entities([WhoisSensor(name, domain)], True)
else:
- _LOGGER.error("WHOIS lookup for %s didn't contain expiration_date", domain)
+ _LOGGER.error(
+ "WHOIS lookup for %s didn't contain an expiration date", domain
+ )
return
except whois.BaseException as ex:
_LOGGER.error("Exception %s occurred during WHOIS lookup for %s", ex, domain)
@@ -53,8 +54,6 @@ class WhoisSensor(Entity):
def __init__(self, name, domain):
"""Initialize the sensor."""
- import whois
-
self.whois = whois.whois
self._name = name
@@ -95,8 +94,6 @@ def _empty_state_and_attributes(self):
def update(self):
"""Get the current WHOIS data for the domain."""
- import whois
-
try:
response = self.whois(self._domain)
except whois.BaseException as ex:
diff --git a/homeassistant/components/withings/.translations/ca.json b/homeassistant/components/withings/.translations/ca.json
new file mode 100644
index 00000000000000..2f2fdbe9b3f742
--- /dev/null
+++ b/homeassistant/components/withings/.translations/ca.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "no_flows": "Necessites configurar Withings abans de poder autenticar't-hi. Llegeix la documentaci\u00f3."
+ },
+ "create_entry": {
+ "default": "Autenticaci\u00f3 exitosa amb Withings per al perfil seleccionat."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "Perfil"
+ },
+ "description": "Selecciona un perfil d'usuari amb el qual vols que Home Assistant s'uneixi amb un perfil de Withings. A la p\u00e0gina de Withings, assegura't de seleccionar el mateix usuari o, les dades no seran les correctes.",
+ "title": "Perfil d'usuari."
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/da.json b/homeassistant/components/withings/.translations/da.json
new file mode 100644
index 00000000000000..d2dddbbd204bfb
--- /dev/null
+++ b/homeassistant/components/withings/.translations/da.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "create_entry": {
+ "default": "Godkendt med Withings for den valgte profil."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "Profil"
+ },
+ "description": "V\u00e6lg en brugerprofil, som du vil have Home Assistant til at tilknytte med en Withings-profil. P\u00e5 siden Withings skal du s\u00f8rge for at v\u00e6lge den samme bruger eller data vil ikke blive m\u00e6rket korrekt.",
+ "title": "Brugerprofil."
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json
new file mode 100644
index 00000000000000..15b6f4e3b01f59
--- /dev/null
+++ b/homeassistant/components/withings/.translations/de.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "create_entry": {
+ "default": "Erfolgreiche Authentifizierung mit Withings f\u00fcr das ausgew\u00e4hlte Profil."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "Profil"
+ },
+ "description": "W\u00e4hlen Sie ein Benutzerprofil aus, dem Home Assistant ein Withings-Profil zuordnen soll. Stellen Sie sicher, dass Sie auf der Withings-Seite denselben Benutzer ausw\u00e4hlen, da sonst die Daten nicht korrekt gekennzeichnet werden.",
+ "title": "Benutzerprofil."
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/en.json b/homeassistant/components/withings/.translations/en.json
index 2b906dd80030fb..16ce491e776d02 100644
--- a/homeassistant/components/withings/.translations/en.json
+++ b/homeassistant/components/withings/.translations/en.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "no_flows": "You need to configure Withings before being able to authenticate with it. Please read the documentation."
+ },
"create_entry": {
"default": "Successfully authenticated with Withings for the selected profile."
},
diff --git a/homeassistant/components/withings/.translations/es.json b/homeassistant/components/withings/.translations/es.json
new file mode 100644
index 00000000000000..fac325a7097645
--- /dev/null
+++ b/homeassistant/components/withings/.translations/es.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "create_entry": {
+ "default": "Autenticado correctamente con Withings para el perfil seleccionado."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "Perfil"
+ },
+ "description": "Seleccione un perfil de usuario para el cual desea que Home Assistant se conecte con el perfil de Withings. En la p\u00e1gina de Withings, aseg\u00farese de seleccionar el mismo usuario o los datos no se identificar\u00e1n correctamente.",
+ "title": "Perfil de usuario."
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json
new file mode 100644
index 00000000000000..ad715d54eb1df5
--- /dev/null
+++ b/homeassistant/components/withings/.translations/fr.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "no_flows": "Vous devez configurer Withings avant de pouvoir vous authentifier avec celui-ci. Veuillez lire la documentation."
+ },
+ "create_entry": {
+ "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "Profil"
+ },
+ "description": "S\u00e9lectionnez l'utilisateur que vous souhaitez associer \u00e0 Withings. Sur la page withings, veillez \u00e0 s\u00e9lectionner le m\u00eame utilisateur, sinon les donn\u00e9es ne seront pas \u00e9tiquet\u00e9es correctement.",
+ "title": "Profil utilisateur"
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/it.json b/homeassistant/components/withings/.translations/it.json
new file mode 100644
index 00000000000000..51276869ec605c
--- /dev/null
+++ b/homeassistant/components/withings/.translations/it.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "no_flows": "\u00c8 necessario configurare Withings prima di potersi autenticare con esso. Si prega di leggere la documentazione."
+ },
+ "create_entry": {
+ "default": "Autenticazione completata con Withings per il profilo selezionato."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "Profilo"
+ },
+ "description": "Seleziona un profilo utente a cui desideri associare Home Assistant con un profilo Withings. Nella pagina Withings, assicurati di selezionare lo stesso utente o i dati non saranno etichettati correttamente.",
+ "title": "Profilo utente."
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/ko.json b/homeassistant/components/withings/.translations/ko.json
new file mode 100644
index 00000000000000..617964e0596aba
--- /dev/null
+++ b/homeassistant/components/withings/.translations/ko.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "no_flows": "Withings \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Withings \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/withings/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694."
+ },
+ "create_entry": {
+ "default": "\uc120\ud0dd\ud55c \ud504\ub85c\ud544\ub85c Withings \uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "\ud504\ub85c\ud544"
+ },
+ "description": "Home Assistant \uac00 Withings \ud504\ub85c\ud544\uacfc \ub9f5\ud551\ud560 \uc0ac\uc6a9\uc790 \ud504\ub85c\ud544\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. Withings \ud398\uc774\uc9c0\uc5d0\uc11c \ub3d9\uc77c\ud55c \uc0ac\uc6a9\uc790\ub97c \uc120\ud0dd\ud574\uc57c\ud569\ub2c8\ub2e4. \uadf8\ub807\uc9c0 \uc54a\uc73c\uba74 \ub370\uc774\ud130\uc5d0 \uc62c\ubc14\ub978 \ub808\uc774\ube14\uc774 \uc9c0\uc815\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
+ "title": "\uc0ac\uc6a9\uc790 \ud504\ub85c\ud544."
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/lb.json b/homeassistant/components/withings/.translations/lb.json
new file mode 100644
index 00000000000000..5ca969f039102b
--- /dev/null
+++ b/homeassistant/components/withings/.translations/lb.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "no_flows": "Dir musst Withingss konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen. Liest w.e.g. d'Instruktioune."
+ },
+ "create_entry": {
+ "default": "Erfollegr\u00e4ich mam ausgewielte Profile mat Withings authentifiz\u00e9iert."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "Profil"
+ },
+ "description": "Wielt ee Benotzer Profile aus dee mam Withings Profile soll verbonne ginn. Stellt s\u00e9cher dass dir op der Withings S\u00e4it deeselwechte Benotzer auswielt, soss ginn d'Donn\u00e9e net richteg ugewisen.",
+ "title": "Benotzer Profil."
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json
new file mode 100644
index 00000000000000..3776621bec208e
--- /dev/null
+++ b/homeassistant/components/withings/.translations/nl.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "no_flows": "U moet Withings configureren voordat u zich ermee kunt verifi\u00ebren. [Gelieve de documentatie te lezen]"
+ },
+ "create_entry": {
+ "default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "Profiel"
+ },
+ "description": "Selecteer een gebruikersprofiel waaraan u Home Assistant wilt toewijzen met een Withings-profiel. Zorg ervoor dat u op de pagina Withings dezelfde gebruiker selecteert, anders worden de gegevens niet correct gelabeld.",
+ "title": "Gebruikersprofiel."
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/no.json b/homeassistant/components/withings/.translations/no.json
new file mode 100644
index 00000000000000..d32c9640fd7636
--- /dev/null
+++ b/homeassistant/components/withings/.translations/no.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "no_flows": "Du m\u00e5 konfigurere Withings f\u00f8r du kan godkjenne med den. Vennligst les dokumentasjonen."
+ },
+ "create_entry": {
+ "default": "Vellykket autentisering for Withings og den valgte profilen."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "Profil"
+ },
+ "description": "Velg en brukerprofil som du vil at Home Assistant skal kartlegge med en Withings-profil. P\u00e5 Withings-siden m\u00e5 du passe p\u00e5 at du velger samme bruker ellers vil ikke dataen bli merket riktig.",
+ "title": "Brukerprofil."
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/pl.json b/homeassistant/components/withings/.translations/pl.json
new file mode 100644
index 00000000000000..3c345a1a788bd6
--- /dev/null
+++ b/homeassistant/components/withings/.translations/pl.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "no_flows": "Musisz skonfigurowa\u0107 Withings, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Przeczytaj prosz\u0119 dokumentacj\u0119."
+ },
+ "create_entry": {
+ "default": "Pomy\u015blnie uwierzytelniono z Withings dla wybranego profilu"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "Profil"
+ },
+ "description": "Wybierz profil u\u017cytkownika Withings, na kt\u00f3ry chcesz po\u0142\u0105czy\u0107 z Home Assistant'em. Na stronie Withings wybierz ten sam profil u\u017cytkownika by dane by\u0142y poprawnie oznaczone.",
+ "title": "Profil u\u017cytkownika"
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/ru.json b/homeassistant/components/withings/.translations/ru.json
new file mode 100644
index 00000000000000..c6c621fbdf33ee
--- /dev/null
+++ b/homeassistant/components/withings/.translations/ru.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Withings \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438."
+ },
+ "create_entry": {
+ "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c"
+ },
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f. \u041d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 Withings \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u043e\u0433\u043e \u0436\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u0438\u043d\u0430\u0447\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u044b \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e.",
+ "title": "Withings"
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json
new file mode 100644
index 00000000000000..71934516ea7f1b
--- /dev/null
+++ b/homeassistant/components/withings/.translations/sl.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "no_flows": "withings morate prvo konfigurirati, preden ga boste lahko uporabili za overitev. Prosimo, preberite dokumentacijo."
+ },
+ "create_entry": {
+ "default": "Uspe\u0161no overjen z Withings za izbrani profil."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "Profil"
+ },
+ "description": "Izberite uporabni\u0161ki profil, za katerega \u017eelite, da se Home Assistant prika\u017ee s profilom Withings. Na Withings strani ne pozabite izbrati istega uporabnika sicer podatki ne bodo pravilno ozna\u010deni.",
+ "title": "Uporabni\u0161ki profil."
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/zh-Hans.json b/homeassistant/components/withings/.translations/zh-Hans.json
new file mode 100644
index 00000000000000..c7485b09248ccd
--- /dev/null
+++ b/homeassistant/components/withings/.translations/zh-Hans.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "description": "\u8bf7\u9009\u62e9\u4f60\u60f3\u8981Home Assistant\u548cWithings\u5bf9\u5e94\u7684\u7528\u6237\u914d\u7f6e\u6587\u4ef6\u3002\u5728Withings\u9875\u9762\u4e0a\uff0c\u8bf7\u52a1\u5fc5\u9009\u62e9\u76f8\u540c\u7684\u7528\u6237\uff0c\u5426\u5219\u6570\u636e\u5c06\u65e0\u6cd5\u6b63\u786e\u6807\u8bb0\u3002",
+ "title": "\u7528\u6237\u8d44\u6599"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/.translations/zh-Hant.json b/homeassistant/components/withings/.translations/zh-Hant.json
new file mode 100644
index 00000000000000..9e408eb0d5c19e
--- /dev/null
+++ b/homeassistant/components/withings/.translations/zh-Hant.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Withings \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002\u8acb\u53c3\u95b1\u6587\u4ef6\u3002"
+ },
+ "create_entry": {
+ "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u88dd\u7f6e\u3002"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "profile": "\u500b\u4eba\u8a2d\u5b9a"
+ },
+ "description": "\u9078\u64c7 Home Assistant \u6240\u8981\u5c0d\u61c9\u4f7f\u7528\u7684 Withings \u500b\u4eba\u8a2d\u5b9a\u3002\u65bc Withings \u9801\u9762\u3001\u78ba\u5b9a\u9078\u53d6\u76f8\u540c\u7684\u4f7f\u7528\u8005\uff0c\u5426\u5247\u8cc7\u6599\u5c07\u7121\u6cd5\u6b63\u78ba\u6a19\u793a\u3002",
+ "title": "\u500b\u4eba\u8a2d\u5b9a\u3002"
+ }
+ },
+ "title": "Withings"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py
index 23cc74281e8b9b..f28a4f59d80195 100644
--- a/homeassistant/components/withings/config_flow.py
+++ b/homeassistant/components/withings/config_flow.py
@@ -88,7 +88,10 @@ async def async_step_import(self, user_input=None):
async def async_step_user(self, user_input=None):
"""Create an entry for selecting a profile."""
- flow = self.hass.data.get(DATA_FLOW_IMPL, {})
+ flow = self.hass.data.get(DATA_FLOW_IMPL)
+
+ if not flow:
+ return self.async_abort(reason="no_flows")
if user_input:
return await self.async_step_auth(user_input)
diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json
index 88b8e6d5ea0944..1a99abc7255651 100644
--- a/homeassistant/components/withings/strings.json
+++ b/homeassistant/components/withings/strings.json
@@ -12,6 +12,9 @@
},
"create_entry": {
"default": "Successfully authenticated with Withings for the selected profile."
+ },
+ "abort": {
+ "no_flows": "You need to configure Withings before being able to authenticate with it. Please read the documentation."
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/wwlln/.translations/it.json b/homeassistant/components/wwlln/.translations/it.json
new file mode 100644
index 00000000000000..f0fc32636072fa
--- /dev/null
+++ b/homeassistant/components/wwlln/.translations/it.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Localit\u00e0 gi\u00e0 registrata"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Latitudine",
+ "longitude": "Longitudine",
+ "radius": "Raggio (utilizzando il tuo sistema di unit\u00e0 di misura di base)"
+ },
+ "title": "Inserisci le informazioni sulla tua posizione."
+ }
+ },
+ "title": "Rete mondiale di localizzazione dei fulmini (WWLLN)"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wwlln/.translations/ko.json b/homeassistant/components/wwlln/.translations/ko.json
index 5e879cd7330311..e5831f5af29224 100644
--- a/homeassistant/components/wwlln/.translations/ko.json
+++ b/homeassistant/components/wwlln/.translations/ko.json
@@ -10,7 +10,7 @@
"longitude": "\uacbd\ub3c4",
"radius": "\ubc18\uacbd (\uae30\ubcf8 \ub2e8\uc704 \uc2dc\uc2a4\ud15c \uc0ac\uc6a9)"
},
- "title": "\uc704\uce58 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694"
+ "title": "\uc704\uce58 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694."
}
},
"title": "\uc138\uacc4 \ub099\ub8b0 \uc704\uce58\ub9dd (WWLLN)"
diff --git a/homeassistant/components/wwlln/.translations/pl.json b/homeassistant/components/wwlln/.translations/pl.json
index 704c7baeecb3c2..652d580644fce6 100644
--- a/homeassistant/components/wwlln/.translations/pl.json
+++ b/homeassistant/components/wwlln/.translations/pl.json
@@ -10,7 +10,7 @@
"longitude": "D\u0142ugo\u015b\u0107 geograficzna",
"radius": "Promie\u0144 (przy u\u017cyciu systemu jednostki bazowej)"
},
- "title": "Wpisz informacje o swojej lokalizacji."
+ "title": "Wprowad\u017a informacje o lokalizacji."
}
},
"title": "\u015awiatowa sie\u0107 lokalizacji wy\u0142adowa\u0144 atmosferycznych (WWLLN)"
diff --git a/homeassistant/components/wwlln/manifest.json b/homeassistant/components/wwlln/manifest.json
index ef9295341c065b..189b9365105159 100644
--- a/homeassistant/components/wwlln/manifest.json
+++ b/homeassistant/components/wwlln/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/wwlln",
"requirements": [
- "aiowwlln==1.0.0"
+ "aiowwlln==2.0.2"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/yandex_transport/__init__.py b/homeassistant/components/yandex_transport/__init__.py
new file mode 100644
index 00000000000000..d007b2d3df8581
--- /dev/null
+++ b/homeassistant/components/yandex_transport/__init__.py
@@ -0,0 +1 @@
+"""Service for obtaining information about closer bus from Transport Yandex Service."""
diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json
new file mode 100644
index 00000000000000..6c633f848c0c68
--- /dev/null
+++ b/homeassistant/components/yandex_transport/manifest.json
@@ -0,0 +1,12 @@
+{
+ "domain": "yandex_transport",
+ "name": "Yandex Transport",
+ "documentation": "https://www.home-assistant.io/components/yandex_transport",
+ "requirements": [
+ "ya_ma==0.3.7"
+ ],
+ "dependencies": [],
+ "codeowners": [
+ "@rishatik92"
+ ]
+}
diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py
new file mode 100644
index 00000000000000..340291807ead98
--- /dev/null
+++ b/homeassistant/components/yandex_transport/sensor.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+"""Service for obtaining information about closer bus from Transport Yandex Service."""
+
+import logging
+from datetime import timedelta
+
+import voluptuous as vol
+from ya_ma import YandexMapsRequester
+
+import homeassistant.helpers.config_validation as cv
+import homeassistant.util.dt as dt_util
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP
+from homeassistant.helpers.entity import Entity
+
+_LOGGER = logging.getLogger(__name__)
+
+STOP_NAME = "stop_name"
+USER_AGENT = "Home Assistant"
+ATTRIBUTION = "Data provided by maps.yandex.ru"
+
+CONF_STOP_ID = "stop_id"
+CONF_ROUTE = "routes"
+
+DEFAULT_NAME = "Yandex Transport"
+ICON = "mdi:bus"
+
+SCAN_INTERVAL = timedelta(minutes=1)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_STOP_ID): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_ROUTE, default=[]): vol.All(cv.ensure_list, [cv.string]),
+ }
+)
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the Yandex transport sensor."""
+ stop_id = config[CONF_STOP_ID]
+ name = config[CONF_NAME]
+ routes = config[CONF_ROUTE]
+
+ data = YandexMapsRequester(user_agent=USER_AGENT)
+ add_entities([DiscoverMoscowYandexTransport(data, stop_id, routes, name)], True)
+
+
+class DiscoverMoscowYandexTransport(Entity):
+ """Implementation of yandex_transport sensor."""
+
+ def __init__(self, requester, stop_id, routes, name):
+ """Initialize sensor."""
+ self.requester = requester
+ self._stop_id = stop_id
+ self._routes = []
+ self._routes = routes
+ self._state = None
+ self._name = name
+ self._attrs = None
+
+ def update(self):
+ """Get the latest data from maps.yandex.ru and update the states."""
+ attrs = {}
+ closer_time = None
+ try:
+ yandex_reply = self.requester.get_stop_info(self._stop_id)
+ data = yandex_reply["data"]
+ stop_metadata = data["properties"]["StopMetaData"]
+ except KeyError as key_error:
+ _LOGGER.warning(
+ "Exception KeyError was captured, missing key is %s. Yandex returned: %s",
+ key_error,
+ yandex_reply,
+ )
+ self.requester.set_new_session()
+ data = self.requester.get_stop_info(self._stop_id)["data"]
+ stop_metadata = data["properties"]["StopMetaData"]
+ stop_name = data["properties"]["name"]
+ transport_list = stop_metadata["Transport"]
+ for transport in transport_list:
+ route = transport["name"]
+ if self._routes and route not in self._routes:
+ # skip unnecessary route info
+ continue
+ if "Events" in transport["BriefSchedule"]:
+ for event in transport["BriefSchedule"]["Events"]:
+ if "Estimated" in event:
+ posix_time_next = int(event["Estimated"]["value"])
+ if closer_time is None or closer_time > posix_time_next:
+ closer_time = posix_time_next
+ if route not in attrs:
+ attrs[route] = []
+ attrs[route].append(event["Estimated"]["text"])
+ attrs[STOP_NAME] = stop_name
+ attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
+ if closer_time is None:
+ self._state = None
+ else:
+ self._state = dt_util.utc_from_timestamp(closer_time).isoformat(
+ timespec="seconds"
+ )
+ self._attrs = attrs
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ return DEVICE_CLASS_TIMESTAMP
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ return self._attrs
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend, if any."""
+ return ICON
diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py
index 431c34aa06ef80..c899c811a47d35 100644
--- a/homeassistant/components/yeelight/__init__.py
+++ b/homeassistant/components/yeelight/__init__.py
@@ -37,6 +37,7 @@
CONF_MODE_MUSIC = "use_music_mode"
CONF_FLOW_PARAMS = "flow_params"
CONF_CUSTOM_EFFECTS = "custom_effects"
+CONF_NIGHTLIGHT_SWITCH_TYPE = "nightlight_switch_type"
ATTR_COUNT = "count"
ATTR_ACTION = "action"
@@ -48,6 +49,8 @@
ACTIVE_MODE_NIGHTLIGHT = "1"
+NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light"
+
SCAN_INTERVAL = timedelta(seconds=30)
YEELIGHT_RGB_TRANSITION = "RGBTransition"
@@ -84,6 +87,9 @@
vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int,
vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean,
vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean,
+ vol.Optional(CONF_NIGHTLIGHT_SWITCH_TYPE): vol.Any(
+ NIGHTLIGHT_SWITCH_TYPE_LIGHT
+ ),
vol.Optional(CONF_MODEL): cv.string,
}
)
@@ -256,10 +262,12 @@ def type(self):
return self._device_type
- def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None):
+ def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None, power_mode=None):
"""Turn on device."""
try:
- self.bulb.turn_on(duration=duration, light_type=light_type)
+ self.bulb.turn_on(
+ duration=duration, light_type=light_type, power_mode=power_mode
+ )
except BulbException as ex:
_LOGGER.error("Unable to turn the bulb on: %s", ex)
diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py
index 8601e0e16322c1..b47cdb981612e4 100644
--- a/homeassistant/components/yeelight/light.py
+++ b/homeassistant/components/yeelight/light.py
@@ -3,9 +3,10 @@
import voluptuous as vol
from yeelight import RGBTransition, SleepTransition, Flow, BulbException
-from yeelight.enums import PowerMode, LightType, BulbType
+from yeelight.enums import PowerMode, LightType, BulbType, SceneClass
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.service import extract_entity_ids
+import homeassistant.helpers.config_validation as cv
from homeassistant.util.color import (
color_temperature_mired_to_kelvin as mired_to_kelvin,
color_temperature_kelvin_to_mired as kelvin_to_mired,
@@ -28,6 +29,8 @@
SUPPORT_FLASH,
SUPPORT_EFFECT,
Light,
+ ATTR_RGB_COLOR,
+ ATTR_KELVIN,
)
import homeassistant.util.color as color_util
from . import (
@@ -45,10 +48,14 @@
CONF_FLOW_PARAMS,
ATTR_ACTION,
ATTR_COUNT,
+ NIGHTLIGHT_SWITCH_TYPE_LIGHT,
+ CONF_NIGHTLIGHT_SWITCH_TYPE,
)
_LOGGER = logging.getLogger(__name__)
+PLATFORM_DATA_KEY = f"{DATA_YEELIGHT}_lights"
+
SUPPORT_YEELIGHT = (
SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_EFFECT
)
@@ -58,9 +65,15 @@
SUPPORT_YEELIGHT_RGB = SUPPORT_YEELIGHT_WHITE_TEMP | SUPPORT_COLOR
ATTR_MODE = "mode"
+ATTR_MINUTES = "minutes"
SERVICE_SET_MODE = "set_mode"
SERVICE_START_FLOW = "start_flow"
+SERVICE_SET_COLOR_SCENE = "set_color_scene"
+SERVICE_SET_HSV_SCENE = "set_hsv_scene"
+SERVICE_SET_COLOR_TEMP_SCENE = "set_color_temp_scene"
+SERVICE_SET_COLOR_FLOW_SCENE = "set_color_flow_scene"
+SERVICE_SET_AUTO_DELAY_OFF_SCENE = "set_auto_delay_off_scene"
EFFECT_DISCO = "Disco"
EFFECT_TEMP = "Slow Temp"
@@ -121,6 +134,60 @@
"ceiling4": BulbType.WhiteTempMood,
}
+VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100))
+
+SERVICE_SCHEMA_SET_MODE = YEELIGHT_SERVICE_SCHEMA.extend(
+ {vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode])}
+)
+
+SERVICE_SCHEMA_START_FLOW = YEELIGHT_SERVICE_SCHEMA.extend(
+ YEELIGHT_FLOW_TRANSITION_SCHEMA
+)
+
+SERVICE_SCHEMA_SET_COLOR_SCENE = YEELIGHT_SERVICE_SCHEMA.extend(
+ {
+ vol.Required(ATTR_RGB_COLOR): vol.All(
+ vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple)
+ ),
+ vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
+ }
+)
+
+SERVICE_SCHEMA_SET_HSV_SCENE = YEELIGHT_SERVICE_SCHEMA.extend(
+ {
+ vol.Required(ATTR_HS_COLOR): vol.All(
+ vol.ExactSequence(
+ (
+ vol.All(vol.Coerce(float), vol.Range(min=0, max=359)),
+ vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
+ )
+ ),
+ vol.Coerce(tuple),
+ ),
+ vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
+ }
+)
+
+SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE = YEELIGHT_SERVICE_SCHEMA.extend(
+ {
+ vol.Required(ATTR_KELVIN): vol.All(
+ vol.Coerce(int), vol.Range(min=1700, max=6500)
+ ),
+ vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
+ }
+)
+
+SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE = YEELIGHT_SERVICE_SCHEMA.extend(
+ YEELIGHT_FLOW_TRANSITION_SCHEMA
+)
+
+SERVICE_SCHEMA_SET_AUTO_DELAY_OFF = YEELIGHT_SERVICE_SCHEMA.extend(
+ {
+ vol.Required(ATTR_MINUTES): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)),
+ vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
+ }
+)
+
def _transitions_config_parser(transitions):
"""Parse transitions config into initialized objects."""
@@ -165,18 +232,20 @@ def _wrap(self, *args, **kwargs):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Yeelight bulbs."""
- data_key = f"{DATA_YEELIGHT}_lights"
if not discovery_info:
return
- if data_key not in hass.data:
- hass.data[data_key] = []
+ if PLATFORM_DATA_KEY not in hass.data:
+ hass.data[PLATFORM_DATA_KEY] = []
device = hass.data[DATA_YEELIGHT][discovery_info[CONF_HOST]]
_LOGGER.debug("Adding %s", device.name)
custom_effects = _parse_custom_effects(discovery_info[CONF_CUSTOM_EFFECTS])
+ nl_switch_light = (
+ discovery_info.get(CONF_NIGHTLIGHT_SWITCH_TYPE) == NIGHTLIGHT_SWITCH_TYPE_LIGHT
+ )
lights = []
@@ -193,9 +262,17 @@ def _lights_setup_helper(klass):
elif device_type == BulbType.Color:
_lights_setup_helper(YeelightColorLight)
elif device_type == BulbType.WhiteTemp:
- _lights_setup_helper(YeelightWhiteTempLight)
+ if nl_switch_light and device.is_nightlight_supported:
+ _lights_setup_helper(YeelightWithNightLight)
+ _lights_setup_helper(YeelightNightLightMode)
+ else:
+ _lights_setup_helper(YeelightWhiteTempWithoutNightlightSwitch)
elif device_type == BulbType.WhiteTempMood:
- _lights_setup_helper(YeelightWithAmbientLight)
+ if nl_switch_light and device.is_nightlight_supported:
+ _lights_setup_helper(YeelightNightLightMode)
+ _lights_setup_helper(YeelightWithAmbientAndNightlight)
+ else:
+ _lights_setup_helper(YeelightWithAmbientWithoutNightlight)
_lights_setup_helper(YeelightAmbientLight)
else:
_lights_setup_helper(YeelightGenericLight)
@@ -205,41 +282,120 @@ def _lights_setup_helper(klass):
device.name,
)
- hass.data[data_key] += lights
+ hass.data[PLATFORM_DATA_KEY] += lights
add_entities(lights, True)
+ setup_services(hass)
- def service_handler(service):
- """Dispatch service calls to target entities."""
- params = {
- key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID
- }
- entity_ids = extract_entity_ids(hass, service)
- target_devices = [
- light for light in hass.data[data_key] if light.entity_id in entity_ids
- ]
-
- for target_device in target_devices:
- if service.service == SERVICE_SET_MODE:
- target_device.set_mode(**params)
- elif service.service == SERVICE_START_FLOW:
- params[ATTR_TRANSITIONS] = _transitions_config_parser(
- params[ATTR_TRANSITIONS]
- )
- target_device.start_flow(**params)
+def setup_services(hass):
+ """Set up the service listeners."""
+
+ def service_call(func):
+ def service_to_entities(service):
+ """Return the known entities that a service call mentions."""
+
+ entity_ids = extract_entity_ids(hass, service)
+ target_devices = [
+ light
+ for light in hass.data[PLATFORM_DATA_KEY]
+ if light.entity_id in entity_ids
+ ]
+
+ return target_devices
- service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend(
- {vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode])}
+ def service_to_params(service):
+ """Return service call params, without entity_id."""
+ return {
+ key: value
+ for key, value in service.data.items()
+ if key != ATTR_ENTITY_ID
+ }
+
+ def wrapper(service):
+ params = service_to_params(service)
+ target_devices = service_to_entities(service)
+ for device in target_devices:
+ func(device, params)
+
+ return wrapper
+
+ @service_call
+ def service_set_mode(target_device, params):
+ target_device.set_mode(**params)
+
+ @service_call
+ def service_start_flow(target_devices, params):
+ params[ATTR_TRANSITIONS] = _transitions_config_parser(params[ATTR_TRANSITIONS])
+ target_devices.start_flow(**params)
+
+ @service_call
+ def service_set_color_scene(target_device, params):
+ target_device.set_scene(
+ SceneClass.COLOR, *[*params[ATTR_RGB_COLOR], params[ATTR_BRIGHTNESS]]
+ )
+
+ @service_call
+ def service_set_hsv_scene(target_device, params):
+ target_device.set_scene(
+ SceneClass.HSV, *[*params[ATTR_HS_COLOR], params[ATTR_BRIGHTNESS]]
+ )
+
+ @service_call
+ def service_set_color_temp_scene(target_device, params):
+ target_device.set_scene(
+ SceneClass.CT, params[ATTR_KELVIN], params[ATTR_BRIGHTNESS]
+ )
+
+ @service_call
+ def service_set_color_flow_scene(target_device, params):
+ flow = Flow(
+ count=params[ATTR_COUNT],
+ action=Flow.actions[params[ATTR_ACTION]],
+ transitions=_transitions_config_parser(params[ATTR_TRANSITIONS]),
+ )
+ target_device.set_scene(SceneClass.CF, flow)
+
+ @service_call
+ def service_set_auto_delay_off_scene(target_device, params):
+ target_device.set_scene(
+ SceneClass.AUTO_DELAY_OFF, params[ATTR_BRIGHTNESS], params[ATTR_MINUTES]
+ )
+
+ hass.services.register(
+ DOMAIN, SERVICE_SET_MODE, service_set_mode, schema=SERVICE_SCHEMA_SET_MODE
)
hass.services.register(
- DOMAIN, SERVICE_SET_MODE, service_handler, schema=service_schema_set_mode
+ DOMAIN, SERVICE_START_FLOW, service_start_flow, schema=SERVICE_SCHEMA_START_FLOW
)
-
- service_schema_start_flow = YEELIGHT_SERVICE_SCHEMA.extend(
- YEELIGHT_FLOW_TRANSITION_SCHEMA
+ hass.services.register(
+ DOMAIN,
+ SERVICE_SET_COLOR_SCENE,
+ service_set_color_scene,
+ schema=SERVICE_SCHEMA_SET_COLOR_SCENE,
+ )
+ hass.services.register(
+ DOMAIN,
+ SERVICE_SET_HSV_SCENE,
+ service_set_hsv_scene,
+ schema=SERVICE_SCHEMA_SET_HSV_SCENE,
+ )
+ hass.services.register(
+ DOMAIN,
+ SERVICE_SET_COLOR_TEMP_SCENE,
+ service_set_color_temp_scene,
+ schema=SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE,
+ )
+ hass.services.register(
+ DOMAIN,
+ SERVICE_SET_COLOR_FLOW_SCENE,
+ service_set_color_flow_scene,
+ schema=SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE,
)
hass.services.register(
- DOMAIN, SERVICE_START_FLOW, service_handler, schema=service_schema_start_flow
+ DOMAIN,
+ SERVICE_SET_AUTO_DELAY_OFF_SCENE,
+ service_set_auto_delay_off_scene,
+ schema=SERVICE_SCHEMA_SET_AUTO_DELAY_OFF,
)
@@ -376,6 +532,10 @@ def _brightness_property(self):
def _power_property(self):
return "power"
+ @property
+ def _turn_on_power_mode(self):
+ return PowerMode.LAST
+
@property
def _predefined_effects(self):
return YEELIGHT_MONO_EFFECT_LIST
@@ -559,7 +719,11 @@ def turn_on(self, **kwargs) -> None:
if ATTR_TRANSITION in kwargs: # passed kwarg overrides config
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
- self.device.turn_on(duration=duration, light_type=self.light_type)
+ self.device.turn_on(
+ duration=duration,
+ light_type=self.light_type,
+ power_mode=self._turn_on_power_mode,
+ )
if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode:
try:
@@ -618,6 +782,18 @@ def start_flow(self, transitions, count=0, action=ACTION_RECOVER):
except BulbException as ex:
_LOGGER.error("Unable to set effect: %s", ex)
+ def set_scene(self, scene_class, *args):
+ """
+ Set the light directly to the specified state.
+
+ If the light is off, it will first be turned on.
+ """
+ try:
+ self._bulb.set_scene(scene_class, *args)
+ self.device.update()
+ except BulbException as ex:
+ _LOGGER.error("Unable to set scene: %s", ex)
+
class YeelightColorLight(YeelightGenericLight):
"""Representation of a Color Yeelight light."""
@@ -632,7 +808,7 @@ def _predefined_effects(self):
return YEELIGHT_COLOR_EFFECT_LIST
-class YeelightWhiteTempLight(YeelightGenericLight):
+class YeelightWhiteTempLightsupport:
"""Representation of a Color Yeelight light."""
@property
@@ -640,17 +816,84 @@ def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_YEELIGHT_WHITE_TEMP
+ @property
+ def _predefined_effects(self):
+ return YEELIGHT_TEMP_ONLY_EFFECT_LIST
+
+
+class YeelightWhiteTempWithoutNightlightSwitch(
+ YeelightWhiteTempLightsupport, YeelightGenericLight
+):
+ """White temp light, when nightlight switch is not set to light."""
+
@property
def _brightness_property(self):
return "current_brightness"
+
+class YeelightWithNightLight(YeelightWhiteTempLightsupport, YeelightGenericLight):
+ """Representation of a Yeelight with nightlight support.
+
+ It represents case when nightlight switch is set to light.
+ """
+
+ @property
+ def is_on(self) -> bool:
+ """Return true if device is on."""
+ return super().is_on and not self.device.is_nightlight_enabled
+
+ @property
+ def _turn_on_power_mode(self):
+ return PowerMode.NORMAL
+
+
+class YeelightNightLightMode(YeelightGenericLight):
+ """Representation of a Yeelight when in nightlight mode."""
+
+ @property
+ def name(self) -> str:
+ """Return the name of the device if any."""
+ return f"{self.device.name} nightlight"
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend, if any."""
+ return "mdi:weather-night"
+
+ @property
+ def is_on(self) -> bool:
+ """Return true if device is on."""
+ return super().is_on and self.device.is_nightlight_enabled
+
+ @property
+ def _brightness_property(self):
+ return "nl_br"
+
+ @property
+ def _turn_on_power_mode(self):
+ return PowerMode.MOONLIGHT
+
@property
def _predefined_effects(self):
return YEELIGHT_TEMP_ONLY_EFFECT_LIST
-class YeelightWithAmbientLight(YeelightWhiteTempLight):
- """Representation of a Yeelight which has ambilight support."""
+class YeelightWithAmbientWithoutNightlight(YeelightWhiteTempWithoutNightlightSwitch):
+ """Representation of a Yeelight which has ambilight support.
+
+ And nightlight switch type is none.
+ """
+
+ @property
+ def _power_property(self):
+ return "main_power"
+
+
+class YeelightWithAmbientAndNightlight(YeelightWithNightLight):
+ """Representation of a Yeelight which has ambilight support.
+
+ And nightlight switch type is set to light.
+ """
@property
def _power_property(self):
diff --git a/homeassistant/components/yeelight/services.yaml b/homeassistant/components/yeelight/services.yaml
index 14dcfb27a4d54a..52106a42063545 100644
--- a/homeassistant/components/yeelight/services.yaml
+++ b/homeassistant/components/yeelight/services.yaml
@@ -7,7 +7,69 @@ set_mode:
mode:
description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'.
example: 'moonlight'
-
+set_color_scene:
+ description: Changes the light to the specified RGB color and brightness. If the light is off, it will be turned on.
+ fields:
+ entity_id:
+ description: Name of the light entity.
+ example: 'light.yeelight'
+ rgb_color:
+ description: Color for the light in RGB-format.
+ example: '[255, 100, 100]'
+ brightness:
+ description: The brightness value to set (1-100).
+ example: 50
+set_hsv_scene:
+ description: Changes the light to the specified HSV color and brightness. If the light is off, it will be turned on.
+ fields:
+ entity_id:
+ description: Name of the light entity.
+ example: 'light.yeelight'
+ hs_color:
+ description: Color for the light in hue/sat format. Hue is 0-359 and Sat is 0-100.
+ example: '[300, 70]'
+ brightness:
+ description: The brightness value to set (1-100).
+ example: 50
+set_color_temp_scene:
+ description: Changes the light to the specified color temperature. If the light is off, it will be turned on.
+ fields:
+ entity_id:
+ description: Name of the light entity.
+ example: 'light.yeelight'
+ kelvin:
+ description: Color temperature for the light in Kelvin.
+ example: 4000
+ brightness:
+ description: The brightness value to set (1-100).
+ example: 50
+set_color_flow_scene:
+ description: starts a color flow. If the light is off, it will be turned on.
+ fields:
+ entity_id:
+ description: Name of the light entity.
+ example: 'light.yeelight'
+ count:
+ description: The number of times to run this flow (0 to run forever).
+ example: 0
+ action:
+ description: The action to take after the flow stops. Can be 'recover', 'stay', 'off'. (default 'recover')
+ example: 'stay'
+ transitions:
+ description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html
+ example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]'
+set_auto_delay_off_scene:
+ description: Turns the light on to the specified brightness and sets a timer to turn it back off after the given number of minutes. If the light is off, Set a color scene, if light is off, it will be turned on.
+ fields:
+ entity_id:
+ description: Name of the light entity.
+ example: 'light.yeelight'
+ minutes:
+ description: The minutes to wait before automatically turning the light off.
+ example: 5
+ brightness:
+ description: The brightness value to set (1-100).
+ example: 50
start_flow:
description: Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects
fields:
diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py
index be079e83fa6bb0..ff9f27d4843c20 100644
--- a/homeassistant/components/zha/api.py
+++ b/homeassistant/components/zha/api.py
@@ -19,9 +19,16 @@
ATTR_COMMAND,
ATTR_COMMAND_TYPE,
ATTR_ENDPOINT_ID,
+ ATTR_LEVEL,
ATTR_MANUFACTURER,
ATTR_NAME,
ATTR_VALUE,
+ ATTR_WARNING_DEVICE_DURATION,
+ ATTR_WARNING_DEVICE_MODE,
+ ATTR_WARNING_DEVICE_STROBE,
+ ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE,
+ ATTR_WARNING_DEVICE_STROBE_INTENSITY,
+ CHANNEL_IAS_WD,
CLUSTER_COMMAND_SERVER,
CLUSTER_COMMANDS_CLIENT,
CLUSTER_COMMANDS_SERVER,
@@ -31,6 +38,11 @@
DATA_ZHA_GATEWAY,
DOMAIN,
MFG_CLUSTER_ID_START,
+ WARNING_DEVICE_MODE_EMERGENCY,
+ WARNING_DEVICE_SOUND_HIGH,
+ WARNING_DEVICE_SQUAWK_MODE_ARMED,
+ WARNING_DEVICE_STROBE_HIGH,
+ WARNING_DEVICE_STROBE_YES,
)
from .core.helpers import async_is_bindable_target, convert_ieee, get_matched_clusters
@@ -56,6 +68,8 @@
SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = "issue_zigbee_cluster_command"
SERVICE_DIRECT_ZIGBEE_BIND = "issue_direct_zigbee_bind"
SERVICE_DIRECT_ZIGBEE_UNBIND = "issue_direct_zigbee_unbind"
+SERVICE_WARNING_DEVICE_SQUAWK = "warning_device_squawk"
+SERVICE_WARNING_DEVICE_WARN = "warning_device_warn"
SERVICE_ZIGBEE_BIND = "service_zigbee_bind"
IEEE_SERVICE = "ieee_based_service"
@@ -80,6 +94,41 @@
vol.Optional(ATTR_MANUFACTURER): cv.positive_int,
}
),
+ SERVICE_WARNING_DEVICE_SQUAWK: vol.Schema(
+ {
+ vol.Required(ATTR_IEEE): convert_ieee,
+ vol.Optional(
+ ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_SQUAWK_MODE_ARMED
+ ): cv.positive_int,
+ vol.Optional(
+ ATTR_WARNING_DEVICE_STROBE, default=WARNING_DEVICE_STROBE_YES
+ ): cv.positive_int,
+ vol.Optional(
+ ATTR_LEVEL, default=WARNING_DEVICE_SOUND_HIGH
+ ): cv.positive_int,
+ }
+ ),
+ SERVICE_WARNING_DEVICE_WARN: vol.Schema(
+ {
+ vol.Required(ATTR_IEEE): convert_ieee,
+ vol.Optional(
+ ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_MODE_EMERGENCY
+ ): cv.positive_int,
+ vol.Optional(
+ ATTR_WARNING_DEVICE_STROBE, default=WARNING_DEVICE_STROBE_YES
+ ): cv.positive_int,
+ vol.Optional(
+ ATTR_LEVEL, default=WARNING_DEVICE_SOUND_HIGH
+ ): cv.positive_int,
+ vol.Optional(ATTR_WARNING_DEVICE_DURATION, default=5): cv.positive_int,
+ vol.Optional(
+ ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE, default=0x00
+ ): cv.positive_int,
+ vol.Optional(
+ ATTR_WARNING_DEVICE_STROBE_INTENSITY, default=WARNING_DEVICE_STROBE_HIGH
+ ): cv.positive_int,
+ }
+ ),
SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema(
{
vol.Required(ATTR_IEEE): convert_ieee,
@@ -610,6 +659,85 @@ async def issue_zigbee_cluster_command(service):
schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND],
)
+ async def warning_device_squawk(service):
+ """Issue the squawk command for an IAS warning device."""
+ ieee = service.data[ATTR_IEEE]
+ mode = service.data.get(ATTR_WARNING_DEVICE_MODE)
+ strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE)
+ level = service.data.get(ATTR_LEVEL)
+
+ zha_device = zha_gateway.get_device(ieee)
+ if zha_device is not None:
+ channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD)
+ if channel:
+ await channel.squawk(mode, strobe, level)
+ else:
+ _LOGGER.error(
+ "Squawking IASWD: %s is missing the required IASWD channel!",
+ "{}: [{}]".format(ATTR_IEEE, str(ieee)),
+ )
+ else:
+ _LOGGER.error(
+ "Squawking IASWD: %s could not be found!",
+ "{}: [{}]".format(ATTR_IEEE, str(ieee)),
+ )
+ _LOGGER.debug(
+ "Squawking IASWD: %s %s %s %s",
+ "{}: [{}]".format(ATTR_IEEE, str(ieee)),
+ "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode),
+ "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe),
+ "{}: [{}]".format(ATTR_LEVEL, level),
+ )
+
+ hass.helpers.service.async_register_admin_service(
+ DOMAIN,
+ SERVICE_WARNING_DEVICE_SQUAWK,
+ warning_device_squawk,
+ schema=SERVICE_SCHEMAS[SERVICE_WARNING_DEVICE_SQUAWK],
+ )
+
+ async def warning_device_warn(service):
+ """Issue the warning command for an IAS warning device."""
+ ieee = service.data[ATTR_IEEE]
+ mode = service.data.get(ATTR_WARNING_DEVICE_MODE)
+ strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE)
+ level = service.data.get(ATTR_LEVEL)
+ duration = service.data.get(ATTR_WARNING_DEVICE_DURATION)
+ duty_mode = service.data.get(ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE)
+ intensity = service.data.get(ATTR_WARNING_DEVICE_STROBE_INTENSITY)
+
+ zha_device = zha_gateway.get_device(ieee)
+ if zha_device is not None:
+ channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD)
+ if channel:
+ await channel.start_warning(
+ mode, strobe, level, duration, duty_mode, intensity
+ )
+ else:
+ _LOGGER.error(
+ "Warning IASWD: %s is missing the required IASWD channel!",
+ "{}: [{}]".format(ATTR_IEEE, str(ieee)),
+ )
+ else:
+ _LOGGER.error(
+ "Warning IASWD: %s could not be found!",
+ "{}: [{}]".format(ATTR_IEEE, str(ieee)),
+ )
+ _LOGGER.debug(
+ "Warning IASWD: %s %s %s %s",
+ "{}: [{}]".format(ATTR_IEEE, str(ieee)),
+ "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode),
+ "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe),
+ "{}: [{}]".format(ATTR_LEVEL, level),
+ )
+
+ hass.helpers.service.async_register_admin_service(
+ DOMAIN,
+ SERVICE_WARNING_DEVICE_WARN,
+ warning_device_warn,
+ schema=SERVICE_SCHEMAS[SERVICE_WARNING_DEVICE_WARN],
+ )
+
websocket_api.async_register_command(hass, websocket_permit_devices)
websocket_api.async_register_command(hass, websocket_get_devices)
websocket_api.async_register_command(hass, websocket_get_device)
@@ -629,3 +757,5 @@ def async_unload_api(hass):
hass.services.async_remove(DOMAIN, SERVICE_REMOVE)
hass.services.async_remove(DOMAIN, SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE)
hass.services.async_remove(DOMAIN, SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND)
+ hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_SQUAWK)
+ hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_WARN)
diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py
index 9e3b69a80df07c..aed12bc65a5495 100644
--- a/homeassistant/components/zha/core/channels/__init__.py
+++ b/homeassistant/components/zha/core/channels/__init__.py
@@ -202,7 +202,7 @@ async def async_configure(self):
# Xiaomi devices don't need this and it disrupts pairing
if self._zha_device.manufacturer != "LUMI":
await self.bind()
- if self.cluster.cluster_id not in self.cluster.endpoint.out_clusters:
+ if self.cluster.is_server:
for report_config in self._report_config:
await self.configure_reporting(
report_config["attr"], report_config["config"]
diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py
index 6ed9de9b30313c..e15acdaf5e31bd 100644
--- a/homeassistant/components/zha/core/channels/manufacturerspecific.py
+++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py
@@ -6,9 +6,17 @@
"""
import logging
-from . import AttributeListeningChannel
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+
+from . import AttributeListeningChannel, ZigbeeChannel
from .. import registries
-from ..const import REPORT_CONFIG_ASAP, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT
+from ..const import (
+ REPORT_CONFIG_ASAP,
+ REPORT_CONFIG_MAX_INT,
+ REPORT_CONFIG_MIN_INT,
+ SIGNAL_ATTR_UPDATED,
+)
_LOGGER = logging.getLogger(__name__)
@@ -26,6 +34,14 @@ class SmartThingsHumidity(AttributeListeningChannel):
]
+@registries.CHANNEL_ONLY_CLUSTERS.register(0xFD00)
+@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFD00)
+class OsramButton(ZigbeeChannel):
+ """Osram button channel."""
+
+ REPORT_CONFIG = []
+
+
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
registries.SMARTTHINGS_ACCELERATION_CLUSTER
)
@@ -38,3 +54,23 @@ class SmartThingsAcceleration(AttributeListeningChannel):
{"attr": "y_axis", "config": REPORT_CONFIG_ASAP},
{"attr": "z_axis", "config": REPORT_CONFIG_ASAP},
]
+
+ @callback
+ def attribute_updated(self, attrid, value):
+ """Handle attribute updates on this cluster."""
+ if attrid == self.value_attribute:
+ async_dispatcher_send(
+ self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value
+ )
+ else:
+ self.zha_send_event(
+ self._cluster,
+ SIGNAL_ATTR_UPDATED,
+ {
+ "attribute_id": attrid,
+ "attribute_name": self._cluster.attributes.get(attrid, ["Unknown"])[
+ 0
+ ],
+ "value": value,
+ },
+ )
diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py
index cd407cfc416b68..25c11a9fd4f1cd 100644
--- a/homeassistant/components/zha/core/channels/security.py
+++ b/homeassistant/components/zha/core/channels/security.py
@@ -13,7 +13,15 @@
from . import ZigbeeChannel
from .. import registries
-from ..const import SIGNAL_ATTR_UPDATED
+from ..const import (
+ CLUSTER_COMMAND_SERVER,
+ SIGNAL_ATTR_UPDATED,
+ WARNING_DEVICE_MODE_EMERGENCY,
+ WARNING_DEVICE_SOUND_HIGH,
+ WARNING_DEVICE_SQUAWK_MODE_ARMED,
+ WARNING_DEVICE_STROBE_HIGH,
+ WARNING_DEVICE_STROBE_YES,
+)
_LOGGER = logging.getLogger(__name__)
@@ -25,11 +33,95 @@ class IasAce(ZigbeeChannel):
pass
+@registries.CHANNEL_ONLY_CLUSTERS.register(security.IasWd.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasWd.cluster_id)
class IasWd(ZigbeeChannel):
"""IAS Warning Device channel."""
- pass
+ @staticmethod
+ def set_bit(destination_value, destination_bit, source_value, source_bit):
+ """Set the specified bit in the value."""
+
+ if IasWd.get_bit(source_value, source_bit):
+ return destination_value | (1 << destination_bit)
+ return destination_value
+
+ @staticmethod
+ def get_bit(value, bit):
+ """Get the specified bit from the value."""
+ return (value & (1 << bit)) != 0
+
+ async def squawk(
+ self,
+ mode=WARNING_DEVICE_SQUAWK_MODE_ARMED,
+ strobe=WARNING_DEVICE_STROBE_YES,
+ squawk_level=WARNING_DEVICE_SOUND_HIGH,
+ ):
+ """Issue a squawk command.
+
+ This command uses the WD capabilities to emit a quick audible/visible pulse called a
+ "squawk". The squawk command has no effect if the WD is currently active
+ (warning in progress).
+ """
+ value = 0
+ value = IasWd.set_bit(value, 0, squawk_level, 0)
+ value = IasWd.set_bit(value, 1, squawk_level, 1)
+
+ value = IasWd.set_bit(value, 3, strobe, 0)
+
+ value = IasWd.set_bit(value, 4, mode, 0)
+ value = IasWd.set_bit(value, 5, mode, 1)
+ value = IasWd.set_bit(value, 6, mode, 2)
+ value = IasWd.set_bit(value, 7, mode, 3)
+
+ await self.device.issue_cluster_command(
+ self.cluster.endpoint.endpoint_id,
+ self.cluster.cluster_id,
+ 0x0001,
+ CLUSTER_COMMAND_SERVER,
+ [value],
+ )
+
+ async def start_warning(
+ self,
+ mode=WARNING_DEVICE_MODE_EMERGENCY,
+ strobe=WARNING_DEVICE_STROBE_YES,
+ siren_level=WARNING_DEVICE_SOUND_HIGH,
+ warning_duration=5, # seconds
+ strobe_duty_cycle=0x00,
+ strobe_intensity=WARNING_DEVICE_STROBE_HIGH,
+ ):
+ """Issue a start warning command.
+
+ This command starts the WD operation. The WD alerts the surrounding area by audible
+ (siren) and visual (strobe) signals.
+
+ strobe_duty_cycle indicates the length of the flash cycle. This provides a means
+ of varying the flash duration for different alarm types (e.g., fire, police, burglar).
+ Valid range is 0-100 in increments of 10. All other values SHALL be rounded to the
+ nearest valid value. Strobe SHALL calculate duty cycle over a duration of one second.
+ The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle Field specifies
+ “40,” then the strobe SHALL flash ON for 4/10ths of a second and then turn OFF for
+ 6/10ths of a second.
+ """
+ value = 0
+ value = IasWd.set_bit(value, 0, siren_level, 0)
+ value = IasWd.set_bit(value, 1, siren_level, 1)
+
+ value = IasWd.set_bit(value, 2, strobe, 0)
+
+ value = IasWd.set_bit(value, 4, mode, 0)
+ value = IasWd.set_bit(value, 5, mode, 1)
+ value = IasWd.set_bit(value, 6, mode, 2)
+ value = IasWd.set_bit(value, 7, mode, 3)
+
+ await self.device.issue_cluster_command(
+ self.cluster.endpoint.endpoint_id,
+ self.cluster.cluster_id,
+ 0x0000,
+ CLUSTER_COMMAND_SERVER,
+ [value, warning_duration, strobe_duty_cycle, strobe_intensity],
+ )
@registries.BINARY_SENSOR_CLUSTERS.register(security.IasZone.cluster_id)
diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py
index c35cb168fdff31..ac83c2cdcd8f41 100644
--- a/homeassistant/components/zha/core/const.py
+++ b/homeassistant/components/zha/core/const.py
@@ -34,6 +34,11 @@
ATTR_SIGNATURE = "signature"
ATTR_TYPE = "type"
ATTR_VALUE = "value"
+ATTR_WARNING_DEVICE_DURATION = "duration"
+ATTR_WARNING_DEVICE_MODE = "mode"
+ATTR_WARNING_DEVICE_STROBE = "strobe"
+ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE = "duty_cycle"
+ATTR_WARNING_DEVICE_STROBE_INTENSITY = "intensity"
BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000]
@@ -44,6 +49,7 @@
CHANNEL_ELECTRICAL_MEASUREMENT = "electrical_measurement"
CHANNEL_EVENT_RELAY = "event_relay"
CHANNEL_FAN = "fan"
+CHANNEL_IAS_WD = "ias_wd"
CHANNEL_LEVEL = ATTR_LEVEL
CHANNEL_ON_OFF = "on_off"
CHANNEL_POWER_CONFIGURATION = "power"
@@ -177,6 +183,30 @@ def list(cls):
UNKNOWN_MANUFACTURER = "unk_manufacturer"
UNKNOWN_MODEL = "unk_model"
+WARNING_DEVICE_MODE_STOP = 0
+WARNING_DEVICE_MODE_BURGLAR = 1
+WARNING_DEVICE_MODE_FIRE = 2
+WARNING_DEVICE_MODE_EMERGENCY = 3
+WARNING_DEVICE_MODE_POLICE_PANIC = 4
+WARNING_DEVICE_MODE_FIRE_PANIC = 5
+WARNING_DEVICE_MODE_EMERGENCY_PANIC = 6
+
+WARNING_DEVICE_STROBE_NO = 0
+WARNING_DEVICE_STROBE_YES = 1
+
+WARNING_DEVICE_SOUND_LOW = 0
+WARNING_DEVICE_SOUND_MEDIUM = 1
+WARNING_DEVICE_SOUND_HIGH = 2
+WARNING_DEVICE_SOUND_VERY_HIGH = 3
+
+WARNING_DEVICE_STROBE_LOW = 0x00
+WARNING_DEVICE_STROBE_MEDIUM = 0x01
+WARNING_DEVICE_STROBE_HIGH = 0x02
+WARNING_DEVICE_STROBE_VERY_HIGH = 0x03
+
+WARNING_DEVICE_SQUAWK_MODE_ARMED = 0
+WARNING_DEVICE_SQUAWK_MODE_DISARMED = 1
+
ZHA_DISCOVERY_NEW = "zha_discovery_new_{}"
ZHA_GW_MSG_RAW_INIT = "raw_device_initialized"
ZHA_GW_MSG = "zha_gateway_message"
diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py
index 85b4261e4ecf3c..cea38517767c85 100644
--- a/homeassistant/components/zha/core/store.py
+++ b/homeassistant/components/zha/core/store.py
@@ -2,7 +2,7 @@
# pylint: disable=W0611
from collections import OrderedDict
import logging
-from typing import MutableMapping # noqa: F401
+from typing import MutableMapping
from typing import cast
import attr
@@ -35,7 +35,7 @@ class ZhaDeviceStorage:
def __init__(self, hass: HomeAssistantType) -> None:
"""Initialize the zha device storage."""
self.hass = hass
- self.devices = {} # type: MutableMapping[str, ZhaDeviceEntry]
+ self.devices: MutableMapping[str, ZhaDeviceEntry] = {}
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
@callback
@@ -88,7 +88,7 @@ async def async_load(self) -> None:
"""Load the registry of zha device entries."""
data = await self._store.async_load()
- devices = OrderedDict() # type: OrderedDict[str, ZhaDeviceEntry]
+ devices: "OrderedDict[str, ZhaDeviceEntry]" = OrderedDict()
if data is not None:
for device in data["devices"]:
diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py
index 379f69febbb82f..c2273c54073ac0 100644
--- a/homeassistant/components/zha/light.py
+++ b/homeassistant/components/zha/light.py
@@ -27,9 +27,15 @@
DEFAULT_DURATION = 5
+CAPABILITIES_COLOR_LOOP = 0x4
CAPABILITIES_COLOR_XY = 0x08
CAPABILITIES_COLOR_TEMP = 0x10
+UPDATE_COLORLOOP_ACTION = 0x1
+UPDATE_COLORLOOP_DIRECTION = 0x2
+UPDATE_COLORLOOP_TIME = 0x4
+UPDATE_COLORLOOP_HUE = 0x8
+
UNSUPPORTED_ATTRIBUTE = 0x86
SCAN_INTERVAL = timedelta(minutes=60)
PARALLEL_UPDATES = 5
@@ -85,6 +91,8 @@ def __init__(self, unique_id, zha_device, channels, **kwargs):
self._color_temp = None
self._hs_color = None
self._brightness = None
+ self._effect_list = []
+ self._effect = None
self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF)
self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL)
self._color_channel = self.cluster_channels.get(CHANNEL_COLOR)
@@ -103,6 +111,10 @@ def __init__(self, unique_id, zha_device, channels, **kwargs):
self._supported_features |= light.SUPPORT_COLOR
self._hs_color = (0, 0)
+ if color_capabilities & CAPABILITIES_COLOR_LOOP:
+ self._supported_features |= light.SUPPORT_EFFECT
+ self._effect_list.append(light.EFFECT_COLORLOOP)
+
@property
def is_on(self) -> bool:
"""Return true if entity is on."""
@@ -141,6 +153,16 @@ def color_temp(self):
"""Return the CT color value in mireds."""
return self._color_temp
+ @property
+ def effect_list(self):
+ """Return the list of supported effects."""
+ return self._effect_list
+
+ @property
+ def effect(self):
+ """Return the current effect."""
+ return self._effect
+
@property
def supported_features(self):
"""Flag supported features."""
@@ -173,12 +195,15 @@ def async_restore_last_state(self, last_state):
self._color_temp = last_state.attributes["color_temp"]
if "hs_color" in last_state.attributes:
self._hs_color = last_state.attributes["hs_color"]
+ if "effect" in last_state.attributes:
+ self._effect = last_state.attributes["effect"]
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
transition = kwargs.get(light.ATTR_TRANSITION)
- duration = transition * 10 if transition else DEFAULT_DURATION
+ duration = transition * 10 if transition is not None else DEFAULT_DURATION
brightness = kwargs.get(light.ATTR_BRIGHTNESS)
+ effect = kwargs.get(light.ATTR_EFFECT)
t_log = {}
if (
@@ -234,6 +259,36 @@ async def async_turn_on(self, **kwargs):
return
self._hs_color = hs_color
+ if (
+ effect == light.EFFECT_COLORLOOP
+ and self.supported_features & light.SUPPORT_EFFECT
+ ):
+ result = await self._color_channel.color_loop_set(
+ UPDATE_COLORLOOP_ACTION
+ | UPDATE_COLORLOOP_DIRECTION
+ | UPDATE_COLORLOOP_TIME,
+ 0x2, # start from current hue
+ 0x1, # only support up
+ transition if transition else 7, # transition
+ 0, # no hue
+ )
+ t_log["color_loop_set"] = result
+ self._effect = light.EFFECT_COLORLOOP
+ elif (
+ self._effect == light.EFFECT_COLORLOOP
+ and effect != light.EFFECT_COLORLOOP
+ and self.supported_features & light.SUPPORT_EFFECT
+ ):
+ result = await self._color_channel.color_loop_set(
+ UPDATE_COLORLOOP_ACTION,
+ 0x0,
+ 0x0,
+ 0x0,
+ 0x0, # update action only, action off, no dir,time,hue
+ )
+ t_log["color_loop_set"] = result
+ self._effect = None
+
self.debug("turned on: %s", t_log)
self.async_schedule_update_ha_state()
@@ -292,6 +347,15 @@ async def async_get_state(self, from_cache=True):
self._hs_color = color_util.color_xy_to_hs(
float(color_x / 65535), float(color_y / 65535)
)
+ if (
+ color_capabilities is not None
+ and color_capabilities & CAPABILITIES_COLOR_LOOP
+ ):
+ color_loop_active = await self._color_channel.get_attribute_value(
+ "color_loop_active", from_cache=from_cache
+ )
+ if color_loop_active is not None and color_loop_active == 1:
+ self._effect = light.EFFECT_COLORLOOP
async def refresh(self, time):
"""Call async_get_state at an interval."""
diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json
index bf97dca1c708eb..e78661a04e534d 100644
--- a/homeassistant/components/zha/manifest.json
+++ b/homeassistant/components/zha/manifest.json
@@ -5,11 +5,11 @@
"documentation": "https://www.home-assistant.io/components/zha",
"requirements": [
"bellows-homeassistant==0.9.1",
- "zha-quirks==0.0.22",
- "zigpy-deconz==0.2.2",
- "zigpy-homeassistant==0.7.1",
+ "zha-quirks==0.0.23",
+ "zigpy-deconz==0.3.0",
+ "zigpy-homeassistant==0.8.0",
"zigpy-xbee-homeassistant==0.4.0",
- "zigpy-zigate==0.2.0"
+ "zigpy-zigate==0.3.1"
],
"dependencies": [],
"codeowners": ["@dmulcahey", "@adminiuga"]
diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml
index ffd5aa21472c83..d279af46335fe1 100644
--- a/homeassistant/components/zha/services.yaml
+++ b/homeassistant/components/zha/services.yaml
@@ -82,3 +82,55 @@ issue_zigbee_cluster_command:
manufacturer:
description: manufacturer code
example: 0x00FC
+
+warning_device_squawk:
+ description: >-
+ This service uses the WD capabilities to emit a quick audible/visible pulse called a "squawk". The squawk command has no effect if the WD is currently active (warning in progress).
+ fields:
+ ieee:
+ description: IEEE address for the device
+ example: "00:0d:6f:00:05:7d:2d:34"
+ mode:
+ description: >-
+ The Squawk Mode field is used as a 4-bit enumeration, and can have one of the values shown in Table 8-24 of the ZCL spec - Squawk Mode Field. The exact operation of each mode (how the WD “squawks”) is implementation specific.
+ example: 1
+ strobe:
+ description: >-
+ The strobe field is used as a Boolean, and determines if the visual indication is also required in addition to the audible squawk, as shown in Table 8-25 of the ZCL spec - Strobe Bit.
+ example: 1
+ level:
+ description: >-
+ The squawk level field is used as a 2-bit enumeration, and determines the intensity of audible squawk sound as shown in Table 8-26 of the ZCL spec - Squawk Level Field Values.
+ example: 2
+
+warning_device_warn:
+ description: >-
+ This service starts the WD operation. The WD alerts the surrounding area by audible (siren) and visual (strobe) signals.
+ fields:
+ ieee:
+ description: IEEE address for the device
+ example: "00:0d:6f:00:05:7d:2d:34"
+ mode:
+ description: >-
+ The Warning Mode field is used as an 4-bit enumeration, can have one of the values defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards.
+ example: 1
+ strobe:
+ description: >-
+ The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated.
+ example: 1
+ level:
+ description: >-
+ The Siren Level field is used as a 2-bit enumeration, and indicates the intensity of audible squawk sound as shown in Table 8-22 of the ZCL spec.
+ example: 2
+ duration:
+ description: >-
+ Requested duration of warning, in seconds. If both Strobe and Warning Mode are "0" this field SHALL be ignored.
+ example: 2
+ duty_cycle:
+ description: >-
+ Indicates the length of the flash cycle. This provides a means of varying the flash duration for different alarm types (e.g., fire, police, burglar). Valid range is 0-100 in increments of 10. All other values SHALL be rounded to the nearest valid value. Strobe SHALL calculate duty cycle over a duration of one second. The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle Field specifies “40,” then the strobe SHALL flash ON for 4/10ths of a second and then turn OFF for 6/10ths of a second.
+ example: 2
+ intensity:
+ description: >-
+ Indicates the intensity of the strobe as shown in Table 8-23 of the ZCL spec. This attribute is designed to vary the output of the strobe (i.e., brightness) and not its frequency, which is detailed in section 8.4.2.3.1.6 of the ZCL spec.
+ example: 2
diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py
index 2c7ce4b18a457c..b40fff669589c6 100644
--- a/homeassistant/components/zwave/climate.py
+++ b/homeassistant/components/zwave/climate.py
@@ -171,6 +171,28 @@ def supported_features(self):
def update_properties(self):
"""Handle the data changes for node values."""
# Operation Mode
+ self._update_operation_mode()
+
+ # Current Temp
+ self._update_current_temp()
+
+ # Fan Mode
+ self._update_fan_mode()
+
+ # Swing mode
+ self._update_swing_mode()
+
+ # Set point
+ self._update_target_temp()
+
+ # Operating state
+ self._update_operating_state()
+
+ # Fan operating state
+ self._update_fan_state()
+
+ def _update_operation_mode(self):
+ """Update hvac and preset modes."""
if self.values.mode:
self._hvac_list = []
self._hvac_mapping = {}
@@ -259,22 +281,27 @@ def update_properties(self):
_LOGGER.debug("self._preset_list=%s", self._preset_list)
_LOGGER.debug("self._preset_mode=%s", self._preset_mode)
- # Current Temp
+ def _update_current_temp(self):
+ """Update current temperature."""
if self.values.temperature:
self._current_temperature = self.values.temperature.data
device_unit = self.values.temperature.units
if device_unit is not None:
self._unit = device_unit
- # Fan Mode
+ def _update_fan_mode(self):
+ """Update fan mode."""
if self.values.fan_mode:
self._current_fan_mode = self.values.fan_mode.data
fan_modes = self.values.fan_mode.data_items
if fan_modes:
self._fan_modes = list(fan_modes)
+
_LOGGER.debug("self._fan_modes=%s", self._fan_modes)
_LOGGER.debug("self._current_fan_mode=%s", self._current_fan_mode)
- # Swing mode
+
+ def _update_swing_mode(self):
+ """Update swing mode."""
if self._zxt_120 == 1:
if self.values.zxt_120_swing_mode:
self._current_swing_mode = self.values.zxt_120_swing_mode.data
@@ -283,7 +310,9 @@ def update_properties(self):
self._swing_modes = list(swing_modes)
_LOGGER.debug("self._swing_modes=%s", self._swing_modes)
_LOGGER.debug("self._current_swing_mode=%s", self._current_swing_mode)
- # Set point
+
+ def _update_target_temp(self):
+ """Update target temperature."""
if self.values.primary.data == 0:
_LOGGER.debug(
"Setpoint is 0, setting default to " "current_temperature=%s",
@@ -294,12 +323,14 @@ def update_properties(self):
else:
self._target_temperature = round((float(self.values.primary.data)), 1)
- # Operating state
+ def _update_operating_state(self):
+ """Update operating state."""
if self.values.operating_state:
mode = self.values.operating_state.data
self._hvac_action = HVAC_CURRENT_MAPPINGS.get(str(mode).lower(), mode)
- # Fan operating state
+ def _update_fan_state(self):
+ """Update fan state."""
if self.values.fan_action:
self._fan_action = self.values.fan_action.data
@@ -448,7 +479,7 @@ def set_preset_mode(self, preset_mode):
return
if preset_mode == PRESET_NONE:
# Activate the current hvac mode
- self.update_properties()
+ self._update_operation_mode()
operation_mode = self._hvac_mapping.get(self.hvac_mode)
_LOGGER.debug("Set operation_mode to %s", operation_mode)
self.values.mode.data = operation_mode
diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py
index 66c3452f7c881d..44241e91daf942 100644
--- a/homeassistant/components/zwave/node_entity.py
+++ b/homeassistant/components/zwave/node_entity.py
@@ -17,6 +17,7 @@
EVENT_NODE_EVENT,
EVENT_SCENE_ACTIVATED,
COMMAND_CLASS_CENTRAL_SCENE,
+ COMMAND_CLASS_VERSION,
DOMAIN,
)
from .util import node_name, is_node_parsed, node_device_id_and_name
@@ -30,6 +31,7 @@
ATTR_PRODUCT_NAME = "product_name"
ATTR_MANUFACTURER_NAME = "manufacturer_name"
ATTR_NODE_NAME = "node_name"
+ATTR_APPLICATION_VERSION = "application_version"
STAGE_COMPLETE = "Complete"
@@ -130,10 +132,14 @@ def __init__(self, node, network):
self._product_name = node.product_name
self._manufacturer_name = node.manufacturer_name
self._unique_id = self._compute_unique_id()
+ self._application_version = None
self._attributes = {}
self.wakeup_interval = None
self.location = None
self.battery_level = None
+ dispatcher.connect(
+ self.network_node_value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED
+ )
dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_NODE)
dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_NOTIFICATION)
@@ -161,6 +167,24 @@ def device_info(self):
info["via_device"] = (DOMAIN, 1)
return info
+ def maybe_update_application_version(self, value):
+ """Update application version if value is a Command Class Version, Application Value."""
+ if (
+ value
+ and value.command_class == COMMAND_CLASS_VERSION
+ and value.label == "Application Version"
+ ):
+ self._application_version = value.data
+
+ def network_node_value_added(self, node=None, value=None, args=None):
+ """Handle a added value to a none on the network."""
+ if node and node.node_id != self.node_id:
+ return
+ if args is not None and "nodeId" in args and args["nodeId"] != self.node_id:
+ return
+
+ self.maybe_update_application_version(value)
+
def network_node_changed(self, node=None, value=None, args=None):
"""Handle a changed node on the network."""
if node and node.node_id != self.node_id:
@@ -172,6 +196,8 @@ def network_node_changed(self, node=None, value=None, args=None):
if value is not None and value.command_class == COMMAND_CLASS_CENTRAL_SCENE:
self.central_scene_activated(value.index, value.data)
+ self.maybe_update_application_version(value)
+
self.node_changed()
def get_node_statistics(self):
@@ -343,6 +369,8 @@ def device_state_attributes(self):
attrs[ATTR_BATTERY_LEVEL] = self.battery_level
if self.wakeup_interval is not None:
attrs[ATTR_WAKEUP] = self.wakeup_interval
+ if self._application_version is not None:
+ attrs[ATTR_APPLICATION_VERSION] = self._application_version
return attrs
diff --git a/homeassistant/config.py b/homeassistant/config.py
index 4b7efed00e4082..d3bd97dad8f777 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -289,7 +289,7 @@ def _write_default_config(config_dir: str) -> Optional[str]:
return config_path
- except IOError:
+ except OSError:
print("Unable to create default configuration file", config_path)
return None
@@ -393,7 +393,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
try:
with open(config_path, "wt", encoding="utf-8") as config_file:
config_file.write(config_raw)
- except IOError:
+ except OSError:
_LOGGER.exception("Migrating to google_translate tts failed")
pass
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 4cfd16b8c9f0cc..26cb3c20942666 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,7 +1,7 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 99
+MINOR_VERSION = 100
PATCH_VERSION = "0.dev0"
__short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION)
__version__ = "{}.{}".format(__short_version__, PATCH_VERSION)
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 32690153221b65..9a534c01bbf2c6 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -3,6 +3,7 @@
To update, run python3 -m script.hassfest
"""
+# fmt: off
FLOWS = [
"adguard",
@@ -24,10 +25,12 @@
"homekit_controller",
"homematicip_cloud",
"hue",
+ "iaqualink",
"ifttt",
"ios",
"ipma",
"iqvia",
+ "izone",
"life360",
"lifx",
"linky",
@@ -43,12 +46,14 @@
"openuv",
"owntracks",
"plaato",
+ "plex",
"point",
"ps4",
"rainmachine",
"simplisafe",
"smartthings",
"smhi",
+ "solaredge",
"somfy",
"sonos",
"tellduslive",
diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py
index 28df05a872cfb0..6d62c47110b2d7 100644
--- a/homeassistant/generated/ssdp.py
+++ b/homeassistant/generated/ssdp.py
@@ -3,6 +3,7 @@
To update, run python3 -m script.hassfest
"""
+# fmt: off
SSDP = {
"device_type": {},
diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py
index 09c1712c061d52..6200e2facb0c60 100644
--- a/homeassistant/generated/zeroconf.py
+++ b/homeassistant/generated/zeroconf.py
@@ -3,6 +3,7 @@
To update, run python3 -m script.hassfest
"""
+# fmt: off
ZEROCONF = {
"_axis-video._tcp.local.": [
diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py
index 40465f83728c0a..133251e779d1db 100644
--- a/homeassistant/helpers/condition.py
+++ b/homeassistant/helpers/condition.py
@@ -1,4 +1,5 @@
"""Offer reusable conditions."""
+import asyncio
from datetime import datetime, timedelta
import functools as ft
import logging
@@ -10,6 +11,9 @@
from homeassistant.core import HomeAssistant, State
from homeassistant.components import zone as zone_cmp
+from homeassistant.components.device_automation import ( # noqa: F401 pylint: disable=unused-import
+ async_device_condition_from_config as async_device_from_config,
+)
from homeassistant.const import (
ATTR_GPS_ACCURACY,
ATTR_LATITUDE,
@@ -41,40 +45,9 @@
_LOGGER = logging.getLogger(__name__)
-# PyLint does not like the use of _threaded_factory
-# pylint: disable=invalid-name
-
-
-def _threaded_factory(
- async_factory: Callable[[ConfigType, bool], Callable[..., bool]]
-) -> Callable[[ConfigType, bool], Callable[..., bool]]:
- """Create threaded versions of async factories."""
-
- @ft.wraps(async_factory)
- def factory(
- config: ConfigType, config_validation: bool = True
- ) -> Callable[..., bool]:
- """Threaded factory."""
- async_check = async_factory(config, config_validation)
-
- def condition_if(
- hass: HomeAssistant, variables: TemplateVarsType = None
- ) -> bool:
- """Validate condition."""
- return cast(
- bool,
- run_callback_threadsafe(
- hass.loop, async_check, hass, variables
- ).result(),
- )
-
- return condition_if
- return factory
-
-
-def async_from_config(
- config: ConfigType, config_validation: bool = True
+async def async_from_config(
+ hass: HomeAssistant, config: ConfigType, config_validation: bool = True
) -> Callable[..., bool]:
"""Turn a condition configuration into a method.
@@ -95,29 +68,30 @@ def async_from_config(
)
)
- return cast(Callable[..., bool], factory(config, config_validation))
-
+ # Check for partials to properly determine if coroutine function
+ check_factory = factory
+ while isinstance(check_factory, ft.partial):
+ check_factory = check_factory.func
-from_config = _threaded_factory(async_from_config)
+ if asyncio.iscoroutinefunction(check_factory):
+ return cast(Callable[..., bool], await factory(hass, config, config_validation))
+ return cast(Callable[..., bool], factory(config, config_validation))
-def async_and_from_config(
- config: ConfigType, config_validation: bool = True
+async def async_and_from_config(
+ hass: HomeAssistant, config: ConfigType, config_validation: bool = True
) -> Callable[..., bool]:
"""Create multi condition matcher using 'AND'."""
if config_validation:
config = cv.AND_CONDITION_SCHEMA(config)
- checks = None
+ checks = [
+ await async_from_config(hass, entry, False) for entry in config["conditions"]
+ ]
def if_and_condition(
hass: HomeAssistant, variables: TemplateVarsType = None
) -> bool:
"""Test and condition."""
- nonlocal checks
-
- if checks is None:
- checks = [async_from_config(entry, False) for entry in config["conditions"]]
-
try:
for check in checks:
if not check(hass, variables):
@@ -131,26 +105,20 @@ def if_and_condition(
return if_and_condition
-and_from_config = _threaded_factory(async_and_from_config)
-
-
-def async_or_from_config(
- config: ConfigType, config_validation: bool = True
+async def async_or_from_config(
+ hass: HomeAssistant, config: ConfigType, config_validation: bool = True
) -> Callable[..., bool]:
"""Create multi condition matcher using 'OR'."""
if config_validation:
config = cv.OR_CONDITION_SCHEMA(config)
- checks = None
+ checks = [
+ await async_from_config(hass, entry, False) for entry in config["conditions"]
+ ]
def if_or_condition(
hass: HomeAssistant, variables: TemplateVarsType = None
) -> bool:
"""Test and condition."""
- nonlocal checks
-
- if checks is None:
- checks = [async_from_config(entry, False) for entry in config["conditions"]]
-
try:
for check in checks:
if check(hass, variables):
@@ -163,9 +131,6 @@ def if_or_condition(
return if_or_condition
-or_from_config = _threaded_factory(async_or_from_config)
-
-
def numeric_state(
hass: HomeAssistant,
entity: Union[None, str, State],
@@ -263,9 +228,6 @@ def if_numeric_state(
return if_numeric_state
-numeric_state_from_config = _threaded_factory(async_numeric_state_from_config)
-
-
def state(
hass: HomeAssistant,
entity: Union[None, str, State],
@@ -423,9 +385,6 @@ def template_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool
return template_if
-template_from_config = _threaded_factory(async_template_from_config)
-
-
def time(
before: Optional[dt_util.dt.time] = None,
after: Optional[dt_util.dt.time] = None,
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index 471c6d50360698..952fa41c42c1ce 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -24,10 +24,14 @@
CONF_ALIAS,
CONF_BELOW,
CONF_CONDITION,
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_ENTITY_NAMESPACE,
+ CONF_FOR,
CONF_PLATFORM,
CONF_SCAN_INTERVAL,
+ CONF_STATE,
CONF_UNIT_SYSTEM_IMPERIAL,
CONF_UNIT_SYSTEM_METRIC,
CONF_VALUE_TEMPLATE,
@@ -48,7 +52,7 @@
from homeassistant.util import slugify as util_slugify
-# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
+# mypy: allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
# pylint: disable=invalid-name
@@ -91,7 +95,7 @@ def validate(obj: Dict) -> Dict:
return validate
-def has_at_most_one_key(*keys: str) -> Callable:
+def has_at_most_one_key(*keys: str) -> Callable[[Dict], Dict]:
"""Validate that zero keys exist or one key exists."""
def validate(obj: Dict) -> Dict:
@@ -220,7 +224,7 @@ def entity_ids(value: Union[str, List]) -> List[str]:
comp_entity_ids = vol.Any(vol.All(vol.Lower, ENTITY_MATCH_ALL), entity_ids)
-def entity_domain(domain: str):
+def entity_domain(domain: str) -> Callable[[Any], str]:
"""Validate that entity belong to domain."""
def validate(value: Any) -> str:
@@ -231,7 +235,7 @@ def validate(value: Any) -> str:
return validate
-def entities_domain(domain: str):
+def entities_domain(domain: str) -> Callable[[Union[str, List]], List[str]]:
"""Validate that entities belong to domain."""
def validate(values: Union[str, List]) -> List[str]:
@@ -280,7 +284,7 @@ def icon(value):
)
-def time(value) -> time_sys:
+def time(value: Any) -> time_sys:
"""Validate and transform a time."""
if isinstance(value, time_sys):
return value
@@ -296,7 +300,7 @@ def time(value) -> time_sys:
return time_val
-def date(value) -> date_sys:
+def date(value: Any) -> date_sys:
"""Validate and transform a date."""
if isinstance(value, date_sys):
return value
@@ -435,7 +439,7 @@ def string(value: Any) -> str:
return str(value)
-def temperature_unit(value) -> str:
+def temperature_unit(value: Any) -> str:
"""Validate and transform temperature unit."""
value = str(value).upper()
if value == "C":
@@ -574,7 +578,7 @@ def deprecated(
replacement_key: Optional[str] = None,
invalidation_version: Optional[str] = None,
default: Optional[Any] = None,
-):
+) -> Callable[[Dict], Dict]:
"""
Log key as deprecated and provide a replacement (if exists).
@@ -622,7 +626,7 @@ def deprecated(
" deprecated, please remove it from your configuration"
)
- def check_for_invalid_version(value: Optional[Any]):
+ def check_for_invalid_version(value: Optional[Any]) -> None:
"""Raise error if current version has reached invalidation."""
if not invalidation_version:
return
@@ -637,7 +641,7 @@ def check_for_invalid_version(value: Optional[Any]):
)
)
- def validator(config: Dict):
+ def validator(config: Dict) -> Dict:
"""Check if key is in config and log warning."""
if key in config:
value = config[key]
@@ -746,8 +750,8 @@ def validator(value):
{
vol.Required(CONF_CONDITION): "state",
vol.Required(CONF_ENTITY_ID): entity_id,
- vol.Required("state"): str,
- vol.Optional("for"): vol.All(time_period, positive_timedelta),
+ vol.Required(CONF_STATE): str,
+ vol.Optional(CONF_FOR): vol.All(time_period, positive_timedelta),
# To support use_trigger_value in automation
# Deprecated 2016/04/25
vol.Optional("from"): str,
@@ -823,6 +827,11 @@ def validator(value):
}
)
+DEVICE_CONDITION_SCHEMA = vol.Schema(
+ {vol.Required(CONF_CONDITION): "device", vol.Required(CONF_DOMAIN): str},
+ extra=vol.ALLOW_EXTRA,
+)
+
CONDITION_SCHEMA: vol.Schema = vol.Any(
NUMERIC_STATE_CONDITION_SCHEMA,
STATE_CONDITION_SCHEMA,
@@ -832,6 +841,7 @@ def validator(value):
ZONE_CONDITION_SCHEMA,
AND_CONDITION_SCHEMA,
OR_CONDITION_SCHEMA,
+ DEVICE_CONDITION_SCHEMA,
)
_SCRIPT_DELAY_SCHEMA = vol.Schema(
@@ -852,6 +862,11 @@ def validator(value):
}
)
+DEVICE_ACTION_SCHEMA = vol.Schema(
+ {vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str},
+ extra=vol.ALLOW_EXTRA,
+)
+
SCRIPT_SCHEMA = vol.All(
ensure_list,
[
@@ -861,6 +876,7 @@ def validator(value):
_SCRIPT_WAIT_TEMPLATE_SCHEMA,
EVENT_SCHEMA,
CONDITION_SCHEMA,
+ DEVICE_ACTION_SCHEMA,
)
],
)
diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py
index 3be00c859a7a41..00671e9c7763ae 100644
--- a/homeassistant/helpers/entity_registry.py
+++ b/homeassistant/helpers/entity_registry.py
@@ -53,7 +53,7 @@ class RegistryEntry:
device_id = attr.ib(type=str, default=None)
config_entry_id = attr.ib(type=str, default=None)
disabled_by = attr.ib(
- type=str,
+ type=Optional[str],
default=None,
validator=attr.validators.in_(
(
@@ -64,7 +64,7 @@ class RegistryEntry:
None,
)
),
- ) # type: Optional[str]
+ )
domain = attr.ib(type=str, init=False, repr=False)
@domain.default
@@ -154,8 +154,8 @@ def async_get_or_create(
if entity_id:
return self._async_update_entity(
entity_id,
- config_entry_id=config_entry_id,
- device_id=device_id,
+ config_entry_id=config_entry_id or _UNDEF,
+ device_id=device_id or _UNDEF,
# When we changed our slugify algorithm, we invalidated some
# stored entity IDs with either a __ or ending in _.
# Fix introduced in 0.86 (Jan 23, 2019). Next line can be
diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py
index 3afb5cb88e46dc..b7707b844d417a 100644
--- a/homeassistant/helpers/event.py
+++ b/homeassistant/helpers/event.py
@@ -1,5 +1,5 @@
"""Helpers for listening to events."""
-from datetime import timedelta
+from datetime import datetime, timedelta
import functools as ft
from typing import Callable
@@ -21,8 +21,7 @@
from homeassistant.util.async_ import run_callback_threadsafe
-# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
-# mypy: no-check-untyped-defs, no-warn-return-any
+# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
# PyLint does not like the use of threaded_listener_factory
# pylint: disable=invalid-name
@@ -187,7 +186,9 @@ def state_for_cancel_listener(entity, from_state, to_state):
@callback
@bind_hass
-def async_track_point_in_time(hass, action, point_in_time) -> CALLBACK_TYPE:
+def async_track_point_in_time(
+ hass: HomeAssistant, action: Callable[..., None], point_in_time: datetime
+) -> CALLBACK_TYPE:
"""Add a listener that fires once after a specific point in time."""
utc_point_in_time = dt_util.as_utc(point_in_time)
@@ -204,7 +205,9 @@ def utc_converter(utc_now):
@callback
@bind_hass
-def async_track_point_in_utc_time(hass, action, point_in_time) -> CALLBACK_TYPE:
+def async_track_point_in_utc_time(
+ hass: HomeAssistant, action: Callable[..., None], point_in_time: datetime
+) -> CALLBACK_TYPE:
"""Add a listener that fires once after a specific point in UTC time."""
# Ensure point_in_time is UTC
point_in_time = dt_util.as_utc(point_in_time)
@@ -284,8 +287,8 @@ class SunListener:
action = attr.ib(type=Callable)
event = attr.ib(type=str)
offset = attr.ib(type=timedelta)
- _unsub_sun = attr.ib(default=None)
- _unsub_config = attr.ib(default=None)
+ _unsub_sun: CALLBACK_TYPE = attr.ib(default=None)
+ _unsub_config: CALLBACK_TYPE = attr.ib(default=None)
@callback
def async_attach(self):
diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py
index 43ef156ef09f3f..23728b651098aa 100644
--- a/homeassistant/helpers/script.py
+++ b/homeassistant/helpers/script.py
@@ -4,12 +4,17 @@
from contextlib import suppress
from datetime import datetime
from itertools import islice
-from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple
+from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple, Any
import voluptuous as vol
from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE
-from homeassistant.const import CONF_CONDITION, CONF_TIMEOUT
+from homeassistant.const import (
+ CONF_CONDITION,
+ CONF_DEVICE_ID,
+ CONF_DOMAIN,
+ CONF_TIMEOUT,
+)
from homeassistant import exceptions
from homeassistant.helpers import (
service,
@@ -22,12 +27,12 @@
async_track_template,
)
from homeassistant.helpers.typing import ConfigType
+from homeassistant.loader import async_get_integration
import homeassistant.util.dt as date_util
from homeassistant.util.async_ import run_coroutine_threadsafe, run_callback_threadsafe
-# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
-# mypy: no-check-untyped-defs
+# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
_LOGGER = logging.getLogger(__name__)
@@ -48,6 +53,7 @@
ACTION_CHECK_CONDITION = "condition"
ACTION_FIRE_EVENT = "event"
ACTION_CALL_SERVICE = "call_service"
+ACTION_DEVICE_AUTOMATION = "device"
def _determine_action(action):
@@ -64,6 +70,9 @@ def _determine_action(action):
if CONF_EVENT in action:
return ACTION_FIRE_EVENT
+ if CONF_DEVICE_ID in action:
+ return ACTION_DEVICE_AUTOMATION
+
return ACTION_CALL_SERVICE
@@ -91,9 +100,9 @@ class Script:
def __init__(
self,
hass: HomeAssistant,
- sequence,
+ sequence: Sequence[Dict[str, Any]],
name: Optional[str] = None,
- change_listener=None,
+ change_listener: Optional[Callable[..., Any]] = None,
) -> None:
"""Initialize the script."""
self.hass = hass
@@ -117,6 +126,7 @@ def __init__(
ACTION_CHECK_CONDITION: self._async_check_condition,
ACTION_FIRE_EVENT: self._async_fire_event,
ACTION_CALL_SERVICE: self._async_call_service,
+ ACTION_DEVICE_AUTOMATION: self._async_device_automation,
}
@property
@@ -318,6 +328,19 @@ async def _async_call_service(self, action, variables, context):
context=context,
)
+ async def _async_device_automation(self, action, variables, context):
+ """Perform the device automation specified in the action.
+
+ This method is a coroutine.
+ """
+ self.last_action = action.get(CONF_ALIAS, "device automation")
+ self._log("Executing step %s" % self.last_action)
+ integration = await async_get_integration(self.hass, action[CONF_DOMAIN])
+ platform = integration.get_platform("device_automation")
+ await platform.async_call_action_from_config(
+ self.hass, action, variables, context
+ )
+
async def _async_fire_event(self, action, variables, context):
"""Fire an event."""
self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT])
@@ -338,7 +361,7 @@ async def _async_check_condition(self, action, variables, context):
config_cache_key = frozenset((k, str(v)) for k, v in action.items())
config = self._config_cache.get(config_cache_key)
if not config:
- config = condition.async_from_config(action, False)
+ config = await condition.async_from_config(self.hass, action, False)
self._config_cache[config_cache_key] = config
self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION])
diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index 98e3849bfb6332..9af1998e894ebe 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -7,7 +7,7 @@
import re
from datetime import datetime
from functools import wraps
-from typing import Iterable
+from typing import Any, Iterable
import jinja2
from jinja2 import contextfilter, contextfunction
@@ -25,13 +25,13 @@
from homeassistant.core import State, callback, split_entity_id, valid_entity_id
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import location as loc_helper
-from homeassistant.helpers.typing import TemplateVarsType
+from homeassistant.helpers.typing import HomeAssistantType, TemplateVarsType
from homeassistant.loader import bind_hass
from homeassistant.util import convert, dt as dt_util, location as loc_util
from homeassistant.util.async_ import run_callback_threadsafe
-# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
+# mypy: allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
_LOGGER = logging.getLogger(__name__)
@@ -106,7 +106,7 @@ def extract_entities(template, variables=None):
return MATCH_ALL
-def _true(arg) -> bool:
+def _true(arg: Any) -> bool:
return True
@@ -191,7 +191,7 @@ def extract_entities(self, variables=None):
"""Extract all entities for state_changed listener."""
return extract_entities(self.template, variables)
- def render(self, variables: TemplateVarsType = None, **kwargs):
+ def render(self, variables: TemplateVarsType = None, **kwargs: Any) -> str:
"""Render given template."""
if variables is not None:
kwargs.update(variables)
@@ -201,7 +201,7 @@ def render(self, variables: TemplateVarsType = None, **kwargs):
).result()
@callback
- def async_render(self, variables: TemplateVarsType = None, **kwargs) -> str:
+ def async_render(self, variables: TemplateVarsType = None, **kwargs: Any) -> str:
"""Render given template.
This method must be run in the event loop.
@@ -218,7 +218,7 @@ def async_render(self, variables: TemplateVarsType = None, **kwargs) -> str:
@callback
def async_render_to_info(
- self, variables: TemplateVarsType = None, **kwargs
+ self, variables: TemplateVarsType = None, **kwargs: Any
) -> RenderInfo:
"""Render the template and collect an entity filter."""
assert self.hass and _RENDER_INFO not in self.hass.data
@@ -479,7 +479,7 @@ def _resolve_state(hass, entity_id_or_state):
return None
-def expand(hass, *args) -> Iterable[State]:
+def expand(hass: HomeAssistantType, *args: Any) -> Iterable[State]:
"""Expand out any groups into entity states."""
search = list(args)
found = {}
@@ -635,7 +635,7 @@ def distance(hass, *args):
)
-def is_state(hass, entity_id: str, state: State) -> bool:
+def is_state(hass: HomeAssistantType, entity_id: str, state: State) -> bool:
"""Test if a state is a specific value."""
state_obj = _get_state(hass, entity_id)
return state_obj is not None and state_obj.state == state
diff --git a/homeassistant/loader.py b/homeassistant/loader.py
index 70284348157e76..1a9a3d256acb2d 100644
--- a/homeassistant/loader.py
+++ b/homeassistant/loader.py
@@ -322,9 +322,7 @@ def __init__(self, from_domain: str, to_domain: str) -> None:
def _load_file(
- hass, # type: HomeAssistant
- comp_or_platform: str,
- base_paths: List[str],
+ hass: "HomeAssistant", comp_or_platform: str, base_paths: List[str]
) -> Optional[ModuleType]:
"""Try to load specified file.
@@ -391,11 +389,7 @@ def _load_file(
class ModuleWrapper:
"""Class to wrap a Python module and auto fill in hass argument."""
- def __init__(
- self,
- hass, # type: HomeAssistant
- module: ModuleType,
- ) -> None:
+ def __init__(self, hass: "HomeAssistant", module: ModuleType) -> None:
"""Initialize the module wrapper."""
self._hass = hass
self._module = module
@@ -414,9 +408,7 @@ def __getattr__(self, attr: str) -> Any:
class Components:
"""Helper to load components."""
- def __init__(
- self, hass # type: HomeAssistant
- ) -> None:
+ def __init__(self, hass: "HomeAssistant") -> None:
"""Initialize the Components class."""
self._hass = hass
@@ -442,9 +434,7 @@ def __getattr__(self, comp_name: str) -> ModuleWrapper:
class Helpers:
"""Helper to load helpers."""
- def __init__(
- self, hass # type: HomeAssistant
- ) -> None:
+ def __init__(self, hass: "HomeAssistant") -> None:
"""Initialize the Helpers class."""
self._hass = hass
@@ -462,10 +452,7 @@ def bind_hass(func: CALLABLE_T) -> CALLABLE_T:
return func
-async def async_component_dependencies(
- hass, # type: HomeAssistant
- domain: str,
-) -> Set[str]:
+async def async_component_dependencies(hass: "HomeAssistant", domain: str) -> Set[str]:
"""Return all dependencies and subdependencies of components.
Raises CircularDependency if a circular dependency is found.
@@ -474,10 +461,7 @@ async def async_component_dependencies(
async def _async_component_dependencies(
- hass, # type: HomeAssistant
- domain: str,
- loaded: Set[str],
- loading: Set,
+ hass: "HomeAssistant", domain: str, loaded: Set[str], loading: Set
) -> Set[str]:
"""Recursive function to get component dependencies.
@@ -508,9 +492,7 @@ async def _async_component_dependencies(
return loaded
-def _async_mount_config_dir(
- hass, # type: HomeAssistant
-) -> bool:
+def _async_mount_config_dir(hass: "HomeAssistant") -> bool:
"""Mount config dir in order to load custom_component.
Async friendly but not a coroutine.
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 2fa5a1cd41ae4b..842cf4840c832a 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -1,6 +1,6 @@
PyJWT==1.7.1
PyNaCl==1.3.0
-aiohttp==3.5.4
+aiohttp==3.6.1
aiohttp_cors==0.7.0
astral==1.10.1
async_timeout==3.0.1
@@ -11,12 +11,12 @@ contextvars==2.4;python_version<"3.7"
cryptography==2.7
distro==1.4.0
hass-nabucasa==0.17
-home-assistant-frontend==20190901.0
-importlib-metadata==0.19
+home-assistant-frontend==20190919.0
+importlib-metadata==0.23
jinja2>=2.10.1
netdisco==2.6.0
pip>=8.0.3
-python-slugify==3.0.3
+python-slugify==3.0.4
pytz>=2019.02
pyyaml==5.1.2
requests==2.22.0
diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py
index 0a9bac301883b4..00f5984c58ba43 100644
--- a/homeassistant/scripts/__init__.py
+++ b/homeassistant/scripts/__init__.py
@@ -5,7 +5,7 @@
import logging
import os
import sys
-from typing import List
+from typing import List, Optional, Sequence, Text
from homeassistant.bootstrap import async_mount_local_lib_path
from homeassistant.config import get_default_config_dir
@@ -13,7 +13,7 @@
from homeassistant.util.package import install_package, is_virtual_env, is_installed
-# mypy: allow-untyped-defs, allow-incomplete-defs, no-warn-return-any
+# mypy: allow-untyped-defs, no-warn-return-any
def run(args: List) -> int:
@@ -62,13 +62,13 @@ def run(args: List) -> int:
return script.run(args[1:]) # type: ignore
-def extract_config_dir(args=None) -> str:
+def extract_config_dir(args: Optional[Sequence[Text]] = None) -> str:
"""Extract the config dir from the arguments or get the default."""
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("-c", "--config", default=None)
- args = parser.parse_known_args(args)[0]
+ parsed_args = parser.parse_known_args(args)[0]
return (
- os.path.join(os.getcwd(), args.config)
- if args.config
+ os.path.join(os.getcwd(), parsed_args.config)
+ if parsed_args.config
else get_default_config_dir()
)
diff --git a/homeassistant/scripts/macos/__init__.py b/homeassistant/scripts/macos/__init__.py
index e8d8306c8ce038..ceb3609dbdb1b7 100644
--- a/homeassistant/scripts/macos/__init__.py
+++ b/homeassistant/scripts/macos/__init__.py
@@ -27,7 +27,7 @@ def install_osx():
try:
with open(path, "w", encoding="utf-8") as outp:
outp.write(plist)
- except IOError as err:
+ except OSError as err:
print("Unable to write to " + path, err)
return
diff --git a/requirements_all.txt b/requirements_all.txt
index 852788e1be305f..3214c5e43ab38a 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1,17 +1,17 @@
# Home Assistant core
-aiohttp==3.5.4
+aiohttp==3.6.1
astral==1.10.1
async_timeout==3.0.1
attrs==19.1.0
bcrypt==3.1.7
certifi>=2019.6.16
contextvars==2.4;python_version<"3.7"
-importlib-metadata==0.19
+importlib-metadata==0.23
jinja2>=2.10.1
PyJWT==1.7.1
cryptography==2.7
pip>=8.0.3
-python-slugify==3.0.3
+python-slugify==3.0.4
pytz>=2019.02
pyyaml==5.1.2
requests==2.22.0
@@ -35,7 +35,7 @@ Adafruit-SHT31==1.0.2
# Adafruit_BBIO==1.0.0
# homeassistant.components.homekit
-HAP-python==2.5.0
+HAP-python==2.6.0
# homeassistant.components.mastodon
Mastodon.py==1.4.6
@@ -74,6 +74,9 @@ PyRMVtransport==0.1.3
# homeassistant.components.transport_nsw
PyTransportNSW==0.1.1
+# homeassistant.components.vicare
+PyViCare==0.1.1
+
# homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.12.4
@@ -176,7 +179,7 @@ aioswitcher==2019.4.26
aiounifi==11
# homeassistant.components.wwlln
-aiowwlln==1.0.0
+aiowwlln==2.0.2
# homeassistant.components.aladdin_connect
aladdin_connect==0.3
@@ -194,7 +197,7 @@ ambiclimate==0.2.1
amcrest==1.5.3
# homeassistant.components.androidtv
-androidtv==0.0.26
+androidtv==0.0.27
# homeassistant.components.anel_pwrctrl
anel_pwrctrl-homeassistant==0.0.1.dev2
@@ -225,7 +228,7 @@ asterisk_mbox==0.5.0
# homeassistant.components.dlna_dmr
# homeassistant.components.upnp
-async-upnp-client==0.14.10
+async-upnp-client==0.14.11
# homeassistant.components.aurora_abb_powerone
aurorapy==0.2.6
@@ -259,7 +262,6 @@ batinfo==0.4.2
# homeassistant.components.linksys_ap
# homeassistant.components.scrape
-# homeassistant.components.sytadin
beautifulsoup4==4.8.0
# homeassistant.components.beewi_smartclim
@@ -351,6 +353,9 @@ colorlog==4.0.2
# homeassistant.components.concord232
concord232==0.15
+# homeassistant.components.upc_connect
+connect-box==0.2.4
+
# homeassistant.components.eddystone_temperature
# homeassistant.components.eq3btsmart
# homeassistant.components.xiaomi_miio
@@ -377,7 +382,6 @@ datapoint==0.4.3
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
# homeassistant.components.ohmconnect
-# homeassistant.components.upc_connect
defusedxml==0.6.0
# homeassistant.components.deluge
@@ -441,7 +445,7 @@ enocean==0.50
enturclient==0.2.0
# homeassistant.components.environment_canada
-env_canada==0.0.24
+env_canada==0.0.25
# homeassistant.components.envirophat
# envirophat==0.0.6
@@ -474,9 +478,6 @@ evohome-async==0.3.3b4
# homeassistant.components.fastdotcom
fastdotcom==0.0.3
-# homeassistant.components.fedex
-fedexdeliverymanager==1.0.6
-
# homeassistant.components.feedreader
feedparser-homeassistant==5.2.2.dev1
@@ -522,7 +523,7 @@ gearbest_parser==1.0.7
geizhals==0.0.9
# homeassistant.components.geniushub
-geniushub-client==0.6.5
+geniushub-client==0.6.13
# homeassistant.components.geo_json_events
# homeassistant.components.nsw_rural_fire_service_feed
@@ -582,6 +583,9 @@ greeneye_monitor==1.0
# homeassistant.components.greenwave
greenwavereality==0.5.1
+# homeassistant.components.growatt_server
+growattServer==0.0.1
+
# homeassistant.components.gstreamer
gstreamer-player==1.1.2
@@ -604,7 +608,7 @@ hangups==0.4.9
hass-nabucasa==0.17
# homeassistant.components.mqtt
-hbmqtt==0.9.4
+hbmqtt==0.9.5
# homeassistant.components.jewish_calendar
hdate==0.9.0
@@ -612,6 +616,9 @@ hdate==0.9.0
# homeassistant.components.heatmiser
heatmiserV3==0.9.1
+# homeassistant.components.here_travel_time
+herepy==0.6.3.1
+
# homeassistant.components.hikvisioncam
hikvision==0.4
@@ -631,7 +638,7 @@ hole==0.5.0
holidays==0.9.11
# homeassistant.components.frontend
-home-assistant-frontend==20190901.0
+home-assistant-frontend==20190919.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.4
@@ -660,6 +667,9 @@ hydrawiser==0.1.1
# homeassistant.components.htu21d
# i2csense==0.0.4
+# homeassistant.components.iaqualink
+iaqualink==0.2.9
+
# homeassistant.components.watson_tts
ibm-watson==3.0.3
@@ -676,13 +686,13 @@ ihcsdk==2.3.0
incomfort-client==0.3.1
# homeassistant.components.influxdb
-influxdb==5.2.0
+influxdb==5.2.3
# homeassistant.components.insteon
insteonplm==0.16.5
# homeassistant.components.iperf3
-iperf3==0.1.10
+iperf3==0.1.11
# homeassistant.components.route53
ipify==1.0.0
@@ -696,6 +706,9 @@ jsonrpc-async==0.6
# homeassistant.components.kodi
jsonrpc-websocket==0.6
+# homeassistant.components.kaiterra
+kaiterra-async-client==0.0.2
+
# homeassistant.components.keba
keba-kecontact==0.2.0
@@ -720,6 +733,9 @@ libpurecool==0.5.0
# homeassistant.components.foscam
libpyfoscam==1.0
+# homeassistant.components.vivotek
+libpyvivotek==0.2.2
+
# homeassistant.components.mikrotik
librouteros==2.3.0
@@ -819,9 +835,6 @@ mychevy==1.2.0
# homeassistant.components.mycroft
mycroftapi==2.0
-# homeassistant.components.usps
-myusps==1.3.2
-
# homeassistant.components.n26
n26==0.2.7
@@ -899,14 +912,11 @@ opensensemap-api==0.1.5
openwebifpy==3.1.1
# homeassistant.components.luci
-openwrt-luci-rpc==1.1.0
+openwrt-luci-rpc==1.1.1
# homeassistant.components.orvibo
orvibo==1.1.1
-# homeassistant.components.luci
-packaging==19.1
-
# homeassistant.components.mqtt
# homeassistant.components.shiftr
paho-mqtt==1.4.0
@@ -1045,7 +1055,7 @@ pyW215==0.6.0
pyW800rf32==0.1
# homeassistant.components.nextbus
-py_nextbus==0.1.2
+py_nextbusnext==0.1.4
# homeassistant.components.noaa_tides
# py_noaa==0.3.0
@@ -1068,6 +1078,9 @@ pyarlo==0.2.3
# homeassistant.components.netatmo
pyatmo==2.2.1
+# homeassistant.components.atome
+pyatome==0.1.1
+
# homeassistant.components.apple_tv
pyatv==0.3.13
@@ -1093,7 +1106,7 @@ pycfdns==0.0.1
pychannels==1.0.0
# homeassistant.components.cast
-pychromecast==3.2.2
+pychromecast==4.0.1
# homeassistant.components.cmus
pycmus==0.1.1
@@ -1125,6 +1138,9 @@ pydelijn==0.5.1
# homeassistant.components.zwave
pydispatcher==2.0.5
+# homeassistant.components.doods
+pydoods==1.0.2
+
# homeassistant.components.android_ip_webcam
pydroid-ipcam==0.8
@@ -1271,7 +1287,7 @@ pyloopenergy==0.1.3
pylutron-caseta==0.5.0
# homeassistant.components.lutron
-pylutron==0.2.2
+pylutron==0.2.5
# homeassistant.components.mailgun
pymailgunner==1.4
@@ -1322,11 +1338,20 @@ pynuki==1.3.3
pynut2==2.1.2
# homeassistant.components.nws
-pynws==0.7.4
+pynws==0.8.1
# homeassistant.components.nx584
pynx584==0.4
+# homeassistant.components.nzbget
+pynzbgetapi==0.2.0
+
+# homeassistant.components.obihai
+pyobihai==1.1.0
+
+# homeassistant.components.ombi
+pyombi==0.1.5
+
# homeassistant.components.openuv
pyopenuv==1.0.9
@@ -1444,7 +1469,7 @@ pytautulli==0.5.0
pyteleloisirs==3.5
# homeassistant.components.tfiac
-pytfiac==0.3
+pytfiac==0.4
# homeassistant.components.thinkingcleaner
pythinkingcleaner==0.0.3
@@ -1482,6 +1507,9 @@ python-gitlab==1.6.0
# homeassistant.components.hp_ilo
python-hpilo==4.3
+# homeassistant.components.izone
+python-izone==1.1.1
+
# homeassistant.components.joaoapps_join
python-join-api==0.0.4
@@ -1543,7 +1571,7 @@ python-velbus==2.0.27
python-vlc==1.1.2
# homeassistant.components.whois
-python-whois==0.7.1
+python-whois==0.7.2
# homeassistant.components.wink
python-wink==1.10.5
@@ -1570,7 +1598,7 @@ pytraccar==0.9.0
pytrackr==0.0.5
# homeassistant.components.tradfri
-pytradfri[async]==6.0.1
+pytradfri[async]==6.3.1
# homeassistant.components.trafikverket_train
# homeassistant.components.trafikverket_weatherstation
@@ -1640,7 +1668,7 @@ recollect-waste==1.0.1
regenmaschine==1.5.1
# homeassistant.components.python_script
-restrictedpython==4.0
+restrictedpython==5.0
# homeassistant.components.idteck_prox
rfk101py==0.0.1
@@ -1694,7 +1722,7 @@ schiene==0.23
scsgate==0.1.0
# homeassistant.components.sendgrid
-sendgrid==6.0.5
+sendgrid==6.1.0
# homeassistant.components.sensehat
sense-hat==2.2.0
@@ -1706,13 +1734,13 @@ sense_energy==0.7.0
sharp_aquos_rc==0.3.2
# homeassistant.components.shodan
-shodan==1.15.0
+shodan==1.17.0
# homeassistant.components.simplepush
simplepush==1.1.4
# homeassistant.components.simplisafe
-simplisafe-python==4.3.0
+simplisafe-python==5.0.1
# homeassistant.components.sisyphus
sisyphus-control==2.2.1
@@ -1753,13 +1781,13 @@ snapcast==2.0.10
socialbladeclient==0.2
# homeassistant.components.solaredge_local
-solaredge-local==0.1.4
+solaredge-local==0.2.0
# homeassistant.components.solaredge
solaredge==0.0.2
# homeassistant.components.solax
-solax==0.1.2
+solax==0.2.2
# homeassistant.components.honeywell
somecomfort==0.5.2
@@ -1783,9 +1811,6 @@ spotipy-homeassistant==2.4.4.dev1
# homeassistant.components.sql
sqlalchemy==1.3.8
-# homeassistant.components.srp_energy
-srpenergy==1.0.6
-
# homeassistant.components.starlingbank
starlingbank==3.1
@@ -1881,9 +1906,6 @@ twilio==6.19.1
# homeassistant.components.upcloud
upcloud-api==0.4.3
-# homeassistant.components.ups
-upsmychoice==1.0.6
-
# homeassistant.components.uscis
uscisstatus==0.1.1
@@ -1965,6 +1987,9 @@ xmltodict==0.12.0
# homeassistant.components.xs1
xs1-api-client==2.3.5
+# homeassistant.components.yandex_transport
+ya_ma==0.3.7
+
# homeassistant.components.yweather
yahooweather==0.10
@@ -1978,7 +2003,7 @@ yeelight==0.5.0
yeelightsunflower==0.0.10
# homeassistant.components.media_extractor
-youtube_dl==2019.09.01
+youtube_dl==2019.09.12.1
# homeassistant.components.zengge
zengge==0.2
@@ -1987,7 +2012,7 @@ zengge==0.2
zeroconf==0.23.0
# homeassistant.components.zha
-zha-quirks==0.0.22
+zha-quirks==0.0.23
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9
@@ -1996,16 +2021,16 @@ zhong_hong_hvac==1.0.9
ziggo-mediabox-xl==1.1.0
# homeassistant.components.zha
-zigpy-deconz==0.2.2
+zigpy-deconz==0.3.0
# homeassistant.components.zha
-zigpy-homeassistant==0.7.1
+zigpy-homeassistant==0.8.0
# homeassistant.components.zha
zigpy-xbee-homeassistant==0.4.0
# homeassistant.components.zha
-zigpy-zigate==0.2.0
+zigpy-zigate==0.3.1
# homeassistant.components.zoneminder
zm-py==0.3.3
diff --git a/requirements_test.txt b/requirements_test.txt
index bfe459b0cfb8d3..b9b919c4bfd077 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -10,12 +10,12 @@ flake8-docstrings==1.3.1
flake8==3.7.8
mock-open==1.3.1
mypy==0.720
-pre-commit==1.18.2
+pre-commit==1.18.3
pydocstyle==4.0.1
pylint==2.3.1
pytest-aiohttp==0.3.0
pytest-cov==2.7.1
pytest-sugar==0.9.2
pytest-timeout==1.3.3
-pytest==5.1.1
-requests_mock==1.6.0
+pytest==5.1.3
+requests_mock==1.7.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index a23bc7ce610ee8..c0e94a5afe58e6 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -11,19 +11,19 @@ flake8-docstrings==1.3.1
flake8==3.7.8
mock-open==1.3.1
mypy==0.720
-pre-commit==1.18.2
+pre-commit==1.18.3
pydocstyle==4.0.1
pylint==2.3.1
pytest-aiohttp==0.3.0
pytest-cov==2.7.1
pytest-sugar==0.9.2
pytest-timeout==1.3.3
-pytest==5.1.1
-requests_mock==1.6.0
+pytest==5.1.3
+requests_mock==1.7.0
# homeassistant.components.homekit
-HAP-python==2.5.0
+HAP-python==2.6.0
# homeassistant.components.mobile_app
# homeassistant.components.owntracks
@@ -73,13 +73,13 @@ aioswitcher==2019.4.26
aiounifi==11
# homeassistant.components.wwlln
-aiowwlln==1.0.0
+aiowwlln==2.0.2
# homeassistant.components.ambiclimate
ambiclimate==0.2.1
# homeassistant.components.androidtv
-androidtv==0.0.26
+androidtv==0.0.27
# homeassistant.components.apns
apns2==0.3.0
@@ -105,7 +105,6 @@ coinmarketcap==5.0.3
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
# homeassistant.components.ohmconnect
-# homeassistant.components.upc_connect
defusedxml==0.6.0
# homeassistant.components.dsmr
@@ -167,11 +166,14 @@ hangups==0.4.9
hass-nabucasa==0.17
# homeassistant.components.mqtt
-hbmqtt==0.9.4
+hbmqtt==0.9.5
# homeassistant.components.jewish_calendar
hdate==0.9.0
+# homeassistant.components.here_travel_time
+herepy==0.6.3.1
+
# homeassistant.components.pi_hole
hole==0.5.0
@@ -179,7 +181,7 @@ hole==0.5.0
holidays==0.9.11
# homeassistant.components.frontend
-home-assistant-frontend==20190901.0
+home-assistant-frontend==20190919.0
# homeassistant.components.homekit_controller
homekit[IP]==0.15.0
@@ -194,8 +196,11 @@ httplib2==0.10.3
# homeassistant.components.huawei_lte
huawei-lte-api==1.3.0
+# homeassistant.components.iaqualink
+iaqualink==0.2.9
+
# homeassistant.components.influxdb
-influxdb==5.2.0
+influxdb==5.2.3
# homeassistant.components.verisure
jsonpath==0.75
@@ -247,6 +252,9 @@ pexpect==4.6.0
# homeassistant.components.pilight
pilight==0.1.1
+# homeassistant.components.plex
+plexapi==3.0.6
+
# homeassistant.components.mhz19
# homeassistant.components.serial_pm
pmsensor==0.4
@@ -276,6 +284,9 @@ pyMetno==0.4.6
# homeassistant.components.blackbird
pyblackbird==0.5
+# homeassistant.components.cast
+pychromecast==4.0.1
+
# homeassistant.components.deconz
pydeconz==62
@@ -304,7 +315,7 @@ pymfy==0.5.2
pymonoprice==0.3
# homeassistant.components.nws
-pynws==0.7.4
+pynws==0.8.1
# homeassistant.components.nx584
pynx584==0.4
@@ -341,6 +352,9 @@ pyspcwebgw==0.4.0
# homeassistant.components.darksky
python-forecastio==1.4.0
+# homeassistant.components.izone
+python-izone==1.1.1
+
# homeassistant.components.nest
python-nest==4.1.0
@@ -351,7 +365,7 @@ python-velbus==2.0.27
python_awair==0.0.4
# homeassistant.components.tradfri
-pytradfri[async]==6.0.1
+pytradfri[async]==6.3.1
# homeassistant.components.vesync
pyvesync==1.1.0
@@ -363,7 +377,7 @@ pywebpush==1.9.2
regenmaschine==1.5.1
# homeassistant.components.python_script
-restrictedpython==4.0
+restrictedpython==5.0
# homeassistant.components.rflink
rflink==0.0.46
@@ -375,7 +389,7 @@ ring_doorbell==0.2.3
rxv==0.6.0
# homeassistant.components.simplisafe
-simplisafe-python==4.3.0
+simplisafe-python==5.0.1
# homeassistant.components.sleepiq
sleepyq==0.7
@@ -383,6 +397,9 @@ sleepyq==0.7
# homeassistant.components.smhi
smhi-pkg==1.0.10
+# homeassistant.components.solaredge
+solaredge==0.0.2
+
# homeassistant.components.honeywell
somecomfort==0.5.2
@@ -390,9 +407,6 @@ somecomfort==0.5.2
# homeassistant.components.sql
sqlalchemy==1.3.8
-# homeassistant.components.srp_energy
-srpenergy==1.0.6
-
# homeassistant.components.statsd
statsd==3.2.1
@@ -420,4 +434,4 @@ wakeonlan==1.1.6
zeroconf==0.23.0
# homeassistant.components.zha
-zigpy-homeassistant==0.7.1
+zigpy-homeassistant==0.8.0
diff --git a/script/dev_docker b/script/dev_docker
deleted file mode 100755
index 514fce734777e9..00000000000000
--- a/script/dev_docker
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/bin/sh
-# Build and run Home Assinstant in Docker.
-
-# Optional: pass in a timezone as first argument
-# If not given will attempt to mount /etc/localtime
-
-# Stop on errors
-set -e
-
-cd "$(dirname "$0")/.."
-
-docker build -t home-assistant-dev -f virtualization/Docker/Dockerfile.dev .
-
-if [ $# -gt 0 ]
-then
- docker run \
- --net=host \
- --device=/dev/ttyUSB0:/zwaveusbstick:rwm \
- -e "TZ=$1" \
- -v `pwd`:/usr/src/app \
- -v `pwd`/config:/config \
- -t -i home-assistant-dev
-
-else
- docker run \
- --net=host \
- -v /etc/localtime:/etc/localtime:ro \
- -v `pwd`:/usr/src/app \
- -v `pwd`/config:/config \
- --rm \
- -t -i home-assistant-dev
-
-fi
diff --git a/script/dev_openzwave_docker b/script/dev_openzwave_docker
deleted file mode 100755
index 7304995f3e18eb..00000000000000
--- a/script/dev_openzwave_docker
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/sh
-# Open a docker that can be used to debug/dev python-openzwave. Pass in a command line argument to build
-
-cd "$(dirname "$0")/.."
-
-if [ $# -gt 0 ]
-then
- docker build -t home-assistant-dev .
-fi
-
-docker run \
- --device=/dev/ttyUSB0:/zwaveusbstick:rwm \
- -v `pwd`:/usr/src/app \
- -p 8123:8123 \
- -t -i home-assistant-dev \
- /bin/bash
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index 1468969d9dd719..d74a57d678d494 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -10,8 +10,8 @@
from script.hassfest.model import Integration
COMMENT_REQUIREMENTS = (
- "Adafruit-DHT",
"Adafruit_BBIO",
+ "Adafruit-DHT",
"avion",
"beacontools",
"blinkt",
@@ -26,7 +26,6 @@
"i2csense",
"opencv-python-headless",
"py_noaa",
- "VL53L1X2",
"pybluez",
"pycups",
"PySwitchbot",
@@ -39,11 +38,11 @@
"RPi.GPIO",
"smbus-cffi",
"tensorflow",
+ "VL53L1X2",
)
TEST_REQUIREMENTS = (
"adguardhome",
- "ambiclimate",
"aio_geojson_geonetnz_quakes",
"aioambient",
"aioautomatic",
@@ -52,14 +51,16 @@
"aiohttp_cors",
"aiohue",
"aionotion",
- "aiounifi",
"aioswitcher",
+ "aiounifi",
"aiowwlln",
+ "ambiclimate",
"androidtv",
"apns2",
"aprslib",
"av",
"axis",
+ "bellows-homeassistant",
"caldav",
"coinmarketcap",
"defusedxml",
@@ -86,6 +87,7 @@
"haversine",
"hbmqtt",
"hdate",
+ "herepy",
"hole",
"holidays",
"home-assistant-frontend",
@@ -93,12 +95,12 @@
"homematicip",
"httplib2",
"huawei-lte-api",
+ "iaqualink",
"influxdb",
"jsonpath",
"libpurecool",
"libsoundtouch",
"luftdaten",
- "pyMetno",
"mbddns",
"mficlient",
"minio",
@@ -109,53 +111,61 @@
"paho-mqtt",
"pexpect",
"pilight",
+ "plexapi",
"pmsensor",
"prometheus_client",
"ptvsd",
"pushbullet.py",
"py-canary",
+ "py17track",
"pyblackbird",
+ "pychromecast",
"pydeconz",
"pydispatcher",
"pyheos",
"pyhomematic",
+ "pyHS100",
"pyiqvia",
"pylinky",
"pylitejet",
+ "pyMetno",
"pymfy",
"pymonoprice",
+ "PyNaCl",
"pynws",
"pynx584",
"pyopenuv",
"pyotp",
"pyps4-homeassistant",
+ "pyqwikswitch",
+ "PyRMVtransport",
"pysma",
"pysmartapp",
"pysmartthings",
"pysonos",
- "pyqwikswitch",
- "PyRMVtransport",
- "PyTransportNSW",
"pyspcwebgw",
+ "python_awair",
"python-forecastio",
+ "python-izone",
"python-nest",
- "python_awair",
"python-velbus",
+ "pythonwhois",
"pytradfri[async]",
+ "PyTransportNSW",
"pyunifi",
"pyupnp-async",
"pyvesync",
"pywebpush",
- "pyHS100",
- "PyNaCl",
"regenmaschine",
"restrictedpython",
"rflink",
"ring_doorbell",
+ "ruamel.yaml",
"rxv",
"simplisafe-python",
"sleepyq",
"smhi-pkg",
+ "solaredge",
"somecomfort",
"sqlalchemy",
"srpenergy",
@@ -164,16 +174,12 @@
"twentemilieu",
"uvcclient",
"vsure",
- "warrant",
- "pythonwhois",
- "wakeonlan",
"vultr",
+ "wakeonlan",
+ "warrant",
"YesssSMS",
- "ruamel.yaml",
"zeroconf",
"zigpy-homeassistant",
- "bellows-homeassistant",
- "py17track",
)
IGNORE_PIN = ("colorlog>2.1,<3", "keyring>=9.3,<10.0", "urllib3")
diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py
index 5376f21db9eb81..4384399f4db393 100644
--- a/script/hassfest/config_flow.py
+++ b/script/hassfest/config_flow.py
@@ -10,6 +10,7 @@
To update, run python3 -m script.hassfest
\"\"\"
+# fmt: off
FLOWS = {}
""".strip()
diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py
index 82068af6a7acbf..3b02ea181514df 100644
--- a/script/hassfest/ssdp.py
+++ b/script/hassfest/ssdp.py
@@ -11,6 +11,7 @@
To update, run python3 -m script.hassfest
\"\"\"
+# fmt: off
SSDP = {}
""".strip()
diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py
index bdd765e315ecd1..3d93d363086162 100644
--- a/script/hassfest/zeroconf.py
+++ b/script/hassfest/zeroconf.py
@@ -11,6 +11,7 @@
To update, run python3 -m script.hassfest
\"\"\"
+# fmt: off
ZEROCONF = {}
diff --git a/script/lint_docker b/script/lint_docker
deleted file mode 100755
index 7e6ff42e074c9e..00000000000000
--- a/script/lint_docker
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/sh
-# Execute lint in a docker container to spot code mistakes.
-
-# Stop on errors
-set -e
-
-cd "$(dirname "$0")/.."
-
-docker build -t home-assistant-test -f virtualization/Docker/Dockerfile.dev .
-docker run --rm \
- -v `pwd`/.tox/:/usr/src/app/.tox/ \
- -t -i home-assistant-test \
- tox -e lint
diff --git a/script/scaffold/__init__.py b/script/scaffold/__init__.py
new file mode 100644
index 00000000000000..2eca398d998245
--- /dev/null
+++ b/script/scaffold/__init__.py
@@ -0,0 +1 @@
+"""Scaffold new integration."""
diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py
new file mode 100644
index 00000000000000..93bcc5aba4172c
--- /dev/null
+++ b/script/scaffold/__main__.py
@@ -0,0 +1,87 @@
+"""Validate manifests."""
+import argparse
+from pathlib import Path
+import subprocess
+import sys
+
+from . import gather_info, generate, error
+from .const import COMPONENT_DIR
+
+
+TEMPLATES = [
+ p.name for p in (Path(__file__).parent / "templates").glob("*") if p.is_dir()
+]
+
+
+def valid_integration(integration):
+ """Test if it's a valid integration."""
+ if not (COMPONENT_DIR / integration).exists():
+ raise argparse.ArgumentTypeError(
+ f"The integration {integration} does not exist."
+ )
+
+ return integration
+
+
+def get_arguments() -> argparse.Namespace:
+ """Get parsed passed in arguments."""
+ parser = argparse.ArgumentParser(description="Home Assistant Scaffolder")
+ parser.add_argument("template", type=str, choices=TEMPLATES)
+ parser.add_argument(
+ "--develop", action="store_true", help="Automatically fill in info"
+ )
+ parser.add_argument(
+ "--integration", type=valid_integration, help="Integration to target."
+ )
+
+ arguments = parser.parse_args()
+
+ return arguments
+
+
+def main():
+ """Scaffold an integration."""
+ if not Path("requirements_all.txt").is_file():
+ print("Run from project root")
+ return 1
+
+ args = get_arguments()
+
+ info = gather_info.gather_info(args)
+
+ generate.generate(args.template, info)
+
+ # If creating new integration, create config flow too
+ if args.template == "integration":
+ if info.authentication or not info.discoverable:
+ template = "config_flow"
+ else:
+ template = "config_flow_discovery"
+
+ generate.generate(template, info)
+
+ print("Running hassfest to pick up new information.")
+ subprocess.run("python -m script.hassfest", shell=True)
+ print()
+
+ print("Running tests")
+ print(f"$ pytest tests/components/{info.domain}")
+ if (
+ subprocess.run(f"pytest tests/components/{info.domain}", shell=True).returncode
+ != 0
+ ):
+ return 1
+ print()
+
+ print(f"Done!")
+
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(main())
+ except error.ExitApp as err:
+ print()
+ print(f"Fatal Error: {err.reason}")
+ sys.exit(err.exit_code)
diff --git a/script/scaffold/const.py b/script/scaffold/const.py
new file mode 100644
index 00000000000000..cf66bb4e2ae4d7
--- /dev/null
+++ b/script/scaffold/const.py
@@ -0,0 +1,5 @@
+"""Constants for scaffolding."""
+from pathlib import Path
+
+COMPONENT_DIR = Path("homeassistant/components")
+TESTS_DIR = Path("tests/components")
diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py
new file mode 100644
index 00000000000000..54a182be31bd63
--- /dev/null
+++ b/script/scaffold/docs.py
@@ -0,0 +1,22 @@
+"""Print links to relevant docs."""
+from .model import Info
+
+
+def print_relevant_docs(template: str, info: Info) -> None:
+ """Print relevant docs."""
+ if template == "integration":
+ print(
+ f"""
+Your integration has been created at {info.integration_dir} . Next step is to fill in the blanks for the code marked with TODO.
+
+For a breakdown of each file, check the developer documentation at:
+https://developers.home-assistant.io/docs/en/creating_integration_file_structure.html
+"""
+ )
+
+ elif template == "config_flow":
+ print(
+ f"""
+The config flow has been added to the {info.domain} integration. Next step is to fill in the blanks for the code marked with TODO.
+"""
+ )
diff --git a/script/scaffold/error.py b/script/scaffold/error.py
new file mode 100644
index 00000000000000..75a869572fd7fc
--- /dev/null
+++ b/script/scaffold/error.py
@@ -0,0 +1,10 @@
+"""Errors for scaffolding."""
+
+
+class ExitApp(Exception):
+ """Exception to indicate app should exit."""
+
+ def __init__(self, reason, exit_code=1):
+ """Initialize the exit app exception."""
+ self.reason = reason
+ self.exit_code = exit_code
diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py
new file mode 100644
index 00000000000000..a7263daaf41982
--- /dev/null
+++ b/script/scaffold/gather_info.py
@@ -0,0 +1,183 @@
+"""Gather info for scaffolding."""
+import json
+
+from homeassistant.util import slugify
+
+from .const import COMPONENT_DIR
+from .model import Info
+from .error import ExitApp
+
+
+CHECK_EMPTY = ["Cannot be empty", lambda value: value]
+
+
+def gather_info(arguments) -> Info:
+ """Gather info."""
+ existing = arguments.template != "integration"
+
+ if arguments.develop:
+ print("Running in developer mode. Automatically filling in info.")
+ print()
+
+ if existing:
+ if arguments.develop:
+ return _load_existing_integration("develop")
+
+ if arguments.integration:
+ return _load_existing_integration(arguments.integration)
+
+ return gather_existing_integration()
+
+ if arguments.develop:
+ return Info(
+ domain="develop",
+ name="Develop Hub",
+ codeowner="@developer",
+ requirement="aiodevelop==1.2.3",
+ )
+
+ return gather_new_integration()
+
+
+def gather_new_integration() -> Info:
+ """Gather info about new integration from user."""
+ return Info(
+ **_gather_info(
+ {
+ "domain": {
+ "prompt": "What is the domain?",
+ "validators": [
+ CHECK_EMPTY,
+ [
+ "Domains cannot contain spaces or special characters.",
+ lambda value: value == slugify(value),
+ ],
+ [
+ "There already is an integration with this domain.",
+ lambda value: not (COMPONENT_DIR / value).exists(),
+ ],
+ ],
+ },
+ "name": {
+ "prompt": "What is the name of your integration?",
+ "validators": [CHECK_EMPTY],
+ },
+ "codeowner": {
+ "prompt": "What is your GitHub handle?",
+ "validators": [
+ CHECK_EMPTY,
+ [
+ 'GitHub handles need to start with an "@"',
+ lambda value: value.startswith("@"),
+ ],
+ ],
+ },
+ "requirement": {
+ "prompt": "What PyPI package and version do you depend on? Leave blank for none.",
+ "validators": [
+ [
+ "Versions should be pinned using '=='.",
+ lambda value: not value or "==" in value,
+ ]
+ ],
+ },
+ "authentication": {
+ "prompt": "Does Home Assistant need the user to authenticate to control the device/service? (yes/no)",
+ "default": "yes",
+ "validators": [
+ [
+ "Type either 'yes' or 'no'",
+ lambda value: value in ("yes", "no"),
+ ]
+ ],
+ "convertor": lambda value: value == "yes",
+ },
+ "discoverable": {
+ "prompt": "Is the device/service discoverable on the local network? (yes/no)",
+ "default": "no",
+ "validators": [
+ [
+ "Type either 'yes' or 'no'",
+ lambda value: value in ("yes", "no"),
+ ]
+ ],
+ "convertor": lambda value: value == "yes",
+ },
+ }
+ )
+ )
+
+
+def gather_existing_integration() -> Info:
+ """Gather info about existing integration from user."""
+ answers = _gather_info(
+ {
+ "domain": {
+ "prompt": "What is the domain?",
+ "validators": [
+ CHECK_EMPTY,
+ [
+ "Domains cannot contain spaces or special characters.",
+ lambda value: value == slugify(value),
+ ],
+ [
+ "This integration does not exist.",
+ lambda value: (COMPONENT_DIR / value).exists(),
+ ],
+ ],
+ }
+ }
+ )
+
+ return _load_existing_integration(answers["domain"])
+
+
+def _load_existing_integration(domain) -> Info:
+ """Load an existing integration."""
+ if not (COMPONENT_DIR / domain).exists():
+ raise ExitApp("Integration does not exist", 1)
+
+ manifest = json.loads((COMPONENT_DIR / domain / "manifest.json").read_text())
+
+ return Info(domain=domain, name=manifest["name"])
+
+
+def _gather_info(fields) -> dict:
+ """Gather info from user."""
+ answers = {}
+
+ for key, info in fields.items():
+ hint = None
+ while key not in answers:
+ if hint is not None:
+ print()
+ print(f"Error: {hint}")
+
+ try:
+ print()
+ msg = info["prompt"]
+ if "default" in info:
+ msg += f" [{info['default']}]"
+ value = input(f"{msg}\n> ")
+ except (KeyboardInterrupt, EOFError):
+ raise ExitApp("Interrupted!", 1)
+
+ value = value.strip()
+
+ if value == "" and "default" in info:
+ value = info["default"]
+
+ hint = None
+
+ for validator_hint, validator in info["validators"]:
+ if not validator(value):
+ hint = validator_hint
+ break
+
+ if hint is None:
+ if "convertor" in info:
+ value = info["convertor"](value)
+ answers[key] = value
+
+ print()
+ return answers
diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py
new file mode 100644
index 00000000000000..6bccf6529feeea
--- /dev/null
+++ b/script/scaffold/generate.py
@@ -0,0 +1,121 @@
+"""Generate an integration."""
+from pathlib import Path
+
+from .error import ExitApp
+from .model import Info
+
+TEMPLATE_DIR = Path(__file__).parent / "templates"
+TEMPLATE_INTEGRATION = TEMPLATE_DIR / "integration"
+TEMPLATE_TESTS = TEMPLATE_DIR / "tests"
+
+
+def generate(template: str, info: Info) -> None:
+ """Generate a template."""
+ _validate(template, info)
+
+ print(f"Scaffolding {template} for the {info.domain} integration...")
+ _ensure_tests_dir_exists(info)
+ _generate(TEMPLATE_DIR / template / "integration", info.integration_dir, info)
+ _generate(TEMPLATE_DIR / template / "tests", info.tests_dir, info)
+ _custom_tasks(template, info)
+ print()
+
+
+def _validate(template, info):
+ """Validate we can run this task."""
+ if template == "config_flow":
+ if (info.integration_dir / "config_flow.py").exists():
+ raise ExitApp(f"Integration {info.domain} already has a config flow.")
+
+
+def _generate(src_dir, target_dir, info: Info) -> None:
+ """Generate an integration."""
+ replaces = {"NEW_DOMAIN": info.domain, "NEW_NAME": info.name}
+
+ if not target_dir.exists():
+ target_dir.mkdir()
+
+ for source_file in src_dir.glob("**/*"):
+ content = source_file.read_text()
+
+ for to_search, to_replace in replaces.items():
+ content = content.replace(to_search, to_replace)
+
+ target_file = target_dir / source_file.relative_to(src_dir)
+ print(f"Writing {target_file}")
+ target_file.write_text(content)
+
+
+def _ensure_tests_dir_exists(info: Info) -> None:
+ """Ensure a test dir exists."""
+ if info.tests_dir.exists():
+ return
+
+ info.tests_dir.mkdir()
+ print(f"Writing {info.tests_dir / '__init__.py'}")
+ (info.tests_dir / "__init__.py").write_text(
+ f'"""Tests for the {info.name} integration."""\n'
+ )
+
+
+def _custom_tasks(template, info) -> None:
+ """Handle custom tasks for templates."""
+ if template == "integration":
+ changes = {"codeowners": [info.codeowner]}
+
+ if info.requirement:
+ changes["requirements"] = [info.requirement]
+
+ info.update_manifest(**changes)
+
+ if template == "config_flow":
+ info.update_manifest(config_flow=True)
+ info.update_strings(
+ config={
+ "title": info.name,
+ "step": {
+ "user": {"title": "Connect to the device", "data": {"host": "Host"}}
+ },
+ "error": {
+ "cannot_connect": "Failed to connect, please try again",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error",
+ },
+ "abort": {"already_configured": "Device is already configured"},
+ }
+ )
+
+ if template == "config_flow_discovery":
+ info.update_manifest(config_flow=True)
+ info.update_strings(
+ config={
+ "title": info.name,
+ "step": {
+ "confirm": {
+ "title": info.name,
+ "description": f"Do you want to set up {info.name}?",
+ }
+ },
+ "abort": {
+ "single_instance_allowed": f"Only a single configuration of {info.name} is possible.",
+ "no_devices_found": f"No {info.name} devices found on the network.",
+ },
+ }
+ )
+
+ if template in ("config_flow", "config_flow_discovery"):
+ init_file = info.integration_dir / "__init__.py"
+ init_file.write_text(
+ init_file.read_text()
+ + """
+
+async def async_setup_entry(hass, entry):
+ \"\"\"Set up a config entry for NEW_NAME.\"\"\"
+ # TODO forward the entry for each platform that you want to set up.
+ # hass.async_create_task(
+ # hass.config_entries.async_forward_entry_setup(entry, "media_player")
+ # )
+
+ return True
+"""
+ )
diff --git a/script/scaffold/model.py b/script/scaffold/model.py
new file mode 100644
index 00000000000000..68ab771122e0ab
--- /dev/null
+++ b/script/scaffold/model.py
@@ -0,0 +1,61 @@
+"""Models for scaffolding."""
+import json
+from pathlib import Path
+
+import attr
+
+from .const import COMPONENT_DIR, TESTS_DIR
+
+
+@attr.s
+class Info:
+ """Info about new integration."""
+
+ domain: str = attr.ib()
+ name: str = attr.ib()
+ codeowner: str = attr.ib(default=None)
+ requirement: str = attr.ib(default=None)
+ authentication: str = attr.ib(default=None)
+ discoverable: str = attr.ib(default=None)
+
+ @property
+ def integration_dir(self) -> Path:
+ """Return directory if integration."""
+ return COMPONENT_DIR / self.domain
+
+ @property
+ def tests_dir(self) -> Path:
+ """Return test directory."""
+ return TESTS_DIR / self.domain
+
+ @property
+ def manifest_path(self) -> Path:
+ """Path to the manifest."""
+ return COMPONENT_DIR / self.domain / "manifest.json"
+
+ def manifest(self) -> dict:
+ """Return integration manifest."""
+ return json.loads(self.manifest_path.read_text())
+
+ def update_manifest(self, **kwargs) -> None:
+ """Update the integration manifest."""
+ print(f"Updating {self.domain} manifest: {kwargs}")
+ self.manifest_path.write_text(
+ json.dumps({**self.manifest(), **kwargs}, indent=2)
+ )
+
+ @property
+ def strings_path(self) -> Path:
+ """Path to the strings."""
+ return COMPONENT_DIR / self.domain / "strings.json"
+
+ def strings(self) -> dict:
+ """Return integration strings."""
+ if not self.strings_path.exists():
+ return {}
+ return json.loads(self.strings_path.read_text())
+
+ def update_strings(self, **kwargs) -> None:
+ """Update the integration strings."""
+ print(f"Updating {self.domain} strings: {list(kwargs)}")
+ self.strings_path.write_text(json.dumps({**self.strings(), **kwargs}, indent=2))
diff --git a/script/scaffold/templates/config_flow/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py
new file mode 100644
index 00000000000000..e08851f47a0358
--- /dev/null
+++ b/script/scaffold/templates/config_flow/integration/config_flow.py
@@ -0,0 +1,64 @@
+"""Config flow for NEW_NAME integration."""
+import logging
+
+import voluptuous as vol
+
+from homeassistant import core, config_entries, exceptions
+
+from .const import DOMAIN # pylint:disable=unused-import
+
+_LOGGER = logging.getLogger(__name__)
+
+# TODO adjust the data schema to the data that you need
+DATA_SCHEMA = vol.Schema({"host": str, "username": str, "password": str})
+
+
+async def validate_input(hass: core.HomeAssistant, data):
+ """Validate the user input allows us to connect.
+
+ Data has the keys from DATA_SCHEMA with values provided by the user.
+ """
+ # TODO validate the data can be used to set up a connection.
+ # If you cannot connect:
+ # throw CannotConnect
+ # If the authentication is wrong:
+ # InvalidAuth
+
+ # Return some info we want to store in the config entry.
+ return {"title": "Name of the device"}
+
+
+class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for NEW_NAME."""
+
+ VERSION = 1
+ # TODO pick one of the available connection classes in homeassistant/config_entries.py
+ CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+ if user_input is not None:
+ try:
+ info = await validate_input(self.hass, user_input)
+
+ return self.async_create_entry(title=info["title"], data=user_input)
+ except CannotConnect:
+ errors["base"] = "cannot_connect"
+ except InvalidAuth:
+ errors["base"] = "invalid_auth"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors=errors
+ )
+
+
+class CannotConnect(exceptions.HomeAssistantError):
+ """Error to indicate we cannot connect."""
+
+
+class InvalidAuth(exceptions.HomeAssistantError):
+ """Error to indicate there is invalid auth."""
diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py
new file mode 100644
index 00000000000000..35d8a96ab2b796
--- /dev/null
+++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py
@@ -0,0 +1,93 @@
+"""Test the NEW_NAME config flow."""
+from unittest.mock import patch
+
+from homeassistant import config_entries, setup
+from homeassistant.components.NEW_DOMAIN.const import DOMAIN
+from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth
+
+from tests.common import mock_coro
+
+
+async def test_form(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.NEW_DOMAIN.config_flow.validate_input",
+ return_value=mock_coro({"title": "Test Title"}),
+ ), patch(
+ "homeassistant.components.NEW_DOMAIN.async_setup", return_value=mock_coro(True)
+ ) as mock_setup, patch(
+ "homeassistant.components.NEW_DOMAIN.async_setup_entry",
+ return_value=mock_coro(True),
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Test Title"
+ assert result2["data"] == {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ }
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_invalid_auth(hass):
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.NEW_DOMAIN.config_flow.validate_input",
+ side_effect=InvalidAuth,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_form_cannot_connect(hass):
+ """Test we handle cannot connect error."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.NEW_DOMAIN.config_flow.validate_input",
+ side_effect=CannotConnect,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {
+ "host": "1.1.1.1",
+ "username": "test-username",
+ "password": "test-password",
+ },
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "cannot_connect"}
diff --git a/script/scaffold/templates/config_flow_discovery/integration/config_flow.py b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py
new file mode 100644
index 00000000000000..16d13aaa99ffc0
--- /dev/null
+++ b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py
@@ -0,0 +1,18 @@
+"""Config flow for NEW_NAME."""
+import my_pypi_dependency
+
+from homeassistant.helpers import config_entry_flow
+from homeassistant import config_entries
+from .const import DOMAIN
+
+
+async def _async_has_devices(hass) -> bool:
+ """Return if there are devices that can be discovered."""
+ # TODO Check if there are any devices that can be discovered in the network.
+ devices = await hass.async_add_executor_job(my_pypi_dependency.discover)
+ return len(devices) > 0
+
+
+config_entry_flow.register_discovery_flow(
+ DOMAIN, "NEW_NAME", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN
+)
diff --git a/script/scaffold/templates/integration/integration/__init__.py b/script/scaffold/templates/integration/integration/__init__.py
new file mode 100644
index 00000000000000..7ab8b736782f62
--- /dev/null
+++ b/script/scaffold/templates/integration/integration/__init__.py
@@ -0,0 +1,12 @@
+"""The NEW_NAME integration."""
+import voluptuous as vol
+
+from .const import DOMAIN
+
+
+CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}})
+
+
+async def async_setup(hass, config):
+ """Set up the NEW_NAME integration."""
+ return True
diff --git a/script/scaffold/templates/integration/integration/const.py b/script/scaffold/templates/integration/integration/const.py
new file mode 100644
index 00000000000000..e8a1c494d49729
--- /dev/null
+++ b/script/scaffold/templates/integration/integration/const.py
@@ -0,0 +1,3 @@
+"""Constants for the NEW_NAME integration."""
+
+DOMAIN = "NEW_DOMAIN"
diff --git a/script/scaffold/templates/integration/integration/manifest.json b/script/scaffold/templates/integration/integration/manifest.json
new file mode 100644
index 00000000000000..cb4ecac61fb76c
--- /dev/null
+++ b/script/scaffold/templates/integration/integration/manifest.json
@@ -0,0 +1,11 @@
+{
+ "domain": "NEW_DOMAIN",
+ "name": "NEW_NAME",
+ "config_flow": false,
+ "documentation": "https://www.home-assistant.io/components/NEW_DOMAIN",
+ "requirements": [],
+ "ssdp": {},
+ "homekit": {},
+ "dependencies": [],
+ "codeowners": []
+}
diff --git a/script/test_docker b/script/test_docker
deleted file mode 100755
index bbea52a3a0bd61..00000000000000
--- a/script/test_docker
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/sh
-# Executes the tests with tox in a docker container.
-# Every argument is passed to tox to allow running only a subset of tests.
-# The following example will only run media_player tests:
-# ./test_docker -- tests/components/media_player/
-
-# Stop on errors
-set -e
-
-cd "$(dirname "$0")/.."
-
-docker build -t home-assistant-test -f virtualization/Docker/Dockerfile.dev .
-docker run --rm \
- -v `pwd`/.tox/:/usr/src/app/.tox/ \
- -t -i home-assistant-test \
- tox -e py36 ${@:2}
diff --git a/script/translations_upload b/script/translations_upload
index 22a2bbceba202f..fec8a3387c1dae 100755
--- a/script/translations_upload
+++ b/script/translations_upload
@@ -27,7 +27,7 @@ LANG_ISO=en
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Check Travis and Azure environment as well
-if [ "${CURRENT_BRANCH-}" != "dev" ] && [ "${TRAVIS_BRANCH-}" != "dev" ] && [ "${AZURE_BRANCH-}" != "dev" ]; then
+if [ "${CURRENT_BRANCH-}" != "dev" ] && [ "${AZURE_BRANCH-}" != "dev" ]; then
echo "Please only run the translations upload script from a clean checkout of dev."
exit 1
fi
diff --git a/script/travis_deploy b/script/travis_deploy
deleted file mode 100755
index 359f6a46077654..00000000000000
--- a/script/travis_deploy
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env bash
-
-# Safe bash settings
-# -e Exit on command fail
-# -u Exit on unset variable
-# -o pipefail Exit if piped command has error code
-set -eu -o pipefail
-
-cd "$(dirname "$0")/.."
-
-script/translations_upload
diff --git a/script/version_bump.py b/script/version_bump.py
index d5df8e66902f0d..de6638df30ba8c 100755
--- a/script/version_bump.py
+++ b/script/version_bump.py
@@ -108,7 +108,7 @@ def write_version(version):
content = re.sub("MAJOR_VERSION = .*\n", f"MAJOR_VERSION = {major}\n", content)
content = re.sub("MINOR_VERSION = .*\n", f"MINOR_VERSION = {minor}\n", content)
- content = re.sub("PATCH_VERSION = .*\n", f"PATCH_VERSION = '{patch}'\n", content)
+ content = re.sub("PATCH_VERSION = .*\n", f'PATCH_VERSION = "{patch}"\n', content)
with open("homeassistant/const.py", "wt") as fil:
content = fil.write(content)
@@ -126,9 +126,15 @@ def main():
"--commit", action="store_true", help="Create a version bump commit."
)
arguments = parser.parse_args()
+
+ if arguments.commit and subprocess.run(["git", "diff", "--quiet"]).returncode == 1:
+ print("Cannot use --commit because git is dirty.")
+ return
+
current = Version(const.__version__)
bumped = bump_version(current, arguments.type)
assert bumped > current, "BUG! New version is not newer than old version"
+
write_version(bumped)
if not arguments.commit:
diff --git a/setup.py b/setup.py
index 5ab8d74c64cf00..26f112bb008207 100755
--- a/setup.py
+++ b/setup.py
@@ -31,20 +31,20 @@
PACKAGES = find_packages(exclude=["tests", "tests.*"])
REQUIRES = [
- "aiohttp==3.5.4",
+ "aiohttp==3.6.1",
"astral==1.10.1",
"async_timeout==3.0.1",
"attrs==19.1.0",
"bcrypt==3.1.7",
"certifi>=2019.6.16",
'contextvars==2.4;python_version<"3.7"',
- "importlib-metadata==0.19",
+ "importlib-metadata==0.23",
"jinja2>=2.10.1",
"PyJWT==1.7.1",
# PyJWT has loose dependency. We want the latest one.
"cryptography==2.7",
"pip>=8.0.3",
- "python-slugify==3.0.3",
+ "python-slugify==3.0.4",
"pytz>=2019.02",
"pyyaml==5.1.2",
"requests==2.22.0",
diff --git a/tests/common.py b/tests/common.py
index 0e2f701c210a75..fda5c743222703 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -602,40 +602,40 @@ def __init__(
)
-class MockToggleDevice(entity.ToggleEntity):
+class MockToggleEntity(entity.ToggleEntity):
"""Provide a mock toggle device."""
- def __init__(self, name, state):
- """Initialize the mock device."""
+ def __init__(self, name, state, unique_id=None):
+ """Initialize the mock entity."""
self._name = name or DEVICE_DEFAULT_NAME
self._state = state
self.calls = []
@property
def name(self):
- """Return the name of the device if any."""
+ """Return the name of the entity if any."""
self.calls.append(("name", {}))
return self._name
@property
def state(self):
- """Return the name of the device if any."""
+ """Return the state of the entity if any."""
self.calls.append(("state", {}))
return self._state
@property
def is_on(self):
- """Return true if device is on."""
+ """Return true if entity is on."""
self.calls.append(("is_on", {}))
return self._state == STATE_ON
def turn_on(self, **kwargs):
- """Turn the device on."""
+ """Turn the entity on."""
self.calls.append(("turn_on", kwargs))
self._state = STATE_ON
def turn_off(self, **kwargs):
- """Turn the device off."""
+ """Turn the entity off."""
self.calls.append(("turn_off", kwargs))
self._state = STATE_OFF
@@ -1030,3 +1030,18 @@ def capture_events(event):
hass.bus.async_listen(event_name, capture_events)
return events
+
+
+@ha.callback
+def async_mock_signal(hass, signal):
+ """Catch all dispatches to a signal."""
+ calls = []
+
+ @ha.callback
+ def mock_signal_handler(*args):
+ """Mock service call."""
+ calls.append(args)
+
+ hass.helpers.dispatcher.async_dispatcher_connect(signal, mock_signal_handler)
+
+ return calls
diff --git a/tests/components/binary_sensor/test_device_automation.py b/tests/components/binary_sensor/test_device_automation.py
new file mode 100644
index 00000000000000..91124d47f4e4ef
--- /dev/null
+++ b/tests/components/binary_sensor/test_device_automation.py
@@ -0,0 +1,309 @@
+"""The test for binary_sensor device automation."""
+import pytest
+
+from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES
+from homeassistant.components.binary_sensor.device_automation import (
+ ENTITY_CONDITIONS,
+ ENTITY_TRIGGERS,
+)
+from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
+from homeassistant.setup import async_setup_component
+import homeassistant.components.automation as automation
+from homeassistant.components.device_automation import (
+ _async_get_device_automations as async_get_device_automations,
+)
+from homeassistant.helpers import device_registry
+
+from tests.common import (
+ MockConfigEntry,
+ async_mock_service,
+ mock_device_registry,
+ mock_registry,
+)
+
+
+@pytest.fixture
+def device_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+@pytest.fixture
+def calls(hass):
+ """Track calls to a mock serivce."""
+ return async_mock_service(hass, "test", "automation")
+
+
+def _same_lists(a, b):
+ if len(a) != len(b):
+ return False
+
+ for d in a:
+ if d not in b:
+ return False
+ return True
+
+
+async def test_get_actions(hass, device_reg, entity_reg):
+ """Test we get the expected actions from a binary_sensor."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ platform.ENTITIES["battery"].unique_id,
+ device_id=device_entry.id,
+ )
+
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ expected_actions = []
+ actions = await async_get_device_automations(
+ hass, "async_get_actions", device_entry.id
+ )
+ assert _same_lists(actions, expected_actions)
+
+
+async def test_get_conditions(hass, device_reg, entity_reg):
+ """Test we get the expected conditions from a binary_sensor."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ for device_class in DEVICE_CLASSES:
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ platform.ENTITIES[device_class].unique_id,
+ device_id=device_entry.id,
+ )
+
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ expected_conditions = [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": condition["type"],
+ "device_id": device_entry.id,
+ "entity_id": platform.ENTITIES[device_class].entity_id,
+ }
+ for device_class in DEVICE_CLASSES
+ for condition in ENTITY_CONDITIONS[device_class]
+ ]
+ conditions = await async_get_device_automations(
+ hass, "async_get_conditions", device_entry.id
+ )
+ assert _same_lists(conditions, expected_conditions)
+
+
+async def test_get_triggers(hass, device_reg, entity_reg):
+ """Test we get the expected triggers from a binary_sensor."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ for device_class in DEVICE_CLASSES:
+ entity_reg.async_get_or_create(
+ DOMAIN,
+ "test",
+ platform.ENTITIES[device_class].unique_id,
+ device_id=device_entry.id,
+ )
+
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ expected_triggers = [
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": trigger["type"],
+ "device_id": device_entry.id,
+ "entity_id": platform.ENTITIES[device_class].entity_id,
+ }
+ for device_class in DEVICE_CLASSES
+ for trigger in ENTITY_TRIGGERS[device_class]
+ ]
+ triggers = await async_get_device_automations(
+ hass, "async_get_triggers", device_entry.id
+ )
+ assert _same_lists(triggers, expected_triggers)
+
+
+async def test_if_fires_on_state_change(hass, calls):
+ """Test for on and off triggers firing."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ sensor1 = platform.ENTITIES["battery"]
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": sensor1.entity_id,
+ "type": "bat_low",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "bat_low {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(
+ (
+ "platform",
+ "entity_id",
+ "from_state.state",
+ "to_state.state",
+ "for",
+ )
+ )
+ },
+ },
+ },
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": sensor1.entity_id,
+ "type": "not_bat_low",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "not_bat_low {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(
+ (
+ "platform",
+ "entity_id",
+ "from_state.state",
+ "to_state.state",
+ "for",
+ )
+ )
+ },
+ },
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(sensor1.entity_id).state == STATE_ON
+ assert len(calls) == 0
+
+ hass.states.async_set(sensor1.entity_id, STATE_OFF)
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "not_bat_low state - {} - on - off - None".format(
+ sensor1.entity_id
+ )
+
+ hass.states.async_set(sensor1.entity_id, STATE_ON)
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["some"] == "bat_low state - {} - off - on - None".format(
+ sensor1.entity_id
+ )
+
+
+async def test_if_state(hass, calls):
+ """Test for turn_on and turn_off conditions."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ sensor1 = platform.ENTITIES["battery"]
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": sensor1.entity_id,
+ "type": "is_bat_low",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_on {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(("platform", "event.event_type"))
+ },
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event2"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": sensor1.entity_id,
+ "type": "is_not_bat_low",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_off {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(("platform", "event.event_type"))
+ },
+ },
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(sensor1.entity_id).state == STATE_ON
+ assert len(calls) == 0
+
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "is_on event - test_event1"
+
+ hass.states.async_set(sensor1.entity_id, STATE_OFF)
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["some"] == "is_off event - test_event2"
diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py
new file mode 100644
index 00000000000000..8db6fd4609e850
--- /dev/null
+++ b/tests/components/cast/test_home_assistant_cast.py
@@ -0,0 +1,50 @@
+"""Test Home Assistant Cast."""
+from unittest.mock import Mock, patch
+from homeassistant.components.cast import home_assistant_cast
+
+from tests.common import MockConfigEntry, async_mock_signal
+
+
+async def test_service_show_view(hass):
+ """Test we don't set app id in prod."""
+ hass.config.api = Mock(base_url="http://example.com")
+ await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry())
+ calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW)
+
+ await hass.services.async_call(
+ "cast",
+ "show_lovelace_view",
+ {"entity_id": "media_player.kitchen", "view_path": "mock_path"},
+ blocking=True,
+ )
+
+ assert len(calls) == 1
+ controller, entity_id, view_path = calls[0]
+ assert controller.hass_url == "http://example.com"
+ assert controller.client_id is None
+ # Verify user did not accidentally submit their dev app id
+ assert controller.supporting_app_id == "B12CE3CA"
+ assert entity_id == "media_player.kitchen"
+ assert view_path == "mock_path"
+
+
+async def test_use_cloud_url(hass):
+ """Test that we fall back to cloud url."""
+ hass.config.api = Mock(base_url="http://example.com")
+ await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry())
+ calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW)
+
+ with patch(
+ "homeassistant.components.cloud.async_remote_ui_url",
+ return_value="https://something.nabu.acas",
+ ):
+ await hass.services.async_call(
+ "cast",
+ "show_lovelace_view",
+ {"entity_id": "media_player.kitchen", "view_path": "mock_path"},
+ blocking=True,
+ )
+
+ assert len(calls) == 1
+ controller = calls[0][0]
+ assert controller.hass_url == "https://something.nabu.acas"
diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py
index 7995ba8f7817e0..8f33709fb2d1a4 100644
--- a/tests/components/cast/test_media_player.py
+++ b/tests/components/cast/test_media_player.py
@@ -22,12 +22,16 @@
@pytest.fixture(autouse=True)
def cast_mock():
"""Mock pychromecast."""
- with patch.dict(
- "sys.modules",
- {
- "pychromecast": MagicMock(),
- "pychromecast.controllers.multizone": MagicMock(),
- },
+ pycast_mock = MagicMock()
+
+ with patch(
+ "homeassistant.components.cast.media_player.pychromecast", pycast_mock
+ ), patch(
+ "homeassistant.components.cast.discovery.pychromecast", pycast_mock
+ ), patch(
+ "homeassistant.components.cast.helpers.dial", MagicMock()
+ ), patch(
+ "homeassistant.components.cast.media_player.MultizoneManager", MagicMock()
):
yield
@@ -73,7 +77,8 @@ async def async_setup_cast_internal_discovery(hass, config=None, discovery_info=
browser = MagicMock(zc={})
with patch(
- "pychromecast.start_discovery", return_value=(listener, browser)
+ "homeassistant.components.cast.discovery.pychromecast.start_discovery",
+ return_value=(listener, browser),
) as start_discovery:
add_entities = await async_setup_cast(hass, config, discovery_info)
await hass.async_block_till_done()
@@ -104,7 +109,8 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas
cast.CastStatusListener = MagicMock()
with patch(
- "pychromecast._get_chromecast_from_host", return_value=chromecast
+ "homeassistant.components.cast.discovery.pychromecast._get_chromecast_from_host",
+ return_value=chromecast,
) as get_chromecast:
await async_setup_component(
hass,
@@ -122,7 +128,8 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas
def test_start_discovery_called_once(hass):
"""Test pychromecast.start_discovery called exactly once."""
with patch(
- "pychromecast.start_discovery", return_value=(None, None)
+ "homeassistant.components.cast.discovery.pychromecast.start_discovery",
+ return_value=(None, None),
) as start_discovery:
yield from async_setup_cast(hass)
@@ -138,14 +145,17 @@ def test_stop_discovery_called_on_stop(hass):
browser = MagicMock(zc={})
with patch(
- "pychromecast.start_discovery", return_value=(None, browser)
+ "homeassistant.components.cast.discovery.pychromecast.start_discovery",
+ return_value=(None, browser),
) as start_discovery:
# start_discovery should be called with empty config
yield from async_setup_cast(hass, {})
assert start_discovery.call_count == 1
- with patch("pychromecast.stop_discovery") as stop_discovery:
+ with patch(
+ "homeassistant.components.cast.discovery.pychromecast.stop_discovery"
+ ) as stop_discovery:
# stop discovery should be called on shutdown
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
yield from hass.async_block_till_done()
@@ -153,7 +163,8 @@ def test_stop_discovery_called_on_stop(hass):
stop_discovery.assert_called_once_with(browser)
with patch(
- "pychromecast.start_discovery", return_value=(None, browser)
+ "homeassistant.components.cast.discovery.pychromecast.start_discovery",
+ return_value=(None, browser),
) as start_discovery:
# start_discovery should be called again on re-startup
yield from async_setup_cast(hass)
@@ -173,7 +184,10 @@ async def test_internal_discovery_callback_fill_out(hass):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
- with patch("pychromecast.dial.get_device_status", return_value=full_info):
+ with patch(
+ "homeassistant.components.cast.helpers.dial.get_device_status",
+ return_value=full_info,
+ ):
signal = MagicMock()
async_dispatcher_connect(hass, "cast_discovered", signal)
@@ -210,7 +224,7 @@ async def test_normal_chromecast_not_starting_discovery(hass):
"""Test cast platform not starting discovery when not required."""
# pylint: disable=no-member
with patch(
- "homeassistant.components.cast.media_player." "_setup_internal_discovery"
+ "homeassistant.components.cast.media_player.setup_internal_discovery"
) as setup_discovery:
# normal (non-group) chromecast shouldn't start discovery.
add_entities = await async_setup_cast(hass, {"host": "host1"})
@@ -275,7 +289,10 @@ async def test_entity_media_states(hass: HomeAssistantType):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
- with patch("pychromecast.dial.get_device_status", return_value=full_info):
+ with patch(
+ "homeassistant.components.cast.helpers.dial.get_device_status",
+ return_value=full_info,
+ ):
chromecast, entity = await async_setup_media_player_cast(hass, info)
entity._available = True
@@ -330,7 +347,10 @@ async def test_group_media_states(hass: HomeAssistantType):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
- with patch("pychromecast.dial.get_device_status", return_value=full_info):
+ with patch(
+ "homeassistant.components.cast.helpers.dial.get_device_status",
+ return_value=full_info,
+ ):
chromecast, entity = await async_setup_media_player_cast(hass, info)
entity._available = True
@@ -377,7 +397,10 @@ async def test_dynamic_group_media_states(hass: HomeAssistantType):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
- with patch("pychromecast.dial.get_device_status", return_value=full_info):
+ with patch(
+ "homeassistant.components.cast.helpers.dial.get_device_status",
+ return_value=full_info,
+ ):
chromecast, entity = await async_setup_media_player_cast(hass, info)
entity._available = True
@@ -426,12 +449,14 @@ async def test_group_media_control(hass: HomeAssistantType):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
- with patch("pychromecast.dial.get_device_status", return_value=full_info):
+ with patch(
+ "homeassistant.components.cast.helpers.dial.get_device_status",
+ return_value=full_info,
+ ):
chromecast, entity = await async_setup_media_player_cast(hass, info)
entity._available = True
- entity.schedule_update_ha_state()
- await hass.async_block_till_done()
+ entity.async_write_ha_state()
state = hass.states.get("media_player.speaker")
assert state is not None
@@ -480,7 +505,10 @@ async def test_dynamic_group_media_control(hass: HomeAssistantType):
info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID
)
- with patch("pychromecast.dial.get_device_status", return_value=full_info):
+ with patch(
+ "homeassistant.components.cast.helpers.dial.get_device_status",
+ return_value=full_info,
+ ):
chromecast, entity = await async_setup_media_player_cast(hass, info)
entity._available = True
@@ -529,7 +557,10 @@ async def test_disconnect_on_stop(hass: HomeAssistantType):
"""Test cast device disconnects socket on stop."""
info = get_fake_chromecast_info()
- with patch("pychromecast.dial.get_device_status", return_value=info):
+ with patch(
+ "homeassistant.components.cast.helpers.dial.get_device_status",
+ return_value=info,
+ ):
chromecast, _ = await async_setup_media_player_cast(hass, info)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py
index f8c99496a563eb..f44e65512e35e5 100644
--- a/tests/components/cert_expiry/test_config_flow.py
+++ b/tests/components/cert_expiry/test_config_flow.py
@@ -8,7 +8,7 @@
from homeassistant.components.cert_expiry.const import DEFAULT_PORT
from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST
-from tests.common import MockConfigEntry
+from tests.common import MockConfigEntry, mock_coro
NAME = "Cert Expiry test 1 2 3"
PORT = 443
@@ -20,7 +20,7 @@ def mock_controller():
"""Mock a successfull _prt_in_configuration_exists."""
with patch(
"homeassistant.components.cert_expiry.config_flow.CertexpiryConfigFlow._test_connection",
- return_value=True,
+ side_effect=lambda *_: mock_coro(True),
):
yield
diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py
index acf06728d0d0d5..2f42193291c8dd 100644
--- a/tests/components/deconz/test_binary_sensor.py
+++ b/tests/components/deconz/test_binary_sensor.py
@@ -1,81 +1,54 @@
"""deCONZ binary sensor platform tests."""
-from unittest.mock import Mock, patch
+from copy import deepcopy
-from tests.common import mock_coro
-
-from homeassistant import config_entries
from homeassistant.components import deconz
-from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
import homeassistant.components.binary_sensor as binary_sensor
+from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration
-SENSOR = {
+SENSORS = {
"1": {
- "id": "Sensor 1 id",
- "name": "Sensor 1 name",
+ "id": "Presence sensor id",
+ "name": "Presence sensor",
"type": "ZHAPresence",
- "state": {"presence": False},
- "config": {},
+ "state": {"dark": False, "presence": False},
+ "config": {"on": True, "reachable": True, "temperature": 10},
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
"2": {
- "id": "Sensor 2 id",
- "name": "Sensor 2 name",
+ "id": "Temperature sensor id",
+ "name": "Temperature sensor",
"type": "ZHATemperature",
"state": {"temperature": False},
"config": {},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ "3": {
+ "id": "CLIP presence sensor id",
+ "name": "CLIP presence sensor",
+ "type": "CLIPPresence",
+ "state": {},
+ "config": {},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ },
+ "4": {
+ "id": "Vibration sensor id",
+ "name": "Vibration sensor",
+ "type": "ZHAVibration",
+ "state": {
+ "orientation": [1, 2, 3],
+ "tiltangle": 36,
+ "vibration": True,
+ "vibrationstrength": 10,
+ },
+ "config": {"on": True, "reachable": True, "temperature": 10},
+ "uniqueid": "00:00:00:00:00:00:00:03-00",
},
}
-ENTRY_CONFIG = {
- deconz.config_flow.CONF_API_KEY: "ABCDEF",
- deconz.config_flow.CONF_BRIDGEID: "0123456789",
- deconz.config_flow.CONF_HOST: "1.2.3.4",
- deconz.config_flow.CONF_PORT: 80,
-}
-
-ENTRY_OPTIONS = {
- deconz.const.CONF_ALLOW_CLIP_SENSOR: True,
- deconz.const.CONF_ALLOW_DECONZ_GROUPS: True,
-}
-
-
-async def setup_gateway(hass, data, allow_clip_sensor=True):
- """Load the deCONZ binary sensor platform."""
- from pydeconz import DeconzSession
-
- loop = Mock()
- session = Mock()
-
- ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor
-
- config_entry = config_entries.ConfigEntry(
- 1,
- deconz.DOMAIN,
- "Mock Title",
- ENTRY_CONFIG,
- "test",
- config_entries.CONN_CLASS_LOCAL_PUSH,
- system_options={},
- options=ENTRY_OPTIONS,
- )
- gateway = deconz.DeconzGateway(hass, config_entry)
- gateway.api = DeconzSession(loop, session, **config_entry.data)
- gateway.api.config = Mock()
- hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway}
-
- with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)):
- await gateway.api.async_load_parameters()
-
- await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor")
- # To flush out the service call to update the group
- await hass.async_block_till_done()
- return gateway
-
-
async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
assert (
@@ -89,58 +62,98 @@ async def test_platform_manually_configured(hass):
async def test_no_binary_sensors(hass):
"""Test that no sensors in deconz results in no sensor entities."""
- data = {}
- gateway = await setup_gateway(hass, data)
- assert len(hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids) == 0
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert len(gateway.deconz_ids) == 0
assert len(hass.states.async_all()) == 0
async def test_binary_sensors(hass):
"""Test successful creation of binary sensor entities."""
- data = {"sensors": SENSOR}
- gateway = await setup_gateway(hass, data)
- assert "binary_sensor.sensor_1_name" in gateway.deconz_ids
- assert "binary_sensor.sensor_2_name" not in gateway.deconz_ids
- assert len(hass.states.async_all()) == 1
-
- hass.data[deconz.DOMAIN][gateway.bridgeid].api.sensors["1"].async_update(
- {"state": {"on": False}}
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = deepcopy(SENSORS)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
)
+ assert "binary_sensor.presence_sensor" in gateway.deconz_ids
+ assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids
+ assert "binary_sensor.clip_presence_sensor" not in gateway.deconz_ids
+ assert "binary_sensor.vibration_sensor" in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 3
+ presence_sensor = hass.states.get("binary_sensor.presence_sensor")
+ assert presence_sensor.state == "off"
-async def test_add_new_sensor(hass):
- """Test successful creation of sensor entities."""
- data = {}
- gateway = await setup_gateway(hass, data)
- sensor = Mock()
- sensor.name = "name"
- sensor.type = "ZHAPresence"
- sensor.BINARY = True
- sensor.uniqueid = "1"
- sensor.register_async_callback = Mock()
- async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor])
- await hass.async_block_till_done()
- assert "binary_sensor.name" in gateway.deconz_ids
-
-
-async def test_do_not_allow_clip_sensor(hass):
- """Test that clip sensors can be ignored."""
- data = {}
- gateway = await setup_gateway(hass, data, allow_clip_sensor=False)
- sensor = Mock()
- sensor.name = "name"
- sensor.type = "CLIPPresence"
- sensor.register_async_callback = Mock()
- async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor])
- await hass.async_block_till_done()
- assert len(gateway.deconz_ids) == 0
+ temperature_sensor = hass.states.get("binary_sensor.temperature_sensor")
+ assert temperature_sensor is None
+ clip_presence_sensor = hass.states.get("binary_sensor.clip_presence_sensor")
+ assert clip_presence_sensor is None
+
+ vibration_sensor = hass.states.get("binary_sensor.vibration_sensor")
+ assert vibration_sensor.state == "on"
+
+ gateway.api.sensors["1"].async_update({"state": {"presence": True}})
+ await hass.async_block_till_done()
-async def test_unload_switch(hass):
- """Test that it works to unload switch entities."""
- data = {"sensors": SENSOR}
- gateway = await setup_gateway(hass, data)
+ presence_sensor = hass.states.get("binary_sensor.presence_sensor")
+ assert presence_sensor.state == "on"
await gateway.async_reset()
assert len(hass.states.async_all()) == 0
+
+
+async def test_allow_clip_sensor(hass):
+ """Test that CLIP sensors can be allowed."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = deepcopy(SENSORS)
+ gateway = await setup_deconz_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True},
+ get_state_response=data,
+ )
+ assert "binary_sensor.presence_sensor" in gateway.deconz_ids
+ assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids
+ assert "binary_sensor.clip_presence_sensor" in gateway.deconz_ids
+ assert "binary_sensor.vibration_sensor" in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 4
+
+ presence_sensor = hass.states.get("binary_sensor.presence_sensor")
+ assert presence_sensor.state == "off"
+
+ temperature_sensor = hass.states.get("binary_sensor.temperature_sensor")
+ assert temperature_sensor is None
+
+ clip_presence_sensor = hass.states.get("binary_sensor.clip_presence_sensor")
+ assert clip_presence_sensor.state == "off"
+
+ vibration_sensor = hass.states.get("binary_sensor.vibration_sensor")
+ assert vibration_sensor.state == "on"
+
+
+async def test_add_new_binary_sensor(hass):
+ """Test that adding a new binary sensor works."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert len(gateway.deconz_ids) == 0
+
+ state_added = {
+ "t": "event",
+ "e": "added",
+ "r": "sensors",
+ "id": "1",
+ "sensor": deepcopy(SENSORS["1"]),
+ }
+ gateway.api.async_event_handler(state_added)
+ await hass.async_block_till_done()
+
+ assert "binary_sensor.presence_sensor" in gateway.deconz_ids
+
+ presence_sensor = hass.states.get("binary_sensor.presence_sensor")
+ assert presence_sensor.state == "off"
diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py
index 1547f58a12b1bc..cee91f00c4283b 100644
--- a/tests/components/deconz/test_climate.py
+++ b/tests/components/deconz/test_climate.py
@@ -1,23 +1,19 @@
"""deCONZ climate platform tests."""
from copy import deepcopy
-from unittest.mock import Mock, patch
-import asynctest
+from asynctest import patch
-from homeassistant import config_entries
from homeassistant.components import deconz
-from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
import homeassistant.components.climate as climate
-from tests.common import mock_coro
+from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration
-
-SENSOR = {
+SENSORS = {
"1": {
- "id": "Climate 1 id",
- "name": "Climate 1 name",
+ "id": "Thermostat id",
+ "name": "Thermostat",
"type": "ZHAThermostat",
"state": {"on": True, "temperature": 2260, "valve": 30},
"config": {
@@ -30,63 +26,23 @@
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
"2": {
- "id": "Sensor 2 id",
- "name": "Sensor 2 name",
+ "id": "Presence sensor id",
+ "name": "Presence sensor",
"type": "ZHAPresence",
"state": {"presence": False},
- "config": {},
+ "config": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ "3": {
+ "id": "CLIP thermostat id",
+ "name": "CLIP thermostat",
+ "type": "CLIPThermostat",
+ "state": {"on": True, "temperature": 2260, "valve": 30},
+ "config": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
},
}
-ENTRY_CONFIG = {
- deconz.config_flow.CONF_API_KEY: "ABCDEF",
- deconz.config_flow.CONF_BRIDGEID: "0123456789",
- deconz.config_flow.CONF_HOST: "1.2.3.4",
- deconz.config_flow.CONF_PORT: 80,
-}
-
-ENTRY_OPTIONS = {
- deconz.const.CONF_ALLOW_CLIP_SENSOR: True,
- deconz.const.CONF_ALLOW_DECONZ_GROUPS: True,
-}
-
-
-async def setup_gateway(hass, data, allow_clip_sensor=True):
- """Load the deCONZ sensor platform."""
- from pydeconz import DeconzSession
-
- response = Mock(
- status=200, json=asynctest.CoroutineMock(), text=asynctest.CoroutineMock()
- )
- response.content_type = "application/json"
-
- session = Mock(put=asynctest.CoroutineMock(return_value=response))
-
- ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor
-
- config_entry = config_entries.ConfigEntry(
- 1,
- deconz.DOMAIN,
- "Mock Title",
- ENTRY_CONFIG,
- "test",
- config_entries.CONN_CLASS_LOCAL_PUSH,
- system_options={},
- options=ENTRY_OPTIONS,
- )
- gateway = deconz.DeconzGateway(hass, config_entry)
- gateway.api = DeconzSession(hass.loop, session, **config_entry.data)
- gateway.api.config = Mock()
- hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway}
-
- with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)):
- await gateway.api.async_load_parameters()
-
- await hass.config_entries.async_forward_entry_setup(config_entry, "climate")
- # To flush out the service call to update the group
- await hass.async_block_till_done()
- return gateway
-
async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
@@ -101,69 +57,159 @@ async def test_platform_manually_configured(hass):
async def test_no_sensors(hass):
"""Test that no sensors in deconz results in no climate entities."""
- gateway = await setup_gateway(hass, {})
- assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids
- assert not hass.states.async_all()
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert len(gateway.deconz_ids) == 0
+ assert len(hass.states.async_all()) == 0
async def test_climate_devices(hass):
"""Test successful creation of sensor entities."""
- gateway = await setup_gateway(hass, {"sensors": deepcopy(SENSOR)})
- assert "climate.climate_1_name" in gateway.deconz_ids
- assert "sensor.sensor_2_name" not in gateway.deconz_ids
- assert len(hass.states.async_all()) == 1
-
- gateway.api.sensors["1"].async_update({"state": {"on": False}})
-
- await hass.services.async_call(
- "climate",
- "set_hvac_mode",
- {"entity_id": "climate.climate_1_name", "hvac_mode": "auto"},
- blocking=True,
- )
- gateway.api.session.put.assert_called_with(
- "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"mode": "auto"}'
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = deepcopy(SENSORS)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
)
+ assert "climate.thermostat" in gateway.deconz_ids
+ assert "sensor.thermostat" not in gateway.deconz_ids
+ assert "sensor.thermostat_battery_level" in gateway.deconz_ids
+ assert "climate.presence_sensor" not in gateway.deconz_ids
+ assert "climate.clip_thermostat" not in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 3
+
+ thermostat = hass.states.get("climate.thermostat")
+ assert thermostat.state == "auto"
- await hass.services.async_call(
- "climate",
- "set_hvac_mode",
- {"entity_id": "climate.climate_1_name", "hvac_mode": "heat"},
- blocking=True,
- )
- gateway.api.session.put.assert_called_with(
- "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"mode": "heat"}'
- )
+ thermostat = hass.states.get("sensor.thermostat")
+ assert thermostat is None
- await hass.services.async_call(
- "climate",
- "set_hvac_mode",
- {"entity_id": "climate.climate_1_name", "hvac_mode": "off"},
- blocking=True,
- )
- gateway.api.session.put.assert_called_with(
- "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"mode": "off"}'
- )
+ thermostat_battery_level = hass.states.get("sensor.thermostat_battery_level")
+ assert thermostat_battery_level.state == "100"
- await hass.services.async_call(
- "climate",
- "set_temperature",
- {"entity_id": "climate.climate_1_name", "temperature": 20},
- blocking=True,
- )
- gateway.api.session.put.assert_called_with(
- "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"heatsetpoint": 2000.0}'
+ presence_sensor = hass.states.get("climate.presence_sensor")
+ assert presence_sensor is None
+
+ clip_thermostat = hass.states.get("climate.clip_thermostat")
+ assert clip_thermostat is None
+
+ thermostat_device = gateway.api.sensors["1"]
+
+ thermostat_device.async_update({"config": {"mode": "off"}})
+ await hass.async_block_till_done()
+
+ thermostat = hass.states.get("climate.thermostat")
+ assert thermostat.state == "off"
+
+ thermostat_device.async_update({"config": {"mode": "other"}, "state": {"on": True}})
+ await hass.async_block_till_done()
+
+ thermostat = hass.states.get("climate.thermostat")
+ assert thermostat.state == "heat"
+
+ thermostat_device.async_update({"state": {"on": False}})
+ await hass.async_block_till_done()
+
+ thermostat = hass.states.get("climate.thermostat")
+ assert thermostat.state == "off"
+
+ # Verify service calls
+
+ with patch.object(
+ thermostat_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ climate.DOMAIN,
+ climate.SERVICE_SET_HVAC_MODE,
+ {"entity_id": "climate.thermostat", "hvac_mode": "auto"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("/sensors/1/config", {"mode": "auto"})
+
+ with patch.object(
+ thermostat_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ climate.DOMAIN,
+ climate.SERVICE_SET_HVAC_MODE,
+ {"entity_id": "climate.thermostat", "hvac_mode": "heat"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("/sensors/1/config", {"mode": "heat"})
+
+ with patch.object(
+ thermostat_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ climate.DOMAIN,
+ climate.SERVICE_SET_HVAC_MODE,
+ {"entity_id": "climate.thermostat", "hvac_mode": "off"},
+ blocking=True,
+ )
+ set_callback.assert_called_with("/sensors/1/config", {"mode": "off"})
+
+ with patch.object(
+ thermostat_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ climate.DOMAIN,
+ climate.SERVICE_SET_TEMPERATURE,
+ {"entity_id": "climate.thermostat", "temperature": 20},
+ blocking=True,
+ )
+ set_callback.assert_called_with("/sensors/1/config", {"heatsetpoint": 2000.0})
+
+ await gateway.async_reset()
+
+ assert len(hass.states.async_all()) == 0
+
+
+async def test_clip_climate_device(hass):
+ """Test successful creation of sensor entities."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = deepcopy(SENSORS)
+ gateway = await setup_deconz_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True},
+ get_state_response=data,
)
+ assert "climate.thermostat" in gateway.deconz_ids
+ assert "sensor.thermostat" not in gateway.deconz_ids
+ assert "sensor.thermostat_battery_level" in gateway.deconz_ids
+ assert "climate.presence_sensor" not in gateway.deconz_ids
+ assert "climate.clip_thermostat" in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 4
+
+ thermostat = hass.states.get("climate.thermostat")
+ assert thermostat.state == "auto"
+
+ thermostat = hass.states.get("sensor.thermostat")
+ assert thermostat is None
- assert len(gateway.api.session.put.mock_calls) == 4
+ thermostat_battery_level = hass.states.get("sensor.thermostat_battery_level")
+ assert thermostat_battery_level.state == "100"
+
+ presence_sensor = hass.states.get("climate.presence_sensor")
+ assert presence_sensor is None
+
+ clip_thermostat = hass.states.get("climate.clip_thermostat")
+ assert clip_thermostat.state == "heat"
async def test_verify_state_update(hass):
"""Test that state update properly."""
- gateway = await setup_gateway(hass, {"sensors": deepcopy(SENSOR)})
- assert "climate.climate_1_name" in gateway.deconz_ids
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = deepcopy(SENSORS)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert "climate.thermostat" in gateway.deconz_ids
- thermostat = hass.states.get("climate.climate_1_name")
+ thermostat = hass.states.get("climate.thermostat")
assert thermostat.state == "auto"
state_update = {
@@ -174,44 +220,32 @@ async def test_verify_state_update(hass):
"state": {"on": False},
}
gateway.api.async_event_handler(state_update)
-
await hass.async_block_till_done()
- assert len(hass.states.async_all()) == 1
- thermostat = hass.states.get("climate.climate_1_name")
+ thermostat = hass.states.get("climate.thermostat")
assert thermostat.state == "auto"
assert gateway.api.sensors["1"].changed_keys == {"state", "r", "t", "on", "e", "id"}
async def test_add_new_climate_device(hass):
- """Test successful creation of climate entities."""
- gateway = await setup_gateway(hass, {})
- sensor = Mock()
- sensor.name = "name"
- sensor.type = "ZHAThermostat"
- sensor.uniqueid = "1"
- sensor.register_async_callback = Mock()
- async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor])
- await hass.async_block_till_done()
- assert "climate.name" in gateway.deconz_ids
-
-
-async def test_do_not_allow_clipsensor(hass):
- """Test that clip sensors can be ignored."""
- gateway = await setup_gateway(hass, {}, allow_clip_sensor=False)
- sensor = Mock()
- sensor.name = "name"
- sensor.type = "CLIPThermostat"
- sensor.register_async_callback = Mock()
- async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor])
- await hass.async_block_till_done()
+ """Test that adding a new climate device works."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
assert len(gateway.deconz_ids) == 0
+ state_added = {
+ "t": "event",
+ "e": "added",
+ "r": "sensors",
+ "id": "1",
+ "sensor": deepcopy(SENSORS["1"]),
+ }
+ gateway.api.async_event_handler(state_added)
+ await hass.async_block_till_done()
-async def test_unload_sensor(hass):
- """Test that it works to unload sensor entities."""
- gateway = await setup_gateway(hass, {"sensors": SENSOR})
-
- await gateway.async_reset()
+ assert "climate.thermostat" in gateway.deconz_ids
- assert len(hass.states.async_all()) == 0
+ thermostat = hass.states.get("climate.thermostat")
+ assert thermostat.state == "auto"
diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py
index ea3abead02870e..d7071d6daef08c 100644
--- a/tests/components/deconz/test_config_flow.py
+++ b/tests/components/deconz/test_config_flow.py
@@ -234,41 +234,6 @@ async def test_bridge_discovery_update_existing_entry(hass):
assert entry.data[config_flow.CONF_HOST] == "mock-deconz"
-async def test_import_without_api_key(hass):
- """Test importing a host without an API key."""
- result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
- data={config_flow.CONF_HOST: "1.2.3.4"},
- context={"source": "import"},
- )
-
- assert result["type"] == "form"
- assert result["step_id"] == "link"
-
-
-async def test_import_with_api_key(hass):
- """Test importing a host with an API key."""
- result = await hass.config_entries.flow.async_init(
- config_flow.DOMAIN,
- data={
- config_flow.CONF_BRIDGEID: "id",
- config_flow.CONF_HOST: "mock-deconz",
- config_flow.CONF_PORT: 80,
- config_flow.CONF_API_KEY: "1234567890ABCDEF",
- },
- context={"source": "import"},
- )
-
- assert result["type"] == "create_entry"
- assert result["title"] == "deCONZ-id"
- assert result["data"] == {
- config_flow.CONF_BRIDGEID: "id",
- config_flow.CONF_HOST: "mock-deconz",
- config_flow.CONF_PORT: 80,
- config_flow.CONF_API_KEY: "1234567890ABCDEF",
- }
-
-
async def test_create_entry(hass, aioclient_mock):
"""Test that _create_entry work and that bridgeid can be requested."""
aioclient_mock.get(
@@ -382,3 +347,29 @@ async def test_hassio_confirm(hass):
config_flow.CONF_BRIDGEID: "id",
config_flow.CONF_API_KEY: "1234567890ABCDEF",
}
+
+
+async def test_option_flow(hass):
+ """Test config flow selection of one of two bridges."""
+ entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=None)
+ hass.config_entries._entries.append(entry)
+
+ flow = await hass.config_entries.options._async_create_flow(
+ entry.entry_id, context={"source": "test"}, data=None
+ )
+
+ result = await flow.async_step_init()
+ assert result["type"] == "form"
+ assert result["step_id"] == "deconz_devices"
+
+ result = await flow.async_step_deconz_devices(
+ user_input={
+ config_flow.CONF_ALLOW_CLIP_SENSOR: False,
+ config_flow.CONF_ALLOW_DECONZ_GROUPS: False,
+ }
+ )
+ assert result["type"] == "create_entry"
+ assert result["data"] == {
+ config_flow.CONF_ALLOW_CLIP_SENSOR: False,
+ config_flow.CONF_ALLOW_DECONZ_GROUPS: False,
+ }
diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py
index 7230ff4fb7bdff..5c7ee48a78a2eb 100644
--- a/tests/components/deconz/test_cover.py
+++ b/tests/components/deconz/test_cover.py
@@ -1,84 +1,42 @@
"""deCONZ cover platform tests."""
-from unittest.mock import Mock, patch
+from copy import deepcopy
+
+from asynctest import patch
-from homeassistant import config_entries
from homeassistant.components import deconz
-from homeassistant.components.deconz.const import COVER_TYPES
-from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
import homeassistant.components.cover as cover
-from tests.common import mock_coro
+from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration
-SUPPORTED_COVERS = {
+COVERS = {
"1": {
- "id": "Cover 1 id",
- "name": "Cover 1 name",
+ "id": "Level controllable cover id",
+ "name": "Level controllable cover",
"type": "Level controllable output",
"state": {"bri": 255, "on": False, "reachable": True},
"modelid": "Not zigbee spec",
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
"2": {
- "id": "Cover 2 id",
- "name": "Cover 2 name",
+ "id": "Window covering device id",
+ "name": "Window covering device",
"type": "Window covering device",
"state": {"bri": 255, "on": True, "reachable": True},
"modelid": "lumi.curtain",
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
},
-}
-
-UNSUPPORTED_COVER = {
- "1": {
- "id": "Cover id",
- "name": "Unsupported switch",
+ "3": {
+ "id": "Unsupported cover id",
+ "name": "Unsupported cover",
"type": "Not a cover",
- "state": {},
- }
-}
-
-
-ENTRY_CONFIG = {
- deconz.const.CONF_ALLOW_CLIP_SENSOR: True,
- deconz.const.CONF_ALLOW_DECONZ_GROUPS: True,
- deconz.config_flow.CONF_API_KEY: "ABCDEF",
- deconz.config_flow.CONF_BRIDGEID: "0123456789",
- deconz.config_flow.CONF_HOST: "1.2.3.4",
- deconz.config_flow.CONF_PORT: 80,
+ "state": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ },
}
-async def setup_gateway(hass, data):
- """Load the deCONZ cover platform."""
- from pydeconz import DeconzSession
-
- loop = Mock()
- session = Mock()
-
- config_entry = config_entries.ConfigEntry(
- 1,
- deconz.DOMAIN,
- "Mock Title",
- ENTRY_CONFIG,
- "test",
- config_entries.CONN_CLASS_LOCAL_PUSH,
- system_options={},
- )
- gateway = deconz.DeconzGateway(hass, config_entry)
- gateway.api = DeconzSession(loop, session, **config_entry.data)
- gateway.api.config = Mock()
- hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway}
-
- with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)):
- await gateway.api.async_load_parameters()
-
- await hass.config_entries.async_forward_entry_setup(config_entry, "cover")
- # To flush out the service call to update the group
- await hass.async_block_till_done()
- return gateway
-
-
async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
assert (
@@ -92,64 +50,73 @@ async def test_platform_manually_configured(hass):
async def test_no_covers(hass):
"""Test that no cover entities are created."""
- gateway = await setup_gateway(hass, {})
- assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert len(gateway.deconz_ids) == 0
assert len(hass.states.async_all()) == 0
async def test_cover(hass):
"""Test that all supported cover entities are created."""
- with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)):
- gateway = await setup_gateway(hass, {"lights": SUPPORTED_COVERS})
- assert "cover.cover_1_name" in gateway.deconz_ids
- assert len(SUPPORTED_COVERS) == len(COVER_TYPES)
- assert len(hass.states.async_all()) == 3
-
- cover_1 = hass.states.get("cover.cover_1_name")
- assert cover_1 is not None
- assert cover_1.state == "open"
-
- gateway.api.lights["1"].async_update({})
-
- await hass.services.async_call(
- "cover", "open_cover", {"entity_id": "cover.cover_1_name"}, blocking=True
- )
- await hass.services.async_call(
- "cover", "close_cover", {"entity_id": "cover.cover_1_name"}, blocking=True
- )
- await hass.services.async_call(
- "cover", "stop_cover", {"entity_id": "cover.cover_1_name"}, blocking=True
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["lights"] = deepcopy(COVERS)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
)
+ assert "cover.level_controllable_cover" in gateway.deconz_ids
+ assert "cover.window_covering_device" in gateway.deconz_ids
+ assert "cover.unsupported_cover" not in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 5
- await hass.services.async_call(
- "cover", "close_cover", {"entity_id": "cover.cover_2_name"}, blocking=True
- )
+ level_controllable_cover = hass.states.get("cover.level_controllable_cover")
+ assert level_controllable_cover.state == "open"
+ level_controllable_cover_device = gateway.api.lights["1"]
-async def test_add_new_cover(hass):
- """Test successful creation of cover entity."""
- data = {}
- gateway = await setup_gateway(hass, data)
- cover = Mock()
- cover.name = "name"
- cover.type = "Level controllable output"
- cover.uniqueid = "1"
- cover.register_async_callback = Mock()
- async_dispatcher_send(hass, gateway.async_event_new_device("light"), [cover])
+ level_controllable_cover_device.async_update({"state": {"on": True}})
await hass.async_block_till_done()
- assert "cover.name" in gateway.deconz_ids
-
-async def test_unsupported_cover(hass):
- """Test that unsupported covers are not created."""
- await setup_gateway(hass, {"lights": UNSUPPORTED_COVER})
- assert len(hass.states.async_all()) == 0
-
-
-async def test_unload_cover(hass):
- """Test that it works to unload switch entities."""
- gateway = await setup_gateway(hass, {"lights": SUPPORTED_COVERS})
+ level_controllable_cover = hass.states.get("cover.level_controllable_cover")
+ assert level_controllable_cover.state == "closed"
+
+ with patch.object(
+ level_controllable_cover_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ cover.DOMAIN,
+ cover.SERVICE_OPEN_COVER,
+ {"entity_id": "cover.level_controllable_cover"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("/lights/1/state", {"on": False})
+
+ with patch.object(
+ level_controllable_cover_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ cover.DOMAIN,
+ cover.SERVICE_CLOSE_COVER,
+ {"entity_id": "cover.level_controllable_cover"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("/lights/1/state", {"on": True, "bri": 255})
+
+ with patch.object(
+ level_controllable_cover_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ cover.DOMAIN,
+ cover.SERVICE_STOP_COVER,
+ {"entity_id": "cover.level_controllable_cover"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("/lights/1/state", {"bri_inc": 0})
await gateway.async_reset()
- assert len(hass.states.async_all()) == 1
+ assert len(hass.states.async_all()) == 2
diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py
new file mode 100644
index 00000000000000..ade9aa02ad4afb
--- /dev/null
+++ b/tests/components/deconz/test_deconz_event.py
@@ -0,0 +1,74 @@
+"""Test deCONZ remote events."""
+from copy import deepcopy
+
+from asynctest import Mock
+
+from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT
+
+from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration
+
+SENSORS = {
+ "1": {
+ "id": "Switch 1 id",
+ "name": "Switch 1",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000},
+ "config": {},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ "2": {
+ "id": "Switch 2 id",
+ "name": "Switch 2",
+ "type": "ZHASwitch",
+ "state": {"buttonevent": 1000},
+ "config": {"battery": 100},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ },
+}
+
+
+async def test_deconz_events(hass):
+ """Test successful creation of deconz events."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = deepcopy(SENSORS)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert "sensor.switch_1" not in gateway.deconz_ids
+ assert "sensor.switch_1_battery_level" not in gateway.deconz_ids
+ assert "sensor.switch_2" not in gateway.deconz_ids
+ assert "sensor.switch_2_battery_level" in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 1
+ assert len(gateway.events) == 2
+
+ switch_1 = hass.states.get("sensor.switch_1")
+ assert switch_1 is None
+
+ switch_1_battery_level = hass.states.get("sensor.switch_1_battery_level")
+ assert switch_1_battery_level is None
+
+ switch_2 = hass.states.get("sensor.switch_2")
+ assert switch_2 is None
+
+ switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level")
+ assert switch_2_battery_level.state == "100"
+
+ mock_listener = Mock()
+ unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener)
+
+ gateway.api.sensors["1"].async_update({"state": {"buttonevent": 2000}})
+ await hass.async_block_till_done()
+
+ assert len(mock_listener.mock_calls) == 1
+ assert mock_listener.mock_calls[0][1][0].data == {
+ "id": "switch_1",
+ "unique_id": "00:00:00:00:00:00:00:01",
+ "event": 2000,
+ }
+
+ unsub()
+
+ await gateway.async_reset()
+
+ assert len(hass.states.async_all()) == 0
+ assert len(gateway.events) == 0
diff --git a/tests/components/deconz/test_device_automation.py b/tests/components/deconz/test_device_automation.py
new file mode 100644
index 00000000000000..0be566d4b52e43
--- /dev/null
+++ b/tests/components/deconz/test_device_automation.py
@@ -0,0 +1,138 @@
+"""deCONZ device automation tests."""
+from asynctest import patch
+
+from homeassistant import config_entries
+from homeassistant.components import deconz
+from homeassistant.components.device_automation import (
+ _async_get_device_automations as async_get_device_automations,
+)
+
+BRIDGEID = "0123456789"
+
+ENTRY_CONFIG = {
+ deconz.config_flow.CONF_API_KEY: "ABCDEF",
+ deconz.config_flow.CONF_BRIDGEID: BRIDGEID,
+ deconz.config_flow.CONF_HOST: "1.2.3.4",
+ deconz.config_flow.CONF_PORT: 80,
+}
+
+DECONZ_CONFIG = {
+ "bridgeid": BRIDGEID,
+ "mac": "00:11:22:33:44:55",
+ "name": "deCONZ mock gateway",
+ "sw_version": "2.05.69",
+ "websocketport": 1234,
+}
+
+DECONZ_SENSOR = {
+ "1": {
+ "config": {
+ "alert": "none",
+ "battery": 60,
+ "group": "10",
+ "on": True,
+ "reachable": True,
+ },
+ "ep": 1,
+ "etag": "1b355c0b6d2af28febd7ca9165881952",
+ "manufacturername": "IKEA of Sweden",
+ "mode": 1,
+ "modelid": "TRADFRI on/off switch",
+ "name": "TRADFRI on/off switch ",
+ "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"},
+ "swversion": "1.4.018",
+ "type": "ZHASwitch",
+ "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000",
+ }
+}
+
+DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG, "sensors": DECONZ_SENSOR}
+
+
+def _same_lists(a, b):
+ if len(a) != len(b):
+ return False
+
+ for d in a:
+ if d not in b:
+ return False
+ return True
+
+
+async def setup_deconz(hass, options):
+ """Create the deCONZ gateway."""
+ config_entry = config_entries.ConfigEntry(
+ version=1,
+ domain=deconz.DOMAIN,
+ title="Mock Title",
+ data=ENTRY_CONFIG,
+ source="test",
+ connection_class=config_entries.CONN_CLASS_LOCAL_PUSH,
+ system_options={},
+ options=options,
+ entry_id="1",
+ )
+
+ with patch(
+ "pydeconz.DeconzSession.async_get_state", return_value=DECONZ_WEB_REQUEST
+ ):
+ await deconz.async_setup_entry(hass, config_entry)
+ await hass.async_block_till_done()
+
+ hass.config_entries._entries.append(config_entry)
+
+ return hass.data[deconz.DOMAIN][BRIDGEID]
+
+
+async def test_get_triggers(hass):
+ """Test triggers work."""
+ gateway = await setup_deconz(hass, options={})
+ device_id = gateway.events[0].device_id
+ triggers = await async_get_device_automations(hass, "async_get_triggers", device_id)
+
+ expected_triggers = [
+ {
+ "device_id": device_id,
+ "domain": "deconz",
+ "platform": "device",
+ "type": deconz.device_automation.CONF_SHORT_PRESS,
+ "subtype": deconz.device_automation.CONF_TURN_ON,
+ },
+ {
+ "device_id": device_id,
+ "domain": "deconz",
+ "platform": "device",
+ "type": deconz.device_automation.CONF_LONG_PRESS,
+ "subtype": deconz.device_automation.CONF_TURN_ON,
+ },
+ {
+ "device_id": device_id,
+ "domain": "deconz",
+ "platform": "device",
+ "type": deconz.device_automation.CONF_LONG_RELEASE,
+ "subtype": deconz.device_automation.CONF_TURN_ON,
+ },
+ {
+ "device_id": device_id,
+ "domain": "deconz",
+ "platform": "device",
+ "type": deconz.device_automation.CONF_SHORT_PRESS,
+ "subtype": deconz.device_automation.CONF_TURN_OFF,
+ },
+ {
+ "device_id": device_id,
+ "domain": "deconz",
+ "platform": "device",
+ "type": deconz.device_automation.CONF_LONG_PRESS,
+ "subtype": deconz.device_automation.CONF_TURN_OFF,
+ },
+ {
+ "device_id": device_id,
+ "domain": "deconz",
+ "platform": "device",
+ "type": deconz.device_automation.CONF_LONG_RELEASE,
+ "subtype": deconz.device_automation.CONF_TURN_OFF,
+ },
+ ]
+
+ assert _same_lists(triggers, expected_triggers)
diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py
index 3750d14cd342a0..25a1cd465c510a 100644
--- a/tests/components/deconz/test_gateway.py
+++ b/tests/components/deconz/test_gateway.py
@@ -1,204 +1,178 @@
"""Test deCONZ gateway."""
-from unittest.mock import Mock, patch
+from copy import deepcopy
+
+from asynctest import Mock, patch
import pytest
+from homeassistant import config_entries
+from homeassistant.components import deconz
+from homeassistant.components import ssdp
from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.components.deconz import errors, gateway
-
-from tests.common import mock_coro
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
import pydeconz
+BRIDGEID = "0123456789"
ENTRY_CONFIG = {
- "host": "1.2.3.4",
- "port": 80,
- "api_key": "1234567890ABCDEF",
- "bridgeid": "0123456789ABCDEF",
- "allow_clip_sensor": True,
- "allow_deconz_groups": True,
+ deconz.config_flow.CONF_API_KEY: "ABCDEF",
+ deconz.config_flow.CONF_BRIDGEID: BRIDGEID,
+ deconz.config_flow.CONF_HOST: "1.2.3.4",
+ deconz.config_flow.CONF_PORT: 80,
}
+DECONZ_CONFIG = {
+ "bridgeid": BRIDGEID,
+ "ipaddress": "1.2.3.4",
+ "mac": "00:11:22:33:44:55",
+ "modelid": "deCONZ",
+ "name": "deCONZ mock gateway",
+ "sw_version": "2.05.69",
+ "uuid": "1234",
+ "websocketport": 1234,
+}
-async def test_gateway_setup():
- """Successful setup."""
- hass = Mock()
- entry = Mock()
- entry.data = ENTRY_CONFIG
- api = Mock()
- api.async_add_remote.return_value = Mock()
- api.sensors = {}
-
- deconz_gateway = gateway.DeconzGateway(hass, entry)
-
- with patch.object(
- gateway, "get_gateway", return_value=mock_coro(api)
- ), patch.object(gateway, "async_dispatcher_connect", return_value=Mock()):
- assert await deconz_gateway.async_setup() is True
-
- assert deconz_gateway.api is api
- assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 7
- assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == (
- entry,
- "binary_sensor",
- )
- assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == (
- entry,
- "climate",
- )
- assert hass.config_entries.async_forward_entry_setup.mock_calls[2][1] == (
- entry,
- "cover",
- )
- assert hass.config_entries.async_forward_entry_setup.mock_calls[3][1] == (
- entry,
- "light",
+DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG}
+
+
+async def setup_deconz_integration(hass, config, options, get_state_response):
+ """Create the deCONZ gateway."""
+ config_entry = config_entries.ConfigEntry(
+ version=1,
+ domain=deconz.DOMAIN,
+ title="Mock Title",
+ data=config,
+ source="test",
+ connection_class=config_entries.CONN_CLASS_LOCAL_PUSH,
+ system_options={},
+ options=options,
+ entry_id="1",
)
- assert hass.config_entries.async_forward_entry_setup.mock_calls[4][1] == (
- entry,
- "scene",
- )
- assert hass.config_entries.async_forward_entry_setup.mock_calls[5][1] == (
- entry,
- "sensor",
- )
- assert hass.config_entries.async_forward_entry_setup.mock_calls[6][1] == (
- entry,
- "switch",
- )
- assert len(api.start.mock_calls) == 1
-
-async def test_gateway_retry():
- """Retry setup."""
- hass = Mock()
- entry = Mock()
- entry.data = ENTRY_CONFIG
+ with patch(
+ "pydeconz.DeconzSession.async_get_state", return_value=get_state_response
+ ), patch("pydeconz.DeconzSession.start", return_value=True):
+ await deconz.async_setup_entry(hass, config_entry)
+ await hass.async_block_till_done()
- deconz_gateway = gateway.DeconzGateway(hass, entry)
+ hass.config_entries._entries.append(config_entry)
- with patch.object(
- gateway, "get_gateway", side_effect=errors.CannotConnect
- ), pytest.raises(ConfigEntryNotReady):
- await deconz_gateway.async_setup()
+ return hass.data[deconz.DOMAIN].get(config[deconz.CONF_BRIDGEID])
-async def test_gateway_setup_fails():
+async def test_gateway_setup(hass):
+ """Successful setup."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ with patch(
+ "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup",
+ return_value=True,
+ ) as forward_entry_setup:
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert gateway.bridgeid == BRIDGEID
+ assert gateway.master is True
+ assert gateway.option_allow_clip_sensor is False
+ assert gateway.option_allow_deconz_groups is True
+
+ assert len(gateway.deconz_ids) == 0
+ assert len(hass.states.async_all()) == 0
+
+ entry = gateway.config_entry
+ assert forward_entry_setup.mock_calls[0][1] == (entry, "binary_sensor")
+ assert forward_entry_setup.mock_calls[1][1] == (entry, "climate")
+ assert forward_entry_setup.mock_calls[2][1] == (entry, "cover")
+ assert forward_entry_setup.mock_calls[3][1] == (entry, "light")
+ assert forward_entry_setup.mock_calls[4][1] == (entry, "scene")
+ assert forward_entry_setup.mock_calls[5][1] == (entry, "sensor")
+ assert forward_entry_setup.mock_calls[6][1] == (entry, "switch")
+
+
+async def test_gateway_retry(hass):
"""Retry setup."""
- hass = Mock()
- entry = Mock()
- entry.data = ENTRY_CONFIG
-
- deconz_gateway = gateway.DeconzGateway(hass, entry)
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ with patch(
+ "homeassistant.components.deconz.gateway.get_gateway",
+ side_effect=deconz.errors.CannotConnect,
+ ), pytest.raises(ConfigEntryNotReady):
+ await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
- with patch.object(gateway, "get_gateway", side_effect=Exception):
- result = await deconz_gateway.async_setup()
- assert not result
+async def test_gateway_setup_fails(hass):
+ """Retry setup."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ with patch(
+ "homeassistant.components.deconz.gateway.get_gateway", side_effect=Exception
+ ):
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert gateway is None
-async def test_connection_status(hass):
+async def test_connection_status_signalling(hass):
"""Make sure that connection status triggers a dispatcher send."""
- entry = Mock()
- entry.data = ENTRY_CONFIG
-
- deconz_gateway = gateway.DeconzGateway(hass, entry)
- with patch.object(gateway, "async_dispatcher_send") as mock_dispatch_send:
- deconz_gateway.async_connection_status_callback(True)
-
- await hass.async_block_till_done()
- assert len(mock_dispatch_send.mock_calls) == 1
- assert len(mock_dispatch_send.mock_calls[0]) == 3
-
-
-async def test_add_device(hass):
- """Successful retry setup."""
- entry = Mock()
- entry.data = ENTRY_CONFIG
-
- deconz_gateway = gateway.DeconzGateway(hass, entry)
- with patch.object(gateway, "async_dispatcher_send") as mock_dispatch_send:
- deconz_gateway.async_add_device_callback("sensor", Mock())
-
- await hass.async_block_till_done()
- assert len(mock_dispatch_send.mock_calls) == 1
- assert len(mock_dispatch_send.mock_calls[0]) == 3
-
-
-async def test_add_remote():
- """Successful add remote."""
- hass = Mock()
- entry = Mock()
- entry.data = ENTRY_CONFIG
-
- remote = Mock()
- remote.name = "name"
- remote.type = "ZHASwitch"
- remote.register_async_callback = Mock()
-
- deconz_gateway = gateway.DeconzGateway(hass, entry)
- deconz_gateway.async_add_remote([remote])
-
- assert len(deconz_gateway.events) == 1
-
-
-async def test_shutdown():
- """Successful shutdown."""
- hass = Mock()
- entry = Mock()
- entry.data = ENTRY_CONFIG
-
- deconz_gateway = gateway.DeconzGateway(hass, entry)
- deconz_gateway.api = Mock()
- deconz_gateway.shutdown(None)
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
- assert len(deconz_gateway.api.close.mock_calls) == 1
+ event_call = Mock()
+ unsub = async_dispatcher_connect(hass, gateway.signal_reachable, event_call)
+ gateway.async_connection_status_callback(False)
+ await hass.async_block_till_done()
-async def test_reset_after_successful_setup():
- """Verify that reset works on a setup component."""
- hass = Mock()
- entry = Mock()
- entry.data = ENTRY_CONFIG
- api = Mock()
- api.async_add_remote.return_value = Mock()
- api.sensors = {}
+ assert gateway.available is False
+ assert len(event_call.mock_calls) == 1
- deconz_gateway = gateway.DeconzGateway(hass, entry)
+ unsub()
- with patch.object(
- gateway, "get_gateway", return_value=mock_coro(api)
- ), patch.object(gateway, "async_dispatcher_connect", return_value=Mock()):
- assert await deconz_gateway.async_setup() is True
- listener = Mock()
- deconz_gateway.listeners = [listener]
- event = Mock()
- event.async_will_remove_from_hass = Mock()
- deconz_gateway.events = [event]
- deconz_gateway.deconz_ids = {"key": "value"}
+async def test_update_address(hass):
+ """Make sure that connection status triggers a dispatcher send."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert gateway.api.host == "1.2.3.4"
+
+ await hass.config_entries.flow.async_init(
+ deconz.config_flow.DOMAIN,
+ data={
+ deconz.config_flow.CONF_HOST: "2.3.4.5",
+ deconz.config_flow.CONF_PORT: 80,
+ ssdp.ATTR_SERIAL: BRIDGEID,
+ ssdp.ATTR_MANUFACTURERURL: deconz.config_flow.DECONZ_MANUFACTURERURL,
+ deconz.config_flow.ATTR_UUID: "uuid:1234",
+ },
+ context={"source": "ssdp"},
+ )
+ await hass.async_block_till_done()
- hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True)
- assert await deconz_gateway.async_reset() is True
+ assert gateway.api.host == "2.3.4.5"
- assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 7
- assert len(listener.mock_calls) == 1
- assert len(deconz_gateway.listeners) == 0
+async def test_reset_after_successful_setup(hass):
+ """Make sure that connection status triggers a dispatcher send."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
- assert len(event.async_will_remove_from_hass.mock_calls) == 1
- assert len(deconz_gateway.events) == 0
+ result = await gateway.async_reset()
+ await hass.async_block_till_done()
- assert len(deconz_gateway.deconz_ids) == 0
+ assert result is True
async def test_get_gateway(hass):
"""Successful call."""
- with patch(
- "pydeconz.DeconzSession.async_load_parameters", return_value=mock_coro(True)
- ):
- assert await gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock())
+ with patch("pydeconz.DeconzSession.async_load_parameters", return_value=True):
+ assert await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock())
async def test_get_gateway_fails_unauthorized(hass):
@@ -206,8 +180,11 @@ async def test_get_gateway_fails_unauthorized(hass):
with patch(
"pydeconz.DeconzSession.async_load_parameters",
side_effect=pydeconz.errors.Unauthorized,
- ), pytest.raises(errors.AuthenticationRequired):
- assert await gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) is False
+ ), pytest.raises(deconz.errors.AuthenticationRequired):
+ assert (
+ await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock())
+ is False
+ )
async def test_get_gateway_fails_cannot_connect(hass):
@@ -215,41 +192,8 @@ async def test_get_gateway_fails_cannot_connect(hass):
with patch(
"pydeconz.DeconzSession.async_load_parameters",
side_effect=pydeconz.errors.RequestError,
- ), pytest.raises(errors.CannotConnect):
- assert await gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) is False
-
-
-async def test_create_event():
- """Successfully created a deCONZ event."""
- hass = Mock()
- remote = Mock()
- remote.name = "Name"
-
- event = gateway.DeconzEvent(hass, remote)
-
- assert event._id == "name"
-
-
-async def test_update_event():
- """Successfully update a deCONZ event."""
- hass = Mock()
- remote = Mock()
- remote.name = "Name"
-
- event = gateway.DeconzEvent(hass, remote)
- remote.changed_keys = {"state": True}
- event.async_update_callback()
-
- assert len(hass.bus.async_fire.mock_calls) == 1
-
-
-async def test_remove_event():
- """Successfully update a deCONZ event."""
- hass = Mock()
- remote = Mock()
- remote.name = "Name"
-
- event = gateway.DeconzEvent(hass, remote)
- event.async_will_remove_from_hass()
-
- assert event._device is None
+ ), pytest.raises(deconz.errors.CannotConnect):
+ assert (
+ await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock())
+ is False
+ )
diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py
index b0456e0b6248bb..7d630498cde14e 100644
--- a/tests/components/deconz/test_init.py
+++ b/tests/components/deconz/test_init.py
@@ -3,10 +3,8 @@
import asyncio
import pytest
-import voluptuous as vol
from homeassistant.exceptions import ConfigEntryNotReady
-from homeassistant.setup import async_setup_component
from homeassistant.components import deconz
from tests.common import mock_coro, MockConfigEntry
@@ -34,74 +32,13 @@ async def setup_entry(hass, entry):
assert await deconz.async_setup_entry(hass, entry) is True
-async def test_config_with_host_passed_to_config_entry(hass):
- """Test that configured options for a host are loaded via config entry."""
- with patch.object(hass.config_entries, "flow") as mock_config_flow:
- assert (
- await async_setup_component(
- hass,
- deconz.DOMAIN,
- {
- deconz.DOMAIN: {
- deconz.CONF_HOST: ENTRY1_HOST,
- deconz.CONF_PORT: ENTRY1_PORT,
- }
- },
- )
- is True
- )
- # Import flow started
- assert len(mock_config_flow.mock_calls) == 1
-
-
-async def test_config_without_host_not_passed_to_config_entry(hass):
- """Test that a configuration without a host does not initiate an import."""
- MockConfigEntry(domain=deconz.DOMAIN, data={}).add_to_hass(hass)
- with patch.object(hass.config_entries, "flow") as mock_config_flow:
- assert (
- await async_setup_component(hass, deconz.DOMAIN, {deconz.DOMAIN: {}})
- is True
- )
- # No flow started
- assert len(mock_config_flow.mock_calls) == 0
-
-
-async def test_config_import_entry_fails_when_entries_exist(hass):
- """Test that an already registered host does not initiate an import."""
- MockConfigEntry(domain=deconz.DOMAIN, data={}).add_to_hass(hass)
- with patch.object(hass.config_entries, "flow") as mock_config_flow:
- assert (
- await async_setup_component(
- hass,
- deconz.DOMAIN,
- {
- deconz.DOMAIN: {
- deconz.CONF_HOST: ENTRY1_HOST,
- deconz.CONF_PORT: ENTRY1_PORT,
- }
- },
- )
- is True
- )
- # No flow started
- assert len(mock_config_flow.mock_calls) == 0
-
-
-async def test_config_discovery(hass):
- """Test that a discovered bridge does not initiate an import."""
- with patch.object(hass, "config_entries") as mock_config_entries:
- assert await async_setup_component(hass, deconz.DOMAIN, {}) is True
- # No flow started
- assert len(mock_config_entries.flow.mock_calls) == 0
-
-
async def test_setup_entry_fails(hass):
"""Test setup entry fails if deCONZ is not available."""
entry = Mock()
entry.data = {
- deconz.CONF_HOST: ENTRY1_HOST,
- deconz.CONF_PORT: ENTRY1_PORT,
- deconz.CONF_API_KEY: ENTRY1_API_KEY,
+ deconz.config_flow.CONF_HOST: ENTRY1_HOST,
+ deconz.config_flow.CONF_PORT: ENTRY1_PORT,
+ deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY,
}
with patch("pydeconz.DeconzSession.async_load_parameters", side_effect=Exception):
await deconz.async_setup_entry(hass, entry)
@@ -111,9 +48,9 @@ async def test_setup_entry_no_available_bridge(hass):
"""Test setup entry fails if deCONZ is not available."""
entry = Mock()
entry.data = {
- deconz.CONF_HOST: ENTRY1_HOST,
- deconz.CONF_PORT: ENTRY1_PORT,
- deconz.CONF_API_KEY: ENTRY1_API_KEY,
+ deconz.config_flow.CONF_HOST: ENTRY1_HOST,
+ deconz.config_flow.CONF_PORT: ENTRY1_PORT,
+ deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY,
}
with patch(
"pydeconz.DeconzSession.async_load_parameters", side_effect=asyncio.TimeoutError
@@ -126,9 +63,9 @@ async def test_setup_entry_successful(hass):
entry = MockConfigEntry(
domain=deconz.DOMAIN,
data={
- deconz.CONF_HOST: ENTRY1_HOST,
- deconz.CONF_PORT: ENTRY1_PORT,
- deconz.CONF_API_KEY: ENTRY1_API_KEY,
+ deconz.config_flow.CONF_HOST: ENTRY1_HOST,
+ deconz.config_flow.CONF_PORT: ENTRY1_PORT,
+ deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY,
deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID,
},
)
@@ -145,9 +82,9 @@ async def test_setup_entry_multiple_gateways(hass):
entry = MockConfigEntry(
domain=deconz.DOMAIN,
data={
- deconz.CONF_HOST: ENTRY1_HOST,
- deconz.CONF_PORT: ENTRY1_PORT,
- deconz.CONF_API_KEY: ENTRY1_API_KEY,
+ deconz.config_flow.CONF_HOST: ENTRY1_HOST,
+ deconz.config_flow.CONF_PORT: ENTRY1_PORT,
+ deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY,
deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID,
},
)
@@ -156,9 +93,9 @@ async def test_setup_entry_multiple_gateways(hass):
entry2 = MockConfigEntry(
domain=deconz.DOMAIN,
data={
- deconz.CONF_HOST: ENTRY2_HOST,
- deconz.CONF_PORT: ENTRY2_PORT,
- deconz.CONF_API_KEY: ENTRY2_API_KEY,
+ deconz.config_flow.CONF_HOST: ENTRY2_HOST,
+ deconz.config_flow.CONF_PORT: ENTRY2_PORT,
+ deconz.config_flow.CONF_API_KEY: ENTRY2_API_KEY,
deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID,
},
)
@@ -178,9 +115,9 @@ async def test_unload_entry(hass):
entry = MockConfigEntry(
domain=deconz.DOMAIN,
data={
- deconz.CONF_HOST: ENTRY1_HOST,
- deconz.CONF_PORT: ENTRY1_PORT,
- deconz.CONF_API_KEY: ENTRY1_API_KEY,
+ deconz.config_flow.CONF_HOST: ENTRY1_HOST,
+ deconz.config_flow.CONF_PORT: ENTRY1_PORT,
+ deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY,
deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID,
},
)
@@ -201,9 +138,9 @@ async def test_unload_entry_multiple_gateways(hass):
entry = MockConfigEntry(
domain=deconz.DOMAIN,
data={
- deconz.CONF_HOST: ENTRY1_HOST,
- deconz.CONF_PORT: ENTRY1_PORT,
- deconz.CONF_API_KEY: ENTRY1_API_KEY,
+ deconz.config_flow.CONF_HOST: ENTRY1_HOST,
+ deconz.config_flow.CONF_PORT: ENTRY1_PORT,
+ deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY,
deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID,
},
)
@@ -212,9 +149,9 @@ async def test_unload_entry_multiple_gateways(hass):
entry2 = MockConfigEntry(
domain=deconz.DOMAIN,
data={
- deconz.CONF_HOST: ENTRY2_HOST,
- deconz.CONF_PORT: ENTRY2_PORT,
- deconz.CONF_API_KEY: ENTRY2_API_KEY,
+ deconz.config_flow.CONF_HOST: ENTRY2_HOST,
+ deconz.config_flow.CONF_PORT: ENTRY2_PORT,
+ deconz.config_flow.CONF_API_KEY: ENTRY2_API_KEY,
deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID,
},
)
@@ -230,98 +167,3 @@ async def test_unload_entry_multiple_gateways(hass):
assert ENTRY2_BRIDGEID in hass.data[deconz.DOMAIN]
assert hass.data[deconz.DOMAIN][ENTRY2_BRIDGEID].master
-
-
-async def test_service_configure(hass):
- """Test that service invokes pydeconz with the correct path and data."""
- entry = MockConfigEntry(
- domain=deconz.DOMAIN,
- data={
- deconz.CONF_HOST: ENTRY1_HOST,
- deconz.CONF_PORT: ENTRY1_PORT,
- deconz.CONF_API_KEY: ENTRY1_API_KEY,
- deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID,
- },
- )
- entry.add_to_hass(hass)
-
- await setup_entry(hass, entry)
-
- hass.data[deconz.DOMAIN][ENTRY1_BRIDGEID].deconz_ids = {"light.test": "/light/1"}
- data = {"on": True, "attr1": 10, "attr2": 20}
-
- # only field
- with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)):
- await hass.services.async_call(
- "deconz", "configure", service_data={"field": "/light/42", "data": data}
- )
- await hass.async_block_till_done()
-
- # only entity
- with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)):
- await hass.services.async_call(
- "deconz", "configure", service_data={"entity": "light.test", "data": data}
- )
- await hass.async_block_till_done()
-
- # entity + field
- with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)):
- await hass.services.async_call(
- "deconz",
- "configure",
- service_data={"entity": "light.test", "field": "/state", "data": data},
- )
- await hass.async_block_till_done()
-
- # non-existing entity (or not from deCONZ)
- with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)):
- await hass.services.async_call(
- "deconz",
- "configure",
- service_data={
- "entity": "light.nonexisting",
- "field": "/state",
- "data": data,
- },
- )
- await hass.async_block_till_done()
-
- # field does not start with /
- with pytest.raises(vol.Invalid):
- with patch(
- "pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)
- ):
- await hass.services.async_call(
- "deconz",
- "configure",
- service_data={"entity": "light.test", "field": "state", "data": data},
- )
- await hass.async_block_till_done()
-
-
-async def test_service_refresh_devices(hass):
- """Test that service can refresh devices."""
- entry = MockConfigEntry(
- domain=deconz.DOMAIN,
- data={
- deconz.CONF_HOST: ENTRY1_HOST,
- deconz.CONF_PORT: ENTRY1_PORT,
- deconz.CONF_API_KEY: ENTRY1_API_KEY,
- deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID,
- },
- )
- entry.add_to_hass(hass)
-
- await setup_entry(hass, entry)
-
- with patch(
- "pydeconz.DeconzSession.async_load_parameters", return_value=mock_coro(True)
- ):
- await hass.services.async_call("deconz", "device_refresh", service_data={})
- await hass.async_block_till_done()
-
- with patch(
- "pydeconz.DeconzSession.async_load_parameters", return_value=mock_coro(False)
- ):
- await hass.services.async_call("deconz", "device_refresh", service_data={})
- await hass.async_block_till_done()
diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py
index afe7ca445e5738..14dc5cc8eac1c7 100644
--- a/tests/components/deconz/test_light.py
+++ b/tests/components/deconz/test_light.py
@@ -1,49 +1,29 @@
"""deCONZ light platform tests."""
-from unittest.mock import Mock, patch
+from copy import deepcopy
+
+from asynctest import patch
-from homeassistant import config_entries
from homeassistant.components import deconz
-from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
import homeassistant.components.light as light
-from tests.common import mock_coro
-
+from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration
-LIGHT = {
+GROUPS = {
"1": {
- "id": "Light 1 id",
- "name": "Light 1 name",
- "state": {
- "on": True,
- "bri": 255,
- "colormode": "xy",
- "xy": (500, 500),
- "reachable": True,
- },
- "uniqueid": "00:00:00:00:00:00:00:00-00",
- },
- "2": {
- "id": "Light 2 id",
- "name": "Light 2 name",
- "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True},
- },
-}
-
-GROUP = {
- "1": {
- "id": "Group 1 id",
- "name": "Group 1 name",
+ "id": "Light group id",
+ "name": "Light group",
"type": "LightGroup",
- "state": {},
+ "state": {"all_on": False, "any_on": True},
"action": {},
"scenes": [],
"lights": ["1", "2"],
},
"2": {
- "id": "Group 2 id",
- "name": "Group 2 name",
+ "id": "Empty group id",
+ "name": "Empty group",
+ "type": "LightGroup",
"state": {},
"action": {},
"scenes": [],
@@ -51,62 +31,38 @@
},
}
-SWITCH = {
+LIGHTS = {
"1": {
- "id": "Switch 1 id",
- "name": "Switch 1 name",
+ "id": "RGB light id",
+ "name": "RGB light",
+ "state": {
+ "on": True,
+ "bri": 255,
+ "colormode": "xy",
+ "effect": "colorloop",
+ "xy": (500, 500),
+ "reachable": True,
+ },
+ "type": "Extended color light",
+ "uniqueid": "00:00:00:00:00:00:00:00-00",
+ },
+ "2": {
+ "id": "Tunable white light id",
+ "name": "Tunable white light",
+ "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True},
+ "type": "Tunable white light",
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ },
+ "3": {
+ "id": "On off switch id",
+ "name": "On off switch",
"type": "On/Off plug-in unit",
- "state": {},
- }
-}
-
-
-ENTRY_CONFIG = {
- deconz.config_flow.CONF_API_KEY: "ABCDEF",
- deconz.config_flow.CONF_BRIDGEID: "0123456789",
- deconz.config_flow.CONF_HOST: "1.2.3.4",
- deconz.config_flow.CONF_PORT: 80,
-}
-
-ENTRY_OPTIONS = {
- deconz.const.CONF_ALLOW_CLIP_SENSOR: True,
- deconz.const.CONF_ALLOW_DECONZ_GROUPS: True,
+ "state": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ },
}
-async def setup_gateway(hass, data, allow_deconz_groups=True):
- """Load the deCONZ light platform."""
- from pydeconz import DeconzSession
-
- loop = Mock()
- session = Mock()
-
- ENTRY_OPTIONS[deconz.const.CONF_ALLOW_DECONZ_GROUPS] = allow_deconz_groups
-
- config_entry = config_entries.ConfigEntry(
- 1,
- deconz.DOMAIN,
- "Mock Title",
- ENTRY_CONFIG,
- "test",
- config_entries.CONN_CLASS_LOCAL_PUSH,
- system_options={},
- options=ENTRY_OPTIONS,
- )
- gateway = deconz.DeconzGateway(hass, config_entry)
- gateway.api = DeconzSession(loop, session, **config_entry.data)
- gateway.api.config = Mock()
- hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway}
-
- with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)):
- await gateway.api.async_load_parameters()
-
- await hass.config_entries.async_forward_entry_setup(config_entry, "light")
- # To flush out the service call to update the group
- await hass.async_block_till_done()
- return gateway
-
-
async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
assert (
@@ -120,118 +76,161 @@ async def test_platform_manually_configured(hass):
async def test_no_lights_or_groups(hass):
"""Test that no lights or groups entities are created."""
- gateway = await setup_gateway(hass, {})
- assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert len(gateway.deconz_ids) == 0
assert len(hass.states.async_all()) == 0
async def test_lights_and_groups(hass):
"""Test that lights or groups entities are created."""
- with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)):
- gateway = await setup_gateway(hass, {"lights": LIGHT, "groups": GROUP})
- assert "light.light_1_name" in gateway.deconz_ids
- assert "light.light_2_name" in gateway.deconz_ids
- assert "light.group_1_name" in gateway.deconz_ids
- assert "light.group_2_name" not in gateway.deconz_ids
- assert len(hass.states.async_all()) == 4
-
- lamp_1 = hass.states.get("light.light_1_name")
- assert lamp_1 is not None
- assert lamp_1.state == "on"
- assert lamp_1.attributes["brightness"] == 255
- assert lamp_1.attributes["hs_color"] == (224.235, 100.0)
-
- light_2 = hass.states.get("light.light_2_name")
- assert light_2 is not None
- assert light_2.state == "on"
- assert light_2.attributes["color_temp"] == 2500
-
- gateway.api.lights["1"].async_update({})
-
- await hass.services.async_call(
- "light",
- "turn_on",
- {
- "entity_id": "light.light_1_name",
- "color_temp": 2500,
- "brightness": 200,
- "transition": 5,
- "flash": "short",
- "effect": "colorloop",
- },
- blocking=True,
- )
- await hass.services.async_call(
- "light",
- "turn_on",
- {
- "entity_id": "light.light_1_name",
- "hs_color": (20, 30),
- "flash": "long",
- "effect": "None",
- },
- blocking=True,
- )
- await hass.services.async_call(
- "light",
- "turn_off",
- {"entity_id": "light.light_1_name", "transition": 5, "flash": "short"},
- blocking=True,
- )
- await hass.services.async_call(
- "light",
- "turn_off",
- {"entity_id": "light.light_1_name", "flash": "long"},
- blocking=True,
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["groups"] = deepcopy(GROUPS)
+ data["lights"] = deepcopy(LIGHTS)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
)
+ assert "light.rgb_light" in gateway.deconz_ids
+ assert "light.tunable_white_light" in gateway.deconz_ids
+ assert "light.light_group" in gateway.deconz_ids
+ assert "light.empty_group" not in gateway.deconz_ids
+ assert "light.on_off_switch" not in gateway.deconz_ids
+ # 4 entities + 2 groups (one for switches and one for lights)
+ assert len(hass.states.async_all()) == 6
+
+ rgb_light = hass.states.get("light.rgb_light")
+ assert rgb_light.state == "on"
+ assert rgb_light.attributes["brightness"] == 255
+ assert rgb_light.attributes["hs_color"] == (224.235, 100.0)
+ assert rgb_light.attributes["is_deconz_group"] is False
+
+ tunable_white_light = hass.states.get("light.tunable_white_light")
+ assert tunable_white_light.state == "on"
+ assert tunable_white_light.attributes["color_temp"] == 2500
+
+ light_group = hass.states.get("light.light_group")
+ assert light_group.state == "on"
+ assert light_group.attributes["all_on"] is False
+
+ empty_group = hass.states.get("light.empty_group")
+ assert empty_group is None
+
+ rgb_light_device = gateway.api.lights["1"]
+
+ rgb_light_device.async_update({"state": {"on": False}})
+ await hass.async_block_till_done()
+ rgb_light = hass.states.get("light.rgb_light")
+ assert rgb_light.state == "off"
+
+ with patch.object(
+ rgb_light_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ light.DOMAIN,
+ light.SERVICE_TURN_ON,
+ {
+ "entity_id": "light.rgb_light",
+ "color_temp": 2500,
+ "brightness": 200,
+ "transition": 5,
+ "flash": "short",
+ "effect": "colorloop",
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with(
+ "/lights/1/state",
+ {
+ "ct": 2500,
+ "bri": 200,
+ "transitiontime": 50,
+ "alert": "select",
+ "effect": "colorloop",
+ },
+ )
-async def test_add_new_light(hass):
- """Test successful creation of light entities."""
- gateway = await setup_gateway(hass, {})
- light = Mock()
- light.name = "name"
- light.uniqueid = "1"
- light.register_async_callback = Mock()
- async_dispatcher_send(hass, gateway.async_event_new_device("light"), [light])
- await hass.async_block_till_done()
- assert "light.name" in gateway.deconz_ids
+ with patch.object(
+ rgb_light_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ light.DOMAIN,
+ light.SERVICE_TURN_ON,
+ {
+ "entity_id": "light.rgb_light",
+ "hs_color": (20, 30),
+ "flash": "long",
+ "effect": "None",
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with(
+ "/lights/1/state",
+ {"xy": (0.411, 0.351), "alert": "lselect", "effect": "none"},
+ )
+ with patch.object(
+ rgb_light_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ light.DOMAIN,
+ light.SERVICE_TURN_OFF,
+ {"entity_id": "light.rgb_light", "transition": 5, "flash": "short"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with(
+ "/lights/1/state", {"bri": 0, "transitiontime": 50, "alert": "select"}
+ )
-async def test_add_new_group(hass):
- """Test successful creation of group entities."""
- gateway = await setup_gateway(hass, {})
- group = Mock()
- group.name = "name"
- group.register_async_callback = Mock()
- async_dispatcher_send(hass, gateway.async_event_new_device("group"), [group])
- await hass.async_block_till_done()
- assert "light.name" in gateway.deconz_ids
+ with patch.object(
+ rgb_light_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ light.DOMAIN,
+ light.SERVICE_TURN_OFF,
+ {"entity_id": "light.rgb_light", "flash": "long"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("/lights/1/state", {"alert": "lselect"})
+ await gateway.async_reset()
-async def test_do_not_add_deconz_groups(hass):
- """Test that clip sensors can be ignored."""
- gateway = await setup_gateway(hass, {}, allow_deconz_groups=False)
- group = Mock()
- group.name = "name"
- group.register_async_callback = Mock()
- async_dispatcher_send(hass, gateway.async_event_new_device("group"), [group])
- await hass.async_block_till_done()
- assert len(gateway.deconz_ids) == 0
+ assert len(hass.states.async_all()) == 2
-async def test_no_switch(hass):
- """Test that a switch doesn't get created as a light entity."""
- gateway = await setup_gateway(hass, {"lights": SWITCH})
- assert len(gateway.deconz_ids) == 0
- assert len(hass.states.async_all()) == 0
+async def test_disable_light_groups(hass):
+ """Test successful creation of sensor entities."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["groups"] = deepcopy(GROUPS)
+ data["lights"] = deepcopy(LIGHTS)
+ gateway = await setup_deconz_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: False},
+ get_state_response=data,
+ )
+ assert "light.rgb_light" in gateway.deconz_ids
+ assert "light.tunable_white_light" in gateway.deconz_ids
+ assert "light.light_group" not in gateway.deconz_ids
+ assert "light.empty_group" not in gateway.deconz_ids
+ assert "light.on_off_switch" not in gateway.deconz_ids
+ # 4 entities + 2 groups (one for switches and one for lights)
+ assert len(hass.states.async_all()) == 5
+ rgb_light = hass.states.get("light.rgb_light")
+ assert rgb_light is not None
-async def test_unload_light(hass):
- """Test that it works to unload switch entities."""
- gateway = await setup_gateway(hass, {"lights": LIGHT, "groups": GROUP})
+ tunable_white_light = hass.states.get("light.tunable_white_light")
+ assert tunable_white_light is not None
- await gateway.async_reset()
+ light_group = hass.states.get("light.light_group")
+ assert light_group is None
- # Group.all_lights will not be removed
- assert len(hass.states.async_all()) == 1
+ empty_group = hass.states.get("light.empty_group")
+ assert empty_group is None
diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py
index 074e943548de40..dcc8ba500c32f7 100644
--- a/tests/components/deconz/test_scene.py
+++ b/tests/components/deconz/test_scene.py
@@ -1,67 +1,28 @@
"""deCONZ scene platform tests."""
-from unittest.mock import Mock, patch
+from copy import deepcopy
+
+from asynctest import patch
-from homeassistant import config_entries
from homeassistant.components import deconz
from homeassistant.setup import async_setup_component
import homeassistant.components.scene as scene
-from tests.common import mock_coro
-
+from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration
-GROUP = {
+GROUPS = {
"1": {
- "id": "Group 1 id",
- "name": "Group 1 name",
- "state": {},
+ "id": "Light group id",
+ "name": "Light group",
+ "type": "LightGroup",
+ "state": {"all_on": False, "any_on": True},
"action": {},
- "scenes": [{"id": "1", "name": "Scene 1"}],
+ "scenes": [{"id": "1", "name": "Scene"}],
"lights": [],
}
}
-ENTRY_CONFIG = {
- deconz.const.CONF_ALLOW_CLIP_SENSOR: True,
- deconz.const.CONF_ALLOW_DECONZ_GROUPS: True,
- deconz.config_flow.CONF_API_KEY: "ABCDEF",
- deconz.config_flow.CONF_BRIDGEID: "0123456789",
- deconz.config_flow.CONF_HOST: "1.2.3.4",
- deconz.config_flow.CONF_PORT: 80,
-}
-
-
-async def setup_gateway(hass, data):
- """Load the deCONZ scene platform."""
- from pydeconz import DeconzSession
-
- loop = Mock()
- session = Mock()
-
- config_entry = config_entries.ConfigEntry(
- 1,
- deconz.DOMAIN,
- "Mock Title",
- ENTRY_CONFIG,
- "test",
- config_entries.CONN_CLASS_LOCAL_PUSH,
- system_options={},
- )
- gateway = deconz.DeconzGateway(hass, config_entry)
- gateway.api = DeconzSession(loop, session, **config_entry.data)
- gateway.api.config = Mock()
- hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway}
-
- with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)):
- await gateway.api.async_load_parameters()
-
- await hass.config_entries.async_forward_entry_setup(config_entry, "scene")
- # To flush out the service call to update the group
- await hass.async_block_till_done()
- return gateway
-
-
async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
assert (
@@ -75,26 +36,38 @@ async def test_platform_manually_configured(hass):
async def test_no_scenes(hass):
"""Test that scenes can be loaded without scenes being available."""
- gateway = await setup_gateway(hass, {})
- assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert len(gateway.deconz_ids) == 0
assert len(hass.states.async_all()) == 0
async def test_scenes(hass):
"""Test that scenes works."""
- with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)):
- gateway = await setup_gateway(hass, {"groups": GROUP})
- assert "scene.group_1_name_scene_1" in gateway.deconz_ids
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["groups"] = deepcopy(GROUPS)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+
+ assert "scene.light_group_scene" in gateway.deconz_ids
assert len(hass.states.async_all()) == 1
- await hass.services.async_call(
- "scene", "turn_on", {"entity_id": "scene.group_1_name_scene_1"}, blocking=True
- )
+ light_group_scene = hass.states.get("scene.light_group_scene")
+ assert light_group_scene
+ group_scene = gateway.api.groups["1"].scenes["1"]
-async def test_unload_scene(hass):
- """Test that it works to unload scene entities."""
- gateway = await setup_gateway(hass, {"groups": GROUP})
+ with patch.object(
+ group_scene, "_async_set_state_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ "scene", "turn_on", {"entity_id": "scene.light_group_scene"}, blocking=True
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("/groups/1/scenes/1/recall", {})
await gateway.async_reset()
diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py
index fa1ba175ed5762..928e527dd0706e 100644
--- a/tests/components/deconz/test_sensor.py
+++ b/tests/components/deconz/test_sensor.py
@@ -1,125 +1,81 @@
"""deCONZ sensor platform tests."""
-from unittest.mock import Mock, patch
+from copy import deepcopy
-from tests.common import mock_coro
-
-from homeassistant import config_entries
from homeassistant.components import deconz
-from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
import homeassistant.components.sensor as sensor
+from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration
-SENSOR = {
+SENSORS = {
"1": {
- "id": "Sensor 1 id",
- "name": "Sensor 1 name",
+ "id": "Light sensor id",
+ "name": "Light level sensor",
"type": "ZHALightLevel",
"state": {"lightlevel": 30000, "dark": False},
- "config": {"reachable": True},
+ "config": {"on": True, "reachable": True, "temperature": 10},
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
"2": {
- "id": "Sensor 2 id",
- "name": "Sensor 2 name",
+ "id": "Presence sensor id",
+ "name": "Presence sensor",
"type": "ZHAPresence",
"state": {"presence": False},
"config": {},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
},
"3": {
- "id": "Sensor 3 id",
- "name": "Sensor 3 name",
+ "id": "Switch 1 id",
+ "name": "Switch 1",
"type": "ZHASwitch",
"state": {"buttonevent": 1000},
"config": {},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
},
"4": {
- "id": "Sensor 4 id",
- "name": "Sensor 4 name",
+ "id": "Switch 2 id",
+ "name": "Switch 2",
"type": "ZHASwitch",
"state": {"buttonevent": 1000},
"config": {"battery": 100},
- "uniqueid": "00:00:00:00:00:00:00:01-00",
+ "uniqueid": "00:00:00:00:00:00:00:03-00",
},
"5": {
- "id": "Sensor 5 id",
- "name": "Sensor 5 name",
- "type": "ZHASwitch",
- "state": {"buttonevent": 1000},
- "config": {"battery": 100},
- "uniqueid": "00:00:00:00:00:00:00:02:00-00",
- },
- "6": {
- "id": "Sensor 6 id",
- "name": "Sensor 6 name",
+ "id": "Daylight sensor id",
+ "name": "Daylight sensor",
"type": "Daylight",
- "state": {"daylight": True},
+ "state": {"daylight": True, "status": 130},
"config": {},
+ "uniqueid": "00:00:00:00:00:00:00:04-00",
},
- "7": {
- "id": "Sensor 7 id",
- "name": "Sensor 7 name",
+ "6": {
+ "id": "Power sensor id",
+ "name": "Power sensor",
"type": "ZHAPower",
"state": {"current": 2, "power": 6, "voltage": 3},
"config": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:05-00",
},
- "8": {
- "id": "Sensor 8 id",
- "name": "Sensor 8 name",
+ "7": {
+ "id": "Consumption id",
+ "name": "Consumption sensor",
"type": "ZHAConsumption",
"state": {"consumption": 2, "power": 6},
"config": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:06-00",
+ },
+ "8": {
+ "id": "CLIP light sensor id",
+ "name": "CLIP light level sensor",
+ "type": "CLIPLightLevel",
+ "state": {"lightlevel": 30000},
+ "config": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:07-00",
},
}
-ENTRY_CONFIG = {
- deconz.config_flow.CONF_API_KEY: "ABCDEF",
- deconz.config_flow.CONF_BRIDGEID: "0123456789",
- deconz.config_flow.CONF_HOST: "1.2.3.4",
- deconz.config_flow.CONF_PORT: 80,
-}
-
-ENTRY_OPTIONS = {
- deconz.const.CONF_ALLOW_CLIP_SENSOR: True,
- deconz.const.CONF_ALLOW_DECONZ_GROUPS: True,
-}
-
-
-async def setup_gateway(hass, data, allow_clip_sensor=True):
- """Load the deCONZ sensor platform."""
- from pydeconz import DeconzSession
-
- loop = Mock()
- session = Mock()
-
- ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor
-
- config_entry = config_entries.ConfigEntry(
- 1,
- deconz.DOMAIN,
- "Mock Title",
- ENTRY_CONFIG,
- "test",
- config_entries.CONN_CLASS_LOCAL_PUSH,
- system_options={},
- options=ENTRY_OPTIONS,
- )
- gateway = deconz.DeconzGateway(hass, config_entry)
- gateway.api = DeconzSession(loop, session, **config_entry.data)
- gateway.api.config = Mock()
- hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway}
-
- with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)):
- await gateway.api.async_load_parameters()
-
- await hass.config_entries.async_forward_entry_setup(config_entry, "sensor")
- # To flush out the service call to update the group
- await hass.async_block_till_done()
- return gateway
-
-
async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
assert (
@@ -133,56 +89,147 @@ async def test_platform_manually_configured(hass):
async def test_no_sensors(hass):
"""Test that no sensors in deconz results in no sensor entities."""
- gateway = await setup_gateway(hass, {})
- assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert len(gateway.deconz_ids) == 0
assert len(hass.states.async_all()) == 0
async def test_sensors(hass):
"""Test successful creation of sensor entities."""
- gateway = await setup_gateway(hass, {"sensors": SENSOR})
- assert "sensor.sensor_1_name" in gateway.deconz_ids
- assert "sensor.sensor_2_name" not in gateway.deconz_ids
- assert "sensor.sensor_3_name" not in gateway.deconz_ids
- assert "sensor.sensor_3_name_battery_level" not in gateway.deconz_ids
- assert "sensor.sensor_4_name" not in gateway.deconz_ids
- assert "sensor.sensor_4_name_battery_level" in gateway.deconz_ids
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = deepcopy(SENSORS)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert "sensor.light_level_sensor" in gateway.deconz_ids
+ assert "sensor.presence_sensor" not in gateway.deconz_ids
+ assert "sensor.switch_1" not in gateway.deconz_ids
+ assert "sensor.switch_1_battery_level" not in gateway.deconz_ids
+ assert "sensor.switch_2" not in gateway.deconz_ids
+ assert "sensor.switch_2_battery_level" in gateway.deconz_ids
+ assert "sensor.daylight_sensor" in gateway.deconz_ids
+ assert "sensor.power_sensor" in gateway.deconz_ids
+ assert "sensor.consumption_sensor" in gateway.deconz_ids
+ assert "sensor.clip_light_level_sensor" not in gateway.deconz_ids
assert len(hass.states.async_all()) == 6
- gateway.api.sensors["1"].async_update({"state": {"on": False}})
- gateway.api.sensors["4"].async_update({"config": {"battery": 75}})
+ light_level_sensor = hass.states.get("sensor.light_level_sensor")
+ assert light_level_sensor.state == "999.8"
+ presence_sensor = hass.states.get("sensor.presence_sensor")
+ assert presence_sensor is None
-async def test_add_new_sensor(hass):
- """Test successful creation of sensor entities."""
- gateway = await setup_gateway(hass, {})
- sensor = Mock()
- sensor.name = "name"
- sensor.type = "ZHATemperature"
- sensor.uniqueid = "1"
- sensor.BINARY = False
- sensor.register_async_callback = Mock()
- async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor])
- await hass.async_block_till_done()
- assert "sensor.name" in gateway.deconz_ids
+ switch_1 = hass.states.get("sensor.switch_1")
+ assert switch_1 is None
+
+ switch_1_battery_level = hass.states.get("sensor.switch_1_battery_level")
+ assert switch_1_battery_level is None
+
+ switch_2 = hass.states.get("sensor.switch_2")
+ assert switch_2 is None
+
+ switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level")
+ assert switch_2_battery_level.state == "100"
+
+ daylight_sensor = hass.states.get("sensor.daylight_sensor")
+ assert daylight_sensor.state == "dawn"
+
+ power_sensor = hass.states.get("sensor.power_sensor")
+ assert power_sensor.state == "6"
+ consumption_sensor = hass.states.get("sensor.consumption_sensor")
+ assert consumption_sensor.state == "0.002"
-async def test_do_not_allow_clipsensor(hass):
- """Test that clip sensors can be ignored."""
- gateway = await setup_gateway(hass, {}, allow_clip_sensor=False)
- sensor = Mock()
- sensor.name = "name"
- sensor.type = "CLIPTemperature"
- sensor.register_async_callback = Mock()
- async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor])
+ gateway.api.sensors["1"].async_update({"state": {"lightlevel": 2000}})
+ gateway.api.sensors["4"].async_update({"config": {"battery": 75}})
await hass.async_block_till_done()
- assert len(gateway.deconz_ids) == 0
+ light_level_sensor = hass.states.get("sensor.light_level_sensor")
+ assert light_level_sensor.state == "1.6"
-async def test_unload_sensor(hass):
- """Test that it works to unload sensor entities."""
- gateway = await setup_gateway(hass, {"sensors": SENSOR})
+ switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level")
+ assert switch_2_battery_level.state == "75"
await gateway.async_reset()
assert len(hass.states.async_all()) == 0
+
+
+async def test_allow_clip_sensors(hass):
+ """Test that CLIP sensors can be allowed."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = deepcopy(SENSORS)
+ gateway = await setup_deconz_integration(
+ hass,
+ ENTRY_CONFIG,
+ options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True},
+ get_state_response=data,
+ )
+ assert "sensor.light_level_sensor" in gateway.deconz_ids
+ assert "sensor.presence_sensor" not in gateway.deconz_ids
+ assert "sensor.switch_1" not in gateway.deconz_ids
+ assert "sensor.switch_1_battery_level" not in gateway.deconz_ids
+ assert "sensor.switch_2" not in gateway.deconz_ids
+ assert "sensor.switch_2_battery_level" in gateway.deconz_ids
+ assert "sensor.daylight_sensor" in gateway.deconz_ids
+ assert "sensor.power_sensor" in gateway.deconz_ids
+ assert "sensor.consumption_sensor" in gateway.deconz_ids
+ assert "sensor.clip_light_level_sensor" in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 7
+
+ light_level_sensor = hass.states.get("sensor.light_level_sensor")
+ assert light_level_sensor.state == "999.8"
+
+ presence_sensor = hass.states.get("sensor.presence_sensor")
+ assert presence_sensor is None
+
+ switch_1 = hass.states.get("sensor.switch_1")
+ assert switch_1 is None
+
+ switch_1_battery_level = hass.states.get("sensor.switch_1_battery_level")
+ assert switch_1_battery_level is None
+
+ switch_2 = hass.states.get("sensor.switch_2")
+ assert switch_2 is None
+
+ switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level")
+ assert switch_2_battery_level.state == "100"
+
+ daylight_sensor = hass.states.get("sensor.daylight_sensor")
+ assert daylight_sensor.state == "dawn"
+
+ power_sensor = hass.states.get("sensor.power_sensor")
+ assert power_sensor.state == "6"
+
+ consumption_sensor = hass.states.get("sensor.consumption_sensor")
+ assert consumption_sensor.state == "0.002"
+
+ clip_light_level_sensor = hass.states.get("sensor.clip_light_level_sensor")
+ assert clip_light_level_sensor.state == "999.8"
+
+
+async def test_add_new_sensor(hass):
+ """Test that adding a new sensor works."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert len(gateway.deconz_ids) == 0
+
+ state_added = {
+ "t": "event",
+ "e": "added",
+ "r": "sensors",
+ "id": "1",
+ "sensor": deepcopy(SENSORS["1"]),
+ }
+ gateway.api.async_event_handler(state_added)
+ await hass.async_block_till_done()
+
+ assert "sensor.light_level_sensor" in gateway.deconz_ids
+
+ light_level_sensor = hass.states.get("sensor.light_level_sensor")
+ assert light_level_sensor.state == "999.8"
diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py
new file mode 100644
index 00000000000000..533d85eef7cbe3
--- /dev/null
+++ b/tests/components/deconz/test_services.py
@@ -0,0 +1,227 @@
+"""deCONZ service tests."""
+from copy import deepcopy
+
+from asynctest import Mock, patch
+
+import pytest
+import voluptuous as vol
+
+from homeassistant.components import deconz
+
+from .test_gateway import (
+ BRIDGEID,
+ ENTRY_CONFIG,
+ DECONZ_WEB_REQUEST,
+ setup_deconz_integration,
+)
+
+GROUP = {
+ "1": {
+ "id": "Group 1 id",
+ "name": "Group 1 name",
+ "type": "LightGroup",
+ "state": {},
+ "action": {},
+ "scenes": [{"id": "1", "name": "Scene 1"}],
+ "lights": ["1"],
+ }
+}
+
+LIGHT = {
+ "1": {
+ "id": "Light 1 id",
+ "name": "Light 1 name",
+ "state": {"reachable": True},
+ "type": "Light",
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
+ }
+}
+
+SENSOR = {
+ "1": {
+ "id": "Sensor 1 id",
+ "name": "Sensor 1 name",
+ "type": "ZHALightLevel",
+ "state": {"lightlevel": 30000, "dark": False},
+ "config": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
+ }
+}
+
+
+async def test_service_setup(hass):
+ """Verify service setup works."""
+ assert deconz.services.DECONZ_SERVICES not in hass.data
+ with patch(
+ "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True)
+ ) as async_register:
+ await deconz.services.async_setup_services(hass)
+ assert hass.data[deconz.services.DECONZ_SERVICES] is True
+ assert async_register.call_count == 2
+
+
+async def test_service_setup_already_registered(hass):
+ """Make sure that services are only registered once."""
+ hass.data[deconz.services.DECONZ_SERVICES] = True
+ with patch(
+ "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True)
+ ) as async_register:
+ await deconz.services.async_setup_services(hass)
+ async_register.assert_not_called()
+
+
+async def test_service_unload(hass):
+ """Verify service unload works."""
+ hass.data[deconz.services.DECONZ_SERVICES] = True
+ with patch(
+ "homeassistant.core.ServiceRegistry.async_remove", return_value=Mock(True)
+ ) as async_remove:
+ await deconz.services.async_unload_services(hass)
+ assert hass.data[deconz.services.DECONZ_SERVICES] is False
+ assert async_remove.call_count == 2
+
+
+async def test_service_unload_not_registered(hass):
+ """Make sure that services can only be unloaded once."""
+ with patch(
+ "homeassistant.core.ServiceRegistry.async_remove", return_value=Mock(True)
+ ) as async_remove:
+ await deconz.services.async_unload_services(hass)
+ assert deconz.services.DECONZ_SERVICES not in hass.data
+ async_remove.assert_not_called()
+
+
+async def test_configure_service_with_field(hass):
+ """Test that service invokes pydeconz with the correct path and data."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+
+ data = {
+ deconz.services.SERVICE_FIELD: "/light/2",
+ deconz.CONF_BRIDGEID: BRIDGEID,
+ deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20},
+ }
+
+ with patch(
+ "pydeconz.DeconzSession.async_put_state", return_value=Mock(True)
+ ) as put_state:
+ await hass.services.async_call(
+ deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data
+ )
+ await hass.async_block_till_done()
+ put_state.assert_called_with("/light/2", {"on": True, "attr1": 10, "attr2": 20})
+
+
+async def test_configure_service_with_entity(hass):
+ """Test that service invokes pydeconz with the correct path and data."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+
+ gateway.deconz_ids["light.test"] = "/light/1"
+ data = {
+ deconz.services.SERVICE_ENTITY: "light.test",
+ deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20},
+ }
+
+ with patch(
+ "pydeconz.DeconzSession.async_put_state", return_value=Mock(True)
+ ) as put_state:
+ await hass.services.async_call(
+ deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data
+ )
+ await hass.async_block_till_done()
+ put_state.assert_called_with("/light/1", {"on": True, "attr1": 10, "attr2": 20})
+
+
+async def test_configure_service_with_entity_and_field(hass):
+ """Test that service invokes pydeconz with the correct path and data."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+
+ gateway.deconz_ids["light.test"] = "/light/1"
+ data = {
+ deconz.services.SERVICE_ENTITY: "light.test",
+ deconz.services.SERVICE_FIELD: "/state",
+ deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20},
+ }
+
+ with patch(
+ "pydeconz.DeconzSession.async_put_state", return_value=Mock(True)
+ ) as put_state:
+ await hass.services.async_call(
+ deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data
+ )
+ await hass.async_block_till_done()
+ put_state.assert_called_with(
+ "/light/1/state", {"on": True, "attr1": 10, "attr2": 20}
+ )
+
+
+async def test_configure_service_with_faulty_field(hass):
+ """Test that service invokes pydeconz with the correct path and data."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+
+ data = {deconz.services.SERVICE_FIELD: "light/2", deconz.services.SERVICE_DATA: {}}
+
+ with pytest.raises(vol.Invalid):
+ await hass.services.async_call(
+ deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data
+ )
+ await hass.async_block_till_done()
+
+
+async def test_configure_service_with_faulty_entity(hass):
+ """Test that service invokes pydeconz with the correct path and data."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+
+ data = {
+ deconz.services.SERVICE_ENTITY: "light.nonexisting",
+ deconz.services.SERVICE_DATA: {},
+ }
+
+ with patch(
+ "pydeconz.DeconzSession.async_put_state", return_value=Mock(True)
+ ) as put_state:
+ await hass.services.async_call(
+ deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data
+ )
+ await hass.async_block_till_done()
+ put_state.assert_not_called()
+
+
+async def test_service_refresh_devices(hass):
+ """Test that service can refresh devices."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+
+ data = {deconz.CONF_BRIDGEID: BRIDGEID}
+
+ with patch(
+ "pydeconz.DeconzSession.async_get_state",
+ return_value={"groups": GROUP, "lights": LIGHT, "sensors": SENSOR},
+ ):
+ await hass.services.async_call(
+ deconz.DOMAIN, deconz.services.SERVICE_DEVICE_REFRESH, service_data=data
+ )
+ await hass.async_block_till_done()
+
+ assert gateway.deconz_ids == {
+ "light.group_1_name": "/groups/1",
+ "light.light_1_name": "/lights/1",
+ "scene.group_1_name_scene_1": "/groups/1/scenes/1",
+ "sensor.sensor_1_name": "/sensors/1",
+ }
diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py
index 746d1b6342c4f8..262bd7001f5846 100644
--- a/tests/components/deconz/test_switch.py
+++ b/tests/components/deconz/test_switch.py
@@ -1,88 +1,47 @@
"""deCONZ switch platform tests."""
-from unittest.mock import Mock, patch
+from copy import deepcopy
+
+from asynctest import patch
-from homeassistant import config_entries
from homeassistant.components import deconz
-from homeassistant.components.deconz.const import SWITCH_TYPES
-from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component
import homeassistant.components.switch as switch
-from tests.common import mock_coro
+from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration
-SUPPORTED_SWITCHES = {
+SWITCHES = {
"1": {
- "id": "Switch 1 id",
- "name": "Switch 1 name",
+ "id": "On off switch id",
+ "name": "On off switch",
"type": "On/Off plug-in unit",
"state": {"on": True, "reachable": True},
"uniqueid": "00:00:00:00:00:00:00:00-00",
},
"2": {
- "id": "Switch 2 id",
- "name": "Switch 2 name",
+ "id": "Smart plug id",
+ "name": "Smart plug",
"type": "Smart plug",
- "state": {"on": True, "reachable": True},
+ "state": {"on": False, "reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:01-00",
},
"3": {
- "id": "Switch 3 id",
- "name": "Switch 3 name",
+ "id": "Warning device id",
+ "name": "Warning device",
"type": "Warning device",
"state": {"alert": "lselect", "reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:02-00",
},
-}
-
-UNSUPPORTED_SWITCH = {
- "1": {
- "id": "Switch id",
+ "4": {
+ "id": "Unsupported switch id",
"name": "Unsupported switch",
"type": "Not a smart plug",
- "state": {},
- }
-}
-
-
-ENTRY_CONFIG = {
- deconz.const.CONF_ALLOW_CLIP_SENSOR: True,
- deconz.const.CONF_ALLOW_DECONZ_GROUPS: True,
- deconz.config_flow.CONF_API_KEY: "ABCDEF",
- deconz.config_flow.CONF_BRIDGEID: "0123456789",
- deconz.config_flow.CONF_HOST: "1.2.3.4",
- deconz.config_flow.CONF_PORT: 80,
+ "state": {"reachable": True},
+ "uniqueid": "00:00:00:00:00:00:00:03-00",
+ },
}
-async def setup_gateway(hass, data):
- """Load the deCONZ switch platform."""
- from pydeconz import DeconzSession
-
- loop = Mock()
- session = Mock()
-
- config_entry = config_entries.ConfigEntry(
- 1,
- deconz.DOMAIN,
- "Mock Title",
- ENTRY_CONFIG,
- "test",
- config_entries.CONN_CLASS_LOCAL_PUSH,
- system_options={},
- )
- gateway = deconz.DeconzGateway(hass, config_entry)
- gateway.api = DeconzSession(loop, session, **config_entry.data)
- gateway.api.config = Mock()
- hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway}
-
- with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)):
- await gateway.api.async_load_parameters()
-
- await hass.config_entries.async_forward_entry_setup(config_entry, "switch")
- # To flush out the service call to update the group
- await hass.async_block_till_done()
- return gateway
-
-
async def test_platform_manually_configured(hass):
"""Test that we do not discover anything or try to set up a gateway."""
assert (
@@ -96,68 +55,97 @@ async def test_platform_manually_configured(hass):
async def test_no_switches(hass):
"""Test that no switch entities are created."""
- gateway = await setup_gateway(hass, {})
- assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
+ )
+ assert len(gateway.deconz_ids) == 0
assert len(hass.states.async_all()) == 0
async def test_switches(hass):
"""Test that all supported switch entities are created."""
- with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)):
- gateway = await setup_gateway(hass, {"lights": SUPPORTED_SWITCHES})
- assert "switch.switch_1_name" in gateway.deconz_ids
- assert "switch.switch_2_name" in gateway.deconz_ids
- assert "switch.switch_3_name" in gateway.deconz_ids
- assert len(SUPPORTED_SWITCHES) == len(SWITCH_TYPES)
- assert len(hass.states.async_all()) == 4
-
- switch_1 = hass.states.get("switch.switch_1_name")
- assert switch_1 is not None
- assert switch_1.state == "on"
- switch_3 = hass.states.get("switch.switch_3_name")
- assert switch_3 is not None
- assert switch_3.state == "on"
-
- gateway.api.lights["1"].async_update({})
-
- await hass.services.async_call(
- "switch", "turn_on", {"entity_id": "switch.switch_1_name"}, blocking=True
- )
- await hass.services.async_call(
- "switch", "turn_off", {"entity_id": "switch.switch_1_name"}, blocking=True
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["lights"] = deepcopy(SWITCHES)
+ gateway = await setup_deconz_integration(
+ hass, ENTRY_CONFIG, options={}, get_state_response=data
)
+ assert "switch.on_off_switch" in gateway.deconz_ids
+ assert "switch.smart_plug" in gateway.deconz_ids
+ assert "switch.warning_device" in gateway.deconz_ids
+ assert "switch.unsupported_switch" not in gateway.deconz_ids
+ assert len(hass.states.async_all()) == 6
- await hass.services.async_call(
- "switch", "turn_on", {"entity_id": "switch.switch_3_name"}, blocking=True
- )
- await hass.services.async_call(
- "switch", "turn_off", {"entity_id": "switch.switch_3_name"}, blocking=True
- )
+ on_off_switch = hass.states.get("switch.on_off_switch")
+ assert on_off_switch.state == "on"
+ smart_plug = hass.states.get("switch.smart_plug")
+ assert smart_plug.state == "off"
-async def test_add_new_switch(hass):
- """Test successful creation of switch entity."""
- gateway = await setup_gateway(hass, {})
- switch = Mock()
- switch.name = "name"
- switch.type = "Smart plug"
- switch.uniqueid = "1"
- switch.register_async_callback = Mock()
- async_dispatcher_send(hass, gateway.async_event_new_device("light"), [switch])
- await hass.async_block_till_done()
- assert "switch.name" in gateway.deconz_ids
+ warning_device = hass.states.get("switch.warning_device")
+ assert warning_device.state == "on"
+ on_off_switch_device = gateway.api.lights["1"]
+ warning_device_device = gateway.api.lights["3"]
-async def test_unsupported_switch(hass):
- """Test that unsupported switches are not created."""
- await setup_gateway(hass, {"lights": UNSUPPORTED_SWITCH})
- assert len(hass.states.async_all()) == 0
+ on_off_switch_device.async_update({"state": {"on": False}})
+ warning_device_device.async_update({"state": {"alert": None}})
+ await hass.async_block_till_done()
+ on_off_switch = hass.states.get("switch.on_off_switch")
+ assert on_off_switch.state == "off"
-async def test_unload_switch(hass):
- """Test that it works to unload switch entities."""
- gateway = await setup_gateway(hass, {"lights": SUPPORTED_SWITCHES})
+ warning_device = hass.states.get("switch.warning_device")
+ assert warning_device.state == "off"
+
+ with patch.object(
+ on_off_switch_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ switch.DOMAIN,
+ switch.SERVICE_TURN_ON,
+ {"entity_id": "switch.on_off_switch"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("/lights/1/state", {"on": True})
+
+ with patch.object(
+ on_off_switch_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ switch.DOMAIN,
+ switch.SERVICE_TURN_OFF,
+ {"entity_id": "switch.on_off_switch"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("/lights/1/state", {"on": False})
+
+ with patch.object(
+ warning_device_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ switch.DOMAIN,
+ switch.SERVICE_TURN_ON,
+ {"entity_id": "switch.warning_device"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("/lights/3/state", {"alert": "lselect"})
+
+ with patch.object(
+ warning_device_device, "_async_set_callback", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ switch.DOMAIN,
+ switch.SERVICE_TURN_OFF,
+ {"entity_id": "switch.warning_device"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("/lights/3/state", {"alert": "none"})
await gateway.async_reset()
- assert len(hass.states.async_all()) == 1
+ assert len(hass.states.async_all()) == 2
diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py
index 16320257b40bc2..b05c04a16f1dc5 100644
--- a/tests/components/device_automation/test_init.py
+++ b/tests/components/device_automation/test_init.py
@@ -21,7 +21,7 @@ def entity_reg(hass):
return mock_registry(hass)
-def _same_triggers(a, b):
+def _same_lists(a, b):
if len(a) != len(b):
return False
@@ -31,6 +31,94 @@ def _same_triggers(a, b):
return True
+async def test_websocket_get_actions(hass, hass_ws_client, device_reg, entity_reg):
+ """Test we get the expected conditions from a light through websocket."""
+ await async_setup_component(hass, "device_automation", {})
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id)
+ expected_actions = [
+ {
+ "domain": "light",
+ "type": "turn_off",
+ "device_id": device_entry.id,
+ "entity_id": "light.test_5678",
+ },
+ {
+ "domain": "light",
+ "type": "turn_on",
+ "device_id": device_entry.id,
+ "entity_id": "light.test_5678",
+ },
+ {
+ "domain": "light",
+ "type": "toggle",
+ "device_id": device_entry.id,
+ "entity_id": "light.test_5678",
+ },
+ ]
+
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {"id": 1, "type": "device_automation/action/list", "device_id": device_entry.id}
+ )
+ msg = await client.receive_json()
+
+ assert msg["id"] == 1
+ assert msg["type"] == TYPE_RESULT
+ assert msg["success"]
+ actions = msg["result"]
+ assert _same_lists(actions, expected_actions)
+
+
+async def test_websocket_get_conditions(hass, hass_ws_client, device_reg, entity_reg):
+ """Test we get the expected conditions from a light through websocket."""
+ await async_setup_component(hass, "device_automation", {})
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id)
+ expected_conditions = [
+ {
+ "condition": "device",
+ "domain": "light",
+ "type": "is_off",
+ "device_id": device_entry.id,
+ "entity_id": "light.test_5678",
+ },
+ {
+ "condition": "device",
+ "domain": "light",
+ "type": "is_on",
+ "device_id": device_entry.id,
+ "entity_id": "light.test_5678",
+ },
+ ]
+
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {
+ "id": 1,
+ "type": "device_automation/condition/list",
+ "device_id": device_entry.id,
+ }
+ )
+ msg = await client.receive_json()
+
+ assert msg["id"] == 1
+ assert msg["type"] == TYPE_RESULT
+ assert msg["success"]
+ conditions = msg["result"]
+ assert _same_lists(conditions, expected_conditions)
+
+
async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_reg):
"""Test we get the expected triggers from a light through websocket."""
await async_setup_component(hass, "device_automation", {})
@@ -45,14 +133,14 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r
{
"platform": "device",
"domain": "light",
- "type": "turn_off",
+ "type": "turned_off",
"device_id": device_entry.id,
"entity_id": "light.test_5678",
},
{
"platform": "device",
"domain": "light",
- "type": "turn_on",
+ "type": "turned_on",
"device_id": device_entry.id,
"entity_id": "light.test_5678",
},
@@ -71,5 +159,5 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]
- triggers = msg["result"]["triggers"]
- assert _same_triggers(triggers, expected_triggers)
+ triggers = msg["result"]
+ assert _same_lists(triggers, expected_triggers)
diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py
index dd23bf9cff6da0..70681a6d1504d4 100644
--- a/tests/components/device_sun_light_trigger/test_init.py
+++ b/tests/components/device_sun_light_trigger/test_init.py
@@ -6,7 +6,12 @@
from homeassistant.setup import async_setup_component
from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME
-from homeassistant.components import device_tracker, light, device_sun_light_trigger
+from homeassistant.components import (
+ device_tracker,
+ light,
+ device_sun_light_trigger,
+ group,
+)
from homeassistant.components.device_tracker.const import (
ENTITY_ID_FORMAT as DT_ENTITY_ID_FORMAT,
)
@@ -90,6 +95,8 @@ async def test_lights_turn_off_when_everyone_leaves(hass, scanner):
hass, device_sun_light_trigger.DOMAIN, {device_sun_light_trigger.DOMAIN: {}}
)
+ assert light.is_on(hass)
+
hass.states.async_set(device_tracker.ENTITY_ID_ALL_DEVICES, STATE_NOT_HOME)
await hass.async_block_till_done()
@@ -111,3 +118,58 @@ async def test_lights_turn_on_when_coming_home_after_sun_set(hass, scanner):
await hass.async_block_till_done()
assert light.is_on(hass)
+
+
+async def test_lights_turn_on_when_coming_home_after_sun_set_person(hass, scanner):
+ """Test lights turn on when coming home after sun set."""
+ device_1 = DT_ENTITY_ID_FORMAT.format("device_1")
+ device_2 = DT_ENTITY_ID_FORMAT.format("device_2")
+
+ test_time = datetime(2017, 4, 5, 3, 2, 3, tzinfo=dt_util.UTC)
+ with patch("homeassistant.util.dt.utcnow", return_value=test_time):
+ await common_light.async_turn_off(hass)
+ hass.states.async_set(device_1, STATE_NOT_HOME)
+ hass.states.async_set(device_2, STATE_NOT_HOME)
+ await hass.async_block_till_done()
+
+ assert not light.is_on(hass)
+ assert hass.states.get(device_tracker.ENTITY_ID_ALL_DEVICES).state == "not_home"
+ assert hass.states.get(device_1).state == "not_home"
+ assert hass.states.get(device_2).state == "not_home"
+
+ assert await async_setup_component(
+ hass,
+ "person",
+ {"person": [{"id": "me", "name": "Me", "device_trackers": [device_1]}]},
+ )
+
+ await group.Group.async_create_group(hass, "person_me", ["person.me"])
+
+ assert await async_setup_component(
+ hass,
+ device_sun_light_trigger.DOMAIN,
+ {device_sun_light_trigger.DOMAIN: {"device_group": "group.person_me"}},
+ )
+
+ assert not light.is_on(hass)
+ assert hass.states.get(device_1).state == "not_home"
+ assert hass.states.get(device_2).state == "not_home"
+ assert hass.states.get("person.me").state == "not_home"
+
+ # Unrelated device has no impact
+ hass.states.async_set(device_2, STATE_HOME)
+ await hass.async_block_till_done()
+
+ assert not light.is_on(hass)
+ assert hass.states.get(device_1).state == "not_home"
+ assert hass.states.get(device_2).state == "home"
+ assert hass.states.get("person.me").state == "not_home"
+
+ # person home switches on
+ hass.states.async_set(device_1, STATE_HOME)
+ await hass.async_block_till_done()
+
+ assert light.is_on(hass)
+ assert hass.states.get(device_1).state == "home"
+ assert hass.states.get(device_2).state == "home"
+ assert hass.states.get("person.me").state == "home"
diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py
index 08a49c4a6670d4..fb35485f5c9685 100644
--- a/tests/components/flux/test_switch.py
+++ b/tests/components/flux/test_switch.py
@@ -82,10 +82,10 @@ async def test_flux_when_switch_is_off(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -113,7 +113,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
}
},
)
@@ -131,10 +131,10 @@ async def test_flux_before_sunrise(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -162,7 +162,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
}
},
)
@@ -184,10 +184,10 @@ async def test_flux_before_sunrise_known_location(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -210,7 +210,7 @@ async def test_flux_before_sunrise_known_location(hass):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
# 'brightness': 255,
# 'disable_brightness_adjust': True,
# 'mode': 'rgb',
@@ -237,10 +237,10 @@ async def test_flux_after_sunrise_before_sunset(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -267,7 +267,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
}
},
)
@@ -290,10 +290,10 @@ async def test_flux_after_sunset_before_stop(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -320,7 +320,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
"stop_time": "22:00",
}
},
@@ -344,10 +344,10 @@ async def test_flux_after_stop_before_sunrise(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -374,7 +374,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
}
},
)
@@ -397,10 +397,10 @@ async def test_flux_with_custom_start_stop_times(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -427,7 +427,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
"start_time": "6:00",
"stop_time": "23:30",
}
@@ -454,10 +454,10 @@ async def test_flux_before_sunrise_stop_next_day(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -484,7 +484,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
"stop_time": "01:00",
}
},
@@ -512,10 +512,10 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -542,7 +542,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
"stop_time": "01:00",
}
},
@@ -570,10 +570,10 @@ async def test_flux_after_sunset_before_midnight_stop_next_day(hass, x):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -600,7 +600,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
"stop_time": "01:00",
}
},
@@ -627,10 +627,10 @@ async def test_flux_after_sunset_after_midnight_stop_next_day(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -657,7 +657,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
"stop_time": "01:00",
}
},
@@ -684,10 +684,10 @@ async def test_flux_after_stop_before_sunrise_stop_next_day(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -714,7 +714,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
"stop_time": "01:00",
}
},
@@ -738,10 +738,10 @@ async def test_flux_with_custom_colortemps(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -768,7 +768,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
"start_colortemp": "1000",
"stop_colortemp": "6000",
"stop_time": "22:00",
@@ -794,10 +794,10 @@ async def test_flux_with_custom_brightness(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -824,7 +824,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
"brightness": 255,
"stop_time": "22:00",
}
@@ -848,23 +848,23 @@ async def test_flux_with_multiple_lights(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1, dev2, dev3 = platform.DEVICES
- common_light.turn_on(hass, entity_id=dev2.entity_id)
+ ent1, ent2, ent3 = platform.ENTITIES
+ common_light.turn_on(hass, entity_id=ent2.entity_id)
await hass.async_block_till_done()
- common_light.turn_on(hass, entity_id=dev3.entity_id)
+ common_light.turn_on(hass, entity_id=ent3.entity_id)
await hass.async_block_till_done()
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
- state = hass.states.get(dev2.entity_id)
+ state = hass.states.get(ent2.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
- state = hass.states.get(dev3.entity_id)
+ state = hass.states.get(ent3.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("xy_color") is None
assert state.attributes.get("brightness") is None
@@ -893,7 +893,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id, dev2.entity_id, dev3.entity_id],
+ "lights": [ent1.entity_id, ent2.entity_id, ent3.entity_id],
}
},
)
@@ -921,10 +921,10 @@ async def test_flux_with_mired(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("color_temp") is None
@@ -950,7 +950,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
"mode": "mired",
}
},
@@ -972,10 +972,10 @@ async def test_flux_with_rgb(hass):
hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1 = platform.DEVICES[0]
+ ent1 = platform.ENTITIES[0]
# Verify initial state of light
- state = hass.states.get(dev1.entity_id)
+ state = hass.states.get(ent1.entity_id)
assert STATE_ON == state.state
assert state.attributes.get("color_temp") is None
@@ -1001,7 +1001,7 @@ def event_date(hass, event, now=None):
switch.DOMAIN: {
"platform": "flux",
"name": "flux",
- "lights": [dev1.entity_id],
+ "lights": [ent1.entity_id],
"mode": "rgb",
}
},
diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py
index 367ea52b3a2bcd..776d8f39f69613 100644
--- a/tests/components/generic_thermostat/test_climate.py
+++ b/tests/components/generic_thermostat/test_climate.py
@@ -116,7 +116,7 @@ async def test_heater_switch(hass, setup_comp_1):
"""Test heater switching test switch."""
platform = getattr(hass.components, "test.switch")
platform.init()
- switch_1 = platform.DEVICES[1]
+ switch_1 = platform.ENTITIES[1]
assert await async_setup_component(
hass, switch.DOMAIN, {"switch": {"platform": "test"}}
)
diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py
index d3b0d8dd3010d3..87898e42d593e9 100644
--- a/tests/components/group/test_light.py
+++ b/tests/components/group/test_light.py
@@ -186,6 +186,56 @@ async def test_color_temp(hass):
assert state.attributes["color_temp"] == 1000
+async def test_emulated_color_temp_group(hass):
+ """Test emulated color temperature in a group."""
+ await async_setup_component(
+ hass,
+ "light",
+ {
+ "light": [
+ {"platform": "demo"},
+ {
+ "platform": "group",
+ "entities": [
+ "light.bed_light",
+ "light.ceiling_lights",
+ "light.kitchen_lights",
+ ],
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+
+ hass.states.async_set("light.bed_light", "on", {"supported_features": 2})
+ await hass.async_block_till_done()
+ hass.states.async_set("light.ceiling_lights", "on", {"supported_features": 63})
+ await hass.async_block_till_done()
+ hass.states.async_set("light.kitchen_lights", "on", {"supported_features": 61})
+ await hass.async_block_till_done()
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {"entity_id": "light.light_group", "color_temp": 200},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.bed_light")
+ assert state.state == "on"
+ assert state.attributes["color_temp"] == 200
+ assert "hs_color" not in state.attributes.keys()
+
+ state = hass.states.get("light.ceiling_lights")
+ assert state.state == "on"
+ assert state.attributes["color_temp"] == 200
+ assert "hs_color" in state.attributes.keys()
+
+ state = hass.states.get("light.kitchen_lights")
+ assert state.state == "on"
+ assert state.attributes["hs_color"] == (27.001, 19.243)
+
+
async def test_min_max_mireds(hass):
"""Test min/max mireds reporting."""
await async_setup_component(
diff --git a/tests/components/here_travel_time/__init__.py b/tests/components/here_travel_time/__init__.py
new file mode 100644
index 00000000000000..ac0ec709654e10
--- /dev/null
+++ b/tests/components/here_travel_time/__init__.py
@@ -0,0 +1 @@
+"""Tests for here_travel_time component."""
diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py
new file mode 100644
index 00000000000000..783209690a389c
--- /dev/null
+++ b/tests/components/here_travel_time/test_sensor.py
@@ -0,0 +1,947 @@
+"""The test for the here_travel_time sensor platform."""
+import logging
+from unittest.mock import patch
+import urllib
+
+import herepy
+import pytest
+
+from homeassistant.components.here_travel_time.sensor import (
+ ATTR_ATTRIBUTION,
+ ATTR_DESTINATION,
+ ATTR_DESTINATION_NAME,
+ ATTR_DISTANCE,
+ ATTR_DURATION,
+ ATTR_DURATION_IN_TRAFFIC,
+ ATTR_ORIGIN,
+ ATTR_ORIGIN_NAME,
+ ATTR_ROUTE,
+ CONF_MODE,
+ CONF_TRAFFIC_MODE,
+ CONF_UNIT_SYSTEM,
+ ICON_BICYCLE,
+ ICON_CAR,
+ ICON_PEDESTRIAN,
+ ICON_PUBLIC,
+ ICON_TRUCK,
+ NO_ROUTE_ERROR_MESSAGE,
+ ROUTE_MODE_FASTEST,
+ ROUTE_MODE_SHORTEST,
+ SCAN_INTERVAL,
+ TRAFFIC_MODE_DISABLED,
+ TRAFFIC_MODE_ENABLED,
+ TRAVEL_MODE_BICYCLE,
+ TRAVEL_MODE_CAR,
+ TRAVEL_MODE_PEDESTRIAN,
+ TRAVEL_MODE_PUBLIC,
+ TRAVEL_MODE_PUBLIC_TIME_TABLE,
+ TRAVEL_MODE_TRUCK,
+ UNIT_OF_MEASUREMENT,
+)
+from homeassistant.const import ATTR_ICON
+from homeassistant.setup import async_setup_component
+import homeassistant.util.dt as dt_util
+
+from tests.common import async_fire_time_changed, load_fixture
+
+DOMAIN = "sensor"
+
+PLATFORM = "here_travel_time"
+
+APP_ID = "test"
+APP_CODE = "test"
+
+TRUCK_ORIGIN_LATITUDE = "41.9798"
+TRUCK_ORIGIN_LONGITUDE = "-87.8801"
+TRUCK_DESTINATION_LATITUDE = "41.9043"
+TRUCK_DESTINATION_LONGITUDE = "-87.9216"
+
+BIKE_ORIGIN_LATITUDE = "41.9798"
+BIKE_ORIGIN_LONGITUDE = "-87.8801"
+BIKE_DESTINATION_LATITUDE = "41.9043"
+BIKE_DESTINATION_LONGITUDE = "-87.9216"
+
+CAR_ORIGIN_LATITUDE = "38.9"
+CAR_ORIGIN_LONGITUDE = "-77.04833"
+CAR_DESTINATION_LATITUDE = "39.0"
+CAR_DESTINATION_LONGITUDE = "-77.1"
+
+
+def _build_mock_url(origin, destination, modes, app_id, app_code, departure):
+ """Construct a url for HERE."""
+ base_url = "https://route.cit.api.here.com/routing/7.2/calculateroute.json?"
+ parameters = {
+ "waypoint0": origin,
+ "waypoint1": destination,
+ "mode": ";".join(str(herepy.RouteMode[mode]) for mode in modes),
+ "app_id": app_id,
+ "app_code": app_code,
+ "departure": departure,
+ }
+ url = base_url + urllib.parse.urlencode(parameters)
+ return url
+
+
+def _assert_truck_sensor(sensor):
+ """Assert that states and attributes are correct for truck_response."""
+ assert sensor.state == "14"
+ assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT
+
+ assert sensor.attributes.get(ATTR_ATTRIBUTION) is None
+ assert sensor.attributes.get(ATTR_DURATION) == 13.533333333333333
+ assert sensor.attributes.get(ATTR_DISTANCE) == 13.049
+ assert sensor.attributes.get(ATTR_ROUTE) == (
+ "I-190; I-294 S - Tri-State Tollway; I-290 W - Eisenhower Expy W; "
+ "IL-64 W - E North Ave; I-290 E - Eisenhower Expy E; I-290"
+ )
+ assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric"
+ assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 13.533333333333333
+ assert sensor.attributes.get(ATTR_ORIGIN) == ",".join(
+ [TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]
+ )
+ assert sensor.attributes.get(ATTR_DESTINATION) == ",".join(
+ [TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE]
+ )
+ assert sensor.attributes.get(ATTR_ORIGIN_NAME) == ""
+ assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Eisenhower Expy E"
+ assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_TRUCK
+ assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False
+
+ assert sensor.attributes.get(ATTR_ICON) == ICON_TRUCK
+
+
+@pytest.fixture
+def requests_mock_credentials_check(requests_mock):
+ """Add the url used in the api validation to all requests mock."""
+ modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED]
+ response_url = _build_mock_url(
+ ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]),
+ ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]),
+ modes,
+ APP_ID,
+ APP_CODE,
+ "now",
+ )
+ requests_mock.get(
+ response_url, text=load_fixture("here_travel_time/car_response.json")
+ )
+ return requests_mock
+
+
+@pytest.fixture
+def requests_mock_truck_response(requests_mock_credentials_check):
+ """Return a requests_mock for truck respones."""
+ modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_TRUCK, TRAFFIC_MODE_DISABLED]
+ response_url = _build_mock_url(
+ ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]),
+ ",".join([TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE]),
+ modes,
+ APP_ID,
+ APP_CODE,
+ "now",
+ )
+ requests_mock_credentials_check.get(
+ response_url, text=load_fixture("here_travel_time/truck_response.json")
+ )
+
+
+@pytest.fixture
+def requests_mock_car_disabled_response(requests_mock_credentials_check):
+ """Return a requests_mock for truck respones."""
+ modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED]
+ response_url = _build_mock_url(
+ ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]),
+ ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]),
+ modes,
+ APP_ID,
+ APP_CODE,
+ "now",
+ )
+ requests_mock_credentials_check.get(
+ response_url, text=load_fixture("here_travel_time/car_response.json")
+ )
+
+
+async def test_car(hass, requests_mock_car_disabled_response):
+ """Test that car works."""
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": CAR_ORIGIN_LATITUDE,
+ "origin_longitude": CAR_ORIGIN_LONGITUDE,
+ "destination_latitude": CAR_DESTINATION_LATITUDE,
+ "destination_longitude": CAR_DESTINATION_LONGITUDE,
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ }
+ }
+
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ assert sensor.state == "30"
+ assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT
+
+ assert sensor.attributes.get(ATTR_ATTRIBUTION) is None
+ assert sensor.attributes.get(ATTR_DURATION) == 30.05
+ assert sensor.attributes.get(ATTR_DISTANCE) == 23.903
+ assert sensor.attributes.get(ATTR_ROUTE) == (
+ "US-29 - K St NW; US-29 - Whitehurst Fwy; "
+ "I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd"
+ )
+ assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric"
+ assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 31.016666666666666
+ assert sensor.attributes.get(ATTR_ORIGIN) == ",".join(
+ [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]
+ )
+ assert sensor.attributes.get(ATTR_DESTINATION) == ",".join(
+ [CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]
+ )
+ assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "22nd St NW"
+ assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Service Rd S"
+ assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_CAR
+ assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False
+
+ assert sensor.attributes.get(ATTR_ICON) == ICON_CAR
+
+ # Test traffic mode disabled
+ assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get(
+ ATTR_DURATION_IN_TRAFFIC
+ )
+
+
+async def test_traffic_mode_enabled(hass, requests_mock_credentials_check):
+ """Test that traffic mode enabled works."""
+ modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_ENABLED]
+ response_url = _build_mock_url(
+ ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]),
+ ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]),
+ modes,
+ APP_ID,
+ APP_CODE,
+ "now",
+ )
+ requests_mock_credentials_check.get(
+ response_url, text=load_fixture("here_travel_time/car_enabled_response.json")
+ )
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": CAR_ORIGIN_LATITUDE,
+ "origin_longitude": CAR_ORIGIN_LONGITUDE,
+ "destination_latitude": CAR_DESTINATION_LATITUDE,
+ "destination_longitude": CAR_DESTINATION_LONGITUDE,
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "traffic_mode": True,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+
+ # Test traffic mode enabled
+ assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get(
+ ATTR_DURATION_IN_TRAFFIC
+ )
+
+
+async def test_imperial(hass, requests_mock_car_disabled_response):
+ """Test that imperial units work."""
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": CAR_ORIGIN_LATITUDE,
+ "origin_longitude": CAR_ORIGIN_LONGITUDE,
+ "destination_latitude": CAR_DESTINATION_LATITUDE,
+ "destination_longitude": CAR_DESTINATION_LONGITUDE,
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "unit_system": "imperial",
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ assert sensor.attributes.get(ATTR_DISTANCE) == 14.852635608048994
+
+
+async def test_route_mode_shortest(hass, requests_mock_credentials_check):
+ """Test that route mode shortest works."""
+ origin = "38.902981,-77.048338"
+ destination = "39.042158,-77.119116"
+ modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED]
+ response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now")
+ requests_mock_credentials_check.get(
+ response_url, text=load_fixture("here_travel_time/car_shortest_response.json")
+ )
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": origin.split(",")[0],
+ "origin_longitude": origin.split(",")[1],
+ "destination_latitude": destination.split(",")[0],
+ "destination_longitude": destination.split(",")[1],
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "route_mode": ROUTE_MODE_SHORTEST,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ assert sensor.attributes.get(ATTR_DISTANCE) == 18.388
+
+
+async def test_route_mode_fastest(hass, requests_mock_credentials_check):
+ """Test that route mode fastest works."""
+ origin = "38.902981,-77.048338"
+ destination = "39.042158,-77.119116"
+ modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_ENABLED]
+ response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now")
+ requests_mock_credentials_check.get(
+ response_url, text=load_fixture("here_travel_time/car_enabled_response.json")
+ )
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": origin.split(",")[0],
+ "origin_longitude": origin.split(",")[1],
+ "destination_latitude": destination.split(",")[0],
+ "destination_longitude": destination.split(",")[1],
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "traffic_mode": True,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ assert sensor.attributes.get(ATTR_DISTANCE) == 23.381
+
+
+async def test_truck(hass, requests_mock_truck_response):
+ """Test that truck works."""
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": TRUCK_ORIGIN_LATITUDE,
+ "origin_longitude": TRUCK_ORIGIN_LONGITUDE,
+ "destination_latitude": TRUCK_DESTINATION_LATITUDE,
+ "destination_longitude": TRUCK_DESTINATION_LONGITUDE,
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "mode": TRAVEL_MODE_TRUCK,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+ sensor = hass.states.get("sensor.test")
+ _assert_truck_sensor(sensor)
+
+
+async def test_public_transport(hass, requests_mock_credentials_check):
+ """Test that publicTransport works."""
+ origin = "41.9798,-87.8801"
+ destination = "41.9043,-87.9216"
+ modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC, TRAFFIC_MODE_DISABLED]
+ response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now")
+ requests_mock_credentials_check.get(
+ response_url, text=load_fixture("here_travel_time/public_response.json")
+ )
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": origin.split(",")[0],
+ "origin_longitude": origin.split(",")[1],
+ "destination_latitude": destination.split(",")[0],
+ "destination_longitude": destination.split(",")[1],
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "mode": TRAVEL_MODE_PUBLIC,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ assert sensor.state == "89"
+ assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT
+
+ assert sensor.attributes.get(ATTR_ATTRIBUTION) is None
+ assert sensor.attributes.get(ATTR_DURATION) == 89.16666666666667
+ assert sensor.attributes.get(ATTR_DISTANCE) == 22.325
+ assert sensor.attributes.get(ATTR_ROUTE) == (
+ "332 - Palmer/Schiller; 332 - Cargo Rd./Delta Cargo; " "332 - Palmer/Schiller"
+ )
+ assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric"
+ assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 89.16666666666667
+ assert sensor.attributes.get(ATTR_ORIGIN) == origin
+ assert sensor.attributes.get(ATTR_DESTINATION) == destination
+ assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd"
+ assert sensor.attributes.get(ATTR_DESTINATION_NAME) == ""
+ assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_PUBLIC
+ assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False
+
+ assert sensor.attributes.get(ATTR_ICON) == ICON_PUBLIC
+
+
+async def test_public_transport_time_table(hass, requests_mock_credentials_check):
+ """Test that publicTransportTimeTable works."""
+ origin = "41.9798,-87.8801"
+ destination = "41.9043,-87.9216"
+ modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_DISABLED]
+ response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now")
+ requests_mock_credentials_check.get(
+ response_url,
+ text=load_fixture("here_travel_time/public_time_table_response.json"),
+ )
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": origin.split(",")[0],
+ "origin_longitude": origin.split(",")[1],
+ "destination_latitude": destination.split(",")[0],
+ "destination_longitude": destination.split(",")[1],
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ assert sensor.state == "80"
+ assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT
+
+ assert sensor.attributes.get(ATTR_ATTRIBUTION) is None
+ assert sensor.attributes.get(ATTR_DURATION) == 79.73333333333333
+ assert sensor.attributes.get(ATTR_DISTANCE) == 14.775
+ assert sensor.attributes.get(ATTR_ROUTE) == (
+ "330 - Archer/Harlem (Terminal); 309 - Elmhurst Metra Station"
+ )
+ assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric"
+ assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 79.73333333333333
+ assert sensor.attributes.get(ATTR_ORIGIN) == origin
+ assert sensor.attributes.get(ATTR_DESTINATION) == destination
+ assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd"
+ assert sensor.attributes.get(ATTR_DESTINATION_NAME) == ""
+ assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_PUBLIC_TIME_TABLE
+ assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False
+
+ assert sensor.attributes.get(ATTR_ICON) == ICON_PUBLIC
+
+
+async def test_pedestrian(hass, requests_mock_credentials_check):
+ """Test that pedestrian works."""
+ origin = "41.9798,-87.8801"
+ destination = "41.9043,-87.9216"
+ modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PEDESTRIAN, TRAFFIC_MODE_DISABLED]
+ response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now")
+ requests_mock_credentials_check.get(
+ response_url, text=load_fixture("here_travel_time/pedestrian_response.json")
+ )
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": origin.split(",")[0],
+ "origin_longitude": origin.split(",")[1],
+ "destination_latitude": destination.split(",")[0],
+ "destination_longitude": destination.split(",")[1],
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "mode": TRAVEL_MODE_PEDESTRIAN,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ assert sensor.state == "211"
+ assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT
+
+ assert sensor.attributes.get(ATTR_ATTRIBUTION) is None
+ assert sensor.attributes.get(ATTR_DURATION) == 210.51666666666668
+ assert sensor.attributes.get(ATTR_DISTANCE) == 12.533
+ assert sensor.attributes.get(ATTR_ROUTE) == (
+ "Mannheim Rd; W Belmont Ave; Cullerton St; E Fullerton Ave; "
+ "La Porte Ave; E Palmer Ave; N Railroad Ave; W North Ave; "
+ "E North Ave; E Third St"
+ )
+ assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric"
+ assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 210.51666666666668
+ assert sensor.attributes.get(ATTR_ORIGIN) == origin
+ assert sensor.attributes.get(ATTR_DESTINATION) == destination
+ assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd"
+ assert sensor.attributes.get(ATTR_DESTINATION_NAME) == ""
+ assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_PEDESTRIAN
+ assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False
+
+ assert sensor.attributes.get(ATTR_ICON) == ICON_PEDESTRIAN
+
+
+async def test_bicycle(hass, requests_mock_credentials_check):
+ """Test that bicycle works."""
+ origin = "41.9798,-87.8801"
+ destination = "41.9043,-87.9216"
+ modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_BICYCLE, TRAFFIC_MODE_DISABLED]
+ response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now")
+ requests_mock_credentials_check.get(
+ response_url, text=load_fixture("here_travel_time/bike_response.json")
+ )
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": origin.split(",")[0],
+ "origin_longitude": origin.split(",")[1],
+ "destination_latitude": destination.split(",")[0],
+ "destination_longitude": destination.split(",")[1],
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "mode": TRAVEL_MODE_BICYCLE,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ assert sensor.state == "55"
+ assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT
+
+ assert sensor.attributes.get(ATTR_ATTRIBUTION) is None
+ assert sensor.attributes.get(ATTR_DURATION) == 54.86666666666667
+ assert sensor.attributes.get(ATTR_DISTANCE) == 12.613
+ assert sensor.attributes.get(ATTR_ROUTE) == (
+ "Mannheim Rd; W Belmont Ave; Cullerton St; N Landen Dr; "
+ "E Fullerton Ave; N Wolf Rd; W North Ave; N Clinton Ave; "
+ "E Third St; N Caroline Ave"
+ )
+ assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric"
+ assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 54.86666666666667
+ assert sensor.attributes.get(ATTR_ORIGIN) == origin
+ assert sensor.attributes.get(ATTR_DESTINATION) == destination
+ assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd"
+ assert sensor.attributes.get(ATTR_DESTINATION_NAME) == ""
+ assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_BICYCLE
+ assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False
+
+ assert sensor.attributes.get(ATTR_ICON) == ICON_BICYCLE
+
+
+async def test_location_zone(hass, requests_mock_truck_response):
+ """Test that origin/destination supplied by a zone works."""
+ utcnow = dt_util.utcnow()
+ # Patching 'utcnow' to gain more control over the timed update.
+ with patch("homeassistant.util.dt.utcnow", return_value=utcnow):
+ zone_config = {
+ "zone": [
+ {
+ "name": "Destination",
+ "latitude": TRUCK_DESTINATION_LATITUDE,
+ "longitude": TRUCK_DESTINATION_LONGITUDE,
+ "radius": 250,
+ "passive": False,
+ },
+ {
+ "name": "Origin",
+ "latitude": TRUCK_ORIGIN_LATITUDE,
+ "longitude": TRUCK_ORIGIN_LONGITUDE,
+ "radius": 250,
+ "passive": False,
+ },
+ ]
+ }
+ assert await async_setup_component(hass, "zone", zone_config)
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_entity_id": "zone.origin",
+ "destination_entity_id": "zone.destination",
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "mode": TRAVEL_MODE_TRUCK,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ _assert_truck_sensor(sensor)
+
+ # Test that update works more than once
+ async_fire_time_changed(hass, utcnow + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ sensor = hass.states.get("sensor.test")
+ _assert_truck_sensor(sensor)
+
+
+async def test_location_sensor(hass, requests_mock_truck_response):
+ """Test that origin/destination supplied by a sensor works."""
+ utcnow = dt_util.utcnow()
+ # Patching 'utcnow' to gain more control over the timed update.
+ with patch("homeassistant.util.dt.utcnow", return_value=utcnow):
+ hass.states.async_set(
+ "sensor.origin", ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE])
+ )
+ hass.states.async_set(
+ "sensor.destination",
+ ",".join([TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE]),
+ )
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_entity_id": "sensor.origin",
+ "destination_entity_id": "sensor.destination",
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "mode": TRAVEL_MODE_TRUCK,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ _assert_truck_sensor(sensor)
+
+ # Test that update works more than once
+ async_fire_time_changed(hass, utcnow + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ sensor = hass.states.get("sensor.test")
+ _assert_truck_sensor(sensor)
+
+
+async def test_location_person(hass, requests_mock_truck_response):
+ """Test that origin/destination supplied by a person works."""
+ utcnow = dt_util.utcnow()
+ # Patching 'utcnow' to gain more control over the timed update.
+ with patch("homeassistant.util.dt.utcnow", return_value=utcnow):
+ hass.states.async_set(
+ "person.origin",
+ "unknown",
+ {
+ "latitude": float(TRUCK_ORIGIN_LATITUDE),
+ "longitude": float(TRUCK_ORIGIN_LONGITUDE),
+ },
+ )
+ hass.states.async_set(
+ "person.destination",
+ "unknown",
+ {
+ "latitude": float(TRUCK_DESTINATION_LATITUDE),
+ "longitude": float(TRUCK_DESTINATION_LONGITUDE),
+ },
+ )
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_entity_id": "person.origin",
+ "destination_entity_id": "person.destination",
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "mode": TRAVEL_MODE_TRUCK,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ _assert_truck_sensor(sensor)
+
+ # Test that update works more than once
+ async_fire_time_changed(hass, utcnow + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ sensor = hass.states.get("sensor.test")
+ _assert_truck_sensor(sensor)
+
+
+async def test_location_device_tracker(hass, requests_mock_truck_response):
+ """Test that origin/destination supplied by a device_tracker works."""
+ utcnow = dt_util.utcnow()
+ # Patching 'utcnow' to gain more control over the timed update.
+ with patch("homeassistant.util.dt.utcnow", return_value=utcnow):
+ hass.states.async_set(
+ "device_tracker.origin",
+ "unknown",
+ {
+ "latitude": float(TRUCK_ORIGIN_LATITUDE),
+ "longitude": float(TRUCK_ORIGIN_LONGITUDE),
+ },
+ )
+ hass.states.async_set(
+ "device_tracker.destination",
+ "unknown",
+ {
+ "latitude": float(TRUCK_DESTINATION_LATITUDE),
+ "longitude": float(TRUCK_DESTINATION_LONGITUDE),
+ },
+ )
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_entity_id": "device_tracker.origin",
+ "destination_entity_id": "device_tracker.destination",
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "mode": TRAVEL_MODE_TRUCK,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ _assert_truck_sensor(sensor)
+
+ # Test that update works more than once
+ async_fire_time_changed(hass, utcnow + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ sensor = hass.states.get("sensor.test")
+ _assert_truck_sensor(sensor)
+
+
+async def test_location_device_tracker_added_after_update(
+ hass, requests_mock_truck_response, caplog
+):
+ """Test that device_tracker added after first update works."""
+ caplog.set_level(logging.ERROR)
+ utcnow = dt_util.utcnow()
+ # Patching 'utcnow' to gain more control over the timed update.
+ with patch("homeassistant.util.dt.utcnow", return_value=utcnow):
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_entity_id": "device_tracker.origin",
+ "destination_entity_id": "device_tracker.destination",
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "mode": TRAVEL_MODE_TRUCK,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ assert len(caplog.records) == 2
+ assert "Unable to find entity" in caplog.text
+ caplog.clear()
+
+ # Device tracker appear after first update
+ hass.states.async_set(
+ "device_tracker.origin",
+ "unknown",
+ {
+ "latitude": float(TRUCK_ORIGIN_LATITUDE),
+ "longitude": float(TRUCK_ORIGIN_LONGITUDE),
+ },
+ )
+ hass.states.async_set(
+ "device_tracker.destination",
+ "unknown",
+ {
+ "latitude": float(TRUCK_DESTINATION_LATITUDE),
+ "longitude": float(TRUCK_DESTINATION_LONGITUDE),
+ },
+ )
+
+ # Test that update works more than once
+ async_fire_time_changed(hass, utcnow + SCAN_INTERVAL)
+ await hass.async_block_till_done()
+
+ sensor = hass.states.get("sensor.test")
+ _assert_truck_sensor(sensor)
+ assert len(caplog.records) == 0
+
+
+async def test_location_device_tracker_in_zone(
+ hass, requests_mock_truck_response, caplog
+):
+ """Test that device_tracker in zone uses device_tracker state works."""
+ caplog.set_level(logging.DEBUG)
+ zone_config = {
+ "zone": [
+ {
+ "name": "Origin",
+ "latitude": TRUCK_ORIGIN_LATITUDE,
+ "longitude": TRUCK_ORIGIN_LONGITUDE,
+ "radius": 250,
+ "passive": False,
+ }
+ ]
+ }
+ assert await async_setup_component(hass, "zone", zone_config)
+ hass.states.async_set(
+ "device_tracker.origin", "origin", {"latitude": None, "longitude": None}
+ )
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_entity_id": "device_tracker.origin",
+ "destination_latitude": TRUCK_DESTINATION_LATITUDE,
+ "destination_longitude": TRUCK_DESTINATION_LONGITUDE,
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "mode": TRAVEL_MODE_TRUCK,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+
+ sensor = hass.states.get("sensor.test")
+ _assert_truck_sensor(sensor)
+ assert ", getting zone location" in caplog.text
+
+
+async def test_route_not_found(hass, requests_mock_credentials_check, caplog):
+ """Test that route not found error is correctly handled."""
+ caplog.set_level(logging.ERROR)
+ origin = "52.516,13.3779"
+ destination = "47.013399,-10.171986"
+ modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED]
+ response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now")
+ requests_mock_credentials_check.get(
+ response_url,
+ text=load_fixture("here_travel_time/routing_error_no_route_found.json"),
+ )
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": origin.split(",")[0],
+ "origin_longitude": origin.split(",")[1],
+ "destination_latitude": destination.split(",")[0],
+ "destination_longitude": destination.split(",")[1],
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+ assert len(caplog.records) == 1
+ assert NO_ROUTE_ERROR_MESSAGE in caplog.text
+
+
+async def test_pattern_origin(hass, caplog):
+ """Test that pattern matching the origin works."""
+ caplog.set_level(logging.ERROR)
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": "138.90",
+ "origin_longitude": "-77.04833",
+ "destination_latitude": CAR_DESTINATION_LATITUDE,
+ "destination_longitude": CAR_DESTINATION_LONGITUDE,
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+ assert len(caplog.records) == 1
+ assert "invalid latitude" in caplog.text
+
+
+async def test_pattern_destination(hass, caplog):
+ """Test that pattern matching the destination works."""
+ caplog.set_level(logging.ERROR)
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": CAR_ORIGIN_LATITUDE,
+ "origin_longitude": CAR_ORIGIN_LONGITUDE,
+ "destination_latitude": "139.0",
+ "destination_longitude": "-77.1",
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+ assert len(caplog.records) == 1
+ assert "invalid latitude" in caplog.text
+
+
+async def test_invalid_credentials(hass, requests_mock, caplog):
+ """Test that invalid credentials error is correctly handled."""
+ caplog.set_level(logging.ERROR)
+ modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED]
+ response_url = _build_mock_url(
+ ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]),
+ ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]),
+ modes,
+ APP_ID,
+ APP_CODE,
+ "now",
+ )
+ requests_mock.get(
+ response_url,
+ text=load_fixture("here_travel_time/routing_error_invalid_credentials.json"),
+ )
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": CAR_ORIGIN_LATITUDE,
+ "origin_longitude": CAR_ORIGIN_LONGITUDE,
+ "destination_latitude": CAR_DESTINATION_LATITUDE,
+ "destination_longitude": CAR_DESTINATION_LONGITUDE,
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+ assert len(caplog.records) == 1
+ assert "Invalid credentials" in caplog.text
+
+
+async def test_attribution(hass, requests_mock_credentials_check):
+ """Test that attributions are correctly displayed."""
+ origin = "50.037751372637686,14.39233448220898"
+ destination = "50.07993838201255,14.42582157361062"
+ modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_ENABLED]
+ response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now")
+ requests_mock_credentials_check.get(
+ response_url, text=load_fixture("here_travel_time/attribution_response.json")
+ )
+
+ config = {
+ DOMAIN: {
+ "platform": PLATFORM,
+ "name": "test",
+ "origin_latitude": origin.split(",")[0],
+ "origin_longitude": origin.split(",")[1],
+ "destination_latitude": destination.split(",")[0],
+ "destination_longitude": destination.split(",")[1],
+ "app_id": APP_ID,
+ "app_code": APP_CODE,
+ "traffic_mode": True,
+ "route_mode": ROUTE_MODE_SHORTEST,
+ "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE,
+ }
+ }
+ assert await async_setup_component(hass, DOMAIN, config)
+ sensor = hass.states.get("sensor.test")
+ assert (
+ sensor.attributes.get(ATTR_ATTRIBUTION)
+ == "With the support of HERE Technologies. All information is provided without warranty of any kind."
+ )
diff --git a/tests/components/iaqualink/__init__.py b/tests/components/iaqualink/__init__.py
new file mode 100644
index 00000000000000..c4e3b75d0ae6b2
--- /dev/null
+++ b/tests/components/iaqualink/__init__.py
@@ -0,0 +1 @@
+"""Tests for the iAqualink component."""
diff --git a/tests/components/iaqualink/test_config_flow.py b/tests/components/iaqualink/test_config_flow.py
new file mode 100644
index 00000000000000..5c4d75ee3c155f
--- /dev/null
+++ b/tests/components/iaqualink/test_config_flow.py
@@ -0,0 +1,77 @@
+"""Tests for iAqualink config flow."""
+from unittest.mock import patch
+
+import iaqualink
+import pytest
+
+from homeassistant.components.iaqualink import config_flow
+from tests.common import MockConfigEntry, mock_coro
+
+DATA = {"username": "test@example.com", "password": "pass"}
+
+
+@pytest.mark.parametrize("step", ["import", "user"])
+async def test_already_configured(hass, step):
+ """Test config flow when iaqualink component is already setup."""
+ MockConfigEntry(domain="iaqualink", data=DATA).add_to_hass(hass)
+
+ flow = config_flow.AqualinkFlowHandler()
+ flow.hass = hass
+ flow.context = {}
+
+ fname = f"async_step_{step}"
+ func = getattr(flow, fname)
+ result = await func(DATA)
+
+ assert result["type"] == "abort"
+
+
+@pytest.mark.parametrize("step", ["import", "user"])
+async def test_without_config(hass, step):
+ """Test with no configuration."""
+ flow = config_flow.AqualinkFlowHandler()
+ flow.hass = hass
+ flow.context = {}
+
+ fname = f"async_step_{step}"
+ func = getattr(flow, fname)
+ result = await func()
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+
+@pytest.mark.parametrize("step", ["import", "user"])
+async def test_with_invalid_credentials(hass, step):
+ """Test config flow with invalid username and/or password."""
+ flow = config_flow.AqualinkFlowHandler()
+ flow.hass = hass
+
+ fname = f"async_step_{step}"
+ func = getattr(flow, fname)
+ with patch(
+ "iaqualink.AqualinkClient.login", side_effect=iaqualink.AqualinkLoginException
+ ):
+ result = await func(DATA)
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"base": "connection_failure"}
+
+
+@pytest.mark.parametrize("step", ["import", "user"])
+async def test_with_existing_config(hass, step):
+ """Test with existing configuration."""
+ flow = config_flow.AqualinkFlowHandler()
+ flow.hass = hass
+ flow.context = {}
+
+ fname = f"async_step_{step}"
+ func = getattr(flow, fname)
+ with patch("iaqualink.AqualinkClient.login", return_value=mock_coro(None)):
+ result = await func(DATA)
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == DATA["username"]
+ assert result["data"] == DATA
diff --git a/tests/components/izone/__init__.py b/tests/components/izone/__init__.py
new file mode 100644
index 00000000000000..1baeb3fee82702
--- /dev/null
+++ b/tests/components/izone/__init__.py
@@ -0,0 +1 @@
+"""IZone tests."""
diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py
new file mode 100644
index 00000000000000..faa920271e385b
--- /dev/null
+++ b/tests/components/izone/test_config_flow.py
@@ -0,0 +1,83 @@
+"""Tests for iZone."""
+
+from unittest.mock import Mock, patch
+
+import pytest
+
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components.izone.const import IZONE, DISPATCH_CONTROLLER_DISCOVERED
+
+from tests.common import mock_coro
+
+
+@pytest.fixture
+def mock_disco():
+ """Mock discovery service."""
+ disco = Mock()
+ disco.pi_disco = Mock()
+ disco.pi_disco.controllers = {}
+ yield disco
+
+
+def _mock_start_discovery(hass, mock_disco):
+ from homeassistant.helpers.dispatcher import async_dispatcher_send
+
+ def do_disovered(*args):
+ async_dispatcher_send(hass, DISPATCH_CONTROLLER_DISCOVERED, True)
+ return mock_coro(mock_disco)
+
+ return do_disovered
+
+
+async def test_not_found(hass, mock_disco):
+ """Test not finding iZone controller."""
+
+ with patch(
+ "homeassistant.components.izone.discovery.async_start_discovery_service"
+ ) as start_disco, patch(
+ "homeassistant.components.izone.discovery.async_stop_discovery_service",
+ return_value=mock_coro(),
+ ) as stop_disco:
+ start_disco.side_effect = _mock_start_discovery(hass, mock_disco)
+ result = await hass.config_entries.flow.async_init(
+ IZONE, context={"source": config_entries.SOURCE_USER}
+ )
+
+ # Confirmation form
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+
+ await hass.async_block_till_done()
+
+ stop_disco.assert_called_once()
+
+
+async def test_found(hass, mock_disco):
+ """Test not finding iZone controller."""
+ mock_disco.pi_disco.controllers["blah"] = object()
+
+ with patch(
+ "homeassistant.components.izone.climate.async_setup_entry",
+ return_value=mock_coro(True),
+ ) as mock_setup, patch(
+ "homeassistant.components.izone.discovery.async_start_discovery_service"
+ ) as start_disco, patch(
+ "homeassistant.components.izone.async_start_discovery_service",
+ return_value=mock_coro(),
+ ):
+ start_disco.side_effect = _mock_start_discovery(hass, mock_disco)
+ result = await hass.config_entries.flow.async_init(
+ IZONE, context={"source": config_entries.SOURCE_USER}
+ )
+
+ # Confirmation form
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ mock_setup.assert_called_once()
diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py
index d6928c189e8c57..54589a640cc0cd 100644
--- a/tests/components/jewish_calendar/__init__.py
+++ b/tests/components/jewish_calendar/__init__.py
@@ -1 +1,72 @@
"""Tests for the jewish_calendar component."""
+from datetime import datetime
+from collections import namedtuple
+from contextlib import contextmanager
+from unittest.mock import patch
+
+from homeassistant.components import jewish_calendar
+import homeassistant.util.dt as dt_util
+
+
+_LatLng = namedtuple("_LatLng", ["lat", "lng"])
+
+NYC_LATLNG = _LatLng(40.7128, -74.0060)
+JERUSALEM_LATLNG = _LatLng(31.778, 35.235)
+
+ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE
+
+
+def teardown_module():
+ """Reset time zone."""
+ dt_util.set_default_time_zone(ORIG_TIME_ZONE)
+
+
+def make_nyc_test_params(dtime, results, havdalah_offset=0):
+ """Make test params for NYC."""
+ if isinstance(results, dict):
+ time_zone = dt_util.get_time_zone("America/New_York")
+ results = {
+ key: time_zone.localize(value) if isinstance(value, datetime) else value
+ for key, value in results.items()
+ }
+ return (
+ dtime,
+ jewish_calendar.CANDLE_LIGHT_DEFAULT,
+ havdalah_offset,
+ True,
+ "America/New_York",
+ NYC_LATLNG.lat,
+ NYC_LATLNG.lng,
+ results,
+ )
+
+
+def make_jerusalem_test_params(dtime, results, havdalah_offset=0):
+ """Make test params for Jerusalem."""
+ if isinstance(results, dict):
+ time_zone = dt_util.get_time_zone("Asia/Jerusalem")
+ results = {
+ key: time_zone.localize(value) if isinstance(value, datetime) else value
+ for key, value in results.items()
+ }
+ return (
+ dtime,
+ jewish_calendar.CANDLE_LIGHT_DEFAULT,
+ havdalah_offset,
+ False,
+ "Asia/Jerusalem",
+ JERUSALEM_LATLNG.lat,
+ JERUSALEM_LATLNG.lng,
+ results,
+ )
+
+
+@contextmanager
+def alter_time(local_time):
+ """Manage multiple time mocks."""
+ utc_time = dt_util.as_utc(local_time)
+ patch1 = patch("homeassistant.util.dt.utcnow", return_value=utc_time)
+ patch2 = patch("homeassistant.util.dt.now", return_value=local_time)
+
+ with patch1, patch2:
+ yield
diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py
new file mode 100644
index 00000000000000..64745d8929f7e7
--- /dev/null
+++ b/tests/components/jewish_calendar/test_binary_sensor.py
@@ -0,0 +1,97 @@
+"""The tests for the Jewish calendar binary sensors."""
+from datetime import timedelta
+from datetime import datetime as dt
+
+import pytest
+
+from homeassistant.const import STATE_ON, STATE_OFF
+import homeassistant.util.dt as dt_util
+from homeassistant.setup import async_setup_component
+from homeassistant.components import jewish_calendar
+
+from tests.common import async_fire_time_changed
+from . import alter_time, make_nyc_test_params, make_jerusalem_test_params
+
+
+MELACHA_PARAMS = [
+ make_nyc_test_params(dt(2018, 9, 1, 16, 0), STATE_ON),
+ make_nyc_test_params(dt(2018, 9, 1, 20, 21), STATE_OFF),
+ make_nyc_test_params(dt(2018, 9, 7, 13, 1), STATE_OFF),
+ make_nyc_test_params(dt(2018, 9, 8, 21, 25), STATE_OFF),
+ make_nyc_test_params(dt(2018, 9, 9, 21, 25), STATE_ON),
+ make_nyc_test_params(dt(2018, 9, 10, 21, 25), STATE_ON),
+ make_nyc_test_params(dt(2018, 9, 28, 21, 25), STATE_ON),
+ make_nyc_test_params(dt(2018, 9, 29, 21, 25), STATE_OFF),
+ make_nyc_test_params(dt(2018, 9, 30, 21, 25), STATE_ON),
+ make_nyc_test_params(dt(2018, 10, 1, 21, 25), STATE_ON),
+ make_jerusalem_test_params(dt(2018, 9, 29, 21, 25), STATE_OFF),
+ make_jerusalem_test_params(dt(2018, 9, 30, 21, 25), STATE_ON),
+ make_jerusalem_test_params(dt(2018, 10, 1, 21, 25), STATE_OFF),
+]
+
+MELACHA_TEST_IDS = [
+ "currently_first_shabbat",
+ "after_first_shabbat",
+ "friday_upcoming_shabbat",
+ "upcoming_rosh_hashana",
+ "currently_rosh_hashana",
+ "second_day_rosh_hashana",
+ "currently_shabbat_chol_hamoed",
+ "upcoming_two_day_yomtov_in_diaspora",
+ "currently_first_day_of_two_day_yomtov_in_diaspora",
+ "currently_second_day_of_two_day_yomtov_in_diaspora",
+ "upcoming_one_day_yom_tov_in_israel",
+ "currently_one_day_yom_tov_in_israel",
+ "after_one_day_yom_tov_in_israel",
+]
+
+
+@pytest.mark.parametrize(
+ [
+ "now",
+ "candle_lighting",
+ "havdalah",
+ "diaspora",
+ "tzname",
+ "latitude",
+ "longitude",
+ "result",
+ ],
+ MELACHA_PARAMS,
+ ids=MELACHA_TEST_IDS,
+)
+async def test_issur_melacha_sensor(
+ hass, now, candle_lighting, havdalah, diaspora, tzname, latitude, longitude, result
+):
+ """Test Issur Melacha sensor output."""
+ time_zone = dt_util.get_time_zone(tzname)
+ test_time = time_zone.localize(now)
+
+ hass.config.time_zone = time_zone
+ hass.config.latitude = latitude
+ hass.config.longitude = longitude
+
+ with alter_time(test_time):
+ assert await async_setup_component(
+ hass,
+ jewish_calendar.DOMAIN,
+ {
+ "jewish_calendar": {
+ "name": "test",
+ "language": "english",
+ "diaspora": diaspora,
+ "candle_lighting_minutes_before_sunset": candle_lighting,
+ "havdalah_minutes_after_sunset": havdalah,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ future = dt_util.utcnow() + timedelta(seconds=30)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ assert (
+ hass.states.get("binary_sensor.test_issur_melacha_in_effect").state
+ == result
+ )
diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py
index f8c214f9800007..8d72830b3698ab 100644
--- a/tests/components/jewish_calendar/test_sensor.py
+++ b/tests/components/jewish_calendar/test_sensor.py
@@ -1,733 +1,565 @@
-"""The tests for the Jewish calendar sensor platform."""
-from collections import namedtuple
-from datetime import time
+"""The tests for the Jewish calendar sensors."""
+from datetime import time, timedelta
from datetime import datetime as dt
-from unittest.mock import patch
import pytest
-from homeassistant.util.async_ import run_coroutine_threadsafe
-from homeassistant.util.dt import get_time_zone, set_default_time_zone
-from homeassistant.setup import setup_component
-from homeassistant.components.jewish_calendar.sensor import (
- JewishCalSensor,
- CANDLE_LIGHT_DEFAULT,
-)
-from tests.common import get_test_home_assistant
+import homeassistant.util.dt as dt_util
+from homeassistant.setup import async_setup_component
+from homeassistant.components import jewish_calendar
+from tests.common import async_fire_time_changed
+from . import alter_time, make_nyc_test_params, make_jerusalem_test_params
-_LatLng = namedtuple("_LatLng", ["lat", "lng"])
-NYC_LATLNG = _LatLng(40.7128, -74.0060)
-JERUSALEM_LATLNG = _LatLng(31.778, 35.235)
+async def test_jewish_calendar_min_config(hass):
+ """Test minimum jewish calendar configuration."""
+ assert await async_setup_component(
+ hass, jewish_calendar.DOMAIN, {"jewish_calendar": {}}
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get("sensor.jewish_calendar_date") is not None
-def make_nyc_test_params(dtime, results, havdalah_offset=0):
- """Make test params for NYC."""
- return (
- dtime,
- CANDLE_LIGHT_DEFAULT,
- havdalah_offset,
- True,
- "America/New_York",
- NYC_LATLNG.lat,
- NYC_LATLNG.lng,
- results,
+async def test_jewish_calendar_hebrew(hass):
+ """Test jewish calendar sensor with language set to hebrew."""
+ assert await async_setup_component(
+ hass, jewish_calendar.DOMAIN, {"jewish_calendar": {"language": "hebrew"}}
)
+ await hass.async_block_till_done()
+ assert hass.states.get("sensor.jewish_calendar_date") is not None
-def make_jerusalem_test_params(dtime, results, havdalah_offset=0):
- """Make test params for Jerusalem."""
- return (
- dtime,
- CANDLE_LIGHT_DEFAULT,
- havdalah_offset,
+TEST_PARAMS = [
+ (dt(2018, 9, 3), "UTC", 31.778, 35.235, "english", "date", False, "23 Elul 5778"),
+ (
+ dt(2018, 9, 3),
+ "UTC",
+ 31.778,
+ 35.235,
+ "hebrew",
+ "date",
+ False,
+ 'כ"ג אלול ה\' תשע"ח',
+ ),
+ (
+ dt(2018, 9, 10),
+ "UTC",
+ 31.778,
+ 35.235,
+ "hebrew",
+ "holiday_name",
+ False,
+ "א' ראש השנה",
+ ),
+ (
+ dt(2018, 9, 10),
+ "UTC",
+ 31.778,
+ 35.235,
+ "english",
+ "holiday_name",
+ False,
+ "Rosh Hashana I",
+ ),
+ (dt(2018, 9, 10), "UTC", 31.778, 35.235, "english", "holiday_type", False, 1),
+ (
+ dt(2018, 9, 8),
+ "UTC",
+ 31.778,
+ 35.235,
+ "hebrew",
+ "parshat_hashavua",
+ False,
+ "נצבים",
+ ),
+ (
+ dt(2018, 9, 8),
+ "America/New_York",
+ 40.7128,
+ -74.0060,
+ "hebrew",
+ "t_set_hakochavim",
+ True,
+ time(19, 48),
+ ),
+ (
+ dt(2018, 9, 8),
+ "Asia/Jerusalem",
+ 31.778,
+ 35.235,
+ "hebrew",
+ "t_set_hakochavim",
False,
+ time(19, 21),
+ ),
+ (
+ dt(2018, 10, 14),
"Asia/Jerusalem",
- JERUSALEM_LATLNG.lat,
- JERUSALEM_LATLNG.lng,
- results,
- )
+ 31.778,
+ 35.235,
+ "hebrew",
+ "parshat_hashavua",
+ False,
+ "לך לך",
+ ),
+ (
+ dt(2018, 10, 14, 17, 0, 0),
+ "Asia/Jerusalem",
+ 31.778,
+ 35.235,
+ "hebrew",
+ "date",
+ False,
+ "ה' מרחשוון ה' תשע\"ט",
+ ),
+ (
+ dt(2018, 10, 14, 19, 0, 0),
+ "Asia/Jerusalem",
+ 31.778,
+ 35.235,
+ "hebrew",
+ "date",
+ False,
+ "ו' מרחשוון ה' תשע\"ט",
+ ),
+]
+TEST_IDS = [
+ "date_output",
+ "date_output_hebrew",
+ "holiday_name",
+ "holiday_name_english",
+ "holiday_type",
+ "torah_reading",
+ "first_stars_ny",
+ "first_stars_jerusalem",
+ "torah_reading_weekday",
+ "date_before_sunset",
+ "date_after_sunset",
+]
-class TestJewishCalenderSensor:
- """Test the Jewish Calendar sensor."""
-
- # pylint: disable=attribute-defined-outside-init
- def setup_method(self, method):
- """Set up things to run when tests begin."""
- self.hass = get_test_home_assistant()
-
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
- # Reset the default timezone, so we don't affect other tests
- set_default_time_zone(get_time_zone("UTC"))
-
- def test_jewish_calendar_min_config(self):
- """Test minimum jewish calendar configuration."""
- config = {"sensor": {"platform": "jewish_calendar"}}
- assert setup_component(self.hass, "sensor", config)
-
- def test_jewish_calendar_hebrew(self):
- """Test jewish calendar sensor with language set to hebrew."""
- config = {"sensor": {"platform": "jewish_calendar", "language": "hebrew"}}
-
- assert setup_component(self.hass, "sensor", config)
-
- def test_jewish_calendar_multiple_sensors(self):
- """Test jewish calendar sensor with multiple sensors setup."""
- config = {
- "sensor": {
- "platform": "jewish_calendar",
- "sensors": [
- "date",
- "weekly_portion",
- "holiday_name",
- "holyness",
- "first_light",
- "gra_end_shma",
- "mga_end_shma",
- "plag_mincha",
- "first_stars",
- ],
- }
- }
-
- assert setup_component(self.hass, "sensor", config)
-
- test_params = [
- (
- dt(2018, 9, 3),
- "UTC",
- 31.778,
- 35.235,
- "english",
- "date",
- False,
- "23 Elul 5778",
- ),
- (
- dt(2018, 9, 3),
- "UTC",
- 31.778,
- 35.235,
- "hebrew",
- "date",
- False,
- 'כ"ג אלול ה\' תשע"ח',
- ),
- (
- dt(2018, 9, 10),
- "UTC",
- 31.778,
- 35.235,
- "hebrew",
- "holiday_name",
- False,
- "א' ראש השנה",
- ),
- (
- dt(2018, 9, 10),
- "UTC",
- 31.778,
- 35.235,
- "english",
- "holiday_name",
- False,
- "Rosh Hashana I",
- ),
- (dt(2018, 9, 10), "UTC", 31.778, 35.235, "english", "holyness", False, 1),
- (
- dt(2018, 9, 8),
- "UTC",
- 31.778,
- 35.235,
- "hebrew",
- "weekly_portion",
- False,
- "נצבים",
- ),
- (
- dt(2018, 9, 8),
- "America/New_York",
- 40.7128,
- -74.0060,
- "hebrew",
- "first_stars",
- True,
- time(19, 48),
- ),
- (
- dt(2018, 9, 8),
- "Asia/Jerusalem",
- 31.778,
- 35.235,
- "hebrew",
- "first_stars",
- False,
- time(19, 21),
- ),
- (
- dt(2018, 10, 14),
- "Asia/Jerusalem",
- 31.778,
- 35.235,
- "hebrew",
- "weekly_portion",
- False,
- "לך לך",
- ),
- (
- dt(2018, 10, 14, 17, 0, 0),
- "Asia/Jerusalem",
- 31.778,
- 35.235,
- "hebrew",
- "date",
- False,
- "ה' מרחשוון ה' תשע\"ט",
- ),
- (
- dt(2018, 10, 14, 19, 0, 0),
- "Asia/Jerusalem",
- 31.778,
- 35.235,
- "hebrew",
- "date",
- False,
- "ו' מרחשוון ה' תשע\"ט",
- ),
- ]
-
- test_ids = [
- "date_output",
- "date_output_hebrew",
- "holiday_name",
- "holiday_name_english",
- "holyness",
- "torah_reading",
- "first_stars_ny",
- "first_stars_jerusalem",
- "torah_reading_weekday",
- "date_before_sunset",
- "date_after_sunset",
- ]
-
- @pytest.mark.parametrize(
- [
- "cur_time",
- "tzname",
- "latitude",
- "longitude",
- "language",
- "sensor",
- "diaspora",
- "result",
- ],
- test_params,
- ids=test_ids,
- )
- def test_jewish_calendar_sensor(
- self, cur_time, tzname, latitude, longitude, language, sensor, diaspora, result
- ):
- """Test Jewish calendar sensor output."""
- time_zone = get_time_zone(tzname)
- set_default_time_zone(time_zone)
- test_time = time_zone.localize(cur_time)
- self.hass.config.latitude = latitude
- self.hass.config.longitude = longitude
- sensor = JewishCalSensor(
- name="test",
- language=language,
- sensor_type=sensor,
- latitude=latitude,
- longitude=longitude,
- timezone=time_zone,
- diaspora=diaspora,
- )
- sensor.hass = self.hass
- with patch("homeassistant.util.dt.now", return_value=test_time):
- run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result()
- assert sensor.state == result
-
- shabbat_params = [
- make_nyc_test_params(
- dt(2018, 9, 1, 16, 0),
- {
- "upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15),
- "upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 14),
- "weekly_portion": "Ki Tavo",
- "hebrew_weekly_portion": "כי תבוא",
- },
- ),
- make_nyc_test_params(
- dt(2018, 9, 1, 16, 0),
- {
- "upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15),
- "upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 22),
- "weekly_portion": "Ki Tavo",
- "hebrew_weekly_portion": "כי תבוא",
- },
- havdalah_offset=50,
- ),
- make_nyc_test_params(
- dt(2018, 9, 1, 20, 0),
- {
- "upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15),
- "upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 14),
- "upcoming_candle_lighting": dt(2018, 8, 31, 19, 15),
- "upcoming_havdalah": dt(2018, 9, 1, 20, 14),
- "weekly_portion": "Ki Tavo",
- "hebrew_weekly_portion": "כי תבוא",
- },
- ),
- make_nyc_test_params(
- dt(2018, 9, 1, 20, 21),
- {
- "upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19, 4),
- "upcoming_shabbat_havdalah": dt(2018, 9, 8, 20, 2),
- "weekly_portion": "Nitzavim",
- "hebrew_weekly_portion": "נצבים",
- },
- ),
- make_nyc_test_params(
- dt(2018, 9, 7, 13, 1),
- {
- "upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19, 4),
- "upcoming_shabbat_havdalah": dt(2018, 9, 8, 20, 2),
- "weekly_portion": "Nitzavim",
- "hebrew_weekly_portion": "נצבים",
- },
- ),
- make_nyc_test_params(
- dt(2018, 9, 8, 21, 25),
- {
- "upcoming_candle_lighting": dt(2018, 9, 9, 19, 1),
- "upcoming_havdalah": dt(2018, 9, 11, 19, 57),
- "upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52),
- "upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50),
- "weekly_portion": "Vayeilech",
- "hebrew_weekly_portion": "וילך",
- "holiday_name": "Erev Rosh Hashana",
- "hebrew_holiday_name": "ערב ראש השנה",
- },
- ),
- make_nyc_test_params(
- dt(2018, 9, 9, 21, 25),
- {
- "upcoming_candle_lighting": dt(2018, 9, 9, 19, 1),
- "upcoming_havdalah": dt(2018, 9, 11, 19, 57),
- "upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52),
- "upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50),
- "weekly_portion": "Vayeilech",
- "hebrew_weekly_portion": "וילך",
- "holiday_name": "Rosh Hashana I",
- "hebrew_holiday_name": "א' ראש השנה",
- },
- ),
- make_nyc_test_params(
- dt(2018, 9, 10, 21, 25),
- {
- "upcoming_candle_lighting": dt(2018, 9, 9, 19, 1),
- "upcoming_havdalah": dt(2018, 9, 11, 19, 57),
- "upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52),
- "upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50),
- "weekly_portion": "Vayeilech",
- "hebrew_weekly_portion": "וילך",
- "holiday_name": "Rosh Hashana II",
- "hebrew_holiday_name": "ב' ראש השנה",
- },
- ),
- make_nyc_test_params(
- dt(2018, 9, 28, 21, 25),
- {
- "upcoming_shabbat_candle_lighting": dt(2018, 9, 28, 18, 28),
- "upcoming_shabbat_havdalah": dt(2018, 9, 29, 19, 25),
- "weekly_portion": "none",
- "hebrew_weekly_portion": "none",
- },
- ),
- make_nyc_test_params(
- dt(2018, 9, 29, 21, 25),
- {
- "upcoming_candle_lighting": dt(2018, 9, 30, 18, 25),
- "upcoming_havdalah": dt(2018, 10, 2, 19, 20),
- "upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17),
- "upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13),
- "weekly_portion": "Bereshit",
- "hebrew_weekly_portion": "בראשית",
- "holiday_name": "Hoshana Raba",
- "hebrew_holiday_name": "הושענא רבה",
- },
- ),
- make_nyc_test_params(
- dt(2018, 9, 30, 21, 25),
- {
- "upcoming_candle_lighting": dt(2018, 9, 30, 18, 25),
- "upcoming_havdalah": dt(2018, 10, 2, 19, 20),
- "upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17),
- "upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13),
- "weekly_portion": "Bereshit",
- "hebrew_weekly_portion": "בראשית",
- "holiday_name": "Shmini Atzeret",
- "hebrew_holiday_name": "שמיני עצרת",
- },
- ),
- make_nyc_test_params(
- dt(2018, 10, 1, 21, 25),
- {
- "upcoming_candle_lighting": dt(2018, 9, 30, 18, 25),
- "upcoming_havdalah": dt(2018, 10, 2, 19, 20),
- "upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17),
- "upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13),
- "weekly_portion": "Bereshit",
- "hebrew_weekly_portion": "בראשית",
- "holiday_name": "Simchat Torah",
- "hebrew_holiday_name": "שמחת תורה",
- },
- ),
- make_jerusalem_test_params(
- dt(2018, 9, 29, 21, 25),
- {
- "upcoming_candle_lighting": dt(2018, 9, 30, 18, 10),
- "upcoming_havdalah": dt(2018, 10, 1, 19, 2),
- "upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3),
- "upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56),
- "weekly_portion": "Bereshit",
- "hebrew_weekly_portion": "בראשית",
- "holiday_name": "Hoshana Raba",
- "hebrew_holiday_name": "הושענא רבה",
- },
- ),
- make_jerusalem_test_params(
- dt(2018, 9, 30, 21, 25),
- {
- "upcoming_candle_lighting": dt(2018, 9, 30, 18, 10),
- "upcoming_havdalah": dt(2018, 10, 1, 19, 2),
- "upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3),
- "upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56),
- "weekly_portion": "Bereshit",
- "hebrew_weekly_portion": "בראשית",
- "holiday_name": "Shmini Atzeret",
- "hebrew_holiday_name": "שמיני עצרת",
- },
- ),
- make_jerusalem_test_params(
- dt(2018, 10, 1, 21, 25),
- {
- "upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3),
- "upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56),
- "weekly_portion": "Bereshit",
- "hebrew_weekly_portion": "בראשית",
- },
- ),
- make_nyc_test_params(
- dt(2016, 6, 11, 8, 25),
- {
- "upcoming_candle_lighting": dt(2016, 6, 10, 20, 7),
- "upcoming_havdalah": dt(2016, 6, 13, 21, 17),
- "upcoming_shabbat_candle_lighting": dt(2016, 6, 10, 20, 7),
- "upcoming_shabbat_havdalah": None,
- "weekly_portion": "Bamidbar",
- "hebrew_weekly_portion": "במדבר",
- "holiday_name": "Erev Shavuot",
- "hebrew_holiday_name": "ערב שבועות",
- },
- ),
- make_nyc_test_params(
- dt(2016, 6, 12, 8, 25),
- {
- "upcoming_candle_lighting": dt(2016, 6, 10, 20, 7),
- "upcoming_havdalah": dt(2016, 6, 13, 21, 17),
- "upcoming_shabbat_candle_lighting": dt(2016, 6, 17, 20, 10),
- "upcoming_shabbat_havdalah": dt(2016, 6, 18, 21, 19),
- "weekly_portion": "Nasso",
- "hebrew_weekly_portion": "נשא",
- "holiday_name": "Shavuot",
- "hebrew_holiday_name": "שבועות",
- },
- ),
- make_jerusalem_test_params(
- dt(2017, 9, 21, 8, 25),
- {
- "upcoming_candle_lighting": dt(2017, 9, 20, 18, 23),
- "upcoming_havdalah": dt(2017, 9, 23, 19, 13),
- "upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14),
- "upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13),
- "weekly_portion": "Ha'Azinu",
- "hebrew_weekly_portion": "האזינו",
- "holiday_name": "Rosh Hashana I",
- "hebrew_holiday_name": "א' ראש השנה",
- },
- ),
- make_jerusalem_test_params(
- dt(2017, 9, 22, 8, 25),
+
+@pytest.mark.parametrize(
+ [
+ "now",
+ "tzname",
+ "latitude",
+ "longitude",
+ "language",
+ "sensor",
+ "diaspora",
+ "result",
+ ],
+ TEST_PARAMS,
+ ids=TEST_IDS,
+)
+async def test_jewish_calendar_sensor(
+ hass, now, tzname, latitude, longitude, language, sensor, diaspora, result
+):
+ """Test Jewish calendar sensor output."""
+ time_zone = dt_util.get_time_zone(tzname)
+ test_time = time_zone.localize(now)
+
+ hass.config.time_zone = time_zone
+ hass.config.latitude = latitude
+ hass.config.longitude = longitude
+
+ with alter_time(test_time):
+ assert await async_setup_component(
+ hass,
+ jewish_calendar.DOMAIN,
{
- "upcoming_candle_lighting": dt(2017, 9, 20, 18, 23),
- "upcoming_havdalah": dt(2017, 9, 23, 19, 13),
- "upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14),
- "upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13),
- "weekly_portion": "Ha'Azinu",
- "hebrew_weekly_portion": "האזינו",
- "holiday_name": "Rosh Hashana II",
- "hebrew_holiday_name": "ב' ראש השנה",
+ "jewish_calendar": {
+ "name": "test",
+ "language": language,
+ "diaspora": diaspora,
+ }
},
- ),
- make_jerusalem_test_params(
- dt(2017, 9, 23, 8, 25),
+ )
+ await hass.async_block_till_done()
+
+ future = dt_util.utcnow() + timedelta(seconds=30)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ assert hass.states.get(f"sensor.test_{sensor}").state == str(result)
+
+
+SHABBAT_PARAMS = [
+ make_nyc_test_params(
+ dt(2018, 9, 1, 16, 0),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 15),
+ "english_upcoming_havdalah": dt(2018, 9, 1, 20, 14),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15),
+ "english_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 14),
+ "english_parshat_hashavua": "Ki Tavo",
+ "hebrew_parshat_hashavua": "כי תבוא",
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 1, 16, 0),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 15),
+ "english_upcoming_havdalah": dt(2018, 9, 1, 20, 22),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15),
+ "english_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 22),
+ "english_parshat_hashavua": "Ki Tavo",
+ "hebrew_parshat_hashavua": "כי תבוא",
+ },
+ havdalah_offset=50,
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 1, 20, 0),
+ {
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15),
+ "english_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 14),
+ "english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 15),
+ "english_upcoming_havdalah": dt(2018, 9, 1, 20, 14),
+ "english_parshat_hashavua": "Ki Tavo",
+ "hebrew_parshat_hashavua": "כי תבוא",
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 1, 20, 21),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 9, 7, 19, 4),
+ "english_upcoming_havdalah": dt(2018, 9, 8, 20, 2),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19, 4),
+ "english_upcoming_shabbat_havdalah": dt(2018, 9, 8, 20, 2),
+ "english_parshat_hashavua": "Nitzavim",
+ "hebrew_parshat_hashavua": "נצבים",
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 7, 13, 1),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 9, 7, 19, 4),
+ "english_upcoming_havdalah": dt(2018, 9, 8, 20, 2),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19, 4),
+ "english_upcoming_shabbat_havdalah": dt(2018, 9, 8, 20, 2),
+ "english_parshat_hashavua": "Nitzavim",
+ "hebrew_parshat_hashavua": "נצבים",
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 8, 21, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 9, 9, 19, 1),
+ "english_upcoming_havdalah": dt(2018, 9, 11, 19, 57),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52),
+ "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50),
+ "english_parshat_hashavua": "Vayeilech",
+ "hebrew_parshat_hashavua": "וילך",
+ "english_holiday_name": "Erev Rosh Hashana",
+ "hebrew_holiday_name": "ערב ראש השנה",
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 9, 21, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 9, 9, 19, 1),
+ "english_upcoming_havdalah": dt(2018, 9, 11, 19, 57),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52),
+ "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50),
+ "english_parshat_hashavua": "Vayeilech",
+ "hebrew_parshat_hashavua": "וילך",
+ "english_holiday_name": "Rosh Hashana I",
+ "hebrew_holiday_name": "א' ראש השנה",
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 10, 21, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 9, 9, 19, 1),
+ "english_upcoming_havdalah": dt(2018, 9, 11, 19, 57),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52),
+ "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50),
+ "english_parshat_hashavua": "Vayeilech",
+ "hebrew_parshat_hashavua": "וילך",
+ "english_holiday_name": "Rosh Hashana II",
+ "hebrew_holiday_name": "ב' ראש השנה",
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 28, 21, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 9, 28, 18, 28),
+ "english_upcoming_havdalah": dt(2018, 9, 29, 19, 25),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 28, 18, 28),
+ "english_upcoming_shabbat_havdalah": dt(2018, 9, 29, 19, 25),
+ "english_parshat_hashavua": "none",
+ "hebrew_parshat_hashavua": "none",
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 29, 21, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 25),
+ "english_upcoming_havdalah": dt(2018, 10, 2, 19, 20),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17),
+ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13),
+ "english_parshat_hashavua": "Bereshit",
+ "hebrew_parshat_hashavua": "בראשית",
+ "english_holiday_name": "Hoshana Raba",
+ "hebrew_holiday_name": "הושענא רבה",
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 9, 30, 21, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 25),
+ "english_upcoming_havdalah": dt(2018, 10, 2, 19, 20),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17),
+ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13),
+ "english_parshat_hashavua": "Bereshit",
+ "hebrew_parshat_hashavua": "בראשית",
+ "english_holiday_name": "Shmini Atzeret",
+ "hebrew_holiday_name": "שמיני עצרת",
+ },
+ ),
+ make_nyc_test_params(
+ dt(2018, 10, 1, 21, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 25),
+ "english_upcoming_havdalah": dt(2018, 10, 2, 19, 20),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17),
+ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13),
+ "english_parshat_hashavua": "Bereshit",
+ "hebrew_parshat_hashavua": "בראשית",
+ "english_holiday_name": "Simchat Torah",
+ "hebrew_holiday_name": "שמחת תורה",
+ },
+ ),
+ make_jerusalem_test_params(
+ dt(2018, 9, 29, 21, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 10),
+ "english_upcoming_havdalah": dt(2018, 10, 1, 19, 2),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3),
+ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56),
+ "english_parshat_hashavua": "Bereshit",
+ "hebrew_parshat_hashavua": "בראשית",
+ "english_holiday_name": "Hoshana Raba",
+ "hebrew_holiday_name": "הושענא רבה",
+ },
+ ),
+ make_jerusalem_test_params(
+ dt(2018, 9, 30, 21, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 10),
+ "english_upcoming_havdalah": dt(2018, 10, 1, 19, 2),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3),
+ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56),
+ "english_parshat_hashavua": "Bereshit",
+ "hebrew_parshat_hashavua": "בראשית",
+ "english_holiday_name": "Shmini Atzeret",
+ "hebrew_holiday_name": "שמיני עצרת",
+ },
+ ),
+ make_jerusalem_test_params(
+ dt(2018, 10, 1, 21, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2018, 10, 5, 18, 3),
+ "english_upcoming_havdalah": dt(2018, 10, 6, 18, 56),
+ "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3),
+ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56),
+ "english_parshat_hashavua": "Bereshit",
+ "hebrew_parshat_hashavua": "בראשית",
+ },
+ ),
+ make_nyc_test_params(
+ dt(2016, 6, 11, 8, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2016, 6, 10, 20, 7),
+ "english_upcoming_havdalah": dt(2016, 6, 13, 21, 17),
+ "english_upcoming_shabbat_candle_lighting": dt(2016, 6, 10, 20, 7),
+ "english_upcoming_shabbat_havdalah": "unknown",
+ "english_parshat_hashavua": "Bamidbar",
+ "hebrew_parshat_hashavua": "במדבר",
+ "english_holiday_name": "Erev Shavuot",
+ "hebrew_holiday_name": "ערב שבועות",
+ },
+ ),
+ make_nyc_test_params(
+ dt(2016, 6, 12, 8, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2016, 6, 10, 20, 7),
+ "english_upcoming_havdalah": dt(2016, 6, 13, 21, 17),
+ "english_upcoming_shabbat_candle_lighting": dt(2016, 6, 17, 20, 10),
+ "english_upcoming_shabbat_havdalah": dt(2016, 6, 18, 21, 19),
+ "english_parshat_hashavua": "Nasso",
+ "hebrew_parshat_hashavua": "נשא",
+ "english_holiday_name": "Shavuot",
+ "hebrew_holiday_name": "שבועות",
+ },
+ ),
+ make_jerusalem_test_params(
+ dt(2017, 9, 21, 8, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2017, 9, 20, 18, 23),
+ "english_upcoming_havdalah": dt(2017, 9, 23, 19, 13),
+ "english_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14),
+ "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13),
+ "english_parshat_hashavua": "Ha'Azinu",
+ "hebrew_parshat_hashavua": "האזינו",
+ "english_holiday_name": "Rosh Hashana I",
+ "hebrew_holiday_name": "א' ראש השנה",
+ },
+ ),
+ make_jerusalem_test_params(
+ dt(2017, 9, 22, 8, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2017, 9, 20, 18, 23),
+ "english_upcoming_havdalah": dt(2017, 9, 23, 19, 13),
+ "english_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14),
+ "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13),
+ "english_parshat_hashavua": "Ha'Azinu",
+ "hebrew_parshat_hashavua": "האזינו",
+ "english_holiday_name": "Rosh Hashana II",
+ "hebrew_holiday_name": "ב' ראש השנה",
+ },
+ ),
+ make_jerusalem_test_params(
+ dt(2017, 9, 23, 8, 25),
+ {
+ "english_upcoming_candle_lighting": dt(2017, 9, 20, 18, 23),
+ "english_upcoming_havdalah": dt(2017, 9, 23, 19, 13),
+ "english_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14),
+ "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13),
+ "english_parshat_hashavua": "Ha'Azinu",
+ "hebrew_parshat_hashavua": "האזינו",
+ "english_holiday_name": "",
+ "hebrew_holiday_name": "",
+ },
+ ),
+]
+
+SHABBAT_TEST_IDS = [
+ "currently_first_shabbat",
+ "currently_first_shabbat_with_havdalah_offset",
+ "currently_first_shabbat_bein_hashmashot_lagging_date",
+ "after_first_shabbat",
+ "friday_upcoming_shabbat",
+ "upcoming_rosh_hashana",
+ "currently_rosh_hashana",
+ "second_day_rosh_hashana",
+ "currently_shabbat_chol_hamoed",
+ "upcoming_two_day_yomtov_in_diaspora",
+ "currently_first_day_of_two_day_yomtov_in_diaspora",
+ "currently_second_day_of_two_day_yomtov_in_diaspora",
+ "upcoming_one_day_yom_tov_in_israel",
+ "currently_one_day_yom_tov_in_israel",
+ "after_one_day_yom_tov_in_israel",
+ # Type 1 = Sat/Sun/Mon
+ "currently_first_day_of_three_day_type1_yomtov_in_diaspora",
+ "currently_second_day_of_three_day_type1_yomtov_in_diaspora",
+ # Type 2 = Thurs/Fri/Sat
+ "currently_first_day_of_three_day_type2_yomtov_in_israel",
+ "currently_second_day_of_three_day_type2_yomtov_in_israel",
+ "currently_third_day_of_three_day_type2_yomtov_in_israel",
+]
+
+
+@pytest.mark.parametrize("language", ["english", "hebrew"])
+@pytest.mark.parametrize(
+ [
+ "now",
+ "candle_lighting",
+ "havdalah",
+ "diaspora",
+ "tzname",
+ "latitude",
+ "longitude",
+ "result",
+ ],
+ SHABBAT_PARAMS,
+ ids=SHABBAT_TEST_IDS,
+)
+async def test_shabbat_times_sensor(
+ hass,
+ language,
+ now,
+ candle_lighting,
+ havdalah,
+ diaspora,
+ tzname,
+ latitude,
+ longitude,
+ result,
+):
+ """Test sensor output for upcoming shabbat/yomtov times."""
+ time_zone = dt_util.get_time_zone(tzname)
+ test_time = time_zone.localize(now)
+
+ hass.config.time_zone = time_zone
+ hass.config.latitude = latitude
+ hass.config.longitude = longitude
+
+ with alter_time(test_time):
+ assert await async_setup_component(
+ hass,
+ jewish_calendar.DOMAIN,
{
- "upcoming_candle_lighting": dt(2017, 9, 20, 18, 23),
- "upcoming_havdalah": dt(2017, 9, 23, 19, 13),
- "upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14),
- "upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13),
- "weekly_portion": "Ha'Azinu",
- "hebrew_weekly_portion": "האזינו",
- "holiday_name": "",
- "hebrew_holiday_name": "",
+ "jewish_calendar": {
+ "name": "test",
+ "language": language,
+ "diaspora": diaspora,
+ "candle_lighting_minutes_before_sunset": candle_lighting,
+ "havdalah_minutes_after_sunset": havdalah,
+ }
},
- ),
- ]
-
- shabbat_test_ids = [
- "currently_first_shabbat",
- "currently_first_shabbat_with_havdalah_offset",
- "currently_first_shabbat_bein_hashmashot_lagging_date",
- "after_first_shabbat",
- "friday_upcoming_shabbat",
- "upcoming_rosh_hashana",
- "currently_rosh_hashana",
- "second_day_rosh_hashana",
- "currently_shabbat_chol_hamoed",
- "upcoming_two_day_yomtov_in_diaspora",
- "currently_first_day_of_two_day_yomtov_in_diaspora",
- "currently_second_day_of_two_day_yomtov_in_diaspora",
- "upcoming_one_day_yom_tov_in_israel",
- "currently_one_day_yom_tov_in_israel",
- "after_one_day_yom_tov_in_israel",
- # Type 1 = Sat/Sun/Mon
- "currently_first_day_of_three_day_type1_yomtov_in_diaspora",
- "currently_second_day_of_three_day_type1_yomtov_in_diaspora",
- # Type 2 = Thurs/Fri/Sat
- "currently_first_day_of_three_day_type2_yomtov_in_israel",
- "currently_second_day_of_three_day_type2_yomtov_in_israel",
- "currently_third_day_of_three_day_type2_yomtov_in_israel",
- ]
-
- @pytest.mark.parametrize(
- [
- "now",
- "candle_lighting",
- "havdalah",
- "diaspora",
- "tzname",
- "latitude",
- "longitude",
- "result",
- ],
- shabbat_params,
- ids=shabbat_test_ids,
- )
- def test_shabbat_times_sensor(
- self,
- now,
- candle_lighting,
- havdalah,
- diaspora,
- tzname,
- latitude,
- longitude,
- result,
- ):
- """Test sensor output for upcoming shabbat/yomtov times."""
- time_zone = get_time_zone(tzname)
- set_default_time_zone(time_zone)
- test_time = time_zone.localize(now)
- for sensor_type, value in result.items():
- if isinstance(value, dt):
- result[sensor_type] = time_zone.localize(value)
- self.hass.config.latitude = latitude
- self.hass.config.longitude = longitude
-
- if (
- "upcoming_shabbat_candle_lighting" in result
- and "upcoming_candle_lighting" not in result
- ):
- result["upcoming_candle_lighting"] = result[
- "upcoming_shabbat_candle_lighting"
- ]
- if "upcoming_shabbat_havdalah" in result and "upcoming_havdalah" not in result:
- result["upcoming_havdalah"] = result["upcoming_shabbat_havdalah"]
-
- for sensor_type, result_value in result.items():
- language = "english"
- if sensor_type.startswith("hebrew_"):
- language = "hebrew"
- sensor_type = sensor_type.replace("hebrew_", "")
- sensor = JewishCalSensor(
- name="test",
- language=language,
- sensor_type=sensor_type,
- latitude=latitude,
- longitude=longitude,
- timezone=time_zone,
- diaspora=diaspora,
- havdalah_offset=havdalah,
- candle_lighting_offset=candle_lighting,
- )
- sensor.hass = self.hass
- with patch("homeassistant.util.dt.now", return_value=test_time):
- run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result()
- assert sensor.state == result_value, "Value for {}".format(sensor_type)
-
- melacha_params = [
- make_nyc_test_params(dt(2018, 9, 1, 16, 0), True),
- make_nyc_test_params(dt(2018, 9, 1, 20, 21), False),
- make_nyc_test_params(dt(2018, 9, 7, 13, 1), False),
- make_nyc_test_params(dt(2018, 9, 8, 21, 25), False),
- make_nyc_test_params(dt(2018, 9, 9, 21, 25), True),
- make_nyc_test_params(dt(2018, 9, 10, 21, 25), True),
- make_nyc_test_params(dt(2018, 9, 28, 21, 25), True),
- make_nyc_test_params(dt(2018, 9, 29, 21, 25), False),
- make_nyc_test_params(dt(2018, 9, 30, 21, 25), True),
- make_nyc_test_params(dt(2018, 10, 1, 21, 25), True),
- make_jerusalem_test_params(dt(2018, 9, 29, 21, 25), False),
- make_jerusalem_test_params(dt(2018, 9, 30, 21, 25), True),
- make_jerusalem_test_params(dt(2018, 10, 1, 21, 25), False),
- ]
- melacha_test_ids = [
- "currently_first_shabbat",
- "after_first_shabbat",
- "friday_upcoming_shabbat",
- "upcoming_rosh_hashana",
- "currently_rosh_hashana",
- "second_day_rosh_hashana",
- "currently_shabbat_chol_hamoed",
- "upcoming_two_day_yomtov_in_diaspora",
- "currently_first_day_of_two_day_yomtov_in_diaspora",
- "currently_second_day_of_two_day_yomtov_in_diaspora",
- "upcoming_one_day_yom_tov_in_israel",
- "currently_one_day_yom_tov_in_israel",
- "after_one_day_yom_tov_in_israel",
- ]
-
- @pytest.mark.parametrize(
- [
- "now",
- "candle_lighting",
- "havdalah",
- "diaspora",
- "tzname",
- "latitude",
- "longitude",
- "result",
- ],
- melacha_params,
- ids=melacha_test_ids,
- )
- def test_issur_melacha_sensor(
- self,
- now,
- candle_lighting,
- havdalah,
- diaspora,
- tzname,
- latitude,
- longitude,
- result,
- ):
- """Test Issur Melacha sensor output."""
- time_zone = get_time_zone(tzname)
- set_default_time_zone(time_zone)
- test_time = time_zone.localize(now)
- self.hass.config.latitude = latitude
- self.hass.config.longitude = longitude
- sensor = JewishCalSensor(
- name="test",
- language="english",
- sensor_type="issur_melacha_in_effect",
- latitude=latitude,
- longitude=longitude,
- timezone=time_zone,
- diaspora=diaspora,
- havdalah_offset=havdalah,
- candle_lighting_offset=candle_lighting,
)
- sensor.hass = self.hass
- with patch("homeassistant.util.dt.now", return_value=test_time):
- run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result()
- assert sensor.state == result
-
- omer_params = [
- make_nyc_test_params(dt(2019, 4, 21, 0, 0), 1),
- make_jerusalem_test_params(dt(2019, 4, 21, 0, 0), 1),
- make_nyc_test_params(dt(2019, 4, 21, 23, 0), 2),
- make_jerusalem_test_params(dt(2019, 4, 21, 23, 0), 2),
- make_nyc_test_params(dt(2019, 5, 23, 0, 0), 33),
- make_jerusalem_test_params(dt(2019, 5, 23, 0, 0), 33),
- make_nyc_test_params(dt(2019, 6, 8, 0, 0), 49),
- make_jerusalem_test_params(dt(2019, 6, 8, 0, 0), 49),
- make_nyc_test_params(dt(2019, 6, 9, 0, 0), 0),
- make_jerusalem_test_params(dt(2019, 6, 9, 0, 0), 0),
- make_nyc_test_params(dt(2019, 1, 1, 0, 0), 0),
- make_jerusalem_test_params(dt(2019, 1, 1, 0, 0), 0),
- ]
- omer_test_ids = [
- "nyc_first_day_of_omer",
- "israel_first_day_of_omer",
- "nyc_first_day_of_omer_after_tzeit",
- "israel_first_day_of_omer_after_tzeit",
- "nyc_lag_baomer",
- "israel_lag_baomer",
- "nyc_last_day_of_omer",
- "israel_last_day_of_omer",
- "nyc_shavuot_no_omer",
- "israel_shavuot_no_omer",
- "nyc_jan_1st_no_omer",
- "israel_jan_1st_no_omer",
- ]
-
- @pytest.mark.parametrize(
- [
- "now",
- "candle_lighting",
- "havdalah",
- "diaspora",
- "tzname",
- "latitude",
- "longitude",
- "result",
- ],
- omer_params,
- ids=omer_test_ids,
- )
- def test_omer_sensor(
- self,
- now,
- candle_lighting,
- havdalah,
- diaspora,
- tzname,
- latitude,
- longitude,
- result,
- ):
- """Test Omer Count sensor output."""
- time_zone = get_time_zone(tzname)
- set_default_time_zone(time_zone)
- test_time = time_zone.localize(now)
- self.hass.config.latitude = latitude
- self.hass.config.longitude = longitude
- sensor = JewishCalSensor(
- name="test",
- language="english",
- sensor_type="omer_count",
- latitude=latitude,
- longitude=longitude,
- timezone=time_zone,
- diaspora=diaspora,
+ await hass.async_block_till_done()
+
+ future = dt_util.utcnow() + timedelta(seconds=30)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ for sensor_type, result_value in result.items():
+ if not sensor_type.startswith(language):
+ print(f"Not checking {sensor_type} for {language}")
+ continue
+
+ sensor_type = sensor_type.replace(f"{language}_", "")
+
+ assert hass.states.get(f"sensor.test_{sensor_type}").state == str(
+ result_value
+ ), f"Value for {sensor_type}"
+
+
+OMER_PARAMS = [
+ (dt(2019, 4, 21, 0), "1"),
+ (dt(2019, 4, 21, 23), "2"),
+ (dt(2019, 5, 23, 0), "33"),
+ (dt(2019, 6, 8, 0), "49"),
+ (dt(2019, 6, 9, 0), "0"),
+ (dt(2019, 1, 1, 0), "0"),
+]
+OMER_TEST_IDS = [
+ "first_day_of_omer",
+ "first_day_of_omer_after_tzeit",
+ "lag_baomer",
+ "last_day_of_omer",
+ "shavuot_no_omer",
+ "jan_1st_no_omer",
+]
+
+
+@pytest.mark.parametrize(["test_time", "result"], OMER_PARAMS, ids=OMER_TEST_IDS)
+async def test_omer_sensor(hass, test_time, result):
+ """Test Omer Count sensor output."""
+ test_time = hass.config.time_zone.localize(test_time)
+
+ with alter_time(test_time):
+ assert await async_setup_component(
+ hass, jewish_calendar.DOMAIN, {"jewish_calendar": {"name": "test"}}
)
- sensor.hass = self.hass
- with patch("homeassistant.util.dt.now", return_value=test_time):
- run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result()
- assert sensor.state == result
+ await hass.async_block_till_done()
+
+ future = dt_util.utcnow() + timedelta(seconds=30)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("sensor.test_day_of_the_omer").state == result
diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py
index 3e92c15ee06d2d..27b8b860d7227c 100644
--- a/tests/components/light/test_device_automation.py
+++ b/tests/components/light/test_device_automation.py
@@ -1,16 +1,15 @@
"""The test for light device automation."""
import pytest
-from homeassistant.components import light
+from homeassistant.components.light import DOMAIN
from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
from homeassistant.setup import async_setup_component
import homeassistant.components.automation as automation
from homeassistant.components.device_automation import (
- async_get_device_automation_triggers,
+ _async_get_device_automations as async_get_device_automations,
)
from homeassistant.helpers import device_registry
-
from tests.common import (
MockConfigEntry,
async_mock_service,
@@ -37,7 +36,7 @@ def calls(hass):
return async_mock_service(hass, "test", "automation")
-def _same_triggers(a, b):
+def _same_lists(a, b):
if len(a) != len(b):
return False
@@ -47,6 +46,72 @@ def _same_triggers(a, b):
return True
+async def test_get_actions(hass, device_reg, entity_reg):
+ """Test we get the expected actions from a light."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_actions = [
+ {
+ "domain": DOMAIN,
+ "type": "turn_off",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ {
+ "domain": DOMAIN,
+ "type": "turn_on",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ {
+ "domain": DOMAIN,
+ "type": "toggle",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ ]
+ actions = await async_get_device_automations(
+ hass, "async_get_actions", device_entry.id
+ )
+ assert _same_lists(actions, expected_actions)
+
+
+async def test_get_conditions(hass, device_reg, entity_reg):
+ """Test we get the expected conditions from a light."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_conditions = [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": "is_off",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": "is_on",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ ]
+ conditions = await async_get_device_automations(
+ hass, "async_get_conditions", device_entry.id
+ )
+ assert _same_lists(conditions, expected_conditions)
+
+
async def test_get_triggers(hass, device_reg, entity_reg):
"""Test we get the expected triggers from a light."""
config_entry = MockConfigEntry(domain="test", data={})
@@ -55,37 +120,37 @@ async def test_get_triggers(hass, device_reg, entity_reg):
config_entry_id=config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
- entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id)
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
expected_triggers = [
{
"platform": "device",
- "domain": "light",
- "type": "turn_off",
+ "domain": DOMAIN,
+ "type": "turned_off",
"device_id": device_entry.id,
- "entity_id": "light.test_5678",
+ "entity_id": f"{DOMAIN}.test_5678",
},
{
"platform": "device",
- "domain": "light",
- "type": "turn_on",
+ "domain": DOMAIN,
+ "type": "turned_on",
"device_id": device_entry.id,
- "entity_id": "light.test_5678",
+ "entity_id": f"{DOMAIN}.test_5678",
},
]
- triggers = await async_get_device_automation_triggers(hass, device_entry.id)
- assert _same_triggers(triggers, expected_triggers)
+ triggers = await async_get_device_automations(
+ hass, "async_get_triggers", device_entry.id
+ )
+ assert _same_lists(triggers, expected_triggers)
async def test_if_fires_on_state_change(hass, calls):
"""Test for turn_on and turn_off triggers firing."""
- platform = getattr(hass.components, "test.light")
+ platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
- assert await async_setup_component(
- hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
- )
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
- dev1, dev2, dev3 = platform.DEVICES
+ ent1, ent2, ent3 = platform.ENTITIES
assert await async_setup_component(
hass,
@@ -95,9 +160,10 @@ async def test_if_fires_on_state_change(hass, calls):
{
"trigger": {
"platform": "device",
- "domain": light.DOMAIN,
- "entity_id": dev1.entity_id,
- "type": "turn_on",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "turned_on",
},
"action": {
"service": "test.automation",
@@ -118,9 +184,10 @@ async def test_if_fires_on_state_change(hass, calls):
{
"trigger": {
"platform": "device",
- "domain": light.DOMAIN,
- "entity_id": dev1.entity_id,
- "type": "turn_off",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "turned_off",
},
"action": {
"service": "test.automation",
@@ -142,19 +209,165 @@ async def test_if_fires_on_state_change(hass, calls):
},
)
await hass.async_block_till_done()
- assert hass.states.get(dev1.entity_id).state == STATE_ON
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
assert len(calls) == 0
- hass.states.async_set(dev1.entity_id, STATE_OFF)
+ hass.states.async_set(ent1.entity_id, STATE_OFF)
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format(
- dev1.entity_id
+ ent1.entity_id
)
- hass.states.async_set(dev1.entity_id, STATE_ON)
+ hass.states.async_set(ent1.entity_id, STATE_ON)
await hass.async_block_till_done()
assert len(calls) == 2
assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format(
- dev1.entity_id
+ ent1.entity_id
)
+
+
+async def test_if_state(hass, calls):
+ """Test for turn_on and turn_off conditions."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ ent1, ent2, ent3 = platform.ENTITIES
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "is_on",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_on {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(("platform", "event.event_type"))
+ },
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event2"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "is_off",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_off {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(("platform", "event.event_type"))
+ },
+ },
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
+ assert len(calls) == 0
+
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "is_on event - test_event1"
+
+ hass.states.async_set(ent1.entity_id, STATE_OFF)
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["some"] == "is_off event - test_event2"
+
+
+async def test_action(hass, calls):
+ """Test for turn_on and turn_off actions."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ ent1, ent2, ent3 = platform.ENTITIES
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "turn_off",
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event2"},
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "turn_on",
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event3"},
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "toggle",
+ },
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
+ assert len(calls) == 0
+
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_OFF
+
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_OFF
+
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
+
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
+
+ hass.bus.async_fire("test_event3")
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_OFF
+
+ hass.bus.async_fire("test_event3")
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py
index dc4cb7502c5765..8ceda6cbd3efa7 100644
--- a/tests/components/light/test_init.py
+++ b/tests/components/light/test_init.py
@@ -137,39 +137,39 @@ def test_services(self):
self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1, dev2, dev3 = platform.DEVICES
+ ent1, ent2, ent3 = platform.ENTITIES
# Test init
- assert light.is_on(self.hass, dev1.entity_id)
- assert not light.is_on(self.hass, dev2.entity_id)
- assert not light.is_on(self.hass, dev3.entity_id)
+ assert light.is_on(self.hass, ent1.entity_id)
+ assert not light.is_on(self.hass, ent2.entity_id)
+ assert not light.is_on(self.hass, ent3.entity_id)
# Test basic turn_on, turn_off, toggle services
- common.turn_off(self.hass, entity_id=dev1.entity_id)
- common.turn_on(self.hass, entity_id=dev2.entity_id)
+ common.turn_off(self.hass, entity_id=ent1.entity_id)
+ common.turn_on(self.hass, entity_id=ent2.entity_id)
self.hass.block_till_done()
- assert not light.is_on(self.hass, dev1.entity_id)
- assert light.is_on(self.hass, dev2.entity_id)
+ assert not light.is_on(self.hass, ent1.entity_id)
+ assert light.is_on(self.hass, ent2.entity_id)
# turn on all lights
common.turn_on(self.hass)
self.hass.block_till_done()
- assert light.is_on(self.hass, dev1.entity_id)
- assert light.is_on(self.hass, dev2.entity_id)
- assert light.is_on(self.hass, dev3.entity_id)
+ assert light.is_on(self.hass, ent1.entity_id)
+ assert light.is_on(self.hass, ent2.entity_id)
+ assert light.is_on(self.hass, ent3.entity_id)
# turn off all lights
common.turn_off(self.hass)
self.hass.block_till_done()
- assert not light.is_on(self.hass, dev1.entity_id)
- assert not light.is_on(self.hass, dev2.entity_id)
- assert not light.is_on(self.hass, dev3.entity_id)
+ assert not light.is_on(self.hass, ent1.entity_id)
+ assert not light.is_on(self.hass, ent2.entity_id)
+ assert not light.is_on(self.hass, ent3.entity_id)
# turn off all lights by setting brightness to 0
common.turn_on(self.hass)
@@ -180,97 +180,97 @@ def test_services(self):
self.hass.block_till_done()
- assert not light.is_on(self.hass, dev1.entity_id)
- assert not light.is_on(self.hass, dev2.entity_id)
- assert not light.is_on(self.hass, dev3.entity_id)
+ assert not light.is_on(self.hass, ent1.entity_id)
+ assert not light.is_on(self.hass, ent2.entity_id)
+ assert not light.is_on(self.hass, ent3.entity_id)
# toggle all lights
common.toggle(self.hass)
self.hass.block_till_done()
- assert light.is_on(self.hass, dev1.entity_id)
- assert light.is_on(self.hass, dev2.entity_id)
- assert light.is_on(self.hass, dev3.entity_id)
+ assert light.is_on(self.hass, ent1.entity_id)
+ assert light.is_on(self.hass, ent2.entity_id)
+ assert light.is_on(self.hass, ent3.entity_id)
# toggle all lights
common.toggle(self.hass)
self.hass.block_till_done()
- assert not light.is_on(self.hass, dev1.entity_id)
- assert not light.is_on(self.hass, dev2.entity_id)
- assert not light.is_on(self.hass, dev3.entity_id)
+ assert not light.is_on(self.hass, ent1.entity_id)
+ assert not light.is_on(self.hass, ent2.entity_id)
+ assert not light.is_on(self.hass, ent3.entity_id)
# Ensure all attributes process correctly
common.turn_on(
- self.hass, dev1.entity_id, transition=10, brightness=20, color_name="blue"
+ self.hass, ent1.entity_id, transition=10, brightness=20, color_name="blue"
)
common.turn_on(
- self.hass, dev2.entity_id, rgb_color=(255, 255, 255), white_value=255
+ self.hass, ent2.entity_id, rgb_color=(255, 255, 255), white_value=255
)
- common.turn_on(self.hass, dev3.entity_id, xy_color=(0.4, 0.6))
+ common.turn_on(self.hass, ent3.entity_id, xy_color=(0.4, 0.6))
self.hass.block_till_done()
- _, data = dev1.last_call("turn_on")
+ _, data = ent1.last_call("turn_on")
assert {
light.ATTR_TRANSITION: 10,
light.ATTR_BRIGHTNESS: 20,
light.ATTR_HS_COLOR: (240, 100),
} == data
- _, data = dev2.last_call("turn_on")
+ _, data = ent2.last_call("turn_on")
assert {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255} == data
- _, data = dev3.last_call("turn_on")
+ _, data = ent3.last_call("turn_on")
assert {light.ATTR_HS_COLOR: (71.059, 100)} == data
# Ensure attributes are filtered when light is turned off
common.turn_on(
- self.hass, dev1.entity_id, transition=10, brightness=0, color_name="blue"
+ self.hass, ent1.entity_id, transition=10, brightness=0, color_name="blue"
)
common.turn_on(
self.hass,
- dev2.entity_id,
+ ent2.entity_id,
brightness=0,
rgb_color=(255, 255, 255),
white_value=0,
)
- common.turn_on(self.hass, dev3.entity_id, brightness=0, xy_color=(0.4, 0.6))
+ common.turn_on(self.hass, ent3.entity_id, brightness=0, xy_color=(0.4, 0.6))
self.hass.block_till_done()
- assert not light.is_on(self.hass, dev1.entity_id)
- assert not light.is_on(self.hass, dev2.entity_id)
- assert not light.is_on(self.hass, dev3.entity_id)
+ assert not light.is_on(self.hass, ent1.entity_id)
+ assert not light.is_on(self.hass, ent2.entity_id)
+ assert not light.is_on(self.hass, ent3.entity_id)
- _, data = dev1.last_call("turn_off")
+ _, data = ent1.last_call("turn_off")
assert {light.ATTR_TRANSITION: 10} == data
- _, data = dev2.last_call("turn_off")
+ _, data = ent2.last_call("turn_off")
assert {} == data
- _, data = dev3.last_call("turn_off")
+ _, data = ent3.last_call("turn_off")
assert {} == data
# One of the light profiles
prof_name, prof_h, prof_s, prof_bri = "relax", 35.932, 69.412, 144
# Test light profiles
- common.turn_on(self.hass, dev1.entity_id, profile=prof_name)
+ common.turn_on(self.hass, ent1.entity_id, profile=prof_name)
# Specify a profile and a brightness attribute to overwrite it
- common.turn_on(self.hass, dev2.entity_id, profile=prof_name, brightness=100)
+ common.turn_on(self.hass, ent2.entity_id, profile=prof_name, brightness=100)
self.hass.block_till_done()
- _, data = dev1.last_call("turn_on")
+ _, data = ent1.last_call("turn_on")
assert {
light.ATTR_BRIGHTNESS: prof_bri,
light.ATTR_HS_COLOR: (prof_h, prof_s),
} == data
- _, data = dev2.last_call("turn_on")
+ _, data = ent2.last_call("turn_on")
assert {
light.ATTR_BRIGHTNESS: 100,
light.ATTR_HS_COLOR: (prof_h, prof_s),
@@ -278,34 +278,34 @@ def test_services(self):
# Test bad data
common.turn_on(self.hass)
- common.turn_on(self.hass, dev1.entity_id, profile="nonexisting")
- common.turn_on(self.hass, dev2.entity_id, xy_color=["bla-di-bla", 5])
- common.turn_on(self.hass, dev3.entity_id, rgb_color=[255, None, 2])
+ common.turn_on(self.hass, ent1.entity_id, profile="nonexisting")
+ common.turn_on(self.hass, ent2.entity_id, xy_color=["bla-di-bla", 5])
+ common.turn_on(self.hass, ent3.entity_id, rgb_color=[255, None, 2])
self.hass.block_till_done()
- _, data = dev1.last_call("turn_on")
+ _, data = ent1.last_call("turn_on")
assert {} == data
- _, data = dev2.last_call("turn_on")
+ _, data = ent2.last_call("turn_on")
assert {} == data
- _, data = dev3.last_call("turn_on")
+ _, data = ent3.last_call("turn_on")
assert {} == data
# faulty attributes will not trigger a service call
common.turn_on(
- self.hass, dev1.entity_id, profile=prof_name, brightness="bright"
+ self.hass, ent1.entity_id, profile=prof_name, brightness="bright"
)
- common.turn_on(self.hass, dev1.entity_id, rgb_color="yellowish")
- common.turn_on(self.hass, dev2.entity_id, white_value="high")
+ common.turn_on(self.hass, ent1.entity_id, rgb_color="yellowish")
+ common.turn_on(self.hass, ent2.entity_id, white_value="high")
self.hass.block_till_done()
- _, data = dev1.last_call("turn_on")
+ _, data = ent1.last_call("turn_on")
assert {} == data
- _, data = dev2.last_call("turn_on")
+ _, data = ent2.last_call("turn_on")
assert {} == data
def test_broken_light_profiles(self):
@@ -340,24 +340,24 @@ def test_light_profiles(self):
self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev1, _, _ = platform.DEVICES
+ ent1, _, _ = platform.ENTITIES
- common.turn_on(self.hass, dev1.entity_id, profile="test")
+ common.turn_on(self.hass, ent1.entity_id, profile="test")
self.hass.block_till_done()
- _, data = dev1.last_call("turn_on")
+ _, data = ent1.last_call("turn_on")
- assert light.is_on(self.hass, dev1.entity_id)
+ assert light.is_on(self.hass, ent1.entity_id)
assert {light.ATTR_HS_COLOR: (71.059, 100), light.ATTR_BRIGHTNESS: 100} == data
- common.turn_on(self.hass, dev1.entity_id, profile="test_off")
+ common.turn_on(self.hass, ent1.entity_id, profile="test_off")
self.hass.block_till_done()
- _, data = dev1.last_call("turn_off")
+ _, data = ent1.last_call("turn_off")
- assert not light.is_on(self.hass, dev1.entity_id)
+ assert not light.is_on(self.hass, ent1.entity_id)
assert {} == data
def test_default_profiles_group(self):
@@ -387,10 +387,10 @@ def _mock_open(path, *args, **kwargs):
self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev, _, _ = platform.DEVICES
- common.turn_on(self.hass, dev.entity_id)
+ ent, _, _ = platform.ENTITIES
+ common.turn_on(self.hass, ent.entity_id)
self.hass.block_till_done()
- _, data = dev.last_call("turn_on")
+ _, data = ent.last_call("turn_on")
assert {light.ATTR_HS_COLOR: (71.059, 100), light.ATTR_BRIGHTNESS: 99} == data
def test_default_profiles_light(self):
@@ -424,7 +424,9 @@ def _mock_open(path, *args, **kwargs):
self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}}
)
- dev = next(filter(lambda x: x.entity_id == "light.ceiling_2", platform.DEVICES))
+ dev = next(
+ filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES)
+ )
common.turn_on(self.hass, dev.entity_id)
self.hass.block_till_done()
_, data = dev.last_call("turn_on")
diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py
index af27ff8c7d1c3d..28f1a7e9720091 100644
--- a/tests/components/mqtt/test_binary_sensor.py
+++ b/tests/components/mqtt/test_binary_sensor.py
@@ -1,7 +1,8 @@
"""The tests for the MQTT binary sensor platform."""
-from datetime import timedelta
+from datetime import datetime, timedelta
import json
-from unittest.mock import ANY
+
+from unittest.mock import ANY, patch
from homeassistant.components import binary_sensor, mqtt
from homeassistant.components.mqtt.discovery import async_start
@@ -24,6 +25,107 @@
)
+async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog):
+ """Test the expiration of the value."""
+ assert await async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ binary_sensor.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "test-topic",
+ "expire_after": 4,
+ "force_update": True,
+ "availability_topic": "availability-topic",
+ }
+ },
+ )
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == STATE_UNAVAILABLE
+
+ async_fire_mqtt_message(hass, "availability-topic", "online")
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state != STATE_UNAVAILABLE
+
+ await expires_helper(hass, mqtt_mock, caplog)
+
+
+async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog):
+ """Test the expiration of the value."""
+ assert await async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ binary_sensor.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "state_topic": "test-topic",
+ "expire_after": 4,
+ "force_update": True,
+ }
+ },
+ )
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == STATE_OFF
+
+ await expires_helper(hass, mqtt_mock, caplog)
+
+
+async def expires_helper(hass, mqtt_mock, caplog):
+ """Run the basic expiry code."""
+
+ now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC)
+ with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now):
+ async_fire_time_changed(hass, now)
+ async_fire_mqtt_message(hass, "test-topic", "ON")
+ await hass.async_block_till_done()
+
+ # Value was set correctly.
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == STATE_ON
+
+ # Time jump +3s
+ now = now + timedelta(seconds=3)
+ async_fire_time_changed(hass, now)
+ await hass.async_block_till_done()
+
+ # Value is not yet expired
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == STATE_ON
+
+ # Next message resets timer
+ with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now):
+ async_fire_time_changed(hass, now)
+ async_fire_mqtt_message(hass, "test-topic", "OFF")
+ await hass.async_block_till_done()
+
+ # Value was updated correctly.
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == STATE_OFF
+
+ # Time jump +3s
+ now = now + timedelta(seconds=3)
+ async_fire_time_changed(hass, now)
+ await hass.async_block_till_done()
+
+ # Value is not yet expired
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == STATE_OFF
+
+ # Time jump +2s
+ now = now + timedelta(seconds=2)
+ async_fire_time_changed(hass, now)
+ await hass.async_block_till_done()
+
+ # Value is expired now
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == STATE_UNAVAILABLE
+
+
async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock):
"""Test the setting of the value via MQTT."""
assert await async_setup_component(
@@ -41,6 +143,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock):
)
state = hass.states.get("binary_sensor.test")
+
assert state.state == STATE_OFF
async_fire_mqtt_message(hass, "test-topic", "ON")
diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py
index ecc54e0e209b0c..70b5e941fe3868 100644
--- a/tests/components/mqtt/test_camera.py
+++ b/tests/components/mqtt/test_camera.py
@@ -1,5 +1,6 @@
"""The tests for mqtt camera component."""
from unittest.mock import ANY
+import json
from homeassistant.components import camera, mqtt
from homeassistant.components.mqtt.discovery import async_start
@@ -167,3 +168,79 @@ async def test_entity_id_update(hass, mqtt_mock):
assert state is not None
assert mock_mqtt.async_subscribe.call_count == 1
mock_mqtt.async_subscribe.assert_any_call("test-topic", ANY, 0, None)
+
+
+async def test_entity_device_info_with_identifier(hass, mqtt_mock):
+ """Test MQTT camera device registry integration."""
+ entry = MockConfigEntry(domain=mqtt.DOMAIN)
+ entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, entry)
+ registry = await hass.helpers.device_registry.async_get_registry()
+
+ data = json.dumps(
+ {
+ "platform": "mqtt",
+ "name": "Test 1",
+ "topic": "test-topic",
+ "device": {
+ "identifiers": ["helloworld"],
+ "connections": [["mac", "02:5b:26:a8:dc:12"]],
+ "manufacturer": "Whatever",
+ "name": "Beer",
+ "model": "Glass",
+ "sw_version": "0.1-beta",
+ },
+ "unique_id": "veryunique",
+ }
+ )
+ async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data)
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({("mqtt", "helloworld")}, set())
+ assert device is not None
+ assert device.identifiers == {("mqtt", "helloworld")}
+ assert device.connections == {("mac", "02:5b:26:a8:dc:12")}
+ assert device.manufacturer == "Whatever"
+ assert device.name == "Beer"
+ assert device.model == "Glass"
+ assert device.sw_version == "0.1-beta"
+
+
+async def test_entity_device_info_update(hass, mqtt_mock):
+ """Test device registry update."""
+ entry = MockConfigEntry(domain=mqtt.DOMAIN)
+ entry.add_to_hass(hass)
+ await async_start(hass, "homeassistant", {}, entry)
+ registry = await hass.helpers.device_registry.async_get_registry()
+
+ config = {
+ "platform": "mqtt",
+ "name": "Test 1",
+ "topic": "test-topic",
+ "device": {
+ "identifiers": ["helloworld"],
+ "connections": [["mac", "02:5b:26:a8:dc:12"]],
+ "manufacturer": "Whatever",
+ "name": "Beer",
+ "model": "Glass",
+ "sw_version": "0.1-beta",
+ },
+ "unique_id": "veryunique",
+ }
+
+ data = json.dumps(config)
+ async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data)
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({("mqtt", "helloworld")}, set())
+ assert device is not None
+ assert device.name == "Beer"
+
+ config["device"]["name"] = "Milk"
+ data = json.dumps(config)
+ async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data)
+ await hass.async_block_till_done()
+
+ device = registry.async_get_device({("mqtt", "helloworld")}, set())
+ assert device is not None
+ assert device.name == "Milk"
diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py
index 436d25750fc518..0e450f06238d94 100644
--- a/tests/components/nws/test_weather.py
+++ b/tests/components/nws/test_weather.py
@@ -110,9 +110,7 @@ async def test_imperial(hass, aioclient_mock):
STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json")
)
aioclient_mock.get(
- OBSURL.format("KMIE"),
- text=load_fixture("nws-weather-obs-valid.json"),
- params={"limit": 1},
+ OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json")
)
aioclient_mock.get(
FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json")
@@ -142,9 +140,7 @@ async def test_metric(hass, aioclient_mock):
STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json")
)
aioclient_mock.get(
- OBSURL.format("KMIE"),
- text=load_fixture("nws-weather-obs-valid.json"),
- params={"limit": 1},
+ OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json")
)
aioclient_mock.get(
FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json")
@@ -174,9 +170,7 @@ async def test_none(hass, aioclient_mock):
STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json")
)
aioclient_mock.get(
- OBSURL.format("KMIE"),
- text=load_fixture("nws-weather-obs-null.json"),
- params={"limit": 1},
+ OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-null.json")
)
aioclient_mock.get(
FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-null.json")
@@ -208,7 +202,6 @@ async def test_fail_obs(hass, aioclient_mock):
aioclient_mock.get(
OBSURL.format("KMIE"),
text=load_fixture("nws-weather-obs-valid.json"),
- params={"limit": 1},
status=400,
)
aioclient_mock.get(
@@ -234,9 +227,7 @@ async def test_fail_stn(hass, aioclient_mock):
status=400,
)
aioclient_mock.get(
- OBSURL.format("KMIE"),
- text=load_fixture("nws-weather-obs-valid.json"),
- params={"limit": 1},
+ OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json")
)
aioclient_mock.get(
FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json")
@@ -257,9 +248,7 @@ async def test_invalid_config(hass, aioclient_mock):
STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json")
)
aioclient_mock.get(
- OBSURL.format("KMIE"),
- text=load_fixture("nws-weather-obs-valid.json"),
- params={"limit": 1},
+ OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json")
)
aioclient_mock.get(
FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json")
diff --git a/tests/components/plex/__init__.py b/tests/components/plex/__init__.py
new file mode 100644
index 00000000000000..9c9c00d87ace68
--- /dev/null
+++ b/tests/components/plex/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Plex component."""
diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py
new file mode 100644
index 00000000000000..d027087828073c
--- /dev/null
+++ b/tests/components/plex/mock_classes.py
@@ -0,0 +1,35 @@
+"""Mock classes used in tests."""
+
+MOCK_HOST_1 = "1.2.3.4"
+MOCK_PORT_1 = "32400"
+MOCK_HOST_2 = "4.3.2.1"
+MOCK_PORT_2 = "32400"
+
+
+class MockAvailableServer: # pylint: disable=too-few-public-methods
+ """Mock avilable server objects."""
+
+ def __init__(self, name, client_id):
+ """Initialize the object."""
+ self.name = name
+ self.clientIdentifier = client_id # pylint: disable=invalid-name
+ self.provides = ["server"]
+
+
+class MockConnection: # pylint: disable=too-few-public-methods
+ """Mock a single account resource connection object."""
+
+ def __init__(self, ssl):
+ """Initialize the object."""
+ prefix = "https" if ssl else "http"
+ self.httpuri = f"{prefix}://{MOCK_HOST_1}:{MOCK_PORT_1}"
+ self.uri = "{prefix}://{MOCK_HOST_2}:{MOCK_PORT_2}"
+ self.local = True
+
+
+class MockConnections: # pylint: disable=too-few-public-methods
+ """Mock a list of resource connections."""
+
+ def __init__(self, ssl=False):
+ """Initialize the object."""
+ self.connections = [MockConnection(ssl)]
diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py
new file mode 100644
index 00000000000000..e98aed793cfdca
--- /dev/null
+++ b/tests/components/plex/test_config_flow.py
@@ -0,0 +1,522 @@
+"""Tests for Plex config flow."""
+from unittest.mock import MagicMock, Mock, patch, PropertyMock
+import plexapi.exceptions
+import requests.exceptions
+
+from homeassistant.components.plex import config_flow
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PORT,
+ CONF_SSL,
+ CONF_VERIFY_SSL,
+ CONF_TOKEN,
+ CONF_URL,
+)
+
+from tests.common import MockConfigEntry
+
+from .mock_classes import MOCK_HOST_1, MOCK_PORT_1, MockAvailableServer, MockConnections
+
+MOCK_NAME_1 = "Plex Server 1"
+MOCK_ID_1 = "unique_id_123"
+MOCK_NAME_2 = "Plex Server 2"
+MOCK_ID_2 = "unique_id_456"
+MOCK_TOKEN = "secret_token"
+MOCK_FILE_CONTENTS = {
+ f"{MOCK_HOST_1}:{MOCK_PORT_1}": {"ssl": False, "token": MOCK_TOKEN, "verify": True}
+}
+MOCK_SERVER_1 = MockAvailableServer(MOCK_NAME_1, MOCK_ID_1)
+MOCK_SERVER_2 = MockAvailableServer(MOCK_NAME_2, MOCK_ID_2)
+
+
+def init_config_flow(hass):
+ """Init a configuration flow."""
+ flow = config_flow.PlexFlowHandler()
+ flow.hass = hass
+ return flow
+
+
+async def test_bad_credentials(hass):
+ """Test when provided credentials are rejected."""
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ with patch(
+ "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized
+ ):
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"]["base"] == "faulty_credentials"
+
+
+async def test_import_file_from_discovery(hass):
+ """Test importing a legacy file during discovery."""
+
+ file_host_and_port, file_config = list(MOCK_FILE_CONTENTS.items())[0]
+ used_url = f"http://{file_host_and_port}"
+
+ with patch("plexapi.server.PlexServer") as mock_plex_server, patch(
+ "homeassistant.components.plex.config_flow.load_json",
+ return_value=MOCK_FILE_CONTENTS,
+ ):
+ type(mock_plex_server.return_value).machineIdentifier = PropertyMock(
+ return_value=MOCK_ID_1
+ )
+ type(mock_plex_server.return_value).friendlyName = PropertyMock(
+ return_value=MOCK_NAME_1
+ )
+ type( # pylint: disable=protected-access
+ mock_plex_server.return_value
+ )._baseurl = PropertyMock(return_value=used_url)
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "discovery"},
+ data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == MOCK_NAME_1
+ assert result["data"][config_flow.CONF_SERVER] == MOCK_NAME_1
+ assert result["data"][config_flow.CONF_SERVER_IDENTIFIER] == MOCK_ID_1
+ assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] == used_url
+ assert (
+ result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN]
+ == file_config[CONF_TOKEN]
+ )
+
+
+async def test_discovery(hass):
+ """Test starting a flow from discovery."""
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "discovery"},
+ data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+
+async def test_discovery_while_in_progress(hass):
+ """Test starting a flow from discovery."""
+
+ await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "discovery"},
+ data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1},
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+
+async def test_import_success(hass):
+ """Test a successful configuration import."""
+
+ mock_connections = MockConnections(ssl=True)
+
+ mm_plex_account = MagicMock()
+ mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1])
+ mm_plex_account.resource = Mock(return_value=mock_connections)
+
+ with patch("plexapi.server.PlexServer") as mock_plex_server:
+ type(mock_plex_server.return_value).machineIdentifier = PropertyMock(
+ return_value=MOCK_SERVER_1.clientIdentifier
+ )
+ type(mock_plex_server.return_value).friendlyName = PropertyMock(
+ return_value=MOCK_SERVER_1.name
+ )
+ type( # pylint: disable=protected-access
+ mock_plex_server.return_value
+ )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri)
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "import"},
+ data={
+ CONF_TOKEN: MOCK_TOKEN,
+ CONF_URL: f"https://{MOCK_HOST_1}:{MOCK_PORT_1}",
+ },
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == MOCK_SERVER_1.name
+ assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name
+ assert (
+ result["data"][config_flow.CONF_SERVER_IDENTIFIER]
+ == MOCK_SERVER_1.clientIdentifier
+ )
+ assert (
+ result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL]
+ == mock_connections.connections[0].httpuri
+ )
+ assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN
+
+
+async def test_import_bad_hostname(hass):
+ """Test when an invalid address is provided."""
+
+ with patch(
+ "plexapi.server.PlexServer", side_effect=requests.exceptions.ConnectionError
+ ):
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "import"},
+ data={
+ CONF_TOKEN: MOCK_TOKEN,
+ CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}",
+ },
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"]["base"] == "not_found"
+
+
+async def test_unknown_exception(hass):
+ """Test when an unknown exception is encountered."""
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception):
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": "user"},
+ data={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False},
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "unknown"
+
+
+async def test_no_servers_found(hass):
+ """Test when no servers are on an account."""
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ mm_plex_account = MagicMock()
+ mm_plex_account.resources = Mock(return_value=[])
+
+ with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account):
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"]["base"] == "no_servers"
+
+
+async def test_single_available_server(hass):
+ """Test creating an entry with one server available."""
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ mock_connections = MockConnections()
+
+ mm_plex_account = MagicMock()
+ mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1])
+ mm_plex_account.resource = Mock(return_value=mock_connections)
+
+ with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch(
+ "plexapi.server.PlexServer"
+ ) as mock_plex_server:
+ type(mock_plex_server.return_value).machineIdentifier = PropertyMock(
+ return_value=MOCK_SERVER_1.clientIdentifier
+ )
+ type(mock_plex_server.return_value).friendlyName = PropertyMock(
+ return_value=MOCK_SERVER_1.name
+ )
+ type( # pylint: disable=protected-access
+ mock_plex_server.return_value
+ )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri)
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == MOCK_SERVER_1.name
+ assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name
+ assert (
+ result["data"][config_flow.CONF_SERVER_IDENTIFIER]
+ == MOCK_SERVER_1.clientIdentifier
+ )
+ assert (
+ result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL]
+ == mock_connections.connections[0].httpuri
+ )
+ assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN
+
+
+async def test_multiple_servers_with_selection(hass):
+ """Test creating an entry with multiple servers available."""
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ mock_connections = MockConnections()
+ mm_plex_account = MagicMock()
+ mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2])
+ mm_plex_account.resource = Mock(return_value=mock_connections)
+
+ with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch(
+ "plexapi.server.PlexServer"
+ ) as mock_plex_server:
+ type(mock_plex_server.return_value).machineIdentifier = PropertyMock(
+ return_value=MOCK_SERVER_1.clientIdentifier
+ )
+ type(mock_plex_server.return_value).friendlyName = PropertyMock(
+ return_value=MOCK_SERVER_1.name
+ )
+ type( # pylint: disable=protected-access
+ mock_plex_server.return_value
+ )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri)
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "select_server"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={config_flow.CONF_SERVER: MOCK_SERVER_1.name}
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == MOCK_SERVER_1.name
+ assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name
+ assert (
+ result["data"][config_flow.CONF_SERVER_IDENTIFIER]
+ == MOCK_SERVER_1.clientIdentifier
+ )
+ assert (
+ result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL]
+ == mock_connections.connections[0].httpuri
+ )
+ assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN
+
+
+async def test_adding_last_unconfigured_server(hass):
+ """Test automatically adding last unconfigured server when multiple servers on account."""
+
+ MockConfigEntry(
+ domain=config_flow.DOMAIN,
+ data={
+ config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_2,
+ config_flow.CONF_SERVER: MOCK_NAME_2,
+ },
+ ).add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ mock_connections = MockConnections()
+ mm_plex_account = MagicMock()
+ mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2])
+ mm_plex_account.resource = Mock(return_value=mock_connections)
+
+ with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch(
+ "plexapi.server.PlexServer"
+ ) as mock_plex_server:
+ type(mock_plex_server.return_value).machineIdentifier = PropertyMock(
+ return_value=MOCK_SERVER_1.clientIdentifier
+ )
+ type(mock_plex_server.return_value).friendlyName = PropertyMock(
+ return_value=MOCK_SERVER_1.name
+ )
+ type( # pylint: disable=protected-access
+ mock_plex_server.return_value
+ )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri)
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == MOCK_SERVER_1.name
+ assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name
+ assert (
+ result["data"][config_flow.CONF_SERVER_IDENTIFIER]
+ == MOCK_SERVER_1.clientIdentifier
+ )
+ assert (
+ result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL]
+ == mock_connections.connections[0].httpuri
+ )
+ assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN
+
+
+async def test_already_configured(hass):
+ """Test a duplicated successful flow."""
+
+ flow = init_config_flow(hass)
+ MockConfigEntry(
+ domain=config_flow.DOMAIN, data={config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_1}
+ ).add_to_hass(hass)
+
+ mock_connections = MockConnections()
+
+ mm_plex_account = MagicMock()
+ mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1])
+ mm_plex_account.resource = Mock(return_value=mock_connections)
+
+ with patch("plexapi.server.PlexServer") as mock_plex_server:
+ type(mock_plex_server.return_value).machineIdentifier = PropertyMock(
+ return_value=MOCK_SERVER_1.clientIdentifier
+ )
+ type(mock_plex_server.return_value).friendlyName = PropertyMock(
+ return_value=MOCK_SERVER_1.name
+ )
+ type( # pylint: disable=protected-access
+ mock_plex_server.return_value
+ )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri)
+ result = await flow.async_step_import(
+ {CONF_TOKEN: MOCK_TOKEN, CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}"}
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+
+async def test_all_available_servers_configured(hass):
+ """Test when all available servers are already configured."""
+
+ MockConfigEntry(
+ domain=config_flow.DOMAIN,
+ data={
+ config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_1,
+ config_flow.CONF_SERVER: MOCK_NAME_1,
+ },
+ ).add_to_hass(hass)
+
+ MockConfigEntry(
+ domain=config_flow.DOMAIN,
+ data={
+ config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_2,
+ config_flow.CONF_SERVER: MOCK_NAME_2,
+ },
+ ).add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ mock_connections = MockConnections()
+ mm_plex_account = MagicMock()
+ mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2])
+ mm_plex_account.resource = Mock(return_value=mock_connections)
+
+ with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account):
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False},
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "all_configured"
+
+
+async def test_manual_config(hass):
+ """Test creating via manual configuration."""
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN, context={"source": "user"}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={CONF_TOKEN: "", "manual_setup": True}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "manual_setup"
+
+ mock_connections = MockConnections(ssl=True)
+
+ with patch("plexapi.server.PlexServer") as mock_plex_server:
+ type(mock_plex_server.return_value).machineIdentifier = PropertyMock(
+ return_value=MOCK_SERVER_1.clientIdentifier
+ )
+ type(mock_plex_server.return_value).friendlyName = PropertyMock(
+ return_value=MOCK_SERVER_1.name
+ )
+ type( # pylint: disable=protected-access
+ mock_plex_server.return_value
+ )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri)
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_HOST: MOCK_HOST_1,
+ CONF_PORT: int(MOCK_PORT_1),
+ CONF_SSL: True,
+ CONF_VERIFY_SSL: True,
+ CONF_TOKEN: MOCK_TOKEN,
+ },
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == MOCK_SERVER_1.name
+ assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name
+ assert (
+ result["data"][config_flow.CONF_SERVER_IDENTIFIER]
+ == MOCK_SERVER_1.clientIdentifier
+ )
+ assert (
+ result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL]
+ == mock_connections.connections[0].httpuri
+ )
+ assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN
diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py
index 9e313fd3694583..4ec40731c5d206 100644
--- a/tests/components/prometheus/test_init.py
+++ b/tests/components/prometheus/test_init.py
@@ -41,6 +41,11 @@ async def prometheus_client(loop, hass, hass_client):
sensor3.entity_id = "sensor.electricity_price"
await sensor3.async_update_ha_state()
+ sensor4 = DemoSensor("Wind Direction", 25, None, "°", None)
+ sensor4.hass = hass
+ sensor4.entity_id = "sensor.wind_direction"
+ await sensor4.async_update_ha_state()
+
return await hass_client()
@@ -103,3 +108,9 @@ def test_view(prometheus_client): # pylint: disable=redefined-outer-name
'entity="sensor.electricity_price",'
'friendly_name="Electricity price"} 0.123' in body
)
+
+ assert (
+ 'sensor_unit_u0xb0{domain="sensor",'
+ 'entity="sensor.wind_direction",'
+ 'friendly_name="Wind Direction"} 25.0' in body
+ )
diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py
index c74630f7cd2274..59de4643cb8e8b 100644
--- a/tests/components/qld_bushfire/test_geo_location.py
+++ b/tests/components/qld_bushfire/test_geo_location.py
@@ -5,23 +5,24 @@
from homeassistant.components import geo_location
from homeassistant.components.geo_location import ATTR_SOURCE
from homeassistant.components.qld_bushfire.geo_location import (
- ATTR_EXTERNAL_ID,
- SCAN_INTERVAL,
ATTR_CATEGORY,
- ATTR_STATUS,
+ ATTR_EXTERNAL_ID,
ATTR_PUBLICATION_DATE,
+ ATTR_STATUS,
ATTR_UPDATED_DATE,
+ SCAN_INTERVAL,
)
from homeassistant.const import (
- EVENT_HOMEASSISTANT_START,
- CONF_RADIUS,
+ ATTR_ATTRIBUTION,
+ ATTR_FRIENDLY_NAME,
+ ATTR_ICON,
ATTR_LATITUDE,
ATTR_LONGITUDE,
- ATTR_FRIENDLY_NAME,
ATTR_UNIT_OF_MEASUREMENT,
- ATTR_ATTRIBUTION,
CONF_LATITUDE,
CONF_LONGITUDE,
+ CONF_RADIUS,
+ EVENT_HOMEASSISTANT_START,
)
from homeassistant.setup import async_setup_component
from tests.common import assert_setup_component, async_fire_time_changed
@@ -122,6 +123,7 @@ async def test_setup(hass):
ATTR_STATUS: "Status 1",
ATTR_UNIT_OF_MEASUREMENT: "km",
ATTR_SOURCE: "qld_bushfire",
+ ATTR_ICON: "mdi:fire",
}
assert float(state.state) == 15.5
@@ -135,6 +137,7 @@ async def test_setup(hass):
ATTR_FRIENDLY_NAME: "Title 2",
ATTR_UNIT_OF_MEASUREMENT: "km",
ATTR_SOURCE: "qld_bushfire",
+ ATTR_ICON: "mdi:fire",
}
assert float(state.state) == 20.5
@@ -148,6 +151,7 @@ async def test_setup(hass):
ATTR_FRIENDLY_NAME: "Title 3",
ATTR_UNIT_OF_MEASUREMENT: "km",
ATTR_SOURCE: "qld_bushfire",
+ ATTR_ICON: "mdi:fire",
}
assert float(state.state) == 25.5
diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py
index 7047e6e8d92f3c..5c8d46cb727753 100644
--- a/tests/components/scene/test_init.py
+++ b/tests/components/scene/test_init.py
@@ -24,7 +24,7 @@ def setUp(self): # pylint: disable=invalid-name
self.hass, light.DOMAIN, {light.DOMAIN: {"platform": "test"}}
)
- self.light_1, self.light_2 = test_light.DEVICES[0:2]
+ self.light_1, self.light_2 = test_light.ENTITIES[0:2]
common_light.turn_off(
self.hass, [self.light_1.entity_id, self.light_2.entity_id]
diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py
index 5724d7a3bac0be..fce0129a7bf399 100644
--- a/tests/components/smartthings/test_config_flow.py
+++ b/tests/components/smartthings/test_config_flow.py
@@ -84,7 +84,10 @@ async def test_token_unauthorized(hass, smartthings_mock):
flow = SmartThingsFlowHandler()
flow.hass = hass
- smartthings_mock.apps.side_effect = ClientResponseError(None, None, status=401)
+ request_info = Mock(real_url="http://example.com")
+ smartthings_mock.apps.side_effect = ClientResponseError(
+ request_info=request_info, history=None, status=401
+ )
result = await flow.async_step_user({"access_token": str(uuid4())})
@@ -98,7 +101,10 @@ async def test_token_forbidden(hass, smartthings_mock):
flow = SmartThingsFlowHandler()
flow.hass = hass
- smartthings_mock.apps.side_effect = ClientResponseError(None, None, status=403)
+ request_info = Mock(real_url="http://example.com")
+ smartthings_mock.apps.side_effect = ClientResponseError(
+ request_info=request_info, history=None, status=403
+ )
result = await flow.async_step_user({"access_token": str(uuid4())})
@@ -113,7 +119,10 @@ async def test_webhook_error(hass, smartthings_mock):
flow.hass = hass
data = {"error": {}}
- error = APIResponseError(None, None, data=data, status=422)
+ request_info = Mock(real_url="http://example.com")
+ error = APIResponseError(
+ request_info=request_info, history=None, data=data, status=422
+ )
error.is_target_error = Mock(return_value=True)
smartthings_mock.apps.side_effect = error
@@ -131,7 +140,10 @@ async def test_api_error(hass, smartthings_mock):
flow.hass = hass
data = {"error": {}}
- error = APIResponseError(None, None, data=data, status=400)
+ request_info = Mock(real_url="http://example.com")
+ error = APIResponseError(
+ request_info=request_info, history=None, data=data, status=400
+ )
smartthings_mock.apps.side_effect = error
@@ -147,7 +159,10 @@ async def test_unknown_api_error(hass, smartthings_mock):
flow = SmartThingsFlowHandler()
flow.hass = hass
- smartthings_mock.apps.side_effect = ClientResponseError(None, None, status=404)
+ request_info = Mock(real_url="http://example.com")
+ smartthings_mock.apps.side_effect = ClientResponseError(
+ request_info=request_info, history=None, status=404
+ )
result = await flow.async_step_user({"access_token": str(uuid4())})
diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py
index 4e1ffce7e22119..9749ab9bb71e3d 100644
--- a/tests/components/smartthings/test_init.py
+++ b/tests/components/smartthings/test_init.py
@@ -54,7 +54,10 @@ async def test_unrecoverable_api_errors_create_new_flow(
"""
assert await async_setup_component(hass, "persistent_notification", {})
config_entry.add_to_hass(hass)
- smartthings_mock.app.side_effect = ClientResponseError(None, None, status=401)
+ request_info = Mock(real_url="http://example.com")
+ smartthings_mock.app.side_effect = ClientResponseError(
+ request_info=request_info, history=None, status=401
+ )
# Assert setup returns false
result = await smartthings.async_setup_entry(hass, config_entry)
@@ -75,7 +78,10 @@ async def test_recoverable_api_errors_raise_not_ready(
):
"""Test config entry not ready raised for recoverable API errors."""
config_entry.add_to_hass(hass)
- smartthings_mock.app.side_effect = ClientResponseError(None, None, status=500)
+ request_info = Mock(real_url="http://example.com")
+ smartthings_mock.app.side_effect = ClientResponseError(
+ request_info=request_info, history=None, status=500
+ )
with pytest.raises(ConfigEntryNotReady):
await smartthings.async_setup_entry(hass, config_entry)
@@ -86,9 +92,12 @@ async def test_scenes_api_errors_raise_not_ready(
):
"""Test if scenes are unauthorized we continue to load platforms."""
config_entry.add_to_hass(hass)
+ request_info = Mock(real_url="http://example.com")
smartthings_mock.app.return_value = app
smartthings_mock.installed_app.return_value = installed_app
- smartthings_mock.scenes.side_effect = ClientResponseError(None, None, status=500)
+ smartthings_mock.scenes.side_effect = ClientResponseError(
+ request_info=request_info, history=None, status=500
+ )
with pytest.raises(ConfigEntryNotReady):
await smartthings.async_setup_entry(hass, config_entry)
@@ -140,10 +149,13 @@ async def test_scenes_unauthorized_loads_platforms(
):
"""Test if scenes are unauthorized we continue to load platforms."""
config_entry.add_to_hass(hass)
+ request_info = Mock(real_url="http://example.com")
smartthings_mock.app.return_value = app
smartthings_mock.installed_app.return_value = installed_app
smartthings_mock.devices.return_value = [device]
- smartthings_mock.scenes.side_effect = ClientResponseError(None, None, status=403)
+ smartthings_mock.scenes.side_effect = ClientResponseError(
+ request_info=request_info, history=None, status=403
+ )
mock_token = Mock()
mock_token.access_token.return_value = str(uuid4())
mock_token.refresh_token.return_value = str(uuid4())
@@ -290,12 +302,13 @@ async def test_remove_entry_app_in_use(hass, config_entry, smartthings_mock):
async def test_remove_entry_already_deleted(hass, config_entry, smartthings_mock):
"""Test handles when the apps have already been removed."""
+ request_info = Mock(real_url="http://example.com")
# Arrange
smartthings_mock.delete_installed_app.side_effect = ClientResponseError(
- None, None, status=403
+ request_info=request_info, history=None, status=403
)
smartthings_mock.delete_app.side_effect = ClientResponseError(
- None, None, status=403
+ request_info=request_info, history=None, status=403
)
# Act
await smartthings.async_remove_entry(hass, config_entry)
@@ -308,9 +321,10 @@ async def test_remove_entry_installedapp_api_error(
hass, config_entry, smartthings_mock
):
"""Test raises exceptions removing the installed app."""
+ request_info = Mock(real_url="http://example.com")
# Arrange
smartthings_mock.delete_installed_app.side_effect = ClientResponseError(
- None, None, status=500
+ request_info=request_info, history=None, status=500
)
# Act
with pytest.raises(ClientResponseError):
@@ -337,8 +351,9 @@ async def test_remove_entry_installedapp_unknown_error(
async def test_remove_entry_app_api_error(hass, config_entry, smartthings_mock):
"""Test raises exceptions removing the app."""
# Arrange
+ request_info = Mock(real_url="http://example.com")
smartthings_mock.delete_app.side_effect = ClientResponseError(
- None, None, status=500
+ request_info=request_info, history=None, status=500
)
# Act
with pytest.raises(ClientResponseError):
diff --git a/tests/components/solaredge/__init__.py b/tests/components/solaredge/__init__.py
new file mode 100644
index 00000000000000..c2a54cfafb623c
--- /dev/null
+++ b/tests/components/solaredge/__init__.py
@@ -0,0 +1 @@
+"""Tests for the SolarEdge component."""
diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py
new file mode 100644
index 00000000000000..c1183147bac5e2
--- /dev/null
+++ b/tests/components/solaredge/test_config_flow.py
@@ -0,0 +1,132 @@
+"""Tests for the SolarEdge config flow."""
+import pytest
+from requests.exceptions import HTTPError, ConnectTimeout
+from unittest.mock import patch, Mock
+
+from homeassistant import data_entry_flow
+from homeassistant.components.solaredge import config_flow
+from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME
+from homeassistant.const import CONF_NAME, CONF_API_KEY
+
+from tests.common import MockConfigEntry
+
+NAME = "solaredge site 1 2 3"
+SITE_ID = "1a2b3c4d5e6f7g8h"
+API_KEY = "a1b2c3d4e5f6g7h8"
+
+
+@pytest.fixture(name="test_api")
+def mock_controller():
+ """Mock a successfull Solaredge API."""
+ api = Mock()
+ api.get_details.return_value = {"details": {"status": "active"}}
+ with patch("solaredge.Solaredge", return_value=api):
+ yield api
+
+
+def init_config_flow(hass):
+ """Init a configuration flow."""
+ flow = config_flow.SolarEdgeConfigFlow()
+ flow.hass = hass
+ return flow
+
+
+async def test_user(hass, test_api):
+ """Test user config."""
+ flow = init_config_flow(hass)
+
+ result = await flow.async_step_user()
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+ # tets with all provided
+ result = await flow.async_step_user(
+ {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "solaredge_site_1_2_3"
+ assert result["data"][CONF_SITE_ID] == SITE_ID
+ assert result["data"][CONF_API_KEY] == API_KEY
+
+
+async def test_import(hass, test_api):
+ """Test import step."""
+ flow = init_config_flow(hass)
+
+ # import with site_id and api_key
+ result = await flow.async_step_import(
+ {CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "solaredge"
+ assert result["data"][CONF_SITE_ID] == SITE_ID
+ assert result["data"][CONF_API_KEY] == API_KEY
+
+ # import with all
+ result = await flow.async_step_import(
+ {CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID, CONF_NAME: NAME}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "solaredge_site_1_2_3"
+ assert result["data"][CONF_SITE_ID] == SITE_ID
+ assert result["data"][CONF_API_KEY] == API_KEY
+
+
+async def test_abort_if_already_setup(hass, test_api):
+ """Test we abort if the site_id is already setup."""
+ flow = init_config_flow(hass)
+ MockConfigEntry(
+ domain="solaredge",
+ data={CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY},
+ ).add_to_hass(hass)
+
+ # import: Should fail, same SITE_ID
+ result = await flow.async_step_import(
+ {CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "site_exists"
+
+ # user: Should fail, same SITE_ID
+ result = await flow.async_step_user(
+ {CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {CONF_SITE_ID: "site_exists"}
+
+
+async def test_asserts(hass, test_api):
+ """Test the _site_in_configuration_exists method."""
+ flow = init_config_flow(hass)
+
+ # test with inactive site
+ test_api.get_details.return_value = {"details": {"status": "NOK"}}
+ result = await flow.async_step_user(
+ {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {CONF_SITE_ID: "site_not_active"}
+
+ # test with api_failure
+ test_api.get_details.return_value = {}
+ result = await flow.async_step_user(
+ {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {CONF_SITE_ID: "api_failure"}
+
+ # test with ConnectionTimeout
+ test_api.get_details.side_effect = ConnectTimeout()
+ result = await flow.async_step_user(
+ {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {CONF_SITE_ID: "could_not_connect"}
+
+ # test with HTTPError
+ test_api.get_details.side_effect = HTTPError()
+ result = await flow.async_step_user(
+ {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {CONF_SITE_ID: "could_not_connect"}
diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py
index 02a6eccc285804..58c417831a966f 100644
--- a/tests/components/spaceapi/test_init.py
+++ b/tests/components/spaceapi/test_init.py
@@ -25,6 +25,34 @@
"temperature": ["test.temp1", "test.temp2"],
"humidity": ["test.hum1"],
},
+ "spacefed": {"spacenet": True, "spacesaml": False, "spacephone": True},
+ "cam": ["https://home-assistant.io/cam1", "https://home-assistant.io/cam2"],
+ "stream": {
+ "m4": "https://home-assistant.io/m4",
+ "mjpeg": "https://home-assistant.io/mjpeg",
+ "ustream": "https://home-assistant.io/ustream",
+ },
+ "feeds": {
+ "blog": {"url": "https://home-assistant.io/blog"},
+ "wiki": {"type": "mediawiki", "url": "https://home-assistant.io/wiki"},
+ "calendar": {"type": "ical", "url": "https://home-assistant.io/calendar"},
+ "flicker": {"url": "https://www.flickr.com/photos/home-assistant"},
+ },
+ "cache": {"schedule": "m.02"},
+ "projects": [
+ "https://home-assistant.io/projects/1",
+ "https://home-assistant.io/projects/2",
+ "https://home-assistant.io/projects/3",
+ ],
+ "radio_show": [
+ {
+ "name": "Radioshow",
+ "url": "https://home-assistant.io/radio",
+ "type": "ogg",
+ "start": "2019-09-02T10:00Z",
+ "end": "2019-09-02T12:00Z",
+ }
+ ],
}
}
@@ -61,11 +89,37 @@ async def test_spaceapi_get(hass, mock_client):
assert data["space"] == "Home"
assert data["contact"]["email"] == "hello@home-assistant.io"
assert data["location"]["address"] == "In your Home"
- assert data["location"]["latitude"] == 32.87336
- assert data["location"]["longitude"] == -117.22743
+ assert data["location"]["lat"] == 32.87336
+ assert data["location"]["lon"] == -117.22743
assert data["state"]["open"] == "null"
assert data["state"]["icon"]["open"] == "https://home-assistant.io/open.png"
assert data["state"]["icon"]["close"] == "https://home-assistant.io/close.png"
+ assert data["spacefed"]["spacenet"] == bool(1)
+ assert data["spacefed"]["spacesaml"] == bool(0)
+ assert data["spacefed"]["spacephone"] == bool(1)
+ assert data["cam"][0] == "https://home-assistant.io/cam1"
+ assert data["cam"][1] == "https://home-assistant.io/cam2"
+ assert data["stream"]["m4"] == "https://home-assistant.io/m4"
+ assert data["stream"]["mjpeg"] == "https://home-assistant.io/mjpeg"
+ assert data["stream"]["ustream"] == "https://home-assistant.io/ustream"
+ assert data["feeds"]["blog"]["url"] == "https://home-assistant.io/blog"
+ assert data["feeds"]["wiki"]["type"] == "mediawiki"
+ assert data["feeds"]["wiki"]["url"] == "https://home-assistant.io/wiki"
+ assert data["feeds"]["calendar"]["type"] == "ical"
+ assert data["feeds"]["calendar"]["url"] == "https://home-assistant.io/calendar"
+ assert (
+ data["feeds"]["flicker"]["url"]
+ == "https://www.flickr.com/photos/home-assistant"
+ )
+ assert data["cache"]["schedule"] == "m.02"
+ assert data["projects"][0] == "https://home-assistant.io/projects/1"
+ assert data["projects"][1] == "https://home-assistant.io/projects/2"
+ assert data["projects"][2] == "https://home-assistant.io/projects/3"
+ assert data["radio_show"][0]["name"] == "Radioshow"
+ assert data["radio_show"][0]["url"] == "https://home-assistant.io/radio"
+ assert data["radio_show"][0]["type"] == "ogg"
+ assert data["radio_show"][0]["start"] == "2019-09-02T10:00Z"
+ assert data["radio_show"][0]["end"] == "2019-09-02T12:00Z"
async def test_spaceapi_state_get(hass, mock_client):
diff --git a/tests/components/srp_energy/__init__.py b/tests/components/srp_energy/__init__.py
deleted file mode 100644
index 2a278cf1d38de2..00000000000000
--- a/tests/components/srp_energy/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Tests for the srp_energy component."""
diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py
deleted file mode 100644
index e33902c3fd8c6e..00000000000000
--- a/tests/components/srp_energy/test_sensor.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""The tests for the Srp Energy Platform."""
-from unittest.mock import patch
-import logging
-from homeassistant.setup import async_setup_component
-
-_LOGGER = logging.getLogger(__name__)
-
-VALID_CONFIG_MINIMAL = {
- "sensor": {
- "platform": "srp_energy",
- "username": "foo",
- "password": "bar",
- "id": 1234,
- }
-}
-
-PATCH_INIT = "srpenergy.client.SrpEnergyClient.__init__"
-PATCH_VALIDATE = "srpenergy.client.SrpEnergyClient.validate"
-PATCH_USAGE = "srpenergy.client.SrpEnergyClient.usage"
-
-
-def mock_usage(self, startdate, enddate): # pylint: disable=invalid-name
- """Mock srpusage usage."""
- _LOGGER.log(logging.INFO, "Calling mock usage")
- usage = [
- ("9/19/2018", "12:00 AM", "2018-09-19T00:00:00-7:00", "1.2", "0.17"),
- ("9/19/2018", "1:00 AM", "2018-09-19T01:00:00-7:00", "2.1", "0.30"),
- ("9/19/2018", "2:00 AM", "2018-09-19T02:00:00-7:00", "1.5", "0.23"),
- ("9/19/2018", "9:00 PM", "2018-09-19T21:00:00-7:00", "1.2", "0.19"),
- ("9/19/2018", "10:00 PM", "2018-09-19T22:00:00-7:00", "1.1", "0.18"),
- ("9/19/2018", "11:00 PM", "2018-09-19T23:00:00-7:00", "0.4", "0.09"),
- ]
- return usage
-
-
-async def test_setup_with_config(hass):
- """Test the platform setup with configuration."""
- with patch(PATCH_INIT, return_value=None), patch(
- PATCH_VALIDATE, return_value=True
- ), patch(PATCH_USAGE, new=mock_usage):
-
- await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL)
-
- state = hass.states.get("sensor.srp_energy")
- assert state is not None
-
-
-async def test_daily_usage(hass):
- """Test the platform daily usage."""
- with patch(PATCH_INIT, return_value=None), patch(
- PATCH_VALIDATE, return_value=True
- ), patch(PATCH_USAGE, new=mock_usage):
-
- await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL)
-
- state = hass.states.get("sensor.srp_energy")
-
- assert state
- assert state.state == "7.50"
-
- assert state.attributes
- assert state.attributes.get("unit_of_measurement")
diff --git a/tests/components/switch/test_device_automation.py b/tests/components/switch/test_device_automation.py
new file mode 100644
index 00000000000000..1ebe4785761aa5
--- /dev/null
+++ b/tests/components/switch/test_device_automation.py
@@ -0,0 +1,373 @@
+"""The test for switch device automation."""
+import pytest
+
+from homeassistant.components.switch import DOMAIN
+from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
+from homeassistant.setup import async_setup_component
+import homeassistant.components.automation as automation
+from homeassistant.components.device_automation import (
+ _async_get_device_automations as async_get_device_automations,
+)
+from homeassistant.helpers import device_registry
+
+from tests.common import (
+ MockConfigEntry,
+ async_mock_service,
+ mock_device_registry,
+ mock_registry,
+)
+
+
+@pytest.fixture
+def device_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+def entity_reg(hass):
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+@pytest.fixture
+def calls(hass):
+ """Track calls to a mock serivce."""
+ return async_mock_service(hass, "test", "automation")
+
+
+def _same_lists(a, b):
+ if len(a) != len(b):
+ return False
+
+ for d in a:
+ if d not in b:
+ return False
+ return True
+
+
+async def test_get_actions(hass, device_reg, entity_reg):
+ """Test we get the expected actions from a switch."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_actions = [
+ {
+ "domain": DOMAIN,
+ "type": "turn_off",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ {
+ "domain": DOMAIN,
+ "type": "turn_on",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ {
+ "domain": DOMAIN,
+ "type": "toggle",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ ]
+ actions = await async_get_device_automations(
+ hass, "async_get_actions", device_entry.id
+ )
+ assert _same_lists(actions, expected_actions)
+
+
+async def test_get_conditions(hass, device_reg, entity_reg):
+ """Test we get the expected conditions from a switch."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_conditions = [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": "is_off",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "type": "is_on",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ ]
+ conditions = await async_get_device_automations(
+ hass, "async_get_conditions", device_entry.id
+ )
+ assert _same_lists(conditions, expected_conditions)
+
+
+async def test_get_triggers(hass, device_reg, entity_reg):
+ """Test we get the expected triggers from a switch."""
+ config_entry = MockConfigEntry(domain="test", data={})
+ config_entry.add_to_hass(hass)
+ device_entry = device_reg.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
+ )
+ entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id)
+ expected_triggers = [
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": "turned_off",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": "turned_on",
+ "device_id": device_entry.id,
+ "entity_id": f"{DOMAIN}.test_5678",
+ },
+ ]
+ triggers = await async_get_device_automations(
+ hass, "async_get_triggers", device_entry.id
+ )
+ assert _same_lists(triggers, expected_triggers)
+
+
+async def test_if_fires_on_state_change(hass, calls):
+ """Test for turn_on and turn_off triggers firing."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ ent1, ent2, ent3 = platform.ENTITIES
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "turned_on",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "turn_on {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(
+ (
+ "platform",
+ "entity_id",
+ "from_state.state",
+ "to_state.state",
+ "for",
+ )
+ )
+ },
+ },
+ },
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "turned_off",
+ },
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "turn_off {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(
+ (
+ "platform",
+ "entity_id",
+ "from_state.state",
+ "to_state.state",
+ "for",
+ )
+ )
+ },
+ },
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
+ assert len(calls) == 0
+
+ hass.states.async_set(ent1.entity_id, STATE_OFF)
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format(
+ ent1.entity_id
+ )
+
+ hass.states.async_set(ent1.entity_id, STATE_ON)
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format(
+ ent1.entity_id
+ )
+
+
+async def test_if_state(hass, calls):
+ """Test for turn_on and turn_off conditions."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ ent1, ent2, ent3 = platform.ENTITIES
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "is_on",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_on {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(("platform", "event.event_type"))
+ },
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event2"},
+ "condition": [
+ {
+ "condition": "device",
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "is_off",
+ }
+ ],
+ "action": {
+ "service": "test.automation",
+ "data_template": {
+ "some": "is_off {{ trigger.%s }}"
+ % "}} - {{ trigger.".join(("platform", "event.event_type"))
+ },
+ },
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
+ assert len(calls) == 0
+
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data["some"] == "is_on event - test_event1"
+
+ hass.states.async_set(ent1.entity_id, STATE_OFF)
+ hass.bus.async_fire("test_event1")
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[1].data["some"] == "is_off event - test_event2"
+
+
+async def test_action(hass, calls):
+ """Test for turn_on and turn_off actions."""
+ platform = getattr(hass.components, f"test.{DOMAIN}")
+
+ platform.init()
+ assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
+
+ ent1, ent2, ent3 = platform.ENTITIES
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {"platform": "event", "event_type": "test_event1"},
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "turn_off",
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event2"},
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "turn_on",
+ },
+ },
+ {
+ "trigger": {"platform": "event", "event_type": "test_event3"},
+ "action": {
+ "domain": DOMAIN,
+ "device_id": "",
+ "entity_id": ent1.entity_id,
+ "type": "toggle",
+ },
+ },
+ ]
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
+ assert len(calls) == 0
+
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_OFF
+
+ hass.bus.async_fire("test_event1")
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_OFF
+
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
+
+ hass.bus.async_fire("test_event2")
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
+
+ hass.bus.async_fire("test_event3")
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_OFF
+
+ hass.bus.async_fire("test_event3")
+ await hass.async_block_till_done()
+ assert hass.states.get(ent1.entity_id).state == STATE_ON
diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py
index c04a30589edd8f..a9463cb78f4fea 100644
--- a/tests/components/switch/test_init.py
+++ b/tests/components/switch/test_init.py
@@ -21,7 +21,7 @@ def setUp(self):
platform = getattr(self.hass.components, "test.switch")
platform.init()
# Switch 1 is ON, switch 2 is OFF
- self.switch_1, self.switch_2, self.switch_3 = platform.DEVICES
+ self.switch_1, self.switch_2, self.switch_3 = platform.ENTITIES
# pylint: disable=invalid-name
def tearDown(self):
diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py
index e06746915335b9..888ffd46c3b136 100644
--- a/tests/components/switcher_kis/conftest.py
+++ b/tests/components/switcher_kis/conftest.py
@@ -93,7 +93,7 @@ def last_state_change(self) -> datetime:
@fixture(name="mock_bridge")
def mock_bridge_fixture() -> Generator[None, Any, None]:
"""Fixture for mocking aioswitcher.bridge.SwitcherV2Bridge."""
- queue = Queue() # type: Queue
+ queue = Queue()
async def mock_queue():
"""Mock asyncio's Queue."""
diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py
index 69fe3bae618dab..4c691f66af872c 100644
--- a/tests/components/tradfri/test_light.py
+++ b/tests/components/tradfri/test_light.py
@@ -4,7 +4,9 @@
from unittest.mock import Mock, MagicMock, patch, PropertyMock
import pytest
-from pytradfri.device import Device, LightControl, Light
+from pytradfri.device import Device
+from pytradfri.device.light import Light
+from pytradfri.device.light_control import LightControl
from homeassistant.components import tradfri
diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py
index 437fabf9689469..969c2a734d3448 100644
--- a/tests/components/unifi/test_device_tracker.py
+++ b/tests/components/unifi/test_device_tracker.py
@@ -1,4 +1,4 @@
-"""The tests for the Unifi WAP device tracker platform."""
+"""The tests for the UniFi device tracker platform."""
from collections import deque
from copy import copy
from unittest.mock import Mock
@@ -32,7 +32,6 @@
from homeassistant.setup import async_setup_component
import homeassistant.components.device_tracker as device_tracker
-import homeassistant.components.unifi.device_tracker as unifi_dt
import homeassistant.util.dt as dt_util
DEFAULT_DETECTION_TIME = timedelta(seconds=300)
@@ -275,14 +274,14 @@ async def test_restoring_client(hass, mock_controller):
registry = await entity_registry.async_get_registry(hass)
registry.async_get_or_create(
device_tracker.DOMAIN,
- unifi_dt.UNIFI_DOMAIN,
+ unifi.DOMAIN,
"{}-mock-site".format(CLIENT_1["mac"]),
suggested_object_id=CLIENT_1["hostname"],
config_entry=config_entry,
)
registry.async_get_or_create(
device_tracker.DOMAIN,
- unifi_dt.UNIFI_DOMAIN,
+ unifi.DOMAIN,
"{}-mock-site".format(CLIENT_2["mac"]),
suggested_object_id=CLIENT_2["hostname"],
config_entry=config_entry,
diff --git a/tests/components/upc_connect/__init__.py b/tests/components/upc_connect/__init__.py
deleted file mode 100644
index d491190d111a25..00000000000000
--- a/tests/components/upc_connect/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Tests for the upc_connect component."""
diff --git a/tests/components/upc_connect/test_device_tracker.py b/tests/components/upc_connect/test_device_tracker.py
deleted file mode 100644
index d04219eb884eee..00000000000000
--- a/tests/components/upc_connect/test_device_tracker.py
+++ /dev/null
@@ -1,221 +0,0 @@
-"""The tests for the UPC ConnextBox device tracker platform."""
-import asyncio
-
-from asynctest import patch
-import pytest
-
-from homeassistant.components.device_tracker import DOMAIN
-import homeassistant.components.upc_connect.device_tracker as platform
-from homeassistant.const import CONF_HOST, CONF_PLATFORM
-from homeassistant.setup import async_setup_component
-
-from tests.common import assert_setup_component, load_fixture, mock_component
-
-HOST = "127.0.0.1"
-
-
-async def async_scan_devices_mock(scanner):
- """Mock async_scan_devices."""
- return []
-
-
-@pytest.fixture(autouse=True)
-def setup_comp_deps(hass, mock_device_tracker_conf):
- """Set up component dependencies."""
- mock_component(hass, "zone")
- mock_component(hass, "group")
- yield
-
-
-async def test_setup_platform_timeout_loginpage(hass, caplog, aioclient_mock):
- """Set up a platform with timeout on loginpage."""
- aioclient_mock.get(
- "http://{}/common_page/login.html".format(HOST), exc=asyncio.TimeoutError()
- )
- aioclient_mock.post("http://{}/xml/getter.xml".format(HOST), content=b"successful")
-
- assert await async_setup_component(
- hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}}
- )
-
- assert len(aioclient_mock.mock_calls) == 1
-
- assert "Error setting up platform" in caplog.text
-
-
-async def test_setup_platform_timeout_webservice(hass, caplog, aioclient_mock):
- """Set up a platform with api timeout."""
- aioclient_mock.get(
- "http://{}/common_page/login.html".format(HOST),
- cookies={"sessionToken": "654321"},
- content=b"successful",
- exc=asyncio.TimeoutError(),
- )
-
- assert await async_setup_component(
- hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}}
- )
-
- assert len(aioclient_mock.mock_calls) == 1
-
- assert "Error setting up platform" in caplog.text
-
-
-@patch(
- "homeassistant.components.upc_connect.device_tracker."
- "UPCDeviceScanner.async_scan_devices",
- return_value=async_scan_devices_mock,
-)
-async def test_setup_platform(scan_mock, hass, aioclient_mock):
- """Set up a platform."""
- aioclient_mock.get(
- "http://{}/common_page/login.html".format(HOST),
- cookies={"sessionToken": "654321"},
- )
- aioclient_mock.post("http://{}/xml/getter.xml".format(HOST), content=b"successful")
-
- with assert_setup_component(1, DOMAIN):
- assert await async_setup_component(
- hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}}
- )
-
- assert len(aioclient_mock.mock_calls) == 1
-
-
-async def test_scan_devices(hass, aioclient_mock):
- """Set up a upc platform and scan device."""
- aioclient_mock.get(
- "http://{}/common_page/login.html".format(HOST),
- cookies={"sessionToken": "654321"},
- )
- aioclient_mock.post(
- "http://{}/xml/getter.xml".format(HOST),
- content=b"successful",
- cookies={"sessionToken": "654321"},
- )
-
- scanner = await platform.async_get_scanner(
- hass, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}}
- )
-
- assert len(aioclient_mock.mock_calls) == 1
-
- aioclient_mock.clear_requests()
- aioclient_mock.post(
- "http://{}/xml/getter.xml".format(HOST),
- text=load_fixture("upc_connect.xml"),
- cookies={"sessionToken": "1235678"},
- )
-
- mac_list = await scanner.async_scan_devices()
-
- assert len(aioclient_mock.mock_calls) == 1
- assert aioclient_mock.mock_calls[0][2] == "token=654321&fun=123"
- assert mac_list == ["30:D3:2D:0:69:21", "5C:AA:FD:25:32:02", "70:EE:50:27:A1:38"]
-
-
-async def test_scan_devices_without_session(hass, aioclient_mock):
- """Set up a upc platform and scan device with no token."""
- aioclient_mock.get(
- "http://{}/common_page/login.html".format(HOST),
- cookies={"sessionToken": "654321"},
- )
- aioclient_mock.post(
- "http://{}/xml/getter.xml".format(HOST),
- content=b"successful",
- cookies={"sessionToken": "654321"},
- )
-
- scanner = await platform.async_get_scanner(
- hass, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}}
- )
-
- assert len(aioclient_mock.mock_calls) == 1
-
- aioclient_mock.clear_requests()
- aioclient_mock.get(
- "http://{}/common_page/login.html".format(HOST),
- cookies={"sessionToken": "654321"},
- )
- aioclient_mock.post(
- "http://{}/xml/getter.xml".format(HOST),
- text=load_fixture("upc_connect.xml"),
- cookies={"sessionToken": "1235678"},
- )
-
- scanner.token = None
- mac_list = await scanner.async_scan_devices()
-
- assert len(aioclient_mock.mock_calls) == 2
- assert aioclient_mock.mock_calls[1][2] == "token=654321&fun=123"
- assert mac_list == ["30:D3:2D:0:69:21", "5C:AA:FD:25:32:02", "70:EE:50:27:A1:38"]
-
-
-async def test_scan_devices_without_session_wrong_re(hass, aioclient_mock):
- """Set up a upc platform and scan device with no token and wrong."""
- aioclient_mock.get(
- "http://{}/common_page/login.html".format(HOST),
- cookies={"sessionToken": "654321"},
- )
- aioclient_mock.post(
- "http://{}/xml/getter.xml".format(HOST),
- content=b"successful",
- cookies={"sessionToken": "654321"},
- )
-
- scanner = await platform.async_get_scanner(
- hass, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}}
- )
-
- assert len(aioclient_mock.mock_calls) == 1
-
- aioclient_mock.clear_requests()
- aioclient_mock.get(
- "http://{}/common_page/login.html".format(HOST),
- cookies={"sessionToken": "654321"},
- )
- aioclient_mock.post(
- "http://{}/xml/getter.xml".format(HOST),
- status=400,
- cookies={"sessionToken": "1235678"},
- )
-
- scanner.token = None
- mac_list = await scanner.async_scan_devices()
-
- assert len(aioclient_mock.mock_calls) == 2
- assert aioclient_mock.mock_calls[1][2] == "token=654321&fun=123"
- assert mac_list == []
-
-
-async def test_scan_devices_parse_error(hass, aioclient_mock):
- """Set up a upc platform and scan device with parse error."""
- aioclient_mock.get(
- "http://{}/common_page/login.html".format(HOST),
- cookies={"sessionToken": "654321"},
- )
- aioclient_mock.post(
- "http://{}/xml/getter.xml".format(HOST),
- content=b"successful",
- cookies={"sessionToken": "654321"},
- )
-
- scanner = await platform.async_get_scanner(
- hass, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}}
- )
-
- assert len(aioclient_mock.mock_calls) == 1
-
- aioclient_mock.clear_requests()
- aioclient_mock.post(
- "http://{}/xml/getter.xml".format(HOST),
- text="Blablebla blabalble",
- cookies={"sessionToken": "1235678"},
- )
-
- mac_list = await scanner.async_scan_devices()
-
- assert len(aioclient_mock.mock_calls) == 1
- assert aioclient_mock.mock_calls[0][2] == "token=654321&fun=123"
- assert scanner.token is None
- assert mac_list == []
diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py
index 93b9a434b7f1ac..3ae9d11c3b6555 100644
--- a/tests/components/withings/test_config_flow.py
+++ b/tests/components/withings/test_config_flow.py
@@ -3,9 +3,7 @@
from asynctest import CoroutineMock, MagicMock
import pytest
-from homeassistant import setup, data_entry_flow
-import homeassistant.components.api as api
-import homeassistant.components.http as http
+from homeassistant import data_entry_flow
from homeassistant.components.withings import const
from homeassistant.components.withings.config_flow import (
register_flow_implementation,
@@ -24,27 +22,6 @@ def flow_handler_fixture(hass: HomeAssistantType):
return flow_handler
-@pytest.fixture(name="setup_hass")
-async def setup_hass_fixture(hass: HomeAssistantType):
- """Provide hass instance."""
- config = {
- http.DOMAIN: {},
- api.DOMAIN: {"base_url": "http://localhost/"},
- const.DOMAIN: {
- const.CLIENT_ID: "my_client_id",
- const.CLIENT_SECRET: "my_secret",
- const.PROFILES: ["Person 1", "Person 2"],
- },
- }
-
- hass.data = {}
-
- await setup.async_setup_component(hass, "http", config)
- await setup.async_setup_component(hass, "api", config)
-
- return hass
-
-
def test_flow_handler_init(flow_handler: WithingsFlowHandler):
"""Test the init of the flow handler."""
assert not flow_handler.flow_profile
@@ -173,3 +150,13 @@ async def test_auth_callback_view_get(hass: HomeAssistantType):
"my_flow_id", {const.PROFILE: "my_profile", const.CODE: "my_code"}
)
hass.config_entries.flow.async_configure.reset_mock()
+
+
+async def test_init_without_config(hass):
+ """Try initializin a configg flow without it being configured."""
+ result = await hass.config_entries.flow.async_init(
+ "withings", context={"source": "user"}
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "no_flows"
diff --git a/tests/components/yandex_transport/__init__.py b/tests/components/yandex_transport/__init__.py
new file mode 100644
index 00000000000000..fe6b0db52d3e05
--- /dev/null
+++ b/tests/components/yandex_transport/__init__.py
@@ -0,0 +1 @@
+"""Tests for the yandex transport platform."""
diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py
new file mode 100644
index 00000000000000..50d945e7fae371
--- /dev/null
+++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py
@@ -0,0 +1,88 @@
+"""Tests for the yandex transport platform."""
+
+import json
+import pytest
+
+import homeassistant.components.sensor as sensor
+import homeassistant.util.dt as dt_util
+from homeassistant.const import CONF_NAME
+from tests.common import (
+ assert_setup_component,
+ async_setup_component,
+ MockDependency,
+ load_fixture,
+)
+
+REPLY = json.loads(load_fixture("yandex_transport_reply.json"))
+
+
+@pytest.fixture
+def mock_requester():
+ """Create a mock ya_ma module and YandexMapsRequester."""
+ with MockDependency("ya_ma") as ya_ma:
+ instance = ya_ma.YandexMapsRequester.return_value
+ instance.get_stop_info.return_value = REPLY
+ yield instance
+
+
+STOP_ID = 9639579
+ROUTES = ["194", "т36", "т47", "м10"]
+NAME = "test_name"
+TEST_CONFIG = {
+ "sensor": {
+ "platform": "yandex_transport",
+ "stop_id": 9639579,
+ "routes": ROUTES,
+ "name": NAME,
+ }
+}
+
+FILTERED_ATTRS = {
+ "т36": ["21:43", "21:47", "22:02"],
+ "т47": ["21:40", "22:01"],
+ "м10": ["21:48", "22:00"],
+ "stop_name": "7-й автобусный парк",
+ "attribution": "Data provided by maps.yandex.ru",
+}
+
+RESULT_STATE = dt_util.utc_from_timestamp(1568659253).isoformat(timespec="seconds")
+
+
+async def assert_setup_sensor(hass, config, count=1):
+ """Set up the sensor and assert it's been created."""
+ with assert_setup_component(count):
+ assert await async_setup_component(hass, sensor.DOMAIN, config)
+
+
+async def test_setup_platform_valid_config(hass, mock_requester):
+ """Test that sensor is set up properly with valid config."""
+ await assert_setup_sensor(hass, TEST_CONFIG)
+
+
+async def test_setup_platform_invalid_config(hass, mock_requester):
+ """Check an invalid configuration."""
+ await assert_setup_sensor(
+ hass, {"sensor": {"platform": "yandex_transport", "stopid": 1234}}, count=0
+ )
+
+
+async def test_name(hass, mock_requester):
+ """Return the name if set in the configuration."""
+ await assert_setup_sensor(hass, TEST_CONFIG)
+ state = hass.states.get("sensor.test_name")
+ assert state.name == TEST_CONFIG["sensor"][CONF_NAME]
+
+
+async def test_state(hass, mock_requester):
+ """Return the contents of _state."""
+ await assert_setup_sensor(hass, TEST_CONFIG)
+ state = hass.states.get("sensor.test_name")
+ assert state.state == RESULT_STATE
+
+
+async def test_filtered_attributes(hass, mock_requester):
+ """Return the contents of attributes."""
+ await assert_setup_sensor(hass, TEST_CONFIG)
+ state = hass.states.get("sensor.test_name")
+ state_attrs = {key: state.attributes[key] for key in FILTERED_ATTRS}
+ assert state_attrs == FILTERED_ATTRS
diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py
index d34c6983528515..fc29e4012cd6f8 100644
--- a/tests/components/zha/common.py
+++ b/tests/components/zha/common.py
@@ -50,7 +50,7 @@ def add_input_cluster(self, cluster_id):
"""Add an input cluster."""
from zigpy.zcl import Cluster
- cluster = Cluster.from_id(self, cluster_id)
+ cluster = Cluster.from_id(self, cluster_id, is_server=True)
patch_cluster(cluster)
self.in_clusters[cluster_id] = cluster
if hasattr(cluster, "ep_attribute"):
@@ -60,7 +60,7 @@ def add_output_cluster(self, cluster_id):
"""Add an output cluster."""
from zigpy.zcl import Cluster
- cluster = Cluster.from_id(self, cluster_id)
+ cluster = Cluster.from_id(self, cluster_id, is_server=False)
patch_cluster(cluster)
self.out_clusters[cluster_id] = cluster
diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py
index 1e5ec615088b09..dba187d7b966a0 100644
--- a/tests/components/zwave/test_node_entity.py
+++ b/tests/components/zwave/test_node_entity.py
@@ -164,6 +164,73 @@ def listener(event):
assert events[0].data[const.ATTR_SCENE_DATA] == scene_data
+async def test_application_version(hass, mock_openzwave):
+ """Test application version."""
+ mock_receivers = {}
+
+ signal_mocks = [
+ mock_zwave.MockNetwork.SIGNAL_VALUE_CHANGED,
+ mock_zwave.MockNetwork.SIGNAL_VALUE_ADDED,
+ ]
+
+ def mock_connect(receiver, signal, *args, **kwargs):
+ if signal in signal_mocks:
+ mock_receivers[signal] = receiver
+
+ node = mock_zwave.MockNode(node_id=11)
+
+ with patch("pydispatch.dispatcher.connect", new=mock_connect):
+ entity = node_entity.ZWaveNodeEntity(node, mock_openzwave)
+
+ for signal_mock in signal_mocks:
+ assert signal_mock in mock_receivers.keys()
+
+ events = []
+
+ def listener(event):
+ events.append(event)
+
+ # Make sure application version isn't set before
+ assert (
+ node_entity.ATTR_APPLICATION_VERSION
+ not in entity.device_state_attributes.keys()
+ )
+
+ # Add entity to hass
+ entity.hass = hass
+ entity.entity_id = "zwave.mock_node"
+
+ # Fire off an added value
+ value = mock_zwave.MockValue(
+ command_class=const.COMMAND_CLASS_VERSION,
+ label="Application Version",
+ data="5.10",
+ )
+ hass.async_add_job(
+ mock_receivers[mock_zwave.MockNetwork.SIGNAL_VALUE_ADDED], node, value
+ )
+ await hass.async_block_till_done()
+
+ assert (
+ entity.device_state_attributes[node_entity.ATTR_APPLICATION_VERSION] == "5.10"
+ )
+
+ # Fire off a changed
+ value = mock_zwave.MockValue(
+ command_class=const.COMMAND_CLASS_VERSION,
+ label="Application Version",
+ data="4.14",
+ )
+ hass.async_add_job(
+ mock_receivers[mock_zwave.MockNetwork.SIGNAL_VALUE_CHANGED], node, value
+ )
+ await hass.async_block_till_done()
+
+ assert (
+ entity.device_state_attributes[node_entity.ATTR_APPLICATION_VERSION] == "4.14"
+ )
+
+
@pytest.mark.usefixtures("mock_openzwave")
class TestZWaveNodeEntity(unittest.TestCase):
"""Class to test ZWaveNodeEntity."""
diff --git a/tests/fixtures/here_travel_time/attribution_response.json b/tests/fixtures/here_travel_time/attribution_response.json
new file mode 100644
index 00000000000000..9b682f6c51fb14
--- /dev/null
+++ b/tests/fixtures/here_travel_time/attribution_response.json
@@ -0,0 +1,276 @@
+{
+ "response": {
+ "metaInfo": {
+ "timestamp": "2019-09-21T15:17:31Z",
+ "mapVersion": "8.30.100.154",
+ "moduleVersion": "7.2.201937-5251",
+ "interfaceVersion": "2.6.70",
+ "availableMapVersion": [
+ "8.30.100.154"
+ ]
+ },
+ "route": [
+ {
+ "waypoint": [
+ {
+ "linkId": "+565790671",
+ "mappedPosition": {
+ "latitude": 50.0378591,
+ "longitude": 14.3924721
+ },
+ "originalPosition": {
+ "latitude": 50.0377513,
+ "longitude": 14.3923344
+ },
+ "type": "stopOver",
+ "spot": 0.3,
+ "sideOfStreet": "left",
+ "mappedRoadName": "V Bokách III",
+ "label": "V Bokách III",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ {
+ "linkId": "+748931502",
+ "mappedPosition": {
+ "latitude": 50.0798786,
+ "longitude": 14.4260037
+ },
+ "originalPosition": {
+ "latitude": 50.0799383,
+ "longitude": 14.4258216
+ },
+ "type": "stopOver",
+ "spot": 1.0,
+ "sideOfStreet": "left",
+ "mappedRoadName": "Štěpánská",
+ "label": "Štěpánská",
+ "shapeIndex": 116,
+ "source": "user"
+ }
+ ],
+ "mode": {
+ "type": "shortest",
+ "transportModes": [
+ "publicTransportTimeTable"
+ ],
+ "trafficMode": "enabled",
+ "feature": []
+ },
+ "leg": [
+ {
+ "start": {
+ "linkId": "+565790671",
+ "mappedPosition": {
+ "latitude": 50.0378591,
+ "longitude": 14.3924721
+ },
+ "originalPosition": {
+ "latitude": 50.0377513,
+ "longitude": 14.3923344
+ },
+ "type": "stopOver",
+ "spot": 0.3,
+ "sideOfStreet": "left",
+ "mappedRoadName": "V Bokách III",
+ "label": "V Bokách III",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ "end": {
+ "linkId": "+748931502",
+ "mappedPosition": {
+ "latitude": 50.0798786,
+ "longitude": 14.4260037
+ },
+ "originalPosition": {
+ "latitude": 50.0799383,
+ "longitude": 14.4258216
+ },
+ "type": "stopOver",
+ "spot": 1.0,
+ "sideOfStreet": "left",
+ "mappedRoadName": "Štěpánská",
+ "label": "Štěpánská",
+ "shapeIndex": 116,
+ "source": "user"
+ },
+ "length": 7835,
+ "travelTime": 2413,
+ "maneuver": [
+ {
+ "position": {
+ "latitude": 50.0378591,
+ "longitude": 14.3924721
+ },
+ "instruction": "Head northwest on Kosořská. Go for 28 m.",
+ "travelTime": 32,
+ "length": 28,
+ "id": "M1",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 50.0380039,
+ "longitude": 14.3921542
+ },
+ "instruction": "Turn left onto Kosořská. Go for 24 m.",
+ "travelTime": 24,
+ "length": 24,
+ "id": "M2",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 50.0380039,
+ "longitude": 14.3918109
+ },
+ "instruction": "Take the street on the left, Slivenecká. Go for 343 m.",
+ "travelTime": 354,
+ "length": 343,
+ "id": "M3",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 50.0376499,
+ "longitude": 14.3871975
+ },
+ "instruction": "Turn left onto Slivenecká. Go for 64 m.",
+ "travelTime": 72,
+ "length": 64,
+ "id": "M4",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 50.0373602,
+ "longitude": 14.3879807
+ },
+ "instruction": "Turn right onto Slivenecká. Go for 91 m.",
+ "travelTime": 95,
+ "length": 91,
+ "id": "M5",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 50.0365448,
+ "longitude": 14.3878305
+ },
+ "instruction": "Turn left onto K Barrandovu. Go for 124 m.",
+ "travelTime": 126,
+ "length": 124,
+ "id": "M6",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 50.0363168,
+ "longitude": 14.3894618
+ },
+ "instruction": "Go to the Tram station Geologicka and take the rail 5 toward Ústřední dílny DP. Follow for 13 stations.",
+ "travelTime": 1440,
+ "length": 6911,
+ "id": "M7",
+ "stopName": "Geologicka",
+ "_type": "PublicTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 50.0800508,
+ "longitude": 14.423403
+ },
+ "instruction": "Get off at Vodickova.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M8",
+ "stopName": "Vodickova",
+ "nextRoadName": "Vodičkova",
+ "_type": "PublicTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 50.0800508,
+ "longitude": 14.423403
+ },
+ "instruction": "Head northeast on Vodičkova. Go for 65 m.",
+ "travelTime": 74,
+ "length": 65,
+ "id": "M9",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 50.0804901,
+ "longitude": 14.4239759
+ },
+ "instruction": "Turn right onto V Jámě. Go for 163 m.",
+ "travelTime": 174,
+ "length": 163,
+ "id": "M10",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 50.0796962,
+ "longitude": 14.4258857
+ },
+ "instruction": "Turn left onto Štěpánská. Go for 22 m.",
+ "travelTime": 22,
+ "length": 22,
+ "id": "M11",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 50.0798786,
+ "longitude": 14.4260037
+ },
+ "instruction": "Arrive at Štěpánská. Your destination is on the left.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M12",
+ "_type": "PrivateTransportManeuverType"
+ }
+ ]
+ }
+ ],
+ "publicTransportLine": [
+ {
+ "lineName": "5",
+ "lineForeground": "#F5ADCE",
+ "lineBackground": "#F5ADCE",
+ "companyName": "HERE Technologies",
+ "destination": "Ústřední dílny DP",
+ "type": "railLight",
+ "id": "L1"
+ }
+ ],
+ "summary": {
+ "distance": 7835,
+ "baseTime": 2413,
+ "flags": [
+ "noThroughRoad",
+ "builtUpArea"
+ ],
+ "text": "The trip takes 7.8 km and 40 mins.",
+ "travelTime": 2413,
+ "departure": "2019-09-21T17:16:17+02:00",
+ "timetableExpiration": "2019-09-21T00:00:00Z",
+ "_type": "PublicTransportRouteSummaryType"
+ }
+ }
+ ],
+ "language": "en-us",
+ "sourceAttribution": {
+ "attribution": "With the support of HERE Technologies. All information is provided without warranty of any kind.",
+ "supplier": [
+ {
+ "title": "HERE Technologies",
+ "href": "https://transit.api.here.com/r?appId=Mt1bOYh3m9uxE7r3wuUx&u=https://wego.here.com"
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/here_travel_time/bike_response.json b/tests/fixtures/here_travel_time/bike_response.json
new file mode 100644
index 00000000000000..a3af39129d01fe
--- /dev/null
+++ b/tests/fixtures/here_travel_time/bike_response.json
@@ -0,0 +1,274 @@
+{
+ "response": {
+ "metaInfo": {
+ "timestamp": "2019-07-24T10:17:40Z",
+ "mapVersion": "8.30.98.154",
+ "moduleVersion": "7.2.201929-4522",
+ "interfaceVersion": "2.6.64",
+ "availableMapVersion": [
+ "8.30.98.154"
+ ]
+ },
+ "route": [
+ {
+ "waypoint": [
+ {
+ "linkId": "-1230414527",
+ "mappedPosition": {
+ "latitude": 41.9797859,
+ "longitude": -87.8790879
+ },
+ "originalPosition": {
+ "latitude": 41.9798,
+ "longitude": -87.8801
+ },
+ "type": "stopOver",
+ "spot": 0.5079365,
+ "sideOfStreet": "right",
+ "mappedRoadName": "Mannheim Rd",
+ "label": "Mannheim Rd - US-12",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ {
+ "linkId": "+924115108",
+ "mappedPosition": {
+ "latitude": 41.90413,
+ "longitude": -87.9223502
+ },
+ "originalPosition": {
+ "latitude": 41.9043,
+ "longitude": -87.9216001
+ },
+ "type": "stopOver",
+ "spot": 0.1925926,
+ "sideOfStreet": "right",
+ "mappedRoadName": "",
+ "label": "",
+ "shapeIndex": 87,
+ "source": "user"
+ }
+ ],
+ "mode": {
+ "type": "fastest",
+ "transportModes": [
+ "bicycle"
+ ],
+ "trafficMode": "enabled",
+ "feature": []
+ },
+ "leg": [
+ {
+ "start": {
+ "linkId": "-1230414527",
+ "mappedPosition": {
+ "latitude": 41.9797859,
+ "longitude": -87.8790879
+ },
+ "originalPosition": {
+ "latitude": 41.9798,
+ "longitude": -87.8801
+ },
+ "type": "stopOver",
+ "spot": 0.5079365,
+ "sideOfStreet": "right",
+ "mappedRoadName": "Mannheim Rd",
+ "label": "Mannheim Rd - US-12",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ "end": {
+ "linkId": "+924115108",
+ "mappedPosition": {
+ "latitude": 41.90413,
+ "longitude": -87.9223502
+ },
+ "originalPosition": {
+ "latitude": 41.9043,
+ "longitude": -87.9216001
+ },
+ "type": "stopOver",
+ "spot": 0.1925926,
+ "sideOfStreet": "right",
+ "mappedRoadName": "",
+ "label": "",
+ "shapeIndex": 87,
+ "source": "user"
+ },
+ "length": 12613,
+ "travelTime": 3292,
+ "maneuver": [
+ {
+ "position": {
+ "latitude": 41.9797859,
+ "longitude": -87.8790879
+ },
+ "instruction": "Head south on Mannheim Rd (US-12/US-45). Go for 2.6 km.",
+ "travelTime": 646,
+ "length": 2648,
+ "id": "M1",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9579244,
+ "longitude": -87.8838551
+ },
+ "instruction": "Keep left onto Mannheim Rd (US-12/US-45). Go for 2.4 km.",
+ "travelTime": 621,
+ "length": 2427,
+ "id": "M2",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9364238,
+ "longitude": -87.8849387
+ },
+ "instruction": "Turn right onto W Belmont Ave. Go for 595 m.",
+ "travelTime": 158,
+ "length": 595,
+ "id": "M3",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9362521,
+ "longitude": -87.8921163
+ },
+ "instruction": "Turn left onto Cullerton St. Go for 669 m.",
+ "travelTime": 180,
+ "length": 669,
+ "id": "M4",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9305658,
+ "longitude": -87.8932428
+ },
+ "instruction": "Continue on N Landen Dr. Go for 976 m.",
+ "travelTime": 246,
+ "length": 976,
+ "id": "M5",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9217896,
+ "longitude": -87.8928781
+ },
+ "instruction": "Turn right onto E Fullerton Ave. Go for 904 m.",
+ "travelTime": 238,
+ "length": 904,
+ "id": "M6",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.921618,
+ "longitude": -87.9038107
+ },
+ "instruction": "Turn left onto N Wolf Rd. Go for 1.6 km.",
+ "travelTime": 417,
+ "length": 1604,
+ "id": "M7",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.907177,
+ "longitude": -87.9032314
+ },
+ "instruction": "Turn right onto W North Ave (IL-64). Go for 2.0 km.",
+ "travelTime": 574,
+ "length": 2031,
+ "id": "M8",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9065225,
+ "longitude": -87.9277039
+ },
+ "instruction": "Turn left onto N Clinton Ave. Go for 275 m.",
+ "travelTime": 78,
+ "length": 275,
+ "id": "M9",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9040549,
+ "longitude": -87.9277253
+ },
+ "instruction": "Turn left onto E Third St. Go for 249 m.",
+ "travelTime": 63,
+ "length": 249,
+ "id": "M10",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9040334,
+ "longitude": -87.9247105
+ },
+ "instruction": "Continue on N Caroline Ave. Go for 96 m.",
+ "travelTime": 37,
+ "length": 96,
+ "id": "M11",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9038832,
+ "longitude": -87.9236054
+ },
+ "instruction": "Turn slightly left. Go for 113 m.",
+ "travelTime": 28,
+ "length": 113,
+ "id": "M12",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9039047,
+ "longitude": -87.9222536
+ },
+ "instruction": "Turn left. Go for 26 m.",
+ "travelTime": 6,
+ "length": 26,
+ "id": "M13",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.90413,
+ "longitude": -87.9223502
+ },
+ "instruction": "Arrive at your destination on the right.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M14",
+ "_type": "PrivateTransportManeuverType"
+ }
+ ]
+ }
+ ],
+ "summary": {
+ "distance": 12613,
+ "baseTime": 3292,
+ "flags": [
+ "noThroughRoad",
+ "builtUpArea",
+ "park"
+ ],
+ "text": "The trip takes 12.6 km and 55 mins.",
+ "travelTime": 3292,
+ "_type": "RouteSummaryType"
+ }
+ }
+ ],
+ "language": "en-us"
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/here_travel_time/car_enabled_response.json b/tests/fixtures/here_travel_time/car_enabled_response.json
new file mode 100644
index 00000000000000..08da738f0464a9
--- /dev/null
+++ b/tests/fixtures/here_travel_time/car_enabled_response.json
@@ -0,0 +1,298 @@
+{
+ "response": {
+ "metaInfo": {
+ "timestamp": "2019-07-21T21:21:31Z",
+ "mapVersion": "8.30.98.154",
+ "moduleVersion": "7.2.201928-4478",
+ "interfaceVersion": "2.6.64",
+ "availableMapVersion": [
+ "8.30.98.154"
+ ]
+ },
+ "route": [
+ {
+ "waypoint": [
+ {
+ "linkId": "-1128310200",
+ "mappedPosition": {
+ "latitude": 38.9026523,
+ "longitude": -77.048338
+ },
+ "originalPosition": {
+ "latitude": 38.9029809,
+ "longitude": -77.048338
+ },
+ "type": "stopOver",
+ "spot": 0.3538462,
+ "sideOfStreet": "right",
+ "mappedRoadName": "K St NW",
+ "label": "K St NW",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ {
+ "linkId": "-18459081",
+ "mappedPosition": {
+ "latitude": 39.0422511,
+ "longitude": -77.1193526
+ },
+ "originalPosition": {
+ "latitude": 39.042158,
+ "longitude": -77.119116
+ },
+ "type": "stopOver",
+ "spot": 0.7253521,
+ "sideOfStreet": "left",
+ "mappedRoadName": "Commonwealth Dr",
+ "label": "Commonwealth Dr",
+ "shapeIndex": 283,
+ "source": "user"
+ }
+ ],
+ "mode": {
+ "type": "fastest",
+ "transportModes": [
+ "car"
+ ],
+ "trafficMode": "enabled",
+ "feature": []
+ },
+ "leg": [
+ {
+ "start": {
+ "linkId": "-1128310200",
+ "mappedPosition": {
+ "latitude": 38.9026523,
+ "longitude": -77.048338
+ },
+ "originalPosition": {
+ "latitude": 38.9029809,
+ "longitude": -77.048338
+ },
+ "type": "stopOver",
+ "spot": 0.3538462,
+ "sideOfStreet": "right",
+ "mappedRoadName": "K St NW",
+ "label": "K St NW",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ "end": {
+ "linkId": "-18459081",
+ "mappedPosition": {
+ "latitude": 39.0422511,
+ "longitude": -77.1193526
+ },
+ "originalPosition": {
+ "latitude": 39.042158,
+ "longitude": -77.119116
+ },
+ "type": "stopOver",
+ "spot": 0.7253521,
+ "sideOfStreet": "left",
+ "mappedRoadName": "Commonwealth Dr",
+ "label": "Commonwealth Dr",
+ "shapeIndex": 283,
+ "source": "user"
+ },
+ "length": 23381,
+ "travelTime": 1817,
+ "maneuver": [
+ {
+ "position": {
+ "latitude": 38.9026523,
+ "longitude": -77.048338
+ },
+ "instruction": "Head toward 22nd St NW on K St NW. Go for 140 m.",
+ "travelTime": 36,
+ "length": 140,
+ "id": "M1",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9027703,
+ "longitude": -77.0494902
+ },
+ "instruction": "Take the 3rd exit from Washington Cir NW roundabout onto K St NW. Go for 325 m.",
+ "travelTime": 81,
+ "length": 325,
+ "id": "M2",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9026523,
+ "longitude": -77.0529449
+ },
+ "instruction": "Keep left onto K St NW (US-29). Go for 201 m.",
+ "travelTime": 29,
+ "length": 201,
+ "id": "M3",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9025235,
+ "longitude": -77.0552516
+ },
+ "instruction": "Keep right onto Whitehurst Fwy (US-29). Go for 1.4 km.",
+ "travelTime": 143,
+ "length": 1381,
+ "id": "M4",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9050448,
+ "longitude": -77.0701969
+ },
+ "instruction": "Turn left onto M St NW. Go for 784 m.",
+ "travelTime": 80,
+ "length": 784,
+ "id": "M5",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9060318,
+ "longitude": -77.0790696
+ },
+ "instruction": "Turn slightly left onto Canal Rd NW. Go for 4.2 km.",
+ "travelTime": 287,
+ "length": 4230,
+ "id": "M6",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9303219,
+ "longitude": -77.1117926
+ },
+ "instruction": "Continue on Clara Barton Pkwy. Go for 844 m.",
+ "travelTime": 55,
+ "length": 844,
+ "id": "M7",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9368558,
+ "longitude": -77.1166742
+ },
+ "instruction": "Continue on Clara Barton Pkwy. Go for 4.7 km.",
+ "travelTime": 294,
+ "length": 4652,
+ "id": "M8",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9706838,
+ "longitude": -77.1461463
+ },
+ "instruction": "Keep right onto Cabin John Pkwy N toward I-495 N. Go for 2.1 km.",
+ "travelTime": 90,
+ "length": 2069,
+ "id": "M9",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9858222,
+ "longitude": -77.1571326
+ },
+ "instruction": "Take left ramp onto I-495 N (Capital Beltway). Go for 2.9 km.",
+ "travelTime": 129,
+ "length": 2890,
+ "id": "M10",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 39.0104449,
+ "longitude": -77.1508026
+ },
+ "instruction": "Keep left onto I-270-SPUR toward I-270/Rockville/Frederick. Go for 1.1 km.",
+ "travelTime": 48,
+ "length": 1136,
+ "id": "M11",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 39.0192747,
+ "longitude": -77.144773
+ },
+ "instruction": "Take exit 1 toward Democracy Blvd/Old Georgetown Rd/MD-187 onto Democracy Blvd. Go for 1.8 km.",
+ "travelTime": 205,
+ "length": 1818,
+ "id": "M12",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 39.0247464,
+ "longitude": -77.1253431
+ },
+ "instruction": "Turn left onto Old Georgetown Rd (MD-187). Go for 2.3 km.",
+ "travelTime": 230,
+ "length": 2340,
+ "id": "M13",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 39.0447772,
+ "longitude": -77.1203649
+ },
+ "instruction": "Turn right onto Nicholson Ln. Go for 208 m.",
+ "travelTime": 31,
+ "length": 208,
+ "id": "M14",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 39.0448952,
+ "longitude": -77.1179724
+ },
+ "instruction": "Turn right onto Commonwealth Dr. Go for 341 m.",
+ "travelTime": 75,
+ "length": 341,
+ "id": "M15",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 39.0422511,
+ "longitude": -77.1193526
+ },
+ "instruction": "Arrive at Commonwealth Dr. Your destination is on the left.",
+ "travelTime": 4,
+ "length": 22,
+ "id": "M16",
+ "_type": "PrivateTransportManeuverType"
+ }
+ ]
+ }
+ ],
+ "summary": {
+ "distance": 23381,
+ "trafficTime": 1782,
+ "baseTime": 1712,
+ "flags": [
+ "noThroughRoad",
+ "motorway",
+ "builtUpArea",
+ "park"
+ ],
+ "text": "The trip takes 23.4 km and 30 mins.",
+ "travelTime": 1782,
+ "_type": "RouteSummaryType"
+ }
+ }
+ ],
+ "language": "en-us"
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/here_travel_time/car_response.json b/tests/fixtures/here_travel_time/car_response.json
new file mode 100644
index 00000000000000..bda8454f3f3780
--- /dev/null
+++ b/tests/fixtures/here_travel_time/car_response.json
@@ -0,0 +1,299 @@
+{
+ "response": {
+ "metaInfo": {
+ "timestamp": "2019-07-19T07:38:39Z",
+ "mapVersion": "8.30.98.154",
+ "moduleVersion": "7.2.201928-4446",
+ "interfaceVersion": "2.6.64",
+ "availableMapVersion": [
+ "8.30.98.154"
+ ]
+ },
+ "route": [
+ {
+ "waypoint": [
+ {
+ "linkId": "+732182239",
+ "mappedPosition": {
+ "latitude": 38.9,
+ "longitude": -77.0488358
+ },
+ "originalPosition": {
+ "latitude": 38.9,
+ "longitude": -77.0483301
+ },
+ "type": "stopOver",
+ "spot": 0.4946237,
+ "sideOfStreet": "right",
+ "mappedRoadName": "22nd St NW",
+ "label": "22nd St NW",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ {
+ "linkId": "+942865877",
+ "mappedPosition": {
+ "latitude": 38.9999735,
+ "longitude": -77.100141
+ },
+ "originalPosition": {
+ "latitude": 38.9999999,
+ "longitude": -77.1000001
+ },
+ "type": "stopOver",
+ "spot": 1,
+ "sideOfStreet": "left",
+ "mappedRoadName": "Service Rd S",
+ "label": "Service Rd S",
+ "shapeIndex": 279,
+ "source": "user"
+ }
+ ],
+ "mode": {
+ "type": "fastest",
+ "transportModes": [
+ "car"
+ ],
+ "trafficMode": "enabled",
+ "feature": []
+ },
+ "leg": [
+ {
+ "start": {
+ "linkId": "+732182239",
+ "mappedPosition": {
+ "latitude": 38.9,
+ "longitude": -77.0488358
+ },
+ "originalPosition": {
+ "latitude": 38.9,
+ "longitude": -77.0483301
+ },
+ "type": "stopOver",
+ "spot": 0.4946237,
+ "sideOfStreet": "right",
+ "mappedRoadName": "22nd St NW",
+ "label": "22nd St NW",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ "end": {
+ "linkId": "+942865877",
+ "mappedPosition": {
+ "latitude": 38.9999735,
+ "longitude": -77.100141
+ },
+ "originalPosition": {
+ "latitude": 38.9999999,
+ "longitude": -77.1000001
+ },
+ "type": "stopOver",
+ "spot": 1,
+ "sideOfStreet": "left",
+ "mappedRoadName": "Service Rd S",
+ "label": "Service Rd S",
+ "shapeIndex": 279,
+ "source": "user"
+ },
+ "length": 23903,
+ "travelTime": 1884,
+ "maneuver": [
+ {
+ "position": {
+ "latitude": 38.9,
+ "longitude": -77.0488358
+ },
+ "instruction": "Head toward I St NW on 22nd St NW. Go for 279 m.",
+ "travelTime": 95,
+ "length": 279,
+ "id": "M1",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9021051,
+ "longitude": -77.048825
+ },
+ "instruction": "Turn left toward Pennsylvania Ave NW. Go for 71 m.",
+ "travelTime": 21,
+ "length": 71,
+ "id": "M2",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.902545,
+ "longitude": -77.0494151
+ },
+ "instruction": "Take the 3rd exit from Washington Cir NW roundabout onto K St NW. Go for 352 m.",
+ "travelTime": 90,
+ "length": 352,
+ "id": "M3",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9026523,
+ "longitude": -77.0529449
+ },
+ "instruction": "Keep left onto K St NW (US-29). Go for 201 m.",
+ "travelTime": 30,
+ "length": 201,
+ "id": "M4",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9025235,
+ "longitude": -77.0552516
+ },
+ "instruction": "Keep right onto Whitehurst Fwy (US-29). Go for 1.4 km.",
+ "travelTime": 131,
+ "length": 1381,
+ "id": "M5",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9050448,
+ "longitude": -77.0701969
+ },
+ "instruction": "Turn left onto M St NW. Go for 784 m.",
+ "travelTime": 78,
+ "length": 784,
+ "id": "M6",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9060318,
+ "longitude": -77.0790696
+ },
+ "instruction": "Turn slightly left onto Canal Rd NW. Go for 4.2 km.",
+ "travelTime": 277,
+ "length": 4230,
+ "id": "M7",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9303219,
+ "longitude": -77.1117926
+ },
+ "instruction": "Continue on Clara Barton Pkwy. Go for 844 m.",
+ "travelTime": 55,
+ "length": 844,
+ "id": "M8",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9368558,
+ "longitude": -77.1166742
+ },
+ "instruction": "Continue on Clara Barton Pkwy. Go for 4.7 km.",
+ "travelTime": 298,
+ "length": 4652,
+ "id": "M9",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9706838,
+ "longitude": -77.1461463
+ },
+ "instruction": "Keep right onto Cabin John Pkwy N toward I-495 N. Go for 2.1 km.",
+ "travelTime": 91,
+ "length": 2069,
+ "id": "M10",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9858222,
+ "longitude": -77.1571326
+ },
+ "instruction": "Take left ramp onto I-495 N (Capital Beltway). Go for 5.5 km.",
+ "travelTime": 238,
+ "length": 5538,
+ "id": "M11",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 39.0153587,
+ "longitude": -77.1221781
+ },
+ "instruction": "Take exit 36 toward Bethesda onto MD-187 S (Old Georgetown Rd). Go for 2.4 km.",
+ "travelTime": 211,
+ "length": 2365,
+ "id": "M12",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9981818,
+ "longitude": -77.1093571
+ },
+ "instruction": "Turn left onto Lincoln Dr. Go for 506 m.",
+ "travelTime": 127,
+ "length": 506,
+ "id": "M13",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9987397,
+ "longitude": -77.1037138
+ },
+ "instruction": "Turn right onto Service Rd W. Go for 121 m.",
+ "travelTime": 36,
+ "length": 121,
+ "id": "M14",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9976454,
+ "longitude": -77.1036172
+ },
+ "instruction": "Turn left onto Service Rd S. Go for 510 m.",
+ "travelTime": 106,
+ "length": 510,
+ "id": "M15",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9999735,
+ "longitude": -77.100141
+ },
+ "instruction": "Arrive at Service Rd S. Your destination is on the left.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M16",
+ "_type": "PrivateTransportManeuverType"
+ }
+ ]
+ }
+ ],
+ "summary": {
+ "distance": 23903,
+ "trafficTime": 1861,
+ "baseTime": 1803,
+ "flags": [
+ "noThroughRoad",
+ "motorway",
+ "builtUpArea",
+ "park",
+ "privateRoad"
+ ],
+ "text": "The trip takes 23.9 km and 31 mins.",
+ "travelTime": 1861,
+ "_type": "RouteSummaryType"
+ }
+ }
+ ],
+ "language": "en-us"
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/here_travel_time/car_shortest_response.json b/tests/fixtures/here_travel_time/car_shortest_response.json
new file mode 100644
index 00000000000000..765c438c1cd14e
--- /dev/null
+++ b/tests/fixtures/here_travel_time/car_shortest_response.json
@@ -0,0 +1,231 @@
+{
+ "response": {
+ "metaInfo": {
+ "timestamp": "2019-07-21T21:05:28Z",
+ "mapVersion": "8.30.98.154",
+ "moduleVersion": "7.2.201928-4478",
+ "interfaceVersion": "2.6.64",
+ "availableMapVersion": [
+ "8.30.98.154"
+ ]
+ },
+ "route": [
+ {
+ "waypoint": [
+ {
+ "linkId": "-1128310200",
+ "mappedPosition": {
+ "latitude": 38.9026523,
+ "longitude": -77.048338
+ },
+ "originalPosition": {
+ "latitude": 38.9029809,
+ "longitude": -77.048338
+ },
+ "type": "stopOver",
+ "spot": 0.3538462,
+ "sideOfStreet": "right",
+ "mappedRoadName": "K St NW",
+ "label": "K St NW",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ {
+ "linkId": "-18459081",
+ "mappedPosition": {
+ "latitude": 39.0422511,
+ "longitude": -77.1193526
+ },
+ "originalPosition": {
+ "latitude": 39.042158,
+ "longitude": -77.119116
+ },
+ "type": "stopOver",
+ "spot": 0.7253521,
+ "sideOfStreet": "left",
+ "mappedRoadName": "Commonwealth Dr",
+ "label": "Commonwealth Dr",
+ "shapeIndex": 162,
+ "source": "user"
+ }
+ ],
+ "mode": {
+ "type": "shortest",
+ "transportModes": [
+ "car"
+ ],
+ "trafficMode": "enabled",
+ "feature": []
+ },
+ "leg": [
+ {
+ "start": {
+ "linkId": "-1128310200",
+ "mappedPosition": {
+ "latitude": 38.9026523,
+ "longitude": -77.048338
+ },
+ "originalPosition": {
+ "latitude": 38.9029809,
+ "longitude": -77.048338
+ },
+ "type": "stopOver",
+ "spot": 0.3538462,
+ "sideOfStreet": "right",
+ "mappedRoadName": "K St NW",
+ "label": "K St NW",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ "end": {
+ "linkId": "-18459081",
+ "mappedPosition": {
+ "latitude": 39.0422511,
+ "longitude": -77.1193526
+ },
+ "originalPosition": {
+ "latitude": 39.042158,
+ "longitude": -77.119116
+ },
+ "type": "stopOver",
+ "spot": 0.7253521,
+ "sideOfStreet": "left",
+ "mappedRoadName": "Commonwealth Dr",
+ "label": "Commonwealth Dr",
+ "shapeIndex": 162,
+ "source": "user"
+ },
+ "length": 18388,
+ "travelTime": 2493,
+ "maneuver": [
+ {
+ "position": {
+ "latitude": 38.9026523,
+ "longitude": -77.048338
+ },
+ "instruction": "Head west on K St NW. Go for 79 m.",
+ "travelTime": 22,
+ "length": 79,
+ "id": "M1",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9026523,
+ "longitude": -77.048825
+ },
+ "instruction": "Turn right onto 22nd St NW. Go for 141 m.",
+ "travelTime": 79,
+ "length": 141,
+ "id": "M2",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9039075,
+ "longitude": -77.048825
+ },
+ "instruction": "Keep left onto 22nd St NW. Go for 841 m.",
+ "travelTime": 256,
+ "length": 841,
+ "id": "M3",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9114928,
+ "longitude": -77.0487821
+ },
+ "instruction": "Turn left onto Massachusetts Ave NW. Go for 145 m.",
+ "travelTime": 22,
+ "length": 145,
+ "id": "M4",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9120293,
+ "longitude": -77.0502949
+ },
+ "instruction": "Take the 1st exit from Massachusetts Ave NW roundabout onto Massachusetts Ave NW. Go for 2.8 km.",
+ "travelTime": 301,
+ "length": 2773,
+ "id": "M5",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9286053,
+ "longitude": -77.073158
+ },
+ "instruction": "Turn right onto Wisconsin Ave NW. Go for 3.8 km.",
+ "travelTime": 610,
+ "length": 3801,
+ "id": "M6",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 38.9607918,
+ "longitude": -77.0857322
+ },
+ "instruction": "Continue on Wisconsin Ave (MD-355). Go for 9.7 km.",
+ "travelTime": 1013,
+ "length": 9686,
+ "id": "M7",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 39.0447664,
+ "longitude": -77.1116638
+ },
+ "instruction": "Turn left onto Nicholson Ln. Go for 559 m.",
+ "travelTime": 111,
+ "length": 559,
+ "id": "M8",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 39.0448952,
+ "longitude": -77.1179724
+ },
+ "instruction": "Turn left onto Commonwealth Dr. Go for 341 m.",
+ "travelTime": 75,
+ "length": 341,
+ "id": "M9",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 39.0422511,
+ "longitude": -77.1193526
+ },
+ "instruction": "Arrive at Commonwealth Dr. Your destination is on the left.",
+ "travelTime": 4,
+ "length": 22,
+ "id": "M10",
+ "_type": "PrivateTransportManeuverType"
+ }
+ ]
+ }
+ ],
+ "summary": {
+ "distance": 18388,
+ "trafficTime": 2427,
+ "baseTime": 2150,
+ "flags": [
+ "noThroughRoad",
+ "builtUpArea",
+ "park"
+ ],
+ "text": "The trip takes 18.4 km and 40 mins.",
+ "travelTime": 2427,
+ "_type": "RouteSummaryType"
+ }
+ }
+ ],
+ "language": "en-us"
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/here_travel_time/pedestrian_response.json b/tests/fixtures/here_travel_time/pedestrian_response.json
new file mode 100644
index 00000000000000..07881e8bd3d06b
--- /dev/null
+++ b/tests/fixtures/here_travel_time/pedestrian_response.json
@@ -0,0 +1,308 @@
+{
+ "response": {
+ "metaInfo": {
+ "timestamp": "2019-07-21T18:40:10Z",
+ "mapVersion": "8.30.98.154",
+ "moduleVersion": "7.2.201928-4478",
+ "interfaceVersion": "2.6.64",
+ "availableMapVersion": [
+ "8.30.98.154"
+ ]
+ },
+ "route": [
+ {
+ "waypoint": [
+ {
+ "linkId": "-1230414527",
+ "mappedPosition": {
+ "latitude": 41.9797859,
+ "longitude": -87.8790879
+ },
+ "originalPosition": {
+ "latitude": 41.9798,
+ "longitude": -87.8801
+ },
+ "type": "stopOver",
+ "spot": 0.5079365,
+ "sideOfStreet": "right",
+ "mappedRoadName": "Mannheim Rd",
+ "label": "Mannheim Rd - US-12",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ {
+ "linkId": "+924115108",
+ "mappedPosition": {
+ "latitude": 41.90413,
+ "longitude": -87.9223502
+ },
+ "originalPosition": {
+ "latitude": 41.9043,
+ "longitude": -87.9216001
+ },
+ "type": "stopOver",
+ "spot": 0.1925926,
+ "sideOfStreet": "right",
+ "mappedRoadName": "",
+ "label": "",
+ "shapeIndex": 122,
+ "source": "user"
+ }
+ ],
+ "mode": {
+ "type": "fastest",
+ "transportModes": [
+ "pedestrian"
+ ],
+ "trafficMode": "disabled",
+ "feature": []
+ },
+ "leg": [
+ {
+ "start": {
+ "linkId": "-1230414527",
+ "mappedPosition": {
+ "latitude": 41.9797859,
+ "longitude": -87.8790879
+ },
+ "originalPosition": {
+ "latitude": 41.9798,
+ "longitude": -87.8801
+ },
+ "type": "stopOver",
+ "spot": 0.5079365,
+ "sideOfStreet": "right",
+ "mappedRoadName": "Mannheim Rd",
+ "label": "Mannheim Rd - US-12",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ "end": {
+ "linkId": "+924115108",
+ "mappedPosition": {
+ "latitude": 41.90413,
+ "longitude": -87.9223502
+ },
+ "originalPosition": {
+ "latitude": 41.9043,
+ "longitude": -87.9216001
+ },
+ "type": "stopOver",
+ "spot": 0.1925926,
+ "sideOfStreet": "right",
+ "mappedRoadName": "",
+ "label": "",
+ "shapeIndex": 122,
+ "source": "user"
+ },
+ "length": 12533,
+ "travelTime": 12631,
+ "maneuver": [
+ {
+ "position": {
+ "latitude": 41.9797859,
+ "longitude": -87.8790879
+ },
+ "instruction": "Head south on Mannheim Rd. Go for 848 m.",
+ "travelTime": 848,
+ "length": 848,
+ "id": "M1",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9722581,
+ "longitude": -87.8776109
+ },
+ "instruction": "Take the street on the left, Mannheim Rd. Go for 4.2 km.",
+ "travelTime": 4239,
+ "length": 4227,
+ "id": "M2",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9364238,
+ "longitude": -87.8849387
+ },
+ "instruction": "Turn right onto W Belmont Ave. Go for 595 m.",
+ "travelTime": 605,
+ "length": 595,
+ "id": "M3",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9362521,
+ "longitude": -87.8921163
+ },
+ "instruction": "Turn left onto Cullerton St. Go for 406 m.",
+ "travelTime": 411,
+ "length": 406,
+ "id": "M4",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9326043,
+ "longitude": -87.8919983
+ },
+ "instruction": "Turn right onto Cullerton St. Go for 1.2 km.",
+ "travelTime": 1249,
+ "length": 1239,
+ "id": "M5",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9217896,
+ "longitude": -87.8928781
+ },
+ "instruction": "Turn right onto E Fullerton Ave. Go for 786 m.",
+ "travelTime": 796,
+ "length": 786,
+ "id": "M6",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9216394,
+ "longitude": -87.9023838
+ },
+ "instruction": "Turn left onto La Porte Ave. Go for 424 m.",
+ "travelTime": 430,
+ "length": 424,
+ "id": "M7",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9180024,
+ "longitude": -87.9028559
+ },
+ "instruction": "Turn right onto E Palmer Ave. Go for 864 m.",
+ "travelTime": 875,
+ "length": 864,
+ "id": "M8",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9175196,
+ "longitude": -87.9132199
+ },
+ "instruction": "Turn left onto N Railroad Ave. Go for 1.2 km.",
+ "travelTime": 1180,
+ "length": 1170,
+ "id": "M9",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9070268,
+ "longitude": -87.9130161
+ },
+ "instruction": "Turn right onto W North Ave. Go for 638 m.",
+ "travelTime": 638,
+ "length": 638,
+ "id": "M10",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9068551,
+ "longitude": -87.9207087
+ },
+ "instruction": "Take the street on the left, E North Ave. Go for 354 m.",
+ "travelTime": 354,
+ "length": 354,
+ "id": "M11",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9065869,
+ "longitude": -87.9249573
+ },
+ "instruction": "Take the street on the left, E North Ave. Go for 228 m.",
+ "travelTime": 242,
+ "length": 228,
+ "id": "M12",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9065225,
+ "longitude": -87.9277039
+ },
+ "instruction": "Turn left. Go for 409 m.",
+ "travelTime": 419,
+ "length": 409,
+ "id": "M13",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9040334,
+ "longitude": -87.9260409
+ },
+ "instruction": "Turn left onto E Third St. Go for 206 m.",
+ "travelTime": 206,
+ "length": 206,
+ "id": "M14",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9038832,
+ "longitude": -87.9236054
+ },
+ "instruction": "Turn left. Go for 113 m.",
+ "travelTime": 113,
+ "length": 113,
+ "id": "M15",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9039047,
+ "longitude": -87.9222536
+ },
+ "instruction": "Turn left. Go for 26 m.",
+ "travelTime": 26,
+ "length": 26,
+ "id": "M16",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.90413,
+ "longitude": -87.9223502
+ },
+ "instruction": "Arrive at your destination on the right.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M17",
+ "_type": "PrivateTransportManeuverType"
+ }
+ ]
+ }
+ ],
+ "summary": {
+ "distance": 12533,
+ "baseTime": 12631,
+ "flags": [
+ "noThroughRoad",
+ "builtUpArea",
+ "park",
+ "privateRoad"
+ ],
+ "text": "The trip takes 12.5 km and 3:31 h.",
+ "travelTime": 12631,
+ "_type": "RouteSummaryType"
+ }
+ }
+ ],
+ "language": "en-us"
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/here_travel_time/public_response.json b/tests/fixtures/here_travel_time/public_response.json
new file mode 100644
index 00000000000000..149b4d06c3975e
--- /dev/null
+++ b/tests/fixtures/here_travel_time/public_response.json
@@ -0,0 +1,294 @@
+{
+ "response": {
+ "metaInfo": {
+ "timestamp": "2019-07-21T18:40:37Z",
+ "mapVersion": "8.30.98.154",
+ "moduleVersion": "7.2.201928-4478",
+ "interfaceVersion": "2.6.64",
+ "availableMapVersion": [
+ "8.30.98.154"
+ ]
+ },
+ "route": [
+ {
+ "waypoint": [
+ {
+ "linkId": "-1230414527",
+ "mappedPosition": {
+ "latitude": 41.9797859,
+ "longitude": -87.8790879
+ },
+ "originalPosition": {
+ "latitude": 41.9798,
+ "longitude": -87.8801
+ },
+ "type": "stopOver",
+ "spot": 0.5079365,
+ "sideOfStreet": "right",
+ "mappedRoadName": "Mannheim Rd",
+ "label": "Mannheim Rd - US-12",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ {
+ "linkId": "+924115108",
+ "mappedPosition": {
+ "latitude": 41.90413,
+ "longitude": -87.9223502
+ },
+ "originalPosition": {
+ "latitude": 41.9043,
+ "longitude": -87.9216001
+ },
+ "type": "stopOver",
+ "spot": 0.1925926,
+ "sideOfStreet": "right",
+ "mappedRoadName": "",
+ "label": "",
+ "shapeIndex": 191,
+ "source": "user"
+ }
+ ],
+ "mode": {
+ "type": "fastest",
+ "transportModes": [
+ "publicTransport"
+ ],
+ "trafficMode": "disabled",
+ "feature": []
+ },
+ "leg": [
+ {
+ "start": {
+ "linkId": "-1230414527",
+ "mappedPosition": {
+ "latitude": 41.9797859,
+ "longitude": -87.8790879
+ },
+ "originalPosition": {
+ "latitude": 41.9798,
+ "longitude": -87.8801
+ },
+ "type": "stopOver",
+ "spot": 0.5079365,
+ "sideOfStreet": "right",
+ "mappedRoadName": "Mannheim Rd",
+ "label": "Mannheim Rd - US-12",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ "end": {
+ "linkId": "+924115108",
+ "mappedPosition": {
+ "latitude": 41.90413,
+ "longitude": -87.9223502
+ },
+ "originalPosition": {
+ "latitude": 41.9043,
+ "longitude": -87.9216001
+ },
+ "type": "stopOver",
+ "spot": 0.1925926,
+ "sideOfStreet": "right",
+ "mappedRoadName": "",
+ "label": "",
+ "shapeIndex": 191,
+ "source": "user"
+ },
+ "length": 22325,
+ "travelTime": 5350,
+ "maneuver": [
+ {
+ "position": {
+ "latitude": 41.9797859,
+ "longitude": -87.8790879
+ },
+ "instruction": "Head south on Mannheim Rd. Go for 848 m.",
+ "travelTime": 848,
+ "length": 848,
+ "id": "M1",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9722581,
+ "longitude": -87.8776109
+ },
+ "instruction": "Take the street on the left, Mannheim Rd. Go for 825 m.",
+ "travelTime": 825,
+ "length": 825,
+ "id": "M2",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9650483,
+ "longitude": -87.8769565
+ },
+ "instruction": "Go to the stop Mannheim/Lawrence and take the bus 332 toward Palmer/Schiller. Follow for 7 stops.",
+ "travelTime": 475,
+ "length": 4360,
+ "id": "M3",
+ "stopName": "Mannheim/Lawrence",
+ "_type": "PublicTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9541478,
+ "longitude": -87.9133594
+ },
+ "instruction": "Get off at Irving Park/Taft.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M4",
+ "stopName": "Irving Park/Taft",
+ "_type": "PublicTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9541478,
+ "longitude": -87.9133594
+ },
+ "instruction": "Take the bus 332 toward Cargo Rd./Delta Cargo. Follow for 1 stop.",
+ "travelTime": 155,
+ "length": 3505,
+ "id": "M5",
+ "stopName": "Irving Park/Taft",
+ "_type": "PublicTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9599199,
+ "longitude": -87.9162776
+ },
+ "instruction": "Get off at Cargo Rd./S. Access Rd./Lufthansa.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M6",
+ "stopName": "Cargo Rd./S. Access Rd./Lufthansa",
+ "_type": "PublicTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9599199,
+ "longitude": -87.9162776
+ },
+ "instruction": "Take the bus 332 toward Palmer/Schiller. Follow for 41 stops.",
+ "travelTime": 1510,
+ "length": 11261,
+ "id": "M7",
+ "stopName": "Cargo Rd./S. Access Rd./Lufthansa",
+ "_type": "PublicTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9041729,
+ "longitude": -87.9399669
+ },
+ "instruction": "Get off at York/Third.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M8",
+ "stopName": "York/Third",
+ "nextRoadName": "N York St",
+ "_type": "PublicTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9041729,
+ "longitude": -87.9399669
+ },
+ "instruction": "Head east on N York St. Go for 33 m.",
+ "travelTime": 43,
+ "length": 33,
+ "id": "M9",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9039476,
+ "longitude": -87.9398811
+ },
+ "instruction": "Turn left onto E Third St. Go for 1.4 km.",
+ "travelTime": 1355,
+ "length": 1354,
+ "id": "M10",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9038832,
+ "longitude": -87.9236054
+ },
+ "instruction": "Turn left. Go for 113 m.",
+ "travelTime": 113,
+ "length": 113,
+ "id": "M11",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9039047,
+ "longitude": -87.9222536
+ },
+ "instruction": "Turn left. Go for 26 m.",
+ "travelTime": 26,
+ "length": 26,
+ "id": "M12",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.90413,
+ "longitude": -87.9223502
+ },
+ "instruction": "Arrive at your destination on the right.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M13",
+ "_type": "PrivateTransportManeuverType"
+ }
+ ]
+ }
+ ],
+ "publicTransportLine": [
+ {
+ "lineName": "332",
+ "companyName": "",
+ "destination": "Palmer/Schiller",
+ "type": "busPublic",
+ "id": "L1"
+ },
+ {
+ "lineName": "332",
+ "companyName": "",
+ "destination": "Cargo Rd./Delta Cargo",
+ "type": "busPublic",
+ "id": "L2"
+ },
+ {
+ "lineName": "332",
+ "companyName": "",
+ "destination": "Palmer/Schiller",
+ "type": "busPublic",
+ "id": "L3"
+ }
+ ],
+ "summary": {
+ "distance": 22325,
+ "baseTime": 5350,
+ "flags": [
+ "noThroughRoad",
+ "builtUpArea",
+ "park"
+ ],
+ "text": "The trip takes 22.3 km and 1:29 h.",
+ "travelTime": 5350,
+ "departure": "1970-01-01T00:00:00Z",
+ "_type": "PublicTransportRouteSummaryType"
+ }
+ }
+ ],
+ "language": "en-us"
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/here_travel_time/public_time_table_response.json b/tests/fixtures/here_travel_time/public_time_table_response.json
new file mode 100644
index 00000000000000..52df0d4eb35ad5
--- /dev/null
+++ b/tests/fixtures/here_travel_time/public_time_table_response.json
@@ -0,0 +1,308 @@
+{
+ "response": {
+ "metaInfo": {
+ "timestamp": "2019-08-06T06:43:24Z",
+ "mapVersion": "8.30.99.152",
+ "moduleVersion": "7.2.201931-4739",
+ "interfaceVersion": "2.6.66",
+ "availableMapVersion": [
+ "8.30.99.152"
+ ]
+ },
+ "route": [
+ {
+ "waypoint": [
+ {
+ "linkId": "-1230414527",
+ "mappedPosition": {
+ "latitude": 41.9797859,
+ "longitude": -87.8790879
+ },
+ "originalPosition": {
+ "latitude": 41.9798,
+ "longitude": -87.8801
+ },
+ "type": "stopOver",
+ "spot": 0.5079365,
+ "sideOfStreet": "right",
+ "mappedRoadName": "Mannheim Rd",
+ "label": "Mannheim Rd - US-12",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ {
+ "linkId": "+924115108",
+ "mappedPosition": {
+ "latitude": 41.90413,
+ "longitude": -87.9223502
+ },
+ "originalPosition": {
+ "latitude": 41.9043,
+ "longitude": -87.9216001
+ },
+ "type": "stopOver",
+ "spot": 0.1925926,
+ "sideOfStreet": "right",
+ "mappedRoadName": "",
+ "label": "",
+ "shapeIndex": 111,
+ "source": "user"
+ }
+ ],
+ "mode": {
+ "type": "fastest",
+ "transportModes": [
+ "publicTransportTimeTable"
+ ],
+ "trafficMode": "disabled",
+ "feature": []
+ },
+ "leg": [
+ {
+ "start": {
+ "linkId": "-1230414527",
+ "mappedPosition": {
+ "latitude": 41.9797859,
+ "longitude": -87.8790879
+ },
+ "originalPosition": {
+ "latitude": 41.9798,
+ "longitude": -87.8801
+ },
+ "type": "stopOver",
+ "spot": 0.5079365,
+ "sideOfStreet": "right",
+ "mappedRoadName": "Mannheim Rd",
+ "label": "Mannheim Rd - US-12",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ "end": {
+ "linkId": "+924115108",
+ "mappedPosition": {
+ "latitude": 41.90413,
+ "longitude": -87.9223502
+ },
+ "originalPosition": {
+ "latitude": 41.9043,
+ "longitude": -87.9216001
+ },
+ "type": "stopOver",
+ "spot": 0.1925926,
+ "sideOfStreet": "right",
+ "mappedRoadName": "",
+ "label": "",
+ "shapeIndex": 111,
+ "source": "user"
+ },
+ "length": 14775,
+ "travelTime": 4784,
+ "maneuver": [
+ {
+ "position": {
+ "latitude": 41.9797859,
+ "longitude": -87.8790879
+ },
+ "instruction": "Head south on Mannheim Rd. Go for 848 m.",
+ "travelTime": 848,
+ "length": 848,
+ "id": "M1",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9722581,
+ "longitude": -87.8776109
+ },
+ "instruction": "Take the street on the left, Mannheim Rd. Go for 812 m.",
+ "travelTime": 812,
+ "length": 812,
+ "id": "M2",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.965051,
+ "longitude": -87.8769591
+ },
+ "instruction": "Go to the Bus stop Mannheim/Lawrence and take the bus 330 toward Archer/Harlem (Terminal). Follow for 33 stops.",
+ "travelTime": 900,
+ "length": 7815,
+ "id": "M3",
+ "stopName": "Mannheim/Lawrence",
+ "_type": "PublicTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.896836,
+ "longitude": -87.883771
+ },
+ "instruction": "Get off at Mannheim/Lake.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M4",
+ "stopName": "Mannheim/Lake",
+ "_type": "PublicTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.896836,
+ "longitude": -87.883771
+ },
+ "instruction": "Walk to Bus Lake/Mannheim.",
+ "travelTime": 300,
+ "length": 72,
+ "id": "M5",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.897263,
+ "longitude": -87.8842648
+ },
+ "instruction": "Take the bus 309 toward Elmhurst Metra Station. Follow for 18 stops.",
+ "travelTime": 1020,
+ "length": 4362,
+ "id": "M6",
+ "stopName": "Lake/Mannheim",
+ "_type": "PublicTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9066347,
+ "longitude": -87.928671
+ },
+ "instruction": "Get off at North/Berteau.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M7",
+ "stopName": "North/Berteau",
+ "nextRoadName": "E Berteau Ave",
+ "_type": "PublicTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9066347,
+ "longitude": -87.928671
+ },
+ "instruction": "Head north on E Berteau Ave. Go for 23 m.",
+ "travelTime": 40,
+ "length": 23,
+ "id": "M8",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9067693,
+ "longitude": -87.9284549
+ },
+ "instruction": "Turn right onto E Berteau Ave. Go for 40 m.",
+ "travelTime": 44,
+ "length": 40,
+ "id": "M9",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9065011,
+ "longitude": -87.9282939
+ },
+ "instruction": "Turn left onto E North Ave. Go for 49 m.",
+ "travelTime": 56,
+ "length": 49,
+ "id": "M10",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9065225,
+ "longitude": -87.9277039
+ },
+ "instruction": "Turn slightly right. Go for 409 m.",
+ "travelTime": 419,
+ "length": 409,
+ "id": "M11",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9040334,
+ "longitude": -87.9260409
+ },
+ "instruction": "Turn left onto E Third St. Go for 206 m.",
+ "travelTime": 206,
+ "length": 206,
+ "id": "M12",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9038832,
+ "longitude": -87.9236054
+ },
+ "instruction": "Turn left. Go for 113 m.",
+ "travelTime": 113,
+ "length": 113,
+ "id": "M13",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9039047,
+ "longitude": -87.9222536
+ },
+ "instruction": "Turn left. Go for 26 m.",
+ "travelTime": 26,
+ "length": 26,
+ "id": "M14",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.90413,
+ "longitude": -87.9223502
+ },
+ "instruction": "Arrive at your destination on the right.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M15",
+ "_type": "PrivateTransportManeuverType"
+ }
+ ]
+ }
+ ],
+ "publicTransportLine": [
+ {
+ "lineName": "330",
+ "companyName": "PACE",
+ "destination": "Archer/Harlem (Terminal)",
+ "type": "busPublic",
+ "id": "L1"
+ },
+ {
+ "lineName": "309",
+ "companyName": "PACE",
+ "destination": "Elmhurst Metra Station",
+ "type": "busPublic",
+ "id": "L2"
+ }
+ ],
+ "summary": {
+ "distance": 14775,
+ "baseTime": 4784,
+ "flags": [
+ "noThroughRoad",
+ "builtUpArea",
+ "park"
+ ],
+ "text": "The trip takes 14.8 km and 1:20 h.",
+ "travelTime": 4784,
+ "departure": "2019-08-06T05:09:20-05:00",
+ "timetableExpiration": "2019-08-04T00:00:00Z",
+ "_type": "PublicTransportRouteSummaryType"
+ }
+ }
+ ],
+ "language": "en-us"
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json b/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json
new file mode 100644
index 00000000000000..81fb246178c938
--- /dev/null
+++ b/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json
@@ -0,0 +1,15 @@
+{
+ "_type": "ns2:RoutingServiceErrorType",
+ "type": "PermissionError",
+ "subtype": "InvalidCredentials",
+ "details": "This is not a valid app_id and app_code pair. Please verify that the values are not swapped between the app_id and app_code and the values provisioned by HERE (either by your customer representative or via http://developer.here.com/myapps) were copied correctly into the request.",
+ "metaInfo": {
+ "timestamp": "2019-07-10T09:43:14Z",
+ "mapVersion": "8.30.98.152",
+ "moduleVersion": "7.2.201927-4307",
+ "interfaceVersion": "2.6.64",
+ "availableMapVersion": [
+ "8.30.98.152"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/here_travel_time/routing_error_no_route_found.json b/tests/fixtures/here_travel_time/routing_error_no_route_found.json
new file mode 100644
index 00000000000000..a776fa91c43b2a
--- /dev/null
+++ b/tests/fixtures/here_travel_time/routing_error_no_route_found.json
@@ -0,0 +1,21 @@
+{
+ "_type": "ns2:RoutingServiceErrorType",
+ "type": "ApplicationError",
+ "subtype": "NoRouteFound",
+ "details": "Error is NGEO_ERROR_ROUTE_NO_END_POINT",
+ "additionalData": [
+ {
+ "key": "error_code",
+ "value": "NGEO_ERROR_ROUTE_NO_END_POINT"
+ }
+ ],
+ "metaInfo": {
+ "timestamp": "2019-07-10T09:51:04Z",
+ "mapVersion": "8.30.98.152",
+ "moduleVersion": "7.2.201927-4307",
+ "interfaceVersion": "2.6.64",
+ "availableMapVersion": [
+ "8.30.98.152"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/here_travel_time/truck_response.json b/tests/fixtures/here_travel_time/truck_response.json
new file mode 100644
index 00000000000000..a302d564902e81
--- /dev/null
+++ b/tests/fixtures/here_travel_time/truck_response.json
@@ -0,0 +1,187 @@
+{
+ "response": {
+ "metaInfo": {
+ "timestamp": "2019-07-21T14:25:00Z",
+ "mapVersion": "8.30.98.154",
+ "moduleVersion": "7.2.201928-4478",
+ "interfaceVersion": "2.6.64",
+ "availableMapVersion": [
+ "8.30.98.154"
+ ]
+ },
+ "route": [
+ {
+ "waypoint": [
+ {
+ "linkId": "+930461269",
+ "mappedPosition": {
+ "latitude": 41.9800687,
+ "longitude": -87.8805614
+ },
+ "originalPosition": {
+ "latitude": 41.9798,
+ "longitude": -87.8801
+ },
+ "type": "stopOver",
+ "spot": 0.5555556,
+ "sideOfStreet": "right",
+ "mappedRoadName": "",
+ "label": "",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ {
+ "linkId": "-1035319462",
+ "mappedPosition": {
+ "latitude": 41.9042909,
+ "longitude": -87.9216528
+ },
+ "originalPosition": {
+ "latitude": 41.9043,
+ "longitude": -87.9216001
+ },
+ "type": "stopOver",
+ "spot": 0,
+ "sideOfStreet": "left",
+ "mappedRoadName": "Eisenhower Expy E",
+ "label": "Eisenhower Expy E - I-290",
+ "shapeIndex": 135,
+ "source": "user"
+ }
+ ],
+ "mode": {
+ "type": "fastest",
+ "transportModes": [
+ "truck"
+ ],
+ "trafficMode": "disabled",
+ "feature": []
+ },
+ "leg": [
+ {
+ "start": {
+ "linkId": "+930461269",
+ "mappedPosition": {
+ "latitude": 41.9800687,
+ "longitude": -87.8805614
+ },
+ "originalPosition": {
+ "latitude": 41.9798,
+ "longitude": -87.8801
+ },
+ "type": "stopOver",
+ "spot": 0.5555556,
+ "sideOfStreet": "right",
+ "mappedRoadName": "",
+ "label": "",
+ "shapeIndex": 0,
+ "source": "user"
+ },
+ "end": {
+ "linkId": "-1035319462",
+ "mappedPosition": {
+ "latitude": 41.9042909,
+ "longitude": -87.9216528
+ },
+ "originalPosition": {
+ "latitude": 41.9043,
+ "longitude": -87.9216001
+ },
+ "type": "stopOver",
+ "spot": 0,
+ "sideOfStreet": "left",
+ "mappedRoadName": "Eisenhower Expy E",
+ "label": "Eisenhower Expy E - I-290",
+ "shapeIndex": 135,
+ "source": "user"
+ },
+ "length": 13049,
+ "travelTime": 812,
+ "maneuver": [
+ {
+ "position": {
+ "latitude": 41.9800687,
+ "longitude": -87.8805614
+ },
+ "instruction": "Take ramp onto I-190. Go for 631 m.",
+ "travelTime": 53,
+ "length": 631,
+ "id": "M1",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.98259,
+ "longitude": -87.8744352
+ },
+ "instruction": "Take exit 1D toward Indiana onto I-294 S (Tri-State Tollway). Go for 10.9 km.",
+ "travelTime": 573,
+ "length": 10872,
+ "id": "M2",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9059324,
+ "longitude": -87.9199362
+ },
+ "instruction": "Take exit 33 toward Rockford/US-20/IL-64 onto I-290 W (Eisenhower Expy W). Go for 475 m.",
+ "travelTime": 54,
+ "length": 475,
+ "id": "M3",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9067156,
+ "longitude": -87.9237771
+ },
+ "instruction": "Take exit 13B toward North Ave onto IL-64 W (E North Ave). Go for 435 m.",
+ "travelTime": 51,
+ "length": 435,
+ "id": "M4",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9065869,
+ "longitude": -87.9249573
+ },
+ "instruction": "Take ramp onto I-290 E (Eisenhower Expy E) toward Chicago/I-294 S. Go for 636 m.",
+ "travelTime": 81,
+ "length": 636,
+ "id": "M5",
+ "_type": "PrivateTransportManeuverType"
+ },
+ {
+ "position": {
+ "latitude": 41.9042909,
+ "longitude": -87.9216528
+ },
+ "instruction": "Arrive at Eisenhower Expy E (I-290). Your destination is on the left.",
+ "travelTime": 0,
+ "length": 0,
+ "id": "M6",
+ "_type": "PrivateTransportManeuverType"
+ }
+ ]
+ }
+ ],
+ "summary": {
+ "distance": 13049,
+ "trafficTime": 812,
+ "baseTime": 812,
+ "flags": [
+ "tollroad",
+ "motorway",
+ "builtUpArea"
+ ],
+ "text": "The trip takes 13.0 km and 14 mins.",
+ "travelTime": 812,
+ "_type": "RouteSummaryType"
+ }
+ }
+ ],
+ "language": "en-us"
+ }
+}
\ No newline at end of file
diff --git a/tests/fixtures/yandex_transport_reply.json b/tests/fixtures/yandex_transport_reply.json
new file mode 100644
index 00000000000000..c5e4857297aa93
--- /dev/null
+++ b/tests/fixtures/yandex_transport_reply.json
@@ -0,0 +1,2106 @@
+{
+ "data": {
+ "geometries": [
+ {
+ "type": "Point",
+ "coordinates": [
+ 37.565280044,
+ 55.851959656
+ ]
+ }
+ ],
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 37.565280044,
+ 55.851959656
+ ]
+ },
+ "properties": {
+ "name": "7-й автобусный парк",
+ "description": "7-й автобусный парк",
+ "currentTime": "Mon Sep 16 2019 21:40:40 GMT+0300 (Moscow Standard Time)",
+ "StopMetaData": {
+ "id": "stop__9639579",
+ "name": "7-й автобусный парк",
+ "type": "urban",
+ "region": {
+ "id": 213,
+ "type": 6,
+ "parent_id": 1,
+ "capital_id": 0,
+ "geo_parent_id": 0,
+ "city_id": 213,
+ "name": "moscow",
+ "native_name": "",
+ "iso_name": "RU MOW",
+ "is_main": true,
+ "en_name": "Moscow",
+ "short_en_name": "MSK",
+ "phone_code": "495 499",
+ "phone_code_old": "095",
+ "zip_code": "",
+ "population": 12506468,
+ "synonyms": "Moskau, Moskva",
+ "latitude": 55.753215,
+ "longitude": 37.622504,
+ "latitude_size": 0.878654,
+ "longitude_size": 1.164423,
+ "zoom": 10,
+ "tzname": "Europe/Moscow",
+ "official_languages": "ru",
+ "widespread_languages": "ru",
+ "suggest_list": [],
+ "is_eu": false,
+ "services_names": [
+ "bs",
+ "yaca",
+ "weather",
+ "afisha",
+ "maps",
+ "tv",
+ "ad",
+ "etrain",
+ "subway",
+ "delivery",
+ "route"
+ ],
+ "ename": "moscow",
+ "bounds": [
+ [
+ 37.0402925,
+ 55.31141404514547
+ ],
+ [
+ 38.2047155,
+ 56.190068045145466
+ ]
+ ],
+ "names": {
+ "ablative": "",
+ "accusative": "Москву",
+ "dative": "Москве",
+ "directional": "",
+ "genitive": "Москвы",
+ "instrumental": "Москвой",
+ "locative": "",
+ "nominative": "Москва",
+ "preposition": "в",
+ "prepositional": "Москве"
+ },
+ "parent": {
+ "id": 1,
+ "type": 5,
+ "parent_id": 3,
+ "capital_id": 213,
+ "geo_parent_id": 0,
+ "city_id": 213,
+ "name": "moscow-and-moscow-oblast",
+ "native_name": "",
+ "iso_name": "RU-MOS",
+ "is_main": true,
+ "en_name": "Moscow and Moscow Oblast",
+ "short_en_name": "RU-MOS",
+ "phone_code": "495 496 498 499",
+ "phone_code_old": "",
+ "zip_code": "",
+ "population": 7503385,
+ "synonyms": "Московская область, Подмосковье, Podmoskovye",
+ "latitude": 55.815792,
+ "longitude": 37.380031,
+ "latitude_size": 2.705659,
+ "longitude_size": 5.060749,
+ "zoom": 8,
+ "tzname": "Europe/Moscow",
+ "official_languages": "ru",
+ "widespread_languages": "ru",
+ "suggest_list": [
+ 213,
+ 10716,
+ 10747,
+ 10758,
+ 20728,
+ 10740,
+ 10738,
+ 20523,
+ 10735,
+ 10734,
+ 10743,
+ 21622
+ ],
+ "is_eu": false,
+ "services_names": [
+ "bs",
+ "yaca",
+ "ad"
+ ],
+ "ename": "moscow-and-moscow-oblast",
+ "bounds": [
+ [
+ 34.8496565,
+ 54.439456064325434
+ ],
+ [
+ 39.9104055,
+ 57.14511506432543
+ ]
+ ],
+ "names": {
+ "ablative": "",
+ "accusative": "Москву и Московскую область",
+ "dative": "Москве и Московской области",
+ "directional": "",
+ "genitive": "Москвы и Московской области",
+ "instrumental": "Москвой и Московской областью",
+ "locative": "",
+ "nominative": "Москва и Московская область",
+ "preposition": "в",
+ "prepositional": "Москве и Московской области"
+ },
+ "parent": {
+ "id": 225,
+ "type": 3,
+ "parent_id": 10001,
+ "capital_id": 213,
+ "geo_parent_id": 0,
+ "city_id": 213,
+ "name": "russia",
+ "native_name": "",
+ "iso_name": "RU",
+ "is_main": false,
+ "en_name": "Russia",
+ "short_en_name": "RU",
+ "phone_code": "7",
+ "phone_code_old": "",
+ "zip_code": "",
+ "population": 146880432,
+ "synonyms": "Russian Federation,Российская Федерация",
+ "latitude": 61.698653,
+ "longitude": 99.505405,
+ "latitude_size": 40.700127,
+ "longitude_size": 171.643239,
+ "zoom": 3,
+ "tzname": "",
+ "official_languages": "ru",
+ "widespread_languages": "ru",
+ "suggest_list": [
+ 213,
+ 2,
+ 65,
+ 54,
+ 47,
+ 43,
+ 66,
+ 51,
+ 56,
+ 172,
+ 39,
+ 62
+ ],
+ "is_eu": false,
+ "services_names": [
+ "bs",
+ "yaca",
+ "ad"
+ ],
+ "ename": "russia",
+ "bounds": [
+ [
+ 13.683785499999999,
+ 35.290400699917846
+ ],
+ [
+ -174.6729755,
+ 75.99052769991785
+ ]
+ ],
+ "names": {
+ "ablative": "",
+ "accusative": "Россию",
+ "dative": "России",
+ "directional": "",
+ "genitive": "России",
+ "instrumental": "Россией",
+ "locative": "",
+ "nominative": "Россия",
+ "preposition": "в",
+ "prepositional": "России"
+ }
+ }
+ }
+ },
+ "Transport": [
+ {
+ "lineId": "2036925416",
+ "name": "194",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "2036927196",
+ "EssentialStops": [
+ {
+ "id": "stop__9711780",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9648742",
+ "name": "Коровино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659860",
+ "tzOffset": 10800,
+ "text": "21:51"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568660760",
+ "tzOffset": 10800,
+ "text": "22:06"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568661840",
+ "tzOffset": 10800,
+ "text": "22:24"
+ }
+ }
+ ],
+ "departureTime": "21:51"
+ }
+ }
+ ],
+ "threadId": "2036927196",
+ "EssentialStops": [
+ {
+ "id": "stop__9711780",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9648742",
+ "name": "Коровино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659860",
+ "tzOffset": 10800,
+ "text": "21:51"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568660760",
+ "tzOffset": 10800,
+ "text": "22:06"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568661840",
+ "tzOffset": 10800,
+ "text": "22:24"
+ }
+ }
+ ],
+ "departureTime": "21:51"
+ }
+ },
+ {
+ "lineId": "213_114_bus_mosgortrans",
+ "name": "114",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213B_114_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9647199",
+ "name": "Метро Войковская"
+ },
+ {
+ "id": "stop__9639588",
+ "name": "Коровинское шоссе"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "15 мин",
+ "value": 900,
+ "begin": {
+ "value": "1568603405",
+ "tzOffset": 10800,
+ "text": "6:10"
+ },
+ "end": {
+ "value": "1568672165",
+ "tzOffset": 10800,
+ "text": "1:16"
+ }
+ }
+ }
+ }
+ ],
+ "threadId": "213B_114_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9647199",
+ "name": "Метро Войковская"
+ },
+ {
+ "id": "stop__9639588",
+ "name": "Коровинское шоссе"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "15 мин",
+ "value": 900,
+ "begin": {
+ "value": "1568603405",
+ "tzOffset": 10800,
+ "text": "6:10"
+ },
+ "end": {
+ "value": "1568672165",
+ "tzOffset": 10800,
+ "text": "1:16"
+ }
+ }
+ }
+ },
+ {
+ "lineId": "213_154_bus_mosgortrans",
+ "name": "154",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213B_154_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9642548",
+ "name": "ВДНХ (южная)"
+ },
+ {
+ "id": "stop__9711744",
+ "name": "Станция Ховрино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659260",
+ "tzOffset": 10800,
+ "text": "21:41"
+ },
+ "Estimated": {
+ "value": "1568659252",
+ "tzOffset": 10800,
+ "text": "21:40"
+ },
+ "vehicleId": "codd%5Fnew|1054764%5F191500"
+ },
+ {
+ "Scheduled": {
+ "value": "1568660580",
+ "tzOffset": 10800,
+ "text": "22:03"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568661900",
+ "tzOffset": 10800,
+ "text": "22:25"
+ }
+ }
+ ],
+ "departureTime": "21:41"
+ }
+ }
+ ],
+ "threadId": "213B_154_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9642548",
+ "name": "ВДНХ (южная)"
+ },
+ {
+ "id": "stop__9711744",
+ "name": "Станция Ховрино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659260",
+ "tzOffset": 10800,
+ "text": "21:41"
+ },
+ "Estimated": {
+ "value": "1568659252",
+ "tzOffset": 10800,
+ "text": "21:40"
+ },
+ "vehicleId": "codd%5Fnew|1054764%5F191500"
+ },
+ {
+ "Scheduled": {
+ "value": "1568660580",
+ "tzOffset": 10800,
+ "text": "22:03"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568661900",
+ "tzOffset": 10800,
+ "text": "22:25"
+ }
+ }
+ ],
+ "departureTime": "21:41"
+ }
+ },
+ {
+ "lineId": "213_179_bus_mosgortrans",
+ "name": "179",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213B_179_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9647199",
+ "name": "Метро Войковская"
+ },
+ {
+ "id": "stop__9639480",
+ "name": "Платформа Лианозово"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659920",
+ "tzOffset": 10800,
+ "text": "21:52"
+ },
+ "Estimated": {
+ "value": "1568659351",
+ "tzOffset": 10800,
+ "text": "21:42"
+ },
+ "vehicleId": "codd%5Fnew|59832%5F31359"
+ },
+ {
+ "Scheduled": {
+ "value": "1568660760",
+ "tzOffset": 10800,
+ "text": "22:06"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568661660",
+ "tzOffset": 10800,
+ "text": "22:21"
+ }
+ }
+ ],
+ "departureTime": "21:52"
+ }
+ }
+ ],
+ "threadId": "213B_179_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9647199",
+ "name": "Метро Войковская"
+ },
+ {
+ "id": "stop__9639480",
+ "name": "Платформа Лианозово"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659920",
+ "tzOffset": 10800,
+ "text": "21:52"
+ },
+ "Estimated": {
+ "value": "1568659351",
+ "tzOffset": 10800,
+ "text": "21:42"
+ },
+ "vehicleId": "codd%5Fnew|59832%5F31359"
+ },
+ {
+ "Scheduled": {
+ "value": "1568660760",
+ "tzOffset": 10800,
+ "text": "22:06"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568661660",
+ "tzOffset": 10800,
+ "text": "22:21"
+ }
+ }
+ ],
+ "departureTime": "21:52"
+ }
+ },
+ {
+ "lineId": "213_191m_minibus_default",
+ "name": "591",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_191m_minibus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9647199",
+ "name": "Метро Войковская"
+ },
+ {
+ "id": "stop__9711744",
+ "name": "Станция Ховрино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1568660525",
+ "tzOffset": 10800,
+ "text": "22:02"
+ },
+ "vehicleId": "codd%5Fnew|38278%5F9345312"
+ }
+ ],
+ "Frequency": {
+ "text": "22 мин",
+ "value": 1320,
+ "begin": {
+ "value": "1568602033",
+ "tzOffset": 10800,
+ "text": "5:47"
+ },
+ "end": {
+ "value": "1568672233",
+ "tzOffset": 10800,
+ "text": "1:17"
+ }
+ }
+ }
+ }
+ ],
+ "threadId": "213A_191m_minibus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9647199",
+ "name": "Метро Войковская"
+ },
+ {
+ "id": "stop__9711744",
+ "name": "Станция Ховрино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1568660525",
+ "tzOffset": 10800,
+ "text": "22:02"
+ },
+ "vehicleId": "codd%5Fnew|38278%5F9345312"
+ }
+ ],
+ "Frequency": {
+ "text": "22 мин",
+ "value": 1320,
+ "begin": {
+ "value": "1568602033",
+ "tzOffset": 10800,
+ "text": "5:47"
+ },
+ "end": {
+ "value": "1568672233",
+ "tzOffset": 10800,
+ "text": "1:17"
+ }
+ }
+ }
+ },
+ {
+ "lineId": "213_206m_minibus_default",
+ "name": "206к",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_206m_minibus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9640756",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9640553",
+ "name": "Лобненская улица"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "22 мин",
+ "value": 1320,
+ "begin": {
+ "value": "1568601239",
+ "tzOffset": 10800,
+ "text": "5:33"
+ },
+ "end": {
+ "value": "1568671439",
+ "tzOffset": 10800,
+ "text": "1:03"
+ }
+ }
+ }
+ }
+ ],
+ "threadId": "213A_206m_minibus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9640756",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9640553",
+ "name": "Лобненская улица"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "22 мин",
+ "value": 1320,
+ "begin": {
+ "value": "1568601239",
+ "tzOffset": 10800,
+ "text": "5:33"
+ },
+ "end": {
+ "value": "1568671439",
+ "tzOffset": 10800,
+ "text": "1:03"
+ }
+ }
+ }
+ },
+ {
+ "lineId": "213_215_bus_mosgortrans",
+ "name": "215",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213B_215_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9711780",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9711744",
+ "name": "Станция Ховрино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "27 мин",
+ "value": 1620,
+ "begin": {
+ "value": "1568601276",
+ "tzOffset": 10800,
+ "text": "5:34"
+ },
+ "end": {
+ "value": "1568671476",
+ "tzOffset": 10800,
+ "text": "1:04"
+ }
+ }
+ }
+ }
+ ],
+ "threadId": "213B_215_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9711780",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9711744",
+ "name": "Станция Ховрино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "27 мин",
+ "value": 1620,
+ "begin": {
+ "value": "1568601276",
+ "tzOffset": 10800,
+ "text": "5:34"
+ },
+ "end": {
+ "value": "1568671476",
+ "tzOffset": 10800,
+ "text": "1:04"
+ }
+ }
+ }
+ },
+ {
+ "lineId": "213_282_bus_mosgortrans",
+ "name": "282",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_282_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9641102",
+ "name": "Улица Корнейчука"
+ },
+ {
+ "id": "2532226085",
+ "name": "Метро Войковская"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1568659888",
+ "tzOffset": 10800,
+ "text": "21:51"
+ },
+ "vehicleId": "codd%5Fnew|34874%5F9345408"
+ }
+ ],
+ "Frequency": {
+ "text": "15 мин",
+ "value": 900,
+ "begin": {
+ "value": "1568602180",
+ "tzOffset": 10800,
+ "text": "5:49"
+ },
+ "end": {
+ "value": "1568673460",
+ "tzOffset": 10800,
+ "text": "1:37"
+ }
+ }
+ }
+ }
+ ],
+ "threadId": "213A_282_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9641102",
+ "name": "Улица Корнейчука"
+ },
+ {
+ "id": "2532226085",
+ "name": "Метро Войковская"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1568659888",
+ "tzOffset": 10800,
+ "text": "21:51"
+ },
+ "vehicleId": "codd%5Fnew|34874%5F9345408"
+ }
+ ],
+ "Frequency": {
+ "text": "15 мин",
+ "value": 900,
+ "begin": {
+ "value": "1568602180",
+ "tzOffset": 10800,
+ "text": "5:49"
+ },
+ "end": {
+ "value": "1568673460",
+ "tzOffset": 10800,
+ "text": "1:37"
+ }
+ }
+ }
+ },
+ {
+ "lineId": "213_294m_minibus_default",
+ "name": "994",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_294m_minibus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9640756",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9649459",
+ "name": "Метро Алтуфьево"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "30 мин",
+ "value": 1800,
+ "begin": {
+ "value": "1568601527",
+ "tzOffset": 10800,
+ "text": "5:38"
+ },
+ "end": {
+ "value": "1568671727",
+ "tzOffset": 10800,
+ "text": "1:08"
+ }
+ }
+ }
+ }
+ ],
+ "threadId": "213A_294m_minibus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9640756",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9649459",
+ "name": "Метро Алтуфьево"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "30 мин",
+ "value": 1800,
+ "begin": {
+ "value": "1568601527",
+ "tzOffset": 10800,
+ "text": "5:38"
+ },
+ "end": {
+ "value": "1568671727",
+ "tzOffset": 10800,
+ "text": "1:08"
+ }
+ }
+ }
+ },
+ {
+ "lineId": "213_36_trolleybus_mosgortrans",
+ "name": "т36",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_36_trolleybus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9642550",
+ "name": "ВДНХ (южная)"
+ },
+ {
+ "id": "stop__9640641",
+ "name": "Дмитровское шоссе, 155"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659680",
+ "tzOffset": 10800,
+ "text": "21:48"
+ },
+ "Estimated": {
+ "value": "1568659426",
+ "tzOffset": 10800,
+ "text": "21:43"
+ },
+ "vehicleId": "codd%5Fnew|1084829%5F430260"
+ },
+ {
+ "Scheduled": {
+ "value": "1568660520",
+ "tzOffset": 10800,
+ "text": "22:02"
+ },
+ "Estimated": {
+ "value": "1568659656",
+ "tzOffset": 10800,
+ "text": "21:47"
+ },
+ "vehicleId": "codd%5Fnew|1117016%5F430280"
+ },
+ {
+ "Scheduled": {
+ "value": "1568661900",
+ "tzOffset": 10800,
+ "text": "22:25"
+ },
+ "Estimated": {
+ "value": "1568660538",
+ "tzOffset": 10800,
+ "text": "22:02"
+ },
+ "vehicleId": "codd%5Fnew|1054576%5F430226"
+ }
+ ],
+ "departureTime": "21:48"
+ }
+ }
+ ],
+ "threadId": "213A_36_trolleybus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9642550",
+ "name": "ВДНХ (южная)"
+ },
+ {
+ "id": "stop__9640641",
+ "name": "Дмитровское шоссе, 155"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659680",
+ "tzOffset": 10800,
+ "text": "21:48"
+ },
+ "Estimated": {
+ "value": "1568659426",
+ "tzOffset": 10800,
+ "text": "21:43"
+ },
+ "vehicleId": "codd%5Fnew|1084829%5F430260"
+ },
+ {
+ "Scheduled": {
+ "value": "1568660520",
+ "tzOffset": 10800,
+ "text": "22:02"
+ },
+ "Estimated": {
+ "value": "1568659656",
+ "tzOffset": 10800,
+ "text": "21:47"
+ },
+ "vehicleId": "codd%5Fnew|1117016%5F430280"
+ },
+ {
+ "Scheduled": {
+ "value": "1568661900",
+ "tzOffset": 10800,
+ "text": "22:25"
+ },
+ "Estimated": {
+ "value": "1568660538",
+ "tzOffset": 10800,
+ "text": "22:02"
+ },
+ "vehicleId": "codd%5Fnew|1054576%5F430226"
+ }
+ ],
+ "departureTime": "21:48"
+ }
+ },
+ {
+ "lineId": "213_47_trolleybus_mosgortrans",
+ "name": "т47",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213B_47_trolleybus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9639568",
+ "name": "Бескудниковский переулок"
+ },
+ {
+ "id": "stop__9641903",
+ "name": "Бескудниковский переулок"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659980",
+ "tzOffset": 10800,
+ "text": "21:53"
+ },
+ "Estimated": {
+ "value": "1568659253",
+ "tzOffset": 10800,
+ "text": "21:40"
+ },
+ "vehicleId": "codd%5Fnew|1112219%5F430329"
+ },
+ {
+ "Scheduled": {
+ "value": "1568660940",
+ "tzOffset": 10800,
+ "text": "22:09"
+ },
+ "Estimated": {
+ "value": "1568660519",
+ "tzOffset": 10800,
+ "text": "22:01"
+ },
+ "vehicleId": "codd%5Fnew|1139620%5F430382"
+ },
+ {
+ "Scheduled": {
+ "value": "1568663580",
+ "tzOffset": 10800,
+ "text": "22:53"
+ }
+ }
+ ],
+ "departureTime": "21:53"
+ }
+ }
+ ],
+ "threadId": "213B_47_trolleybus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9639568",
+ "name": "Бескудниковский переулок"
+ },
+ {
+ "id": "stop__9641903",
+ "name": "Бескудниковский переулок"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659980",
+ "tzOffset": 10800,
+ "text": "21:53"
+ },
+ "Estimated": {
+ "value": "1568659253",
+ "tzOffset": 10800,
+ "text": "21:40"
+ },
+ "vehicleId": "codd%5Fnew|1112219%5F430329"
+ },
+ {
+ "Scheduled": {
+ "value": "1568660940",
+ "tzOffset": 10800,
+ "text": "22:09"
+ },
+ "Estimated": {
+ "value": "1568660519",
+ "tzOffset": 10800,
+ "text": "22:01"
+ },
+ "vehicleId": "codd%5Fnew|1139620%5F430382"
+ },
+ {
+ "Scheduled": {
+ "value": "1568663580",
+ "tzOffset": 10800,
+ "text": "22:53"
+ }
+ }
+ ],
+ "departureTime": "21:53"
+ }
+ },
+ {
+ "lineId": "213_56_trolleybus_mosgortrans",
+ "name": "т56",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_56_trolleybus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9639561",
+ "name": "Коровинское шоссе"
+ },
+ {
+ "id": "stop__9639588",
+ "name": "Коровинское шоссе"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1568660675",
+ "tzOffset": 10800,
+ "text": "22:04"
+ },
+ "vehicleId": "codd%5Fnew|146304%5F31207"
+ }
+ ],
+ "Frequency": {
+ "text": "8 мин",
+ "value": 480,
+ "begin": {
+ "value": "1568606244",
+ "tzOffset": 10800,
+ "text": "6:57"
+ },
+ "end": {
+ "value": "1568670144",
+ "tzOffset": 10800,
+ "text": "0:42"
+ }
+ }
+ }
+ }
+ ],
+ "threadId": "213A_56_trolleybus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9639561",
+ "name": "Коровинское шоссе"
+ },
+ {
+ "id": "stop__9639588",
+ "name": "Коровинское шоссе"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1568660675",
+ "tzOffset": 10800,
+ "text": "22:04"
+ },
+ "vehicleId": "codd%5Fnew|146304%5F31207"
+ }
+ ],
+ "Frequency": {
+ "text": "8 мин",
+ "value": 480,
+ "begin": {
+ "value": "1568606244",
+ "tzOffset": 10800,
+ "text": "6:57"
+ },
+ "end": {
+ "value": "1568670144",
+ "tzOffset": 10800,
+ "text": "0:42"
+ }
+ }
+ }
+ },
+ {
+ "lineId": "213_63_bus_mosgortrans",
+ "name": "63",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_63_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9640554",
+ "name": "Лобненская улица"
+ },
+ {
+ "id": "stop__9640553",
+ "name": "Лобненская улица"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1568659369",
+ "tzOffset": 10800,
+ "text": "21:42"
+ },
+ "vehicleId": "codd%5Fnew|38921%5F9215306"
+ },
+ {
+ "Estimated": {
+ "value": "1568660136",
+ "tzOffset": 10800,
+ "text": "21:55"
+ },
+ "vehicleId": "codd%5Fnew|38918%5F9215303"
+ }
+ ],
+ "Frequency": {
+ "text": "17 мин",
+ "value": 1020,
+ "begin": {
+ "value": "1568600987",
+ "tzOffset": 10800,
+ "text": "5:29"
+ },
+ "end": {
+ "value": "1568670227",
+ "tzOffset": 10800,
+ "text": "0:43"
+ }
+ }
+ }
+ }
+ ],
+ "threadId": "213A_63_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9640554",
+ "name": "Лобненская улица"
+ },
+ {
+ "id": "stop__9640553",
+ "name": "Лобненская улица"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1568659369",
+ "tzOffset": 10800,
+ "text": "21:42"
+ },
+ "vehicleId": "codd%5Fnew|38921%5F9215306"
+ },
+ {
+ "Estimated": {
+ "value": "1568660136",
+ "tzOffset": 10800,
+ "text": "21:55"
+ },
+ "vehicleId": "codd%5Fnew|38918%5F9215303"
+ }
+ ],
+ "Frequency": {
+ "text": "17 мин",
+ "value": 1020,
+ "begin": {
+ "value": "1568600987",
+ "tzOffset": 10800,
+ "text": "5:29"
+ },
+ "end": {
+ "value": "1568670227",
+ "tzOffset": 10800,
+ "text": "0:43"
+ }
+ }
+ }
+ },
+ {
+ "lineId": "213_677_bus_mosgortrans",
+ "name": "677",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213B_677_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9639495",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9639480",
+ "name": "Платформа Лианозово"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1568659369",
+ "tzOffset": 10800,
+ "text": "21:42"
+ },
+ "vehicleId": "codd%5Fnew|11731%5F31376"
+ }
+ ],
+ "Frequency": {
+ "text": "4 мин",
+ "value": 240,
+ "begin": {
+ "value": "1568600940",
+ "tzOffset": 10800,
+ "text": "5:29"
+ },
+ "end": {
+ "value": "1568672640",
+ "tzOffset": 10800,
+ "text": "1:24"
+ }
+ }
+ }
+ }
+ ],
+ "threadId": "213B_677_bus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9639495",
+ "name": "Метро Петровско-Разумовская"
+ },
+ {
+ "id": "stop__9639480",
+ "name": "Платформа Лианозово"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1568659369",
+ "tzOffset": 10800,
+ "text": "21:42"
+ },
+ "vehicleId": "codd%5Fnew|11731%5F31376"
+ }
+ ],
+ "Frequency": {
+ "text": "4 мин",
+ "value": 240,
+ "begin": {
+ "value": "1568600940",
+ "tzOffset": 10800,
+ "text": "5:29"
+ },
+ "end": {
+ "value": "1568672640",
+ "tzOffset": 10800,
+ "text": "1:24"
+ }
+ }
+ }
+ },
+ {
+ "lineId": "213_692_bus_mosgortrans",
+ "name": "692",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "2036928706",
+ "EssentialStops": [
+ {
+ "id": "3163417967",
+ "name": "Платформа Дегунино"
+ },
+ {
+ "id": "3163417967",
+ "name": "Платформа Дегунино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568660280",
+ "tzOffset": 10800,
+ "text": "21:58"
+ },
+ "Estimated": {
+ "value": "1568660255",
+ "tzOffset": 10800,
+ "text": "21:57"
+ },
+ "vehicleId": "codd%5Fnew|63029%5F31485"
+ },
+ {
+ "Scheduled": {
+ "value": "1568693340",
+ "tzOffset": 10800,
+ "text": "7:09"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568696940",
+ "tzOffset": 10800,
+ "text": "8:09"
+ }
+ }
+ ],
+ "departureTime": "21:58"
+ }
+ }
+ ],
+ "threadId": "2036928706",
+ "EssentialStops": [
+ {
+ "id": "3163417967",
+ "name": "Платформа Дегунино"
+ },
+ {
+ "id": "3163417967",
+ "name": "Платформа Дегунино"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568660280",
+ "tzOffset": 10800,
+ "text": "21:58"
+ },
+ "Estimated": {
+ "value": "1568660255",
+ "tzOffset": 10800,
+ "text": "21:57"
+ },
+ "vehicleId": "codd%5Fnew|63029%5F31485"
+ },
+ {
+ "Scheduled": {
+ "value": "1568693340",
+ "tzOffset": 10800,
+ "text": "7:09"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568696940",
+ "tzOffset": 10800,
+ "text": "8:09"
+ }
+ }
+ ],
+ "departureTime": "21:58"
+ }
+ },
+ {
+ "lineId": "213_78_trolleybus_mosgortrans",
+ "name": "т78",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "213A_78_trolleybus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9887464",
+ "name": "9-я Северная линия"
+ },
+ {
+ "id": "stop__9887464",
+ "name": "9-я Северная линия"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659620",
+ "tzOffset": 10800,
+ "text": "21:47"
+ },
+ "Estimated": {
+ "value": "1568659898",
+ "tzOffset": 10800,
+ "text": "21:51"
+ },
+ "vehicleId": "codd%5Fnew|147522%5F31184"
+ },
+ {
+ "Scheduled": {
+ "value": "1568660760",
+ "tzOffset": 10800,
+ "text": "22:06"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568661900",
+ "tzOffset": 10800,
+ "text": "22:25"
+ }
+ }
+ ],
+ "departureTime": "21:47"
+ }
+ }
+ ],
+ "threadId": "213A_78_trolleybus_mosgortrans",
+ "EssentialStops": [
+ {
+ "id": "stop__9887464",
+ "name": "9-я Северная линия"
+ },
+ {
+ "id": "stop__9887464",
+ "name": "9-я Северная линия"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659620",
+ "tzOffset": 10800,
+ "text": "21:47"
+ },
+ "Estimated": {
+ "value": "1568659898",
+ "tzOffset": 10800,
+ "text": "21:51"
+ },
+ "vehicleId": "codd%5Fnew|147522%5F31184"
+ },
+ {
+ "Scheduled": {
+ "value": "1568660760",
+ "tzOffset": 10800,
+ "text": "22:06"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568661900",
+ "tzOffset": 10800,
+ "text": "22:25"
+ }
+ }
+ ],
+ "departureTime": "21:47"
+ }
+ },
+ {
+ "lineId": "213_82_bus_mosgortrans",
+ "name": "82",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "2036925244",
+ "EssentialStops": [
+ {
+ "id": "2310890052",
+ "name": "Метро Верхние Лихоборы"
+ },
+ {
+ "id": "2310890052",
+ "name": "Метро Верхние Лихоборы"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659680",
+ "tzOffset": 10800,
+ "text": "21:48"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568661780",
+ "tzOffset": 10800,
+ "text": "22:23"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568663760",
+ "tzOffset": 10800,
+ "text": "22:56"
+ }
+ }
+ ],
+ "departureTime": "21:48"
+ }
+ }
+ ],
+ "threadId": "2036925244",
+ "EssentialStops": [
+ {
+ "id": "2310890052",
+ "name": "Метро Верхние Лихоборы"
+ },
+ {
+ "id": "2310890052",
+ "name": "Метро Верхние Лихоборы"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659680",
+ "tzOffset": 10800,
+ "text": "21:48"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568661780",
+ "tzOffset": 10800,
+ "text": "22:23"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568663760",
+ "tzOffset": 10800,
+ "text": "22:56"
+ }
+ }
+ ],
+ "departureTime": "21:48"
+ }
+ },
+ {
+ "lineId": "2465131598",
+ "name": "179к",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "2465131758",
+ "EssentialStops": [
+ {
+ "id": "stop__9640244",
+ "name": "Платформа Лианозово"
+ },
+ {
+ "id": "stop__9639480",
+ "name": "Платформа Лианозово"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659500",
+ "tzOffset": 10800,
+ "text": "21:45"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568659980",
+ "tzOffset": 10800,
+ "text": "21:53"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568660880",
+ "tzOffset": 10800,
+ "text": "22:08"
+ }
+ }
+ ],
+ "departureTime": "21:45"
+ }
+ }
+ ],
+ "threadId": "2465131758",
+ "EssentialStops": [
+ {
+ "id": "stop__9640244",
+ "name": "Платформа Лианозово"
+ },
+ {
+ "id": "stop__9639480",
+ "name": "Платформа Лианозово"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659500",
+ "tzOffset": 10800,
+ "text": "21:45"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568659980",
+ "tzOffset": 10800,
+ "text": "21:53"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568660880",
+ "tzOffset": 10800,
+ "text": "22:08"
+ }
+ }
+ ],
+ "departureTime": "21:45"
+ }
+ },
+ {
+ "lineId": "466_bus_default",
+ "name": "466",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "466B_bus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9640546",
+ "name": "Станция Бескудниково"
+ },
+ {
+ "id": "stop__9640545",
+ "name": "Станция Бескудниково"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "22 мин",
+ "value": 1320,
+ "begin": {
+ "value": "1568604647",
+ "tzOffset": 10800,
+ "text": "6:30"
+ },
+ "end": {
+ "value": "1568675447",
+ "tzOffset": 10800,
+ "text": "2:10"
+ }
+ }
+ }
+ }
+ ],
+ "threadId": "466B_bus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9640546",
+ "name": "Станция Бескудниково"
+ },
+ {
+ "id": "stop__9640545",
+ "name": "Станция Бескудниково"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [],
+ "Frequency": {
+ "text": "22 мин",
+ "value": 1320,
+ "begin": {
+ "value": "1568604647",
+ "tzOffset": 10800,
+ "text": "6:30"
+ },
+ "end": {
+ "value": "1568675447",
+ "tzOffset": 10800,
+ "text": "2:10"
+ }
+ }
+ }
+ },
+ {
+ "lineId": "677k_bus_default",
+ "name": "677к",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "677kA_bus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9640244",
+ "name": "Платформа Лианозово"
+ },
+ {
+ "id": "stop__9639480",
+ "name": "Платформа Лианозово"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659920",
+ "tzOffset": 10800,
+ "text": "21:52"
+ },
+ "Estimated": {
+ "value": "1568660003",
+ "tzOffset": 10800,
+ "text": "21:53"
+ },
+ "vehicleId": "codd%5Fnew|130308%5F31319"
+ },
+ {
+ "Scheduled": {
+ "value": "1568661240",
+ "tzOffset": 10800,
+ "text": "22:14"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568662500",
+ "tzOffset": 10800,
+ "text": "22:35"
+ }
+ }
+ ],
+ "departureTime": "21:52"
+ }
+ }
+ ],
+ "threadId": "677kA_bus_default",
+ "EssentialStops": [
+ {
+ "id": "stop__9640244",
+ "name": "Платформа Лианозово"
+ },
+ {
+ "id": "stop__9639480",
+ "name": "Платформа Лианозово"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Scheduled": {
+ "value": "1568659920",
+ "tzOffset": 10800,
+ "text": "21:52"
+ },
+ "Estimated": {
+ "value": "1568660003",
+ "tzOffset": 10800,
+ "text": "21:53"
+ },
+ "vehicleId": "codd%5Fnew|130308%5F31319"
+ },
+ {
+ "Scheduled": {
+ "value": "1568661240",
+ "tzOffset": 10800,
+ "text": "22:14"
+ }
+ },
+ {
+ "Scheduled": {
+ "value": "1568662500",
+ "tzOffset": 10800,
+ "text": "22:35"
+ }
+ }
+ ],
+ "departureTime": "21:52"
+ }
+ },
+ {
+ "lineId": "m10_bus_default",
+ "name": "м10",
+ "Types": [
+ "bus"
+ ],
+ "type": "bus",
+ "threads": [
+ {
+ "threadId": "2036926048",
+ "EssentialStops": [
+ {
+ "id": "stop__9640554",
+ "name": "Лобненская улица"
+ },
+ {
+ "id": "stop__9640553",
+ "name": "Лобненская улица"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1568659718",
+ "tzOffset": 10800,
+ "text": "21:48"
+ },
+ "vehicleId": "codd%5Fnew|146260%5F31212"
+ },
+ {
+ "Estimated": {
+ "value": "1568660422",
+ "tzOffset": 10800,
+ "text": "22:00"
+ },
+ "vehicleId": "codd%5Fnew|13997%5F31247"
+ }
+ ],
+ "Frequency": {
+ "text": "15 мин",
+ "value": 900,
+ "begin": {
+ "value": "1568606903",
+ "tzOffset": 10800,
+ "text": "7:08"
+ },
+ "end": {
+ "value": "1568675183",
+ "tzOffset": 10800,
+ "text": "2:06"
+ }
+ }
+ }
+ }
+ ],
+ "threadId": "2036926048",
+ "EssentialStops": [
+ {
+ "id": "stop__9640554",
+ "name": "Лобненская улица"
+ },
+ {
+ "id": "stop__9640553",
+ "name": "Лобненская улица"
+ }
+ ],
+ "BriefSchedule": {
+ "Events": [
+ {
+ "Estimated": {
+ "value": "1568659718",
+ "tzOffset": 10800,
+ "text": "21:48"
+ },
+ "vehicleId": "codd%5Fnew|146260%5F31212"
+ },
+ {
+ "Estimated": {
+ "value": "1568660422",
+ "tzOffset": 10800,
+ "text": "22:00"
+ },
+ "vehicleId": "codd%5Fnew|13997%5F31247"
+ }
+ ],
+ "Frequency": {
+ "text": "15 мин",
+ "value": 900,
+ "begin": {
+ "value": "1568606903",
+ "tzOffset": 10800,
+ "text": "7:08"
+ },
+ "end": {
+ "value": "1568675183",
+ "tzOffset": 10800,
+ "text": "2:06"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "toponymSeoname": "dmitrovskoye_shosse"
+ }
+}
diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py
index ddd22107fa08df..b603f98bb04b93 100644
--- a/tests/helpers/test_condition.py
+++ b/tests/helpers/test_condition.py
@@ -4,182 +4,175 @@
from homeassistant.helpers import condition
from homeassistant.util import dt
-from tests.common import get_test_home_assistant
-
-
-class TestConditionHelper:
- """Test condition helpers."""
-
- def setup_method(self, method):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
-
- def teardown_method(self, method):
- """Stop everything that was started."""
- self.hass.stop()
-
- def test_and_condition(self):
- """Test the 'and' condition."""
- test = condition.from_config(
- {
- "condition": "and",
- "conditions": [
- {
- "condition": "state",
- "entity_id": "sensor.temperature",
- "state": "100",
- },
- {
- "condition": "numeric_state",
- "entity_id": "sensor.temperature",
- "below": 110,
- },
- ],
- }
- )
-
- self.hass.states.set("sensor.temperature", 120)
- assert not test(self.hass)
-
- self.hass.states.set("sensor.temperature", 105)
- assert not test(self.hass)
-
- self.hass.states.set("sensor.temperature", 100)
- assert test(self.hass)
-
- def test_and_condition_with_template(self):
- """Test the 'and' condition."""
- test = condition.from_config(
- {
- "condition": "and",
- "conditions": [
- {
- "condition": "template",
- "value_template": '{{ states.sensor.temperature.state == "100" }}',
- },
- {
- "condition": "numeric_state",
- "entity_id": "sensor.temperature",
- "below": 110,
- },
- ],
- }
- )
-
- self.hass.states.set("sensor.temperature", 120)
- assert not test(self.hass)
-
- self.hass.states.set("sensor.temperature", 105)
- assert not test(self.hass)
-
- self.hass.states.set("sensor.temperature", 100)
- assert test(self.hass)
-
- def test_or_condition(self):
- """Test the 'or' condition."""
- test = condition.from_config(
- {
- "condition": "or",
- "conditions": [
- {
- "condition": "state",
- "entity_id": "sensor.temperature",
- "state": "100",
- },
- {
- "condition": "numeric_state",
- "entity_id": "sensor.temperature",
- "below": 110,
- },
- ],
- }
- )
-
- self.hass.states.set("sensor.temperature", 120)
- assert not test(self.hass)
-
- self.hass.states.set("sensor.temperature", 105)
- assert test(self.hass)
-
- self.hass.states.set("sensor.temperature", 100)
- assert test(self.hass)
-
- def test_or_condition_with_template(self):
- """Test the 'or' condition."""
- test = condition.from_config(
- {
- "condition": "or",
- "conditions": [
- {
- "condition": "template",
- "value_template": '{{ states.sensor.temperature.state == "100" }}',
- },
- {
- "condition": "numeric_state",
- "entity_id": "sensor.temperature",
- "below": 110,
- },
- ],
- }
- )
-
- self.hass.states.set("sensor.temperature", 120)
- assert not test(self.hass)
-
- self.hass.states.set("sensor.temperature", 105)
- assert test(self.hass)
-
- self.hass.states.set("sensor.temperature", 100)
- assert test(self.hass)
-
- def test_time_window(self):
- """Test time condition windows."""
- sixam = dt.parse_time("06:00:00")
- sixpm = dt.parse_time("18:00:00")
-
- with patch(
- "homeassistant.helpers.condition.dt_util.now",
- return_value=dt.now().replace(hour=3),
- ):
- assert not condition.time(after=sixam, before=sixpm)
- assert condition.time(after=sixpm, before=sixam)
-
- with patch(
- "homeassistant.helpers.condition.dt_util.now",
- return_value=dt.now().replace(hour=9),
- ):
- assert condition.time(after=sixam, before=sixpm)
- assert not condition.time(after=sixpm, before=sixam)
-
- with patch(
- "homeassistant.helpers.condition.dt_util.now",
- return_value=dt.now().replace(hour=15),
- ):
- assert condition.time(after=sixam, before=sixpm)
- assert not condition.time(after=sixpm, before=sixam)
-
- with patch(
- "homeassistant.helpers.condition.dt_util.now",
- return_value=dt.now().replace(hour=21),
- ):
- assert not condition.time(after=sixam, before=sixpm)
- assert condition.time(after=sixpm, before=sixam)
-
- def test_if_numeric_state_not_raise_on_unavailable(self):
- """Test numeric_state doesn't raise on unavailable/unknown state."""
- test = condition.from_config(
- {
- "condition": "numeric_state",
- "entity_id": "sensor.temperature",
- "below": 42,
- }
- )
-
- with patch("homeassistant.helpers.condition._LOGGER.warning") as logwarn:
- self.hass.states.set("sensor.temperature", "unavailable")
- assert not test(self.hass)
- assert len(logwarn.mock_calls) == 0
-
- self.hass.states.set("sensor.temperature", "unknown")
- assert not test(self.hass)
- assert len(logwarn.mock_calls) == 0
+
+async def test_and_condition(hass):
+ """Test the 'and' condition."""
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "and",
+ "conditions": [
+ {
+ "condition": "state",
+ "entity_id": "sensor.temperature",
+ "state": "100",
+ },
+ {
+ "condition": "numeric_state",
+ "entity_id": "sensor.temperature",
+ "below": 110,
+ },
+ ],
+ },
+ )
+
+ hass.states.async_set("sensor.temperature", 120)
+ assert not test(hass)
+
+ hass.states.async_set("sensor.temperature", 105)
+ assert not test(hass)
+
+ hass.states.async_set("sensor.temperature", 100)
+ assert test(hass)
+
+
+async def test_and_condition_with_template(hass):
+ """Test the 'and' condition."""
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "and",
+ "conditions": [
+ {
+ "condition": "template",
+ "value_template": '{{ states.sensor.temperature.state == "100" }}',
+ },
+ {
+ "condition": "numeric_state",
+ "entity_id": "sensor.temperature",
+ "below": 110,
+ },
+ ],
+ },
+ )
+
+ hass.states.async_set("sensor.temperature", 120)
+ assert not test(hass)
+
+ hass.states.async_set("sensor.temperature", 105)
+ assert not test(hass)
+
+ hass.states.async_set("sensor.temperature", 100)
+ assert test(hass)
+
+
+async def test_or_condition(hass):
+ """Test the 'or' condition."""
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "or",
+ "conditions": [
+ {
+ "condition": "state",
+ "entity_id": "sensor.temperature",
+ "state": "100",
+ },
+ {
+ "condition": "numeric_state",
+ "entity_id": "sensor.temperature",
+ "below": 110,
+ },
+ ],
+ },
+ )
+
+ hass.states.async_set("sensor.temperature", 120)
+ assert not test(hass)
+
+ hass.states.async_set("sensor.temperature", 105)
+ assert test(hass)
+
+ hass.states.async_set("sensor.temperature", 100)
+ assert test(hass)
+
+
+async def test_or_condition_with_template(hass):
+ """Test the 'or' condition."""
+ test = await condition.async_from_config(
+ hass,
+ {
+ "condition": "or",
+ "conditions": [
+ {
+ "condition": "template",
+ "value_template": '{{ states.sensor.temperature.state == "100" }}',
+ },
+ {
+ "condition": "numeric_state",
+ "entity_id": "sensor.temperature",
+ "below": 110,
+ },
+ ],
+ },
+ )
+
+ hass.states.async_set("sensor.temperature", 120)
+ assert not test(hass)
+
+ hass.states.async_set("sensor.temperature", 105)
+ assert test(hass)
+
+ hass.states.async_set("sensor.temperature", 100)
+ assert test(hass)
+
+
+async def test_time_window(hass):
+ """Test time condition windows."""
+ sixam = dt.parse_time("06:00:00")
+ sixpm = dt.parse_time("18:00:00")
+
+ with patch(
+ "homeassistant.helpers.condition.dt_util.now",
+ return_value=dt.now().replace(hour=3),
+ ):
+ assert not condition.time(after=sixam, before=sixpm)
+ assert condition.time(after=sixpm, before=sixam)
+
+ with patch(
+ "homeassistant.helpers.condition.dt_util.now",
+ return_value=dt.now().replace(hour=9),
+ ):
+ assert condition.time(after=sixam, before=sixpm)
+ assert not condition.time(after=sixpm, before=sixam)
+
+ with patch(
+ "homeassistant.helpers.condition.dt_util.now",
+ return_value=dt.now().replace(hour=15),
+ ):
+ assert condition.time(after=sixam, before=sixpm)
+ assert not condition.time(after=sixpm, before=sixam)
+
+ with patch(
+ "homeassistant.helpers.condition.dt_util.now",
+ return_value=dt.now().replace(hour=21),
+ ):
+ assert not condition.time(after=sixam, before=sixpm)
+ assert condition.time(after=sixpm, before=sixam)
+
+
+async def test_if_numeric_state_not_raise_on_unavailable(hass):
+ """Test numeric_state doesn't raise on unavailable/unknown state."""
+ test = await condition.async_from_config(
+ hass,
+ {"condition": "numeric_state", "entity_id": "sensor.temperature", "below": 42},
+ )
+
+ with patch("homeassistant.helpers.condition._LOGGER.warning") as logwarn:
+ hass.states.async_set("sensor.temperature", "unavailable")
+ assert not test(hass)
+ assert len(logwarn.mock_calls) == 0
+
+ hass.states.async_set("sensor.temperature", "unknown")
+ assert not test(hass)
+ assert len(logwarn.mock_calls) == 0
diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py
index f4218fb1a7e52a..1c3748250a5fdb 100644
--- a/tests/helpers/test_translation.py
+++ b/tests/helpers/test_translation.py
@@ -95,30 +95,23 @@ async def test_get_translations(hass, mock_config_flows):
assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}})
translations = await translation.async_get_translations(hass, "en")
- assert translations == {
- "component.switch.state.string1": "Value 1",
- "component.switch.state.string2": "Value 2",
- }
+
+ assert translations["component.switch.state.string1"] == "Value 1"
+ assert translations["component.switch.state.string2"] == "Value 2"
translations = await translation.async_get_translations(hass, "de")
- assert translations == {
- "component.switch.state.string1": "German Value 1",
- "component.switch.state.string2": "German Value 2",
- }
+ assert translations["component.switch.state.string1"] == "German Value 1"
+ assert translations["component.switch.state.string2"] == "German Value 2"
# Test a partial translation
translations = await translation.async_get_translations(hass, "es")
- assert translations == {
- "component.switch.state.string1": "Spanish Value 1",
- "component.switch.state.string2": "Value 2",
- }
+ assert translations["component.switch.state.string1"] == "Spanish Value 1"
+ assert translations["component.switch.state.string2"] == "Value 2"
# Test that an untranslated language falls back to English.
translations = await translation.async_get_translations(hass, "invalid-language")
- assert translations == {
- "component.switch.state.string1": "Value 1",
- "component.switch.state.string2": "Value 2",
- }
+ assert translations["component.switch.state.string1"] == "Value 1"
+ assert translations["component.switch.state.string2"] == "Value 2"
async def test_get_translations_loads_config_flows(hass, mock_config_flows):
diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py
index 98fc70f3bf51f3..ce13ca5a594aa5 100644
--- a/tests/test_util/aiohttp.py
+++ b/tests/test_util/aiohttp.py
@@ -244,8 +244,12 @@ def release(self):
def raise_for_status(self):
"""Raise error if status is 400 or higher."""
if self.status >= 400:
+ request_info = mock.Mock(real_url="http://example.com")
raise ClientResponseError(
- None, None, code=self.status, headers=self.headers
+ request_info=request_info,
+ history=None,
+ code=self.status,
+ headers=self.headers,
)
def close(self):
diff --git a/tests/testing_config/custom_components/test/binary_sensor.py b/tests/testing_config/custom_components/test/binary_sensor.py
new file mode 100644
index 00000000000000..5052b8e47f10b2
--- /dev/null
+++ b/tests/testing_config/custom_components/test/binary_sensor.py
@@ -0,0 +1,50 @@
+"""
+Provide a mock binary sensor platform.
+
+Call init before using it in your tests to ensure clean test data.
+"""
+from homeassistant.components.binary_sensor import BinarySensorDevice, DEVICE_CLASSES
+from tests.common import MockEntity
+
+
+ENTITIES = {}
+
+
+def init(empty=False):
+ """Initialize the platform with entities."""
+ global ENTITIES
+
+ ENTITIES = (
+ {}
+ if empty
+ else {
+ device_class: MockBinarySensor(
+ name=f"{device_class} sensor",
+ is_on=True,
+ unique_id=f"unique_{device_class}",
+ device_class=device_class,
+ )
+ for device_class in DEVICE_CLASSES
+ }
+ )
+
+
+async def async_setup_platform(
+ hass, config, async_add_entities_callback, discovery_info=None
+):
+ """Return mock entities."""
+ async_add_entities_callback(list(ENTITIES.values()))
+
+
+class MockBinarySensor(MockEntity, BinarySensorDevice):
+ """Mock Binary Sensor class."""
+
+ @property
+ def is_on(self):
+ """Return true if the binary sensor is on."""
+ return self._handle("is_on")
+
+ @property
+ def device_class(self):
+ """Return the class of this sensor."""
+ return self._handle("device_class")
diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py
index 43338c9e14ed1f..0a48388b718b1e 100644
--- a/tests/testing_config/custom_components/test/light.py
+++ b/tests/testing_config/custom_components/test/light.py
@@ -4,23 +4,23 @@
Call init before using it in your tests to ensure clean test data.
"""
from homeassistant.const import STATE_ON, STATE_OFF
-from tests.common import MockToggleDevice
+from tests.common import MockToggleEntity
-DEVICES = []
+ENTITIES = []
def init(empty=False):
- """Initialize the platform with devices."""
- global DEVICES
+ """Initialize the platform with entities."""
+ global ENTITIES
- DEVICES = (
+ ENTITIES = (
[]
if empty
else [
- MockToggleDevice("Ceiling", STATE_ON),
- MockToggleDevice("Ceiling", STATE_OFF),
- MockToggleDevice(None, STATE_OFF),
+ MockToggleEntity("Ceiling", STATE_ON),
+ MockToggleEntity("Ceiling", STATE_OFF),
+ MockToggleEntity(None, STATE_OFF),
]
)
@@ -28,5 +28,5 @@ def init(empty=False):
async def async_setup_platform(
hass, config, async_add_entities_callback, discovery_info=None
):
- """Return mock devices."""
- async_add_entities_callback(DEVICES)
+ """Return mock entities."""
+ async_add_entities_callback(ENTITIES)
diff --git a/tests/testing_config/custom_components/test/switch.py b/tests/testing_config/custom_components/test/switch.py
index f4226ecc63014f..484c47d1190e3c 100644
--- a/tests/testing_config/custom_components/test/switch.py
+++ b/tests/testing_config/custom_components/test/switch.py
@@ -4,23 +4,23 @@
Call init before using it in your tests to ensure clean test data.
"""
from homeassistant.const import STATE_ON, STATE_OFF
-from tests.common import MockToggleDevice
+from tests.common import MockToggleEntity
-DEVICES = []
+ENTITIES = []
def init(empty=False):
- """Initialize the platform with devices."""
- global DEVICES
+ """Initialize the platform with entities."""
+ global ENTITIES
- DEVICES = (
+ ENTITIES = (
[]
if empty
else [
- MockToggleDevice("AC", STATE_ON),
- MockToggleDevice("AC", STATE_OFF),
- MockToggleDevice(None, STATE_OFF),
+ MockToggleEntity("AC", STATE_ON),
+ MockToggleEntity("AC", STATE_OFF),
+ MockToggleEntity(None, STATE_OFF),
]
)
@@ -28,5 +28,5 @@ def init(empty=False):
async def async_setup_platform(
hass, config, async_add_entities_callback, discovery_info=None
):
- """Find and return test switches."""
- async_add_entities_callback(DEVICES)
+ """Return mock entities."""
+ async_add_entities_callback(ENTITIES)