Merge pull request #41406 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2020-10-07 17:50:57 +02:00 committed by GitHub
commit 55958bcfb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1625 changed files with 30967 additions and 8947 deletions

View File

@ -33,7 +33,11 @@ omit =
homeassistant/components/airvisual/air_quality.py
homeassistant/components/airvisual/sensor.py
homeassistant/components/aladdin_connect/cover.py
homeassistant/components/alarmdecoder/*
homeassistant/components/alarmdecoder/__init__.py
homeassistant/components/alarmdecoder/alarm_control_panel.py
homeassistant/components/alarmdecoder/binary_sensor.py
homeassistant/components/alarmdecoder/const.py
homeassistant/components/alarmdecoder/sensor.py
homeassistant/components/alpha_vantage/sensor.py
homeassistant/components/amazon_polly/tts.py
homeassistant/components/ambiclimate/climate.py
@ -117,7 +121,6 @@ omit =
homeassistant/components/buienradar/util.py
homeassistant/components/buienradar/weather.py
homeassistant/components/caldav/calendar.py
homeassistant/components/canary/alarm_control_panel.py
homeassistant/components/canary/camera.py
homeassistant/components/cast/*
homeassistant/components/cert_expiry/helper.py
@ -266,7 +269,9 @@ omit =
homeassistant/components/firmata/board.py
homeassistant/components/firmata/const.py
homeassistant/components/firmata/entity.py
homeassistant/components/firmata/light.py
homeassistant/components/firmata/pin.py
homeassistant/components/firmata/sensor.py
homeassistant/components/firmata/switch.py
homeassistant/components/fitbit/sensor.py
homeassistant/components/fixer/sensor.py
@ -315,6 +320,8 @@ omit =
homeassistant/components/glances/sensor.py
homeassistant/components/gntp/notify.py
homeassistant/components/goalfeed/*
homeassistant/components/goalzero/__init__.py
homeassistant/components/goalzero/binary_sensor.py
homeassistant/components/google/*
homeassistant/components/google_cloud/tts.py
homeassistant/components/google_maps/device_tracker.py
@ -369,6 +376,7 @@ omit =
homeassistant/components/hunterdouglas_powerview/sensor.py
homeassistant/components/hunterdouglas_powerview/cover.py
homeassistant/components/hunterdouglas_powerview/entity.py
homeassistant/components/hvv_departures/binary_sensor.py
homeassistant/components/hvv_departures/sensor.py
homeassistant/components/hvv_departures/__init__.py
homeassistant/components/hydrawise/*
@ -478,7 +486,8 @@ omit =
homeassistant/components/london_underground/sensor.py
homeassistant/components/loopenergy/sensor.py
homeassistant/components/luci/device_tracker.py
homeassistant/components/luftdaten/*
homeassistant/components/luftdaten/__init__.py
homeassistant/components/luftdaten/sensor.py
homeassistant/components/lupusec/*
homeassistant/components/lutron/*
homeassistant/components/lutron_caseta/__init__.py
@ -530,7 +539,9 @@ omit =
homeassistant/components/mjpeg/camera.py
homeassistant/components/mobile_app/*
homeassistant/components/mochad/*
homeassistant/components/modbus/*
homeassistant/components/modbus/climate.py
homeassistant/components/modbus/cover.py
homeassistant/components/modbus/switch.py
homeassistant/components/modem_callerid/sensor.py
homeassistant/components/mpchc/media_player.py
homeassistant/components/mpd/media_player.py
@ -595,6 +606,10 @@ omit =
homeassistant/components/oasa_telematics/sensor.py
homeassistant/components/ohmconnect/sensor.py
homeassistant/components/ombi/*
homeassistant/components/omnilogic/__init__.py
homeassistant/components/omnilogic/common.py
homeassistant/components/omnilogic/sensor.py
homeassistant/components/onewire/const.py
homeassistant/components/onewire/sensor.py
homeassistant/components/onkyo/media_player.py
homeassistant/components/onvif/__init__.py
@ -803,6 +818,7 @@ omit =
homeassistant/components/spc/*
homeassistant/components/speedtestdotnet/*
homeassistant/components/spider/*
homeassistant/components/splunk/*
homeassistant/components/spotcrime/sensor.py
homeassistant/components/spotify/__init__.py
homeassistant/components/spotify/media_player.py
@ -830,7 +846,9 @@ omit =
homeassistant/components/synology_chat/notify.py
homeassistant/components/synology_dsm/__init__.py
homeassistant/components/synology_dsm/binary_sensor.py
homeassistant/components/synology_dsm/camera.py
homeassistant/components/synology_dsm/sensor.py
homeassistant/components/synology_dsm/switch.py
homeassistant/components/synology_srm/device_tracker.py
homeassistant/components/syslog/notify.py
homeassistant/components/systemmonitor/sensor.py
@ -844,7 +862,13 @@ omit =
homeassistant/components/ted5000/sensor.py
homeassistant/components/telegram/notify.py
homeassistant/components/telegram_bot/*
homeassistant/components/tellduslive/*
homeassistant/components/tellduslive/__init__.py
homeassistant/components/tellduslive/binary_sensor.py
homeassistant/components/tellduslive/cover.py
homeassistant/components/tellduslive/entry.py
homeassistant/components/tellduslive/light.py
homeassistant/components/tellduslive/sensor.py
homeassistant/components/tellduslive/switch.py
homeassistant/components/tellstick/*
homeassistant/components/telnet/switch.py
homeassistant/components/temper/sensor.py
@ -863,7 +887,9 @@ omit =
homeassistant/components/thingspeak/*
homeassistant/components/thinkingcleaner/*
homeassistant/components/thomson/device_tracker.py
homeassistant/components/tibber/*
homeassistant/components/tibber/__init__.py
homeassistant/components/tibber/notify.py
homeassistant/components/tibber/sensor.py
homeassistant/components/tikteck/light.py
homeassistant/components/tile/__init__.py
homeassistant/components/tile/device_tracker.py

View File

@ -518,25 +518,24 @@ jobs:
hassfest:
name: Check hassfest
runs-on: ubuntu-latest
needs: prepare-base
needs: prepare-tests
strategy:
matrix:
python-version: [3.7]
container: homeassistant/ci-azure:${{ matrix.python-version }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2.1.2
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment
- name:
Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache@v2
with:
path: venv
key: >-
${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{
steps.python.outputs.python-version }}-${{
hashFiles('requirements.txt') }}-${{
hashFiles('requirements_test.txt') }}-${{
${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{
matrix.python-version }}-${{ hashFiles('requirements_test.txt')
}}-${{ hashFiles('requirements_all.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
@ -546,7 +545,7 @@ jobs:
- name: Run hassfest
run: |
. venv/bin/activate
python -m script.hassfest --action validate
python -m script.hassfest --requirements --action validate
gen-requirements-all:
name: Check all requirements

View File

@ -39,7 +39,7 @@ repos:
- --configfile=tests/bandit.yaml
files: ^(homeassistant|script|tests)/.+\.py$
- repo: https://github.com/PyCQA/isort
rev: 5.5.1
rev: 5.5.3
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks

View File

@ -157,6 +157,7 @@ homeassistant/components/geonetnz_volcano/* @exxamalte
homeassistant/components/gios/* @bieniu
homeassistant/components/gitter/* @fabaff
homeassistant/components/glances/* @fabaff @engrbm87
homeassistant/components/goalzero/* @tkdrob
homeassistant/components/gogogate2/* @vangorra
homeassistant/components/google_assistant/* @home-assistant/cloud
homeassistant/components/google_cloud/* @lufton
@ -169,6 +170,7 @@ homeassistant/components/growatt_server/* @indykoning
homeassistant/components/guardian/* @bachya
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco
homeassistant/components/hassio/* @home-assistant/hass-io
homeassistant/components/hdmi_cec/* @newAM
homeassistant/components/heatmiser/* @andylockran
homeassistant/components/heos/* @andrewsayre
homeassistant/components/here_travel_time/* @eifinger
@ -193,6 +195,7 @@ homeassistant/components/humidifier/* @home-assistant/core @Shulyaka
homeassistant/components/hunterdouglas_powerview/* @bdraco
homeassistant/components/hvv_departures/* @vigonotion
homeassistant/components/hydrawise/* @ptcryan
homeassistant/components/hyperion/* @dermotduffy
homeassistant/components/iammeter/* @lewei50
homeassistant/components/iaqualink/* @flz
homeassistant/components/icloud/* @Quentame
@ -226,7 +229,7 @@ homeassistant/components/keenetic_ndms2/* @foxel
homeassistant/components/kef/* @basnijholt
homeassistant/components/keyboard_remote/* @bendavid
homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
homeassistant/components/kodi/* @OnFreund
homeassistant/components/kodi/* @OnFreund @cgtobi
homeassistant/components/konnected/* @heythisisnate @kit-klein
homeassistant/components/lametric/* @robbiet480
homeassistant/components/launch_library/* @ludeeus
@ -238,7 +241,7 @@ homeassistant/components/logger/* @home-assistant/core
homeassistant/components/logi_circle/* @evanjd
homeassistant/components/loopenergy/* @pavoni
homeassistant/components/lovelace/* @home-assistant/frontend
homeassistant/components/luci/* @fbradyirl @mzdrale
homeassistant/components/luci/* @mzdrale
homeassistant/components/luftdaten/* @fabaff
homeassistant/components/lupusec/* @majuss
homeassistant/components/lutron/* @JonGilmore
@ -261,7 +264,7 @@ homeassistant/components/min_max/* @fabaff
homeassistant/components/minecraft_server/* @elmurato
homeassistant/components/minio/* @tkislan
homeassistant/components/mobile_app/* @robbiet480
homeassistant/components/modbus/* @adamchengtkc @janiversen
homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik
homeassistant/components/monoprice/* @etsinko @OnFreund
homeassistant/components/moon/* @fabaff
homeassistant/components/mpd/* @fabaff
@ -300,6 +303,7 @@ homeassistant/components/nzbget/* @chriscla
homeassistant/components/obihai/* @dshokouhi
homeassistant/components/ohmconnect/* @robbiet480
homeassistant/components/ombi/* @larssont
homeassistant/components/omnilogic/* @oliver84 @djtimca @gentoosu
homeassistant/components/onboarding/* @home-assistant/core
homeassistant/components/onewire/* @garbled1
homeassistant/components/onvif/* @hunterjm
@ -349,6 +353,7 @@ homeassistant/components/raincloud/* @vanstinator
homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert
homeassistant/components/rainmachine/* @bachya
homeassistant/components/random/* @fabaff
homeassistant/components/rejseplanen/* @DarkFox
homeassistant/components/repetier/* @MTrab
homeassistant/components/rfxtrx/* @danielhiversen @elupus
homeassistant/components/ring/* @balloob
@ -357,6 +362,7 @@ homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roku/* @ctalkington
homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn
homeassistant/components/roon/* @pavoni
homeassistant/components/rpi_power/* @shenxn @swetoast
homeassistant/components/safe_mode/* @home-assistant/core
homeassistant/components/saj/* @fredericvl
homeassistant/components/salt/* @bjornorri
@ -399,9 +405,11 @@ homeassistant/components/soma/* @ratsept
homeassistant/components/somfy/* @tetienne
homeassistant/components/sonarr/* @ctalkington
homeassistant/components/songpal/* @rytilahti @shenxn
homeassistant/components/sonos/* @cgtobi
homeassistant/components/spaceapi/* @fabaff
homeassistant/components/speedtestdotnet/* @rohankapoorcom @engrbm87
homeassistant/components/spider/* @peternijssen
homeassistant/components/splunk/* @Bre77
homeassistant/components/spotify/* @frenck
homeassistant/components/sql/* @dgomes
homeassistant/components/squeezebox/* @rajlaud
@ -421,7 +429,7 @@ homeassistant/components/switchbot/* @danielhiversen
homeassistant/components/switcher_kis/* @tomerfi
homeassistant/components/switchmate/* @danielhiversen
homeassistant/components/syncthru/* @nielstron
homeassistant/components/synology_dsm/* @ProtoThis @Quentame
homeassistant/components/synology_dsm/* @hacf-fr @Quentame
homeassistant/components/synology_srm/* @aerialls
homeassistant/components/syslog/* @fabaff
homeassistant/components/tado/* @michaelarnauts @bdraco
@ -502,6 +510,7 @@ homeassistant/components/yi/* @bachya
homeassistant/components/zeroconf/* @Kane610
homeassistant/components/zerproc/* @emlove
homeassistant/components/zha/* @dmulcahey @adminiuga
homeassistant/components/zodiac/* @JulienTant
homeassistant/components/zone/* @home-assistant/core
homeassistant/components/zoneminder/* @rohankapoorcom
homeassistant/components/zwave/* @home-assistant/z-wave

View File

@ -5,6 +5,7 @@ from homeassistant.const import (
LENGTH_FEET,
LENGTH_INCHES,
LENGTH_METERS,
LENGTH_MILLIMETERS,
PERCENTAGE,
SPEED_KILOMETERS_PER_HOUR,
SPEED_MILES_PER_HOUR,
@ -24,7 +25,6 @@ ATTR_UNIT_METRIC = "Metric"
CONCENTRATION_PARTS_PER_CUBIC_METER = f"p/{VOLUME_CUBIC_METERS}"
COORDINATOR = "coordinator"
DOMAIN = "accuweather"
LENGTH_MILIMETERS = "mm"
MANUFACTURER = "AccuWeather, Inc."
NAME = "AccuWeather"
UNDO_UPDATE_LISTENER = "undo_update_listener"
@ -238,7 +238,7 @@ SENSOR_TYPES = {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-rainy",
ATTR_LABEL: "Precipitation",
ATTR_UNIT_METRIC: LENGTH_MILIMETERS,
ATTR_UNIT_METRIC: LENGTH_MILLIMETERS,
ATTR_UNIT_IMPERIAL: LENGTH_INCHES,
},
"PressureTendency": {

View File

@ -0,0 +1,13 @@
{
"config": {
"step": {
"user": {
"data": {
"latitude": "Breitengrad",
"longitude": "L\u00e4ngengrad"
},
"title": "AccuWeather"
}
}
}
}

View File

@ -0,0 +1,35 @@
{
"config": {
"abort": {
"single_instance_allowed": "Sidumine juba tehtud. V\u00f5imalik on ainult 1 sidumine,"
},
"error": {
"cannot_connect": "\u00dchendus eba\u00f5nnestus",
"invalid_api_key": "API v\u00f5ti on vale",
"requests_exceeded": "Accuweatheri API-le esitatud p\u00e4ringute piirarv on \u00fcletatud. Peate ootama (v\u00f5i muutma API v\u00f5tit)."
},
"step": {
"user": {
"data": {
"api_key": "API v\u00f5ti",
"latitude": "Laiuskraad",
"longitude": "Pikkuskraad",
"name": "Sidumise nimi"
},
"description": "Kui vajate seadistamisel abi vaadake siit: https://www.home-assistant.io/integrations/accuweather/ \n\n M\u00f5ni andur pole vaikimisi lubatud. P\u00e4rast sidumise seadistamist saate need \u00fcksused lubada. \n Ilmapennustus pole vaikimisi lubatud. Saate selle lubada sidumise s\u00e4tetes.",
"title": "AccuWeather"
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "Ilmateade"
},
"description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 32 minuti asemel iga 64 minuti j\u00e4rel.",
"title": "AccuWeatheri valikud"
}
}
}
}

View File

@ -3,9 +3,32 @@
"abort": {
"single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible."
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
"invalid_api_key": "Cl\u00e9 API invalide",
"requests_exceeded": "Le nombre autoris\u00e9 de requ\u00eates adress\u00e9es \u00e0 l'API AccuWeather a \u00e9t\u00e9 d\u00e9pass\u00e9. Vous devez attendre ou modifier la cl\u00e9 API."
},
"step": {
"user": {
"description": "Si vous avez besoin d'aide pour la configuration, consultez le site suivant : https://www.home-assistant.io/integrations/accuweather/\n\nCertains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s apr\u00e8s la configuration de l'int\u00e9gration.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez l'activer dans les options d'int\u00e9gration."
"data": {
"api_key": "Cl\u00e9 d'API",
"latitude": "Latitude",
"longitude": "Longitude",
"name": "Nom de l'int\u00e9gration"
},
"description": "Si vous avez besoin d'aide pour la configuration, consultez le site suivant : https://www.home-assistant.io/integrations/accuweather/\n\nCertains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s apr\u00e8s la configuration de l'int\u00e9gration.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez l'activer dans les options d'int\u00e9gration.",
"title": "AccuWeather"
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "Pr\u00e9visions m\u00e9t\u00e9orologiques"
},
"description": "En raison des limitations de la version gratuite de la cl\u00e9 API AccuWeather, lorsque vous activez les pr\u00e9visions m\u00e9t\u00e9orologiques, les mises \u00e0 jour des donn\u00e9es seront effectu\u00e9es toutes les 64 minutes au lieu de toutes les 32 minutes.",
"title": "Options AccuWeather"
}
}
}

View File

@ -16,7 +16,8 @@
"longitude": "Lengdegrad",
"name": "Navn p\u00e5 integrasjon"
},
"description": "Hvis du trenger hjelp med konfigurasjonen, kan du se her: https://www.home-assistant.io/integrations/accuweather/ \n\n Noen sensorer er ikke aktivert som standard. Du kan aktivere dem i enhetsregisteret etter integrasjonskonfigurasjonen. \n V\u00e6rmelding er ikke aktivert som standard. Du kan aktivere det i integrasjonsalternativene."
"description": "Hvis du trenger hjelp med konfigurasjonen, kan du se her: https://www.home-assistant.io/integrations/accuweather/ \n\n Noen sensorer er ikke aktivert som standard. Du kan aktivere dem i enhetsregisteret etter integrasjonskonfigurasjonen. \n V\u00e6rmelding er ikke aktivert som standard. Du kan aktivere det i integrasjonsalternativene.",
"title": ""
}
}
},

View File

@ -4,7 +4,7 @@
"single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"invalid_api_key": "Nieprawid\u0142owy klucz API.",
"requests_exceeded": "Dozwolona liczba zapyta\u0144 do interfejsu API AccuWeather zosta\u0142a przekroczona. Musisz poczeka\u0107 lub zmieni\u0107 klucz API."
},

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "Langev",
"rising": "T\u00f5usev",
"steady": "\u00dchtlane"
}
}
}

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "En baisse",
"rising": "En hausse",
"steady": "Stable"
}
}
}

View File

@ -11,5 +11,6 @@
"title": "Velg en hub du vil legge til"
}
}
}
},
"title": ""
}

View File

@ -16,6 +16,7 @@
"data": {
"host": "Vert",
"password": "Passord",
"port": "",
"ssl": "AdGuard Hjem bruker et SSL-sertifikat",
"username": "Brukernavn",
"verify_ssl": "AdGuard Home bruker et riktig sertifikat"

View File

@ -5,7 +5,7 @@
"single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home."
},
"error": {
"connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia."
"connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"step": {
"hassio_confirm": {

View File

@ -4,6 +4,7 @@ import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_MOVING,
DEVICE_CLASSES_SCHEMA,
PLATFORM_SCHEMA,
BinarySensorEntity,
@ -43,7 +44,7 @@ class AdsBinarySensor(AdsEntity, BinarySensorEntity):
def __init__(self, ads_hub, name, ads_var, device_class):
"""Initialize ADS binary sensor."""
super().__init__(ads_hub, name, ads_var)
self._device_class = device_class or "moving"
self._device_class = device_class or DEVICE_CLASS_MOVING
async def async_added_to_hass(self):
"""Register device notification."""

View File

@ -10,7 +10,8 @@
"step": {
"user": {
"data": {
"host": "Vert"
"host": "Vert",
"port": ""
},
"title": "Konfigurere Agent DVR"
}

View File

@ -1,7 +1,7 @@
{
"config": {
"abort": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.",

View File

@ -0,0 +1,14 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.exclude_domain()

View File

@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"data": {
"latitude": "Laiuskraad",
"longitude": "Pikkuskraad"
}
}
}
}
}

View File

@ -15,7 +15,8 @@
"longitude": "Lengdegrad",
"name": "Navn p\u00e5 integrasjonen"
},
"description": "Sett opp Airly luftkvalitet integrasjon. For \u00e5 opprette API-n\u00f8kkel, g\u00e5 til [https://developer.airly.eu/register](https://developer.airly.eu/register)"
"description": "Sett opp Airly luftkvalitet integrasjon. For \u00e5 opprette API-n\u00f8kkel, g\u00e5 til [https://developer.airly.eu/register](https://developer.airly.eu/register)",
"title": ""
}
}
}

View File

@ -3,9 +3,11 @@
"step": {
"user": {
"data": {
"api_key": "",
"latitude": "Latitude",
"longitude": "Longitude"
}
},
"title": ""
}
}
}

View File

@ -3,8 +3,13 @@ import asyncio
from datetime import timedelta
from math import ceil
from pyairvisual import Client
from pyairvisual.errors import AirVisualError, NodeProError
from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import (
AirVisualError,
InvalidKeyError,
KeyExpiredError,
NodeProError,
)
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
@ -206,29 +211,36 @@ def _standardize_node_pro_config_entry(hass, config_entry):
async def async_setup_entry(hass, config_entry):
"""Set up AirVisual as config entry."""
websession = aiohttp_client.async_get_clientsession(hass)
if CONF_API_KEY in config_entry.data:
_standardize_geography_config_entry(hass, config_entry)
client = Client(api_key=config_entry.data[CONF_API_KEY], session=websession)
websession = aiohttp_client.async_get_clientsession(hass)
cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession)
async def async_update_data():
"""Get new data from the API."""
if CONF_CITY in config_entry.data:
api_coro = client.api.city(
api_coro = cloud_api.air_quality.city(
config_entry.data[CONF_CITY],
config_entry.data[CONF_STATE],
config_entry.data[CONF_COUNTRY],
)
else:
api_coro = client.api.nearest_city(
api_coro = cloud_api.air_quality.nearest_city(
config_entry.data[CONF_LATITUDE],
config_entry.data[CONF_LONGITUDE],
)
try:
return await api_coro
except (InvalidKeyError, KeyExpiredError):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "reauth"},
data=config_entry.data,
)
)
except AirVisualError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err
@ -254,17 +266,13 @@ async def async_setup_entry(hass, config_entry):
else:
_standardize_node_pro_config_entry(hass, config_entry)
client = Client(session=websession)
async def async_update_data():
"""Get new data from the API."""
try:
return await client.node.from_samba(
config_entry.data[CONF_IP_ADDRESS],
config_entry.data[CONF_PASSWORD],
include_history=False,
include_trends=False,
)
async with NodeSamba(
config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD]
) as node:
return await node.async_get_latest_measurements()
except NodeProError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err

View File

@ -40,9 +40,9 @@ class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity):
@property
def air_quality_index(self):
"""Return the Air Quality Index (AQI)."""
if self.coordinator.data["current"]["settings"]["is_aqi_usa"]:
return self.coordinator.data["current"]["measurements"]["aqi_us"]
return self.coordinator.data["current"]["measurements"]["aqi_cn"]
if self.coordinator.data["settings"]["is_aqi_usa"]:
return self.coordinator.data["measurements"]["aqi_us"]
return self.coordinator.data["measurements"]["aqi_cn"]
@property
def available(self):
@ -52,61 +52,59 @@ class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity):
@property
def carbon_dioxide(self):
"""Return the CO2 (carbon dioxide) level."""
return self.coordinator.data["current"]["measurements"].get("co2")
return self.coordinator.data["measurements"].get("co2")
@property
def device_info(self):
"""Return device registry information for this entity."""
return {
"identifiers": {
(DOMAIN, self.coordinator.data["current"]["serial_number"])
},
"name": self.coordinator.data["current"]["settings"]["node_name"],
"identifiers": {(DOMAIN, self.coordinator.data["serial_number"])},
"name": self.coordinator.data["settings"]["node_name"],
"manufacturer": "AirVisual",
"model": f'{self.coordinator.data["current"]["status"]["model"]}',
"model": f'{self.coordinator.data["status"]["model"]}',
"sw_version": (
f'Version {self.coordinator.data["current"]["status"]["system_version"]}'
f'{self.coordinator.data["current"]["status"]["app_version"]}'
f'Version {self.coordinator.data["status"]["system_version"]}'
f'{self.coordinator.data["status"]["app_version"]}'
),
}
@property
def name(self):
"""Return the name."""
node_name = self.coordinator.data["current"]["settings"]["node_name"]
node_name = self.coordinator.data["settings"]["node_name"]
return f"{node_name} Node/Pro: Air Quality"
@property
def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level."""
return self.coordinator.data["current"]["measurements"].get("pm2_5")
return self.coordinator.data["measurements"].get("pm2_5")
@property
def particulate_matter_10(self):
"""Return the particulate matter 10 level."""
return self.coordinator.data["current"]["measurements"].get("pm1_0")
return self.coordinator.data["measurements"].get("pm1_0")
@property
def particulate_matter_0_1(self):
"""Return the particulate matter 0.1 level."""
return self.coordinator.data["current"]["measurements"].get("pm0_1")
return self.coordinator.data["measurements"].get("pm0_1")
@property
def unique_id(self):
"""Return a unique, Home Assistant friendly identifier for this entity."""
return self.coordinator.data["current"]["serial_number"]
return self.coordinator.data["serial_number"]
@callback
def update_from_latest_data(self):
"""Update the entity from the latest data."""
self._attrs.update(
{
ATTR_VOC: self.coordinator.data["current"]["measurements"].get("voc"),
ATTR_VOC: self.coordinator.data["measurements"].get("voc"),
**{
ATTR_SENSOR_LIFE.format(pollutant): lifespan
for pollutant, lifespan in self.coordinator.data["current"][
"status"
]["sensor_life"].items()
for pollutant, lifespan in self.coordinator.data["status"][
"sensor_life"
].items()
},
}
)

View File

@ -1,7 +1,7 @@
"""Define a config flow manager for AirVisual."""
import asyncio
from pyairvisual import Client
from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import InvalidKeyError, NodeProError
import voluptuous as vol
@ -34,12 +34,19 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize the config flow."""
self._geo_id = None
self._latitude = None
self._longitude = None
self.api_key_data_schema = vol.Schema({vol.Required(CONF_API_KEY): str})
@property
def geography_schema(self):
"""Return the data schema for the cloud API."""
return vol.Schema(
return self.api_key_data_schema.extend(
{
vol.Required(CONF_API_KEY): str,
vol.Required(
CONF_LATITUDE, default=self.hass.config.latitude
): cv.latitude,
@ -85,8 +92,8 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
step_id="geography", data_schema=self.geography_schema
)
geo_id = async_get_geography_id(user_input)
await self._async_set_unique_id(geo_id)
self._geo_id = async_get_geography_id(user_input)
await self._async_set_unique_id(self._geo_id)
self._abort_if_unique_id_configured()
# Find older config entries without unique ID:
@ -95,13 +102,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
continue
if any(
geo_id == async_get_geography_id(geography)
self._geo_id == async_get_geography_id(geography)
for geography in entry.data[CONF_GEOGRAPHIES]
):
return self.async_abort(reason="already_configured")
websession = aiohttp_client.async_get_clientsession(self.hass)
client = Client(session=websession, api_key=user_input[CONF_API_KEY])
cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession)
# If this is the first (and only the first) time we've seen this API key, check
# that it's valid:
@ -113,7 +120,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async with check_keys_lock:
if user_input[CONF_API_KEY] not in checked_keys:
try:
await client.api.nearest_city()
await cloud_api.air_quality.nearest_city()
except InvalidKeyError:
return self.async_show_form(
step_id="geography",
@ -123,10 +130,19 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
checked_keys.add(user_input[CONF_API_KEY])
return self.async_create_entry(
title=f"Cloud API ({geo_id})",
data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY},
)
return await self.async_step_geography_finish(user_input)
async def async_step_geography_finish(self, user_input=None):
"""Handle the finalization of a Cloud API config entry."""
existing_entry = await self.async_set_unique_id(self._geo_id)
if existing_entry:
self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(
title=f"Cloud API ({self._geo_id})",
data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY},
)
async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
@ -141,16 +157,10 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
await self._async_set_unique_id(user_input[CONF_IP_ADDRESS])
websession = aiohttp_client.async_get_clientsession(self.hass)
client = Client(session=websession)
node = NodeSamba(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD])
try:
await client.node.from_samba(
user_input[CONF_IP_ADDRESS],
user_input[CONF_PASSWORD],
include_history=False,
include_trends=False,
)
await node.async_connect()
except NodeProError as err:
LOGGER.error("Error connecting to Node/Pro unit: %s", err)
return self.async_show_form(
@ -159,11 +169,37 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors={CONF_IP_ADDRESS: "unable_to_connect"},
)
await node.async_disconnect()
return self.async_create_entry(
title=f"Node/Pro ({user_input[CONF_IP_ADDRESS]})",
data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO},
)
async def async_step_reauth(self, data):
"""Handle configuration by re-auth."""
self._latitude = data[CONF_LATITUDE]
self._longitude = data[CONF_LONGITUDE]
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(self, user_input=None):
"""Handle re-auth completion."""
if not user_input:
return self.async_show_form(
step_id="reauth_confirm", data_schema=self.api_key_data_schema
)
conf = {
CONF_API_KEY: user_input[CONF_API_KEY],
CONF_LATITUDE: self._latitude,
CONF_LONGITUDE: self._longitude,
}
self._geo_id = async_get_geography_id(conf)
return await self.async_step_geography_finish(conf)
async def async_step_user(self, user_input=None):
"""Handle the start of the config flow."""
if not user_input:

View File

@ -3,6 +3,6 @@
"name": "AirVisual",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airvisual",
"requirements": ["pyairvisual==4.4.0"],
"requirements": ["pyairvisual==5.0.2"],
"codeowners": ["@bachya"]
}

View File

@ -38,10 +38,6 @@ ATTR_POLLUTANT_SYMBOL = "pollutant_symbol"
ATTR_POLLUTANT_UNIT = "pollutant_unit"
ATTR_REGION = "region"
MASS_PARTS_PER_MILLION = "ppm"
MASS_PARTS_PER_BILLION = "ppb"
VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m3"
SENSOR_KIND_LEVEL = "air_pollution_level"
SENSOR_KIND_AQI = "air_quality_index"
SENSOR_KIND_POLLUTANT = "main_pollutant"
@ -229,22 +225,20 @@ class AirVisualNodeProSensor(AirVisualEntity):
def device_info(self):
"""Return device registry information for this entity."""
return {
"identifiers": {
(DOMAIN, self.coordinator.data["current"]["serial_number"])
},
"name": self.coordinator.data["current"]["settings"]["node_name"],
"identifiers": {(DOMAIN, self.coordinator.data["serial_number"])},
"name": self.coordinator.data["settings"]["node_name"],
"manufacturer": "AirVisual",
"model": f'{self.coordinator.data["current"]["status"]["model"]}',
"model": f'{self.coordinator.data["status"]["model"]}',
"sw_version": (
f'Version {self.coordinator.data["current"]["status"]["system_version"]}'
f'{self.coordinator.data["current"]["status"]["app_version"]}'
f'Version {self.coordinator.data["status"]["system_version"]}'
f'{self.coordinator.data["status"]["app_version"]}'
),
}
@property
def name(self):
"""Return the name."""
node_name = self.coordinator.data["current"]["settings"]["node_name"]
node_name = self.coordinator.data["settings"]["node_name"]
return f"{node_name} Node/Pro: {self._name}"
@property
@ -255,18 +249,14 @@ class AirVisualNodeProSensor(AirVisualEntity):
@property
def unique_id(self):
"""Return a unique, Home Assistant friendly identifier for this entity."""
return f"{self.coordinator.data['current']['serial_number']}_{self._kind}"
return f"{self.coordinator.data['serial_number']}_{self._kind}"
@callback
def update_from_latest_data(self):
"""Update the entity from the latest data."""
if self._kind == SENSOR_KIND_BATTERY_LEVEL:
self._state = self.coordinator.data["current"]["status"]["battery"]
self._state = self.coordinator.data["status"]["battery"]
elif self._kind == SENSOR_KIND_HUMIDITY:
self._state = self.coordinator.data["current"]["measurements"].get(
"humidity"
)
self._state = self.coordinator.data["measurements"].get("humidity")
elif self._kind == SENSOR_KIND_TEMPERATURE:
self._state = self.coordinator.data["current"]["measurements"].get(
"temperature_C"
)
self._state = self.coordinator.data["measurements"].get("temperature_C")

View File

@ -0,0 +1,17 @@
{
"config": {
"step": {
"geography": {
"data": {
"latitude": "Laiuskraad",
"longitude": "Pikkuskraad"
}
},
"user": {
"data": {
"cloud_api": "Geograafiline asukoht"
}
}
}
}
}

View File

@ -29,6 +29,7 @@
"user": {
"data": {
"cloud_api": "Geografisk plassering",
"node_pro": "",
"type": "Integrasjonstype"
},
"description": "Velg hvilken type AirVisual-data du vil overv\u00e5ke.",

View File

@ -4,7 +4,7 @@
"already_configured": "Ten klucz API jest ju\u017c w u\u017cyciu."
},
"error": {
"general_error": "Nieoczekiwany b\u0142\u0105d.",
"general_error": "Nieoczekiwany b\u0142\u0105d",
"invalid_api_key": "Nieprawid\u0142owy klucz API.",
"unable_to_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z jednostk\u0105 Node/Pro."
},

View File

@ -6,6 +6,7 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_CODE,
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
CONF_CODE,
CONF_DEVICE_ID,
CONF_DOMAIN,
@ -56,7 +57,7 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
if state is None:
continue
supported_features = state.attributes["supported_features"]
supported_features = state.attributes[ATTR_SUPPORTED_FEATURES]
# Add actions for each entity that belongs to this integration
if supported_features & SUPPORT_ALARM_ARM_AWAY:

View File

@ -11,6 +11,7 @@ from homeassistant.components.alarm_control_panel.const import (
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
CONF_CONDITION,
CONF_DEVICE_ID,
CONF_DOMAIN,
@ -73,7 +74,7 @@ async def async_get_conditions(
if state is None:
continue
supported_features = state.attributes["supported_features"]
supported_features = state.attributes[ATTR_SUPPORTED_FEATURES]
# Add conditions for each entity that belongs to this integration
conditions += [

View File

@ -12,6 +12,7 @@ from homeassistant.components.automation import AutomationActionType
from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
from homeassistant.components.homeassistant.triggers import state as state_trigger
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
@ -64,7 +65,7 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
if entity_state is None:
continue
supported_features = entity_state.attributes["supported_features"]
supported_features = entity_state.attributes[ATTR_SUPPORTED_FEATURES]
# Add triggers for each entity that belongs to this integration
triggers += [

View File

@ -0,0 +1,31 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_TRIGGERED,
STATE_OFF,
)
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states(
{
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_TRIGGERED,
},
STATE_OFF,
)

View File

@ -1,4 +1,27 @@
{
"device_automation": {
"action_type": {
"arm_away": "Valvesta {entity_name}",
"arm_home": "Valvesta {entity_name} kodus re\u017eiimis",
"arm_night": "Valvesta {entity_name} \u00f6\u00f6re\u017eiimis",
"disarm": "V\u00f5ta {entity_name} valvest maha",
"trigger": "K\u00e4ivita {entity_name}"
},
"condition_type": {
"is_armed_away": "{entity_name} on valvestatud",
"is_armed_home": "{entity_name} on valvestatud kodure\u017eiimis",
"is_armed_night": "{entity_name} on valvestatud \u00f6\u00f6re\u017eiimis",
"is_disarmed": "{entity_name} on valve alt maas",
"is_triggered": "{entity_name} on h\u00e4iret andnud"
},
"trigger_type": {
"armed_away": "{entity_name} valvestatus",
"armed_home": "{entity_name} valvestatus kodure\u017eiimis",
"armed_night": "{entity_name} valvestatus \u00f6\u00f6re\u017eiimis",
"disarmed": "{entity_name} v\u00f5eti valvest maha",
"triggered": "{entity_name} andis h\u00e4iret"
}
},
"state": {
"_": {
"armed": "Valves",

View File

@ -25,7 +25,7 @@
"state": {
"_": {
"armed": "Ingeschakeld",
"armed_away": "Afwezig Ingeschakeld",
"armed_away": "Ingeschakeld afwezig",
"armed_custom_bypass": "Ingeschakeld met overbrugging(en)",
"armed_home": "Ingeschakeld thuis",
"armed_night": "Ingeschakeld nacht",

View File

@ -1,167 +1,82 @@
"""Support for AlarmDecoder devices."""
import asyncio
from datetime import timedelta
import logging
from adext import AdExt
from alarmdecoder.devices import SerialDevice, SocketDevice, USBDevice
from alarmdecoder.devices import SerialDevice, SocketDevice
from alarmdecoder.util import NoDeviceError
import voluptuous as vol
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_PORT,
CONF_PROTOCOL,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import dt as dt_util
from .const import (
CONF_DEVICE_BAUD,
CONF_DEVICE_PATH,
DATA_AD,
DATA_REMOVE_STOP_LISTENER,
DATA_REMOVE_UPDATE_LISTENER,
DATA_RESTART,
DOMAIN,
PROTOCOL_SERIAL,
PROTOCOL_SOCKET,
SIGNAL_PANEL_MESSAGE,
SIGNAL_REL_MESSAGE,
SIGNAL_RFX_MESSAGE,
SIGNAL_ZONE_FAULT,
SIGNAL_ZONE_RESTORE,
)
_LOGGER = logging.getLogger(__name__)
DOMAIN = "alarmdecoder"
DATA_AD = "alarmdecoder"
CONF_DEVICE = "device"
CONF_DEVICE_BAUD = "baudrate"
CONF_DEVICE_PATH = "path"
CONF_DEVICE_PORT = "port"
CONF_DEVICE_TYPE = "type"
CONF_AUTO_BYPASS = "autobypass"
CONF_PANEL_DISPLAY = "panel_display"
CONF_ZONE_NAME = "name"
CONF_ZONE_TYPE = "type"
CONF_ZONE_LOOP = "loop"
CONF_ZONE_RFID = "rfid"
CONF_ZONES = "zones"
CONF_RELAY_ADDR = "relayaddr"
CONF_RELAY_CHAN = "relaychan"
CONF_CODE_ARM_REQUIRED = "code_arm_required"
DEFAULT_DEVICE_TYPE = "socket"
DEFAULT_DEVICE_HOST = "localhost"
DEFAULT_DEVICE_PORT = 10000
DEFAULT_DEVICE_PATH = "/dev/ttyUSB0"
DEFAULT_DEVICE_BAUD = 115200
DEFAULT_AUTO_BYPASS = False
DEFAULT_PANEL_DISPLAY = False
DEFAULT_CODE_ARM_REQUIRED = True
DEFAULT_ZONE_TYPE = "opening"
SIGNAL_PANEL_MESSAGE = "alarmdecoder.panel_message"
SIGNAL_PANEL_ARM_AWAY = "alarmdecoder.panel_arm_away"
SIGNAL_PANEL_ARM_HOME = "alarmdecoder.panel_arm_home"
SIGNAL_PANEL_DISARM = "alarmdecoder.panel_disarm"
SIGNAL_ZONE_FAULT = "alarmdecoder.zone_fault"
SIGNAL_ZONE_RESTORE = "alarmdecoder.zone_restore"
SIGNAL_RFX_MESSAGE = "alarmdecoder.rfx_message"
SIGNAL_REL_MESSAGE = "alarmdecoder.rel_message"
DEVICE_SOCKET_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_TYPE): "socket",
vol.Optional(CONF_HOST, default=DEFAULT_DEVICE_HOST): cv.string,
vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port,
}
)
DEVICE_SERIAL_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_TYPE): "serial",
vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string,
vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string,
}
)
DEVICE_USB_SCHEMA = vol.Schema({vol.Required(CONF_DEVICE_TYPE): "usb"})
ZONE_SCHEMA = vol.Schema(
{
vol.Required(CONF_ZONE_NAME): cv.string,
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): vol.Any(
DEVICE_CLASSES_SCHEMA
),
vol.Optional(CONF_ZONE_RFID): cv.string,
vol.Optional(CONF_ZONE_LOOP): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
vol.Inclusive(
CONF_RELAY_ADDR,
"relaylocation",
"Relay address and channel must exist together",
): cv.byte,
vol.Inclusive(
CONF_RELAY_CHAN,
"relaylocation",
"Relay address and channel must exist together",
): cv.byte,
}
)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_DEVICE): vol.Any(
DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA, DEVICE_USB_SCHEMA
),
vol.Optional(
CONF_PANEL_DISPLAY, default=DEFAULT_PANEL_DISPLAY
): cv.boolean,
vol.Optional(CONF_AUTO_BYPASS, default=DEFAULT_AUTO_BYPASS): cv.boolean,
vol.Optional(
CONF_CODE_ARM_REQUIRED, default=DEFAULT_CODE_ARM_REQUIRED
): cv.boolean,
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
}
)
},
extra=vol.ALLOW_EXTRA,
)
PLATFORMS = ["alarm_control_panel", "sensor", "binary_sensor"]
def setup(hass, config):
async def async_setup(hass, config):
"""Set up for the AlarmDecoder devices."""
conf = config.get(DOMAIN)
return True
restart = False
device = conf[CONF_DEVICE]
display = conf[CONF_PANEL_DISPLAY]
auto_bypass = conf[CONF_AUTO_BYPASS]
code_arm_required = conf[CONF_CODE_ARM_REQUIRED]
zones = conf.get(CONF_ZONES)
device_type = device[CONF_DEVICE_TYPE]
host = DEFAULT_DEVICE_HOST
port = DEFAULT_DEVICE_PORT
path = DEFAULT_DEVICE_PATH
baud = DEFAULT_DEVICE_BAUD
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Set up AlarmDecoder config flow."""
undo_listener = entry.add_update_listener(_update_listener)
ad_connection = entry.data
protocol = ad_connection[CONF_PROTOCOL]
def stop_alarmdecoder(event):
"""Handle the shutdown of AlarmDecoder."""
if not hass.data.get(DOMAIN):
return
_LOGGER.debug("Shutting down alarmdecoder")
nonlocal restart
restart = False
hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False
controller.close()
def open_connection(now=None):
async def open_connection(now=None):
"""Open a connection to AlarmDecoder."""
nonlocal restart
try:
controller.open(baud)
await hass.async_add_executor_job(controller.open, baud)
except NoDeviceError:
_LOGGER.debug("Failed to connect. Retrying in 5 seconds")
hass.helpers.event.track_point_in_time(
_LOGGER.debug("Failed to connect. Retrying in 5 seconds")
hass.helpers.event.async_track_point_in_time(
open_connection, dt_util.utcnow() + timedelta(seconds=5)
)
return
_LOGGER.debug("Established a connection with the alarmdecoder")
restart = True
hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = True
def handle_closed_connection(event):
"""Restart after unexpected loss of connection."""
nonlocal restart
if not restart:
if not hass.data[DOMAIN][entry.entry_id][DATA_RESTART]:
return
restart = False
hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False
_LOGGER.warning("AlarmDecoder unexpectedly lost connection")
hass.add_job(open_connection)
@ -185,18 +100,14 @@ def setup(hass, config):
"""Handle relay or zone expander message from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message)
controller = False
if device_type == "socket":
host = device[CONF_HOST]
port = device[CONF_DEVICE_PORT]
baud = ad_connection.get(CONF_DEVICE_BAUD)
if protocol == PROTOCOL_SOCKET:
host = ad_connection[CONF_HOST]
port = ad_connection[CONF_PORT]
controller = AdExt(SocketDevice(interface=(host, port)))
elif device_type == "serial":
path = device[CONF_DEVICE_PATH]
baud = device[CONF_DEVICE_BAUD]
if protocol == PROTOCOL_SERIAL:
path = ad_connection[CONF_DEVICE_PATH]
controller = AdExt(SerialDevice(interface=path))
elif device_type == "usb":
AdExt(USBDevice.find())
return False
controller.on_message += handle_message
controller.on_rfx_message += handle_rfx_message
@ -205,24 +116,56 @@ def setup(hass, config):
controller.on_close += handle_closed_connection
controller.on_expander_message += handle_rel_message
hass.data[DATA_AD] = controller
open_connection()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
load_platform(
hass,
"alarm_control_panel",
DOMAIN,
{CONF_AUTO_BYPASS: auto_bypass, CONF_CODE_ARM_REQUIRED: code_arm_required},
config,
remove_stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder
)
if zones:
load_platform(hass, "binary_sensor", DOMAIN, {CONF_ZONES: zones}, config)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
DATA_AD: controller,
DATA_REMOVE_UPDATE_LISTENER: undo_listener,
DATA_REMOVE_STOP_LISTENER: remove_stop_listener,
DATA_RESTART: False,
}
if display:
load_platform(hass, "sensor", DOMAIN, conf, config)
await open_connection()
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Unload a AlarmDecoder entry."""
hass.data[DOMAIN][entry.entry_id][DATA_RESTART] = False
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if not unload_ok:
return False
hass.data[DOMAIN][entry.entry_id][DATA_REMOVE_UPDATE_LISTENER]()
hass.data[DOMAIN][entry.entry_id][DATA_REMOVE_STOP_LISTENER]()
await hass.async_add_executor_job(hass.data[DOMAIN][entry.entry_id][DATA_AD].close)
if hass.data[DOMAIN][entry.entry_id]:
hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
return True
async def _update_listener(hass: HomeAssistantType, entry: ConfigEntry):
"""Handle options update."""
_LOGGER.debug("AlarmDecoder options updated: %s", entry.as_dict()["options"])
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -12,6 +12,7 @@ from homeassistant.components.alarm_control_panel.const import (
SUPPORT_ALARM_ARM_HOME,
SUPPORT_ALARM_ARM_NIGHT,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_CODE,
STATE_ALARM_ARMED_AWAY,
@ -20,66 +21,70 @@ from homeassistant.const import (
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
from . import (
from .const import (
CONF_ALT_NIGHT_MODE,
CONF_AUTO_BYPASS,
CONF_CODE_ARM_REQUIRED,
DATA_AD,
DEFAULT_ARM_OPTIONS,
DOMAIN,
OPTIONS_ARM,
SIGNAL_PANEL_MESSAGE,
)
_LOGGER = logging.getLogger(__name__)
SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime"
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string})
SERVICE_ALARM_KEYPRESS = "alarm_keypress"
ATTR_KEYPRESS = "keypress"
ALARM_KEYPRESS_SCHEMA = vol.Schema({vol.Required(ATTR_KEYPRESS): cv.string})
def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
):
"""Set up for AlarmDecoder alarm panels."""
if discovery_info is None:
return
options = entry.options
arm_options = options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS)
client = hass.data[DOMAIN][entry.entry_id][DATA_AD]
auto_bypass = discovery_info[CONF_AUTO_BYPASS]
code_arm_required = discovery_info[CONF_CODE_ARM_REQUIRED]
entity = AlarmDecoderAlarmPanel(auto_bypass, code_arm_required)
add_entities([entity])
entity = AlarmDecoderAlarmPanel(
client=client,
auto_bypass=arm_options[CONF_AUTO_BYPASS],
code_arm_required=arm_options[CONF_CODE_ARM_REQUIRED],
alt_night_mode=arm_options[CONF_ALT_NIGHT_MODE],
)
async_add_entities([entity])
def alarm_toggle_chime_handler(service):
"""Register toggle chime handler."""
code = service.data.get(ATTR_CODE)
entity.alarm_toggle_chime(code)
platform = entity_platform.current_platform.get()
hass.services.register(
DOMAIN,
platform.async_register_entity_service(
SERVICE_ALARM_TOGGLE_CHIME,
alarm_toggle_chime_handler,
schema=ALARM_TOGGLE_CHIME_SCHEMA,
{
vol.Required(ATTR_CODE): cv.string,
},
"alarm_toggle_chime",
)
def alarm_keypress_handler(service):
"""Register keypress handler."""
keypress = service.data[ATTR_KEYPRESS]
entity.alarm_keypress(keypress)
hass.services.register(
DOMAIN,
platform.async_register_entity_service(
SERVICE_ALARM_KEYPRESS,
alarm_keypress_handler,
schema=ALARM_KEYPRESS_SCHEMA,
{
vol.Required(ATTR_KEYPRESS): cv.string,
},
"alarm_keypress",
)
class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
"""Representation of an AlarmDecoder-based alarm panel."""
def __init__(self, auto_bypass, code_arm_required):
def __init__(self, client, auto_bypass, code_arm_required, alt_night_mode):
"""Initialize the alarm panel."""
self._client = client
self._display = ""
self._name = "Alarm Panel"
self._state = None
@ -95,6 +100,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
self._zone_bypassed = None
self._auto_bypass = auto_bypass
self._code_arm_required = code_arm_required
self._alt_night_mode = alt_night_mode
async def async_added_to_hass(self):
"""Register callbacks."""
@ -180,11 +186,11 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
def alarm_disarm(self, code=None):
"""Send disarm command."""
if code:
self.hass.data[DATA_AD].send(f"{code!s}1")
self._client.send(f"{code!s}1")
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self.hass.data[DATA_AD].arm_away(
self._client.arm_away(
code=code,
code_arm_required=self._code_arm_required,
auto_bypass=self._auto_bypass,
@ -192,7 +198,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
def alarm_arm_home(self, code=None):
"""Send arm home command."""
self.hass.data[DATA_AD].arm_home(
self._client.arm_home(
code=code,
code_arm_required=self._code_arm_required,
auto_bypass=self._auto_bypass,
@ -200,18 +206,19 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
def alarm_arm_night(self, code=None):
"""Send arm night command."""
self.hass.data[DATA_AD].arm_night(
self._client.arm_night(
code=code,
code_arm_required=self._code_arm_required,
alt_night_mode=self._alt_night_mode,
auto_bypass=self._auto_bypass,
)
def alarm_toggle_chime(self, code=None):
"""Send toggle chime command."""
if code:
self.hass.data[DATA_AD].send(f"{code!s}9")
self._client.send(f"{code!s}9")
def alarm_keypress(self, keypress):
"""Send custom keypresses."""
if keypress:
self.hass.data[DATA_AD].send(keypress)
self._client.send(keypress)

View File

@ -2,20 +2,23 @@
import logging
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
from . import (
from .const import (
CONF_RELAY_ADDR,
CONF_RELAY_CHAN,
CONF_ZONE_LOOP,
CONF_ZONE_NAME,
CONF_ZONE_NUMBER,
CONF_ZONE_RFID,
CONF_ZONE_TYPE,
CONF_ZONES,
DEFAULT_ZONE_OPTIONS,
OPTIONS_ZONES,
SIGNAL_REL_MESSAGE,
SIGNAL_RFX_MESSAGE,
SIGNAL_ZONE_FAULT,
SIGNAL_ZONE_RESTORE,
ZONE_SCHEMA,
)
_LOGGER = logging.getLogger(__name__)
@ -30,27 +33,28 @@ ATTR_RF_LOOP4 = "rf_loop4"
ATTR_RF_LOOP1 = "rf_loop1"
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the AlarmDecoder binary sensor devices."""
configured_zones = discovery_info[CONF_ZONES]
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
):
"""Set up for AlarmDecoder sensor."""
devices = []
for zone_num in configured_zones:
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
zone_type = device_config_data[CONF_ZONE_TYPE]
zone_name = device_config_data[CONF_ZONE_NAME]
zone_rfid = device_config_data.get(CONF_ZONE_RFID)
zone_loop = device_config_data.get(CONF_ZONE_LOOP)
relay_addr = device_config_data.get(CONF_RELAY_ADDR)
relay_chan = device_config_data.get(CONF_RELAY_CHAN)
device = AlarmDecoderBinarySensor(
zones = entry.options.get(OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS)
entities = []
for zone_num in zones:
zone_info = zones[zone_num]
zone_type = zone_info[CONF_ZONE_TYPE]
zone_name = zone_info[CONF_ZONE_NAME]
zone_rfid = zone_info.get(CONF_ZONE_RFID)
zone_loop = zone_info.get(CONF_ZONE_LOOP)
relay_addr = zone_info.get(CONF_RELAY_ADDR)
relay_chan = zone_info.get(CONF_RELAY_CHAN)
entity = AlarmDecoderBinarySensor(
zone_num, zone_name, zone_type, zone_rfid, zone_loop, relay_addr, relay_chan
)
devices.append(device)
entities.append(entity)
add_entities(devices)
return True
async_add_entities(entities)
class AlarmDecoderBinarySensor(BinarySensorEntity):
@ -67,7 +71,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity):
relay_chan,
):
"""Initialize the binary_sensor."""
self._zone_number = zone_number
self._zone_number = int(zone_number)
self._zone_type = zone_type
self._state = None
self._name = zone_name
@ -117,6 +121,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity):
def device_state_attributes(self):
"""Return the state attributes."""
attr = {}
attr[CONF_ZONE_NUMBER] = self._zone_number
if self._rfid and self._rfstate is not None:
attr[ATTR_RF_BIT0] = bool(self._rfstate & 0x01)
attr[ATTR_RF_LOW_BAT] = bool(self._rfstate & 0x02)

View File

@ -0,0 +1,360 @@
"""Config flow for AlarmDecoder."""
import logging
from adext import AdExt
from alarmdecoder.devices import SerialDevice, SocketDevice
from alarmdecoder.util import NoDeviceError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.binary_sensor import DEVICE_CLASSES
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL
from homeassistant.core import callback
from .const import ( # pylint: disable=unused-import
CONF_ALT_NIGHT_MODE,
CONF_AUTO_BYPASS,
CONF_CODE_ARM_REQUIRED,
CONF_DEVICE_BAUD,
CONF_DEVICE_PATH,
CONF_RELAY_ADDR,
CONF_RELAY_CHAN,
CONF_ZONE_LOOP,
CONF_ZONE_NAME,
CONF_ZONE_NUMBER,
CONF_ZONE_RFID,
CONF_ZONE_TYPE,
DEFAULT_ARM_OPTIONS,
DEFAULT_DEVICE_BAUD,
DEFAULT_DEVICE_HOST,
DEFAULT_DEVICE_PATH,
DEFAULT_DEVICE_PORT,
DEFAULT_ZONE_OPTIONS,
DEFAULT_ZONE_TYPE,
DOMAIN,
OPTIONS_ARM,
OPTIONS_ZONES,
PROTOCOL_SERIAL,
PROTOCOL_SOCKET,
)
EDIT_KEY = "edit_selection"
EDIT_ZONES = "Zones"
EDIT_SETTINGS = "Arming Settings"
_LOGGER = logging.getLogger(__name__)
class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a AlarmDecoder config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
def __init__(self):
"""Initialize AlarmDecoder ConfigFlow."""
self.protocol = None
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for AlarmDecoder."""
return AlarmDecoderOptionsFlowHandler(config_entry)
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if user_input is not None:
self.protocol = user_input[CONF_PROTOCOL]
return await self.async_step_protocol()
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_PROTOCOL): vol.In(
[PROTOCOL_SOCKET, PROTOCOL_SERIAL]
),
}
),
)
async def async_step_protocol(self, user_input=None):
"""Handle AlarmDecoder protocol setup."""
errors = {}
if user_input is not None:
if _device_already_added(
self._async_current_entries(), user_input, self.protocol
):
return self.async_abort(reason="already_configured")
connection = {}
baud = None
if self.protocol == PROTOCOL_SOCKET:
host = connection[CONF_HOST] = user_input[CONF_HOST]
port = connection[CONF_PORT] = user_input[CONF_PORT]
title = f"{host}:{port}"
device = SocketDevice(interface=(host, port))
if self.protocol == PROTOCOL_SERIAL:
path = connection[CONF_DEVICE_PATH] = user_input[CONF_DEVICE_PATH]
baud = connection[CONF_DEVICE_BAUD] = user_input[CONF_DEVICE_BAUD]
title = path
device = SerialDevice(interface=path)
controller = AdExt(device)
def test_connection():
controller.open(baud)
controller.close()
try:
await self.hass.async_add_executor_job(test_connection)
return self.async_create_entry(
title=title, data={CONF_PROTOCOL: self.protocol, **connection}
)
except NoDeviceError:
errors["base"] = "service_unavailable"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception during AlarmDecoder setup")
errors["base"] = "unknown"
if self.protocol == PROTOCOL_SOCKET:
schema = vol.Schema(
{
vol.Required(CONF_HOST, default=DEFAULT_DEVICE_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_DEVICE_PORT): int,
}
)
if self.protocol == PROTOCOL_SERIAL:
schema = vol.Schema(
{
vol.Required(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): str,
vol.Required(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): int,
}
)
return self.async_show_form(
step_id="protocol",
data_schema=schema,
errors=errors,
)
class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle AlarmDecoder options."""
def __init__(self, config_entry: config_entries.ConfigEntry):
"""Initialize AlarmDecoder options flow."""
self.arm_options = config_entry.options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS)
self.zone_options = config_entry.options.get(
OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS
)
self.selected_zone = None
async def async_step_init(self, user_input=None):
"""Manage the options."""
if user_input is not None:
if user_input[EDIT_KEY] == EDIT_SETTINGS:
return await self.async_step_arm_settings()
if user_input[EDIT_KEY] == EDIT_ZONES:
return await self.async_step_zone_select()
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(EDIT_KEY, default=EDIT_SETTINGS): vol.In(
[EDIT_SETTINGS, EDIT_ZONES]
)
},
),
)
async def async_step_arm_settings(self, user_input=None):
"""Arming options form."""
if user_input is not None:
return self.async_create_entry(
title="",
data={OPTIONS_ARM: user_input, OPTIONS_ZONES: self.zone_options},
)
return self.async_show_form(
step_id="arm_settings",
data_schema=vol.Schema(
{
vol.Optional(
CONF_ALT_NIGHT_MODE,
default=self.arm_options[CONF_ALT_NIGHT_MODE],
): bool,
vol.Optional(
CONF_AUTO_BYPASS, default=self.arm_options[CONF_AUTO_BYPASS]
): bool,
vol.Optional(
CONF_CODE_ARM_REQUIRED,
default=self.arm_options[CONF_CODE_ARM_REQUIRED],
): bool,
},
),
)
async def async_step_zone_select(self, user_input=None):
"""Zone selection form."""
errors = _validate_zone_input(user_input)
if user_input is not None and not errors:
self.selected_zone = str(
int(user_input[CONF_ZONE_NUMBER])
) # remove leading zeros
return await self.async_step_zone_details()
return self.async_show_form(
step_id="zone_select",
data_schema=vol.Schema({vol.Required(CONF_ZONE_NUMBER): str}),
errors=errors,
)
async def async_step_zone_details(self, user_input=None):
"""Zone details form."""
errors = _validate_zone_input(user_input)
if user_input is not None and not errors:
zone_options = self.zone_options.copy()
zone_id = self.selected_zone
zone_options[zone_id] = _fix_input_types(user_input)
# Delete zone entry if zone_name is omitted
if CONF_ZONE_NAME not in zone_options[zone_id]:
zone_options.pop(zone_id)
return self.async_create_entry(
title="",
data={OPTIONS_ARM: self.arm_options, OPTIONS_ZONES: zone_options},
)
existing_zone_settings = self.zone_options.get(self.selected_zone, {})
return self.async_show_form(
step_id="zone_details",
description_placeholders={CONF_ZONE_NUMBER: self.selected_zone},
data_schema=vol.Schema(
{
vol.Optional(
CONF_ZONE_NAME,
description={
"suggested_value": existing_zone_settings.get(
CONF_ZONE_NAME
)
},
): str,
vol.Optional(
CONF_ZONE_TYPE,
default=existing_zone_settings.get(
CONF_ZONE_TYPE, DEFAULT_ZONE_TYPE
),
): vol.In(DEVICE_CLASSES),
vol.Optional(
CONF_ZONE_RFID,
description={
"suggested_value": existing_zone_settings.get(
CONF_ZONE_RFID
)
},
): str,
vol.Optional(
CONF_ZONE_LOOP,
description={
"suggested_value": existing_zone_settings.get(
CONF_ZONE_LOOP
)
},
): str,
vol.Optional(
CONF_RELAY_ADDR,
description={
"suggested_value": existing_zone_settings.get(
CONF_RELAY_ADDR
)
},
): str,
vol.Optional(
CONF_RELAY_CHAN,
description={
"suggested_value": existing_zone_settings.get(
CONF_RELAY_CHAN
)
},
): str,
}
),
errors=errors,
)
def _validate_zone_input(zone_input):
if not zone_input:
return {}
errors = {}
# CONF_RELAY_ADDR & CONF_RELAY_CHAN are inclusive
if (CONF_RELAY_ADDR in zone_input and CONF_RELAY_CHAN not in zone_input) or (
CONF_RELAY_ADDR not in zone_input and CONF_RELAY_CHAN in zone_input
):
errors["base"] = "relay_inclusive"
# The following keys must be int
for key in [CONF_ZONE_NUMBER, CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN]:
if key in zone_input:
try:
int(zone_input[key])
except ValueError:
errors[key] = "int"
# CONF_ZONE_LOOP depends on CONF_ZONE_RFID
if CONF_ZONE_LOOP in zone_input and CONF_ZONE_RFID not in zone_input:
errors[CONF_ZONE_LOOP] = "loop_rfid"
# CONF_ZONE_LOOP must be 1-4
if (
CONF_ZONE_LOOP in zone_input
and zone_input[CONF_ZONE_LOOP].isdigit()
and int(zone_input[CONF_ZONE_LOOP]) not in list(range(1, 5))
):
errors[CONF_ZONE_LOOP] = "loop_range"
return errors
def _fix_input_types(zone_input):
"""Convert necessary keys to int.
Since ConfigFlow inputs of type int cannot default to an empty string, we collect the values below as
strings and then convert them to ints.
"""
for key in [CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN]:
if key in zone_input:
zone_input[key] = int(zone_input[key])
return zone_input
def _device_already_added(current_entries, user_input, protocol):
"""Determine if entry has already been added to HA."""
user_host = user_input.get(CONF_HOST)
user_port = user_input.get(CONF_PORT)
user_path = user_input.get(CONF_DEVICE_PATH)
user_baud = user_input.get(CONF_DEVICE_BAUD)
for entry in current_entries:
entry_host = entry.data.get(CONF_HOST)
entry_port = entry.data.get(CONF_PORT)
entry_path = entry.data.get(CONF_DEVICE_PATH)
entry_baud = entry.data.get(CONF_DEVICE_BAUD)
if protocol == PROTOCOL_SOCKET:
if user_host == entry_host and user_port == entry_port:
return True
if protocol == PROTOCOL_SERIAL:
if user_baud == entry_baud and user_path == entry_path:
return True
return False

View File

@ -0,0 +1,49 @@
"""Constants for the AlarmDecoder component."""
CONF_ALT_NIGHT_MODE = "alt_night_mode"
CONF_AUTO_BYPASS = "auto_bypass"
CONF_CODE_ARM_REQUIRED = "code_arm_required"
CONF_DEVICE_BAUD = "device_baudrate"
CONF_DEVICE_PATH = "device_path"
CONF_RELAY_ADDR = "zone_relayaddr"
CONF_RELAY_CHAN = "zone_relaychan"
CONF_ZONE_LOOP = "zone_loop"
CONF_ZONE_NAME = "zone_name"
CONF_ZONE_NUMBER = "zone_number"
CONF_ZONE_RFID = "zone_rfid"
CONF_ZONE_TYPE = "zone_type"
DATA_AD = "alarmdecoder"
DATA_REMOVE_STOP_LISTENER = "rm_stop_listener"
DATA_REMOVE_UPDATE_LISTENER = "rm_update_listener"
DATA_RESTART = "restart"
DEFAULT_ALT_NIGHT_MODE = False
DEFAULT_AUTO_BYPASS = False
DEFAULT_CODE_ARM_REQUIRED = True
DEFAULT_DEVICE_BAUD = 115200
DEFAULT_DEVICE_HOST = "alarmdecoder"
DEFAULT_DEVICE_PATH = "/dev/ttyUSB0"
DEFAULT_DEVICE_PORT = 10000
DEFAULT_ZONE_TYPE = "window"
DEFAULT_ARM_OPTIONS = {
CONF_ALT_NIGHT_MODE: DEFAULT_ALT_NIGHT_MODE,
CONF_AUTO_BYPASS: DEFAULT_AUTO_BYPASS,
CONF_CODE_ARM_REQUIRED: DEFAULT_CODE_ARM_REQUIRED,
}
DEFAULT_ZONE_OPTIONS = {}
DOMAIN = "alarmdecoder"
OPTIONS_ARM = "arm_options"
OPTIONS_ZONES = "zone_options"
PROTOCOL_SERIAL = "serial"
PROTOCOL_SOCKET = "socket"
SIGNAL_PANEL_MESSAGE = "alarmdecoder.panel_message"
SIGNAL_REL_MESSAGE = "alarmdecoder.rel_message"
SIGNAL_RFX_MESSAGE = "alarmdecoder.rfx_message"
SIGNAL_ZONE_FAULT = "alarmdecoder.zone_fault"
SIGNAL_ZONE_RESTORE = "alarmdecoder.zone_restore"

View File

@ -3,5 +3,6 @@
"name": "AlarmDecoder",
"documentation": "https://www.home-assistant.io/integrations/alarmdecoder",
"requirements": ["adext==0.3"],
"codeowners": ["@ajschmidt8"]
"codeowners": ["@ajschmidt8"],
"config_flow": true
}

View File

@ -1,26 +1,29 @@
"""Support for AlarmDecoder sensors (Shows Panel Display)."""
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
from . import SIGNAL_PANEL_MESSAGE
from .const import SIGNAL_PANEL_MESSAGE
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up for AlarmDecoder sensor devices."""
_LOGGER.debug("AlarmDecoderSensor: setup_platform")
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
):
"""Set up for AlarmDecoder sensor."""
device = AlarmDecoderSensor(hass)
add_entities([device])
entity = AlarmDecoderSensor()
async_add_entities([entity])
return True
class AlarmDecoderSensor(Entity):
"""Representation of an AlarmDecoder keypad."""
def __init__(self, hass):
def __init__(self):
"""Initialize the alarm panel."""
self._display = ""
self._state = None

View File

@ -1,6 +1,9 @@
alarm_keypress:
description: Send custom keypresses to the alarm.
fields:
entity_id:
description: Name of alarm control panel to deliver keypress.
example: "alarm_control_panel.main"
keypress:
description: "String to send to the alarm panel."
example: "*71"
@ -8,6 +11,9 @@ alarm_keypress:
alarm_toggle_chime:
description: Send the alarm the toggle chime command.
fields:
entity_id:
description: Name of alarm control panel to toggle chime.
example: "alarm_control_panel.main"
code:
description: A required code to toggle the alarm control panel chime with.
example: 1234

View File

@ -0,0 +1,72 @@
{
"config": {
"step": {
"user": {
"title": "Choose AlarmDecoder Protocol",
"data": {
"protocol": "Protocol"
}
},
"protocol": {
"title": "Configure connection settings",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
"device_baudrate": "Device Baud Rate",
"device_path": "Device Path"
}
}
},
"error": {
"service_unavailable": "[%key:common::config_flow::error::cannot_connect%]"
},
"create_entry": { "default": "Successfully connected to AlarmDecoder." },
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
"step": {
"init": {
"title": "Configure AlarmDecoder",
"description": "What would you like to edit?",
"data": {
"edit_select": "Edit"
}
},
"arm_settings": {
"title": "Configure AlarmDecoder",
"data": {
"auto_bypass": "Auto Bypass on Arm",
"code_arm_required": "Code Required for Arming",
"alt_night_mode": "Alternative Night Mode"
}
},
"zone_select": {
"title": "Configure AlarmDecoder",
"description": "Enter the zone number you'd like to to add, edit, or remove.",
"data": {
"zone_number": "Zone Number"
}
},
"zone_details": {
"title": "Configure AlarmDecoder",
"description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.",
"data": {
"zone_name": "Zone Name",
"zone_type": "Zone Type",
"zone_rfid": "RF Serial",
"zone_loop": "RF Loop",
"zone_relayaddr": "Relay Address",
"zone_relaychan": "Relay Channel"
}
}
},
"error": {
"relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together.",
"int": "The field below must be an integer.",
"loop_rfid": "RF Loop cannot be used without RF Serial.",
"loop_range": "RF Loop must be an integer between 1 and 4."
}
}
}

View File

@ -0,0 +1,74 @@
{
"config": {
"abort": {
"already_configured": "El dispositiu ja est\u00e0 configurat"
},
"create_entry": {
"default": "S'ha connectat correctament amb AlarmDecoder."
},
"error": {
"service_unavailable": "Ha fallat la connexi\u00f3"
},
"step": {
"protocol": {
"data": {
"device_baudrate": "Velocitat, en baudis, del dispositiu",
"device_path": "Ruta del dispositiu",
"host": "Amfitri\u00f3",
"port": "Port"
},
"title": "Configuraci\u00f3 dels par\u00e0metres de connexi\u00f3"
},
"user": {
"data": {
"protocol": "Protocol"
},
"title": "Selecciona el protocol d'AlarmDecoder"
}
}
},
"options": {
"error": {
"int": "El camp seg\u00fcent ha de ser un nombre enter.",
"loop_range": "El bucle RF ha de ser un nombre enter entre 1 i 4.",
"loop_rfid": "El bucle RF no es pot utilitzar sense RF s\u00e8rie.",
"relay_inclusive": "L'adre\u00e7a i el canal de rel\u00e9 s\u00f3n codependents i s'han d'incloure junts."
},
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "Mode nocturn alternatiu",
"auto_bypass": "Bypass autom\u00e0tic en l'activaci\u00f3",
"code_arm_required": "Codi necessari per a l'activaci\u00f3"
},
"title": "Configuraci\u00f3 d'AlarmDecoder"
},
"init": {
"data": {
"edit_select": "Edita"
},
"description": "Qu\u00e8 voldries editar?",
"title": "Configuraci\u00f3 d'AlarmDecoder"
},
"zone_details": {
"data": {
"zone_loop": "Bucle RF",
"zone_name": "Nom de la zona",
"zone_relayaddr": "Adre\u00e7a del rel\u00e9",
"zone_relaychan": "Canal del rel\u00e9",
"zone_rfid": "RF s\u00e8rie",
"zone_type": "Tipus de zona"
},
"description": "Introdueix els detalls de la zona {zone_number}. Per suprimir la zona {zone_number}, deixa el nom de la zona en blanc.",
"title": "Configuraci\u00f3 d'AlarmDecoder"
},
"zone_select": {
"data": {
"zone_number": "N\u00famero de zona"
},
"description": "Introdueix el n\u00famero de zona que vulguis afegir, editar o eliminar.",
"title": "Configuraci\u00f3 d'AlarmDecoder"
}
}
}
}

View File

@ -0,0 +1,35 @@
{
"options": {
"step": {
"arm_settings": {
"title": "Konfigurovat AlarmDecoder"
},
"init": {
"data": {
"edit_select": "Upravit"
},
"description": "Co chcete upravit?",
"title": "Konfigurovat AlarmDecoder"
},
"zone_details": {
"data": {
"zone_loop": "RF Loop",
"zone_name": "N\u00e1zev z\u00f3ny",
"zone_relayaddr": "Relay adresa",
"zone_relaychan": "Relay kan\u00e1l",
"zone_rfid": "RF Serial",
"zone_type": "Typ z\u00f3ny"
},
"description": "Zadejte podrobnosti pro z\u00f3nu {zone_number}. Chcete-li odstranit z\u00f3nu {zone_number}, ponechejte n\u00e1zev z\u00f3ny pr\u00e1zdn\u00fd.",
"title": "Konfigurovat AlarmDecoder"
},
"zone_select": {
"data": {
"zone_number": "\u010c\u00edslo z\u00f3ny"
},
"description": "Zadejte \u010d\u00edslo z\u00f3ny, kterou chcete p\u0159idat, upravit nebo odstranit.",
"title": "Konfigurovat AlarmDecoder"
}
}
}
}

View File

@ -0,0 +1,48 @@
{
"config": {
"error": {
"service_unavailable": "Verbindung konnte nicht hergestellt werden"
},
"step": {
"protocol": {
"data": {
"host": "Host",
"port": "Port"
}
},
"user": {
"data": {
"protocol": "Protokoll"
}
}
}
},
"options": {
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "Alternativer Nachtmodus"
}
},
"init": {
"data": {
"edit_select": "Bearbeiten"
},
"description": "Was m\u00f6chtest du bearbeiten?"
},
"zone_details": {
"data": {
"zone_name": "Zonenname",
"zone_relayaddr": "Relais-Adresse",
"zone_type": "Zonentyp"
}
},
"zone_select": {
"data": {
"zone_number": "Zonennummer"
},
"description": "Geben Sie die Zonennummer ein, die Sie hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chten."
}
}
}
}

View File

@ -0,0 +1,34 @@
{
"config": {
"create_entry": {
"default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf AlarmDecoder."
},
"error": {
"service_unavailable": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2"
},
"step": {
"protocol": {
"data": {
"device_baudrate": "\u03a1\u03c5\u03b8\u03bc\u03cc\u03c2 Baud \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2",
"device_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2"
}
}
}
},
"options": {
"step": {
"zone_details": {
"data": {
"zone_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2"
},
"title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 AlarmDecoder"
},
"zone_select": {
"data": {
"zone_number": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2"
},
"title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 AlarmDecoder"
}
}
}
}

View File

@ -0,0 +1,74 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"create_entry": {
"default": "Successfully connected to AlarmDecoder."
},
"error": {
"service_unavailable": "Failed to connect"
},
"step": {
"protocol": {
"data": {
"device_baudrate": "Device Baud Rate",
"device_path": "Device Path",
"host": "Host",
"port": "Port"
},
"title": "Configure connection settings"
},
"user": {
"data": {
"protocol": "Protocol"
},
"title": "Choose AlarmDecoder Protocol"
}
}
},
"options": {
"error": {
"int": "The field below must be an integer.",
"loop_range": "RF Loop must be an integer between 1 and 4.",
"loop_rfid": "RF Loop cannot be used without RF Serial.",
"relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together."
},
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "Alternative Night Mode",
"auto_bypass": "Auto Bypass on Arm",
"code_arm_required": "Code Required for Arming"
},
"title": "Configure AlarmDecoder"
},
"init": {
"data": {
"edit_select": "Edit"
},
"description": "What would you like to edit?",
"title": "Configure AlarmDecoder"
},
"zone_details": {
"data": {
"zone_loop": "RF Loop",
"zone_name": "Zone Name",
"zone_relayaddr": "Relay Address",
"zone_relaychan": "Relay Channel",
"zone_rfid": "RF Serial",
"zone_type": "Zone Type"
},
"description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.",
"title": "Configure AlarmDecoder"
},
"zone_select": {
"data": {
"zone_number": "Zone Number"
},
"description": "Enter the zone number you'd like to to add, edit, or remove.",
"title": "Configure AlarmDecoder"
}
}
}
}

View File

@ -0,0 +1,74 @@
{
"config": {
"abort": {
"already_configured": "El dispositivo AlarmDecoder ya est\u00e1 configurado."
},
"create_entry": {
"default": "Conectado con \u00e9xito a AlarmDecoder."
},
"error": {
"service_unavailable": "No se pudo conectar"
},
"step": {
"protocol": {
"data": {
"device_baudrate": "Velocidad en baudios del dispositivo",
"device_path": "Ruta del dispositivo",
"host": "Host",
"port": "Puerto"
},
"title": "Configurar los ajustes de conexi\u00f3n"
},
"user": {
"data": {
"protocol": "Protocolo"
},
"title": "Elige el protocolo del AlarmDecoder"
}
}
},
"options": {
"error": {
"int": "El campo siguiente debe ser un n\u00famero entero.",
"loop_range": "El bucle RF debe ser un n\u00famero entero entre 1 y 4.",
"loop_rfid": "El bucle de RF no puede utilizarse sin el serie RF.",
"relay_inclusive": "La direcci\u00f3n de retransmisi\u00f3n y el canal de retransmisi\u00f3n son codependientes y deben incluirse a la vez."
},
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "Modo noche alternativo",
"auto_bypass": "Desv\u00edo autom\u00e1tico al armar",
"code_arm_required": "C\u00f3digo requerido para el armado"
},
"title": "Configurar AlarmDecoder"
},
"init": {
"data": {
"edit_select": "Editar"
},
"description": "\u00bfQu\u00e9 te gustar\u00eda editar?",
"title": "Configurar AlarmDecoder"
},
"zone_details": {
"data": {
"zone_loop": "Bucle RF",
"zone_name": "Nombre de zona",
"zone_relayaddr": "Direcci\u00f3n de retransmisi\u00f3n",
"zone_relaychan": "Canal de retransmisi\u00f3n",
"zone_rfid": "Serie RF",
"zone_type": "Tipo de zona"
},
"description": "Introduce los detalles para la zona {zona_number}. Para borrar la zona {zone_number}, deja el nombre de la zona en blanco.",
"title": "Configurar AlarmDecoder"
},
"zone_select": {
"data": {
"zone_number": "N\u00famero de zona"
},
"description": "Introduce el n\u00famero de zona que deseas a\u00f1adir, editar o eliminar.",
"title": "Configurar AlarmDecoder"
}
}
}
}

View File

@ -0,0 +1,49 @@
{
"config": {
"step": {
"user": {
"data": {
"protocol": "Protokoll"
}
}
}
},
"options": {
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "Alternatiivne \u00f6\u00f6re\u017eiim",
"auto_bypass": "Automaatne m\u00f6\u00f6daviik valvestamisel",
"code_arm_required": "Valvestamise kood"
},
"title": "Seadista AlarmDecoder"
},
"init": {
"data": {
"edit_select": "Muuda"
},
"description": "Mida Te soovite muuta?",
"title": "Seadista AlarmDecoder"
},
"zone_details": {
"data": {
"zone_loop": "RF silmus",
"zone_name": "Ala nimi",
"zone_relayaddr": "Relee aadress",
"zone_relaychan": "Relee kanalinumber",
"zone_rfid": "RF jada\u00fchendus",
"zone_type": "Ala t\u00fc\u00fcp"
},
"description": "Sisestage ala {zone_number} \u00fcksikasjad. Ala {zone_number} kustutamiseks j\u00e4tke ala nimi t\u00fchjaks.",
"title": "Seadista AlarmDecoder"
},
"zone_select": {
"data": {
"zone_number": "Ala number"
},
"description": "Sisestage ala number mida soovite lisada, muuta v\u00f5i eemaldada.",
"title": "Seadista AlarmDecoder"
}
}
}
}

View File

@ -0,0 +1,74 @@
{
"config": {
"abort": {
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
},
"create_entry": {
"default": "Connexion r\u00e9ussie \u00e0 AlarmDecoder."
},
"error": {
"service_unavailable": "\u00c9chec de connexion"
},
"step": {
"protocol": {
"data": {
"device_baudrate": "D\u00e9bit en bauds de l'appareil",
"device_path": "Chemin du p\u00e9riph\u00e9rique",
"host": "H\u00f4te",
"port": "Port"
},
"title": "Configurer les param\u00e8tres de connexion"
},
"user": {
"data": {
"protocol": "Protocole"
},
"title": "Choisissez le protocole AlarmDecoder"
}
}
},
"options": {
"error": {
"int": "Le champ ci-dessous doit \u00eatre un entier.",
"loop_range": "La boucle RF doit \u00eatre un entier compris entre 1 et 4.",
"loop_rfid": "La boucle RF ne peut pas \u00eatre utilis\u00e9e sans s\u00e9rie RF.",
"relay_inclusive": "L'adresse de relais et le canal de relais d\u00e9pendent du codage et doivent \u00eatre inclus ensemble."
},
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "Mode nuit alternatif",
"auto_bypass": "Bypass automatique \u00e0 l'armement",
"code_arm_required": "Code requis pour l'armement"
},
"title": "Configurer AlarmDecoder"
},
"init": {
"data": {
"edit_select": "Modifier"
},
"description": "Que voulez-vous modifier?",
"title": "Configurer AlarmDecoder"
},
"zone_details": {
"data": {
"zone_loop": "Boucle RF",
"zone_name": "Nom de zone",
"zone_relayaddr": "Adresse de relais",
"zone_relaychan": "Canal de relais",
"zone_rfid": "RF S\u00e9rie",
"zone_type": "Type de zone"
},
"description": "Entrez les d\u00e9tails de la zone {zone_number} . Pour supprimer la zone {zone_number} , laissez le nom de zone vide.",
"title": "Configurer AlarmDecoder"
},
"zone_select": {
"data": {
"zone_number": "Num\u00e9ro de zone"
},
"description": "Saisissez le num\u00e9ro de zone que vous souhaitez ajouter, modifier ou supprimer.",
"title": "Configurer AlarmDecoder"
}
}
}
}

View File

@ -0,0 +1,74 @@
{
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"create_entry": {
"default": "Collegato con successo ad AlarmDecoder."
},
"error": {
"service_unavailable": "Impossibile connettersi"
},
"step": {
"protocol": {
"data": {
"device_baudrate": "Velocit\u00e0 di trasmissione del dispositivo",
"device_path": "Percorso del dispositivo",
"host": "Host",
"port": "Porta"
},
"title": "Configurare le impostazioni di connessione"
},
"user": {
"data": {
"protocol": "Protocollo"
},
"title": "Scegliere il protocollo AlarmDecoder"
}
}
},
"options": {
"error": {
"int": "Il campo sottostante deve essere un numero intero.",
"loop_range": "Il Ciclo RF deve essere un numero intero compreso tra 1 e 4.",
"loop_rfid": "Il Ciclo RF non pu\u00f2 essere utilizzato senza il Seriale RF ",
"relay_inclusive": "L'indirizzo del rel\u00e8 e il canale del rel\u00e8 sono codipendenti e devono essere inclusi insieme."
},
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "Modalit\u00e0 notturna alternativa",
"auto_bypass": "Bypass automatico all'attivazione",
"code_arm_required": "Codice richiesto per l'attivazione"
},
"title": "Configurare AlarmDecoder"
},
"init": {
"data": {
"edit_select": "Modifica"
},
"description": "Cosa vorresti modificare?",
"title": "Configurare AlarmDecoder"
},
"zone_details": {
"data": {
"zone_loop": "Ciclo RF",
"zone_name": "Nome zona",
"zone_relayaddr": "Indirizzo rel\u00e8",
"zone_relaychan": "Canale rel\u00e8",
"zone_rfid": "Seriale RF",
"zone_type": "Tipo di zona"
},
"description": "Immettere i dettagli per la zona {zone_number}. Per eliminare la zona {zone_number}, lasciare vuoto il campo Nome zona.",
"title": "Configurare AlarmDecoder"
},
"zone_select": {
"data": {
"zone_number": "Numero di zona"
},
"description": "Immettere il numero di zona che si desidera aggiungere, modificare o rimuovere.",
"title": "Configurare AlarmDecoder"
}
}
}
}

View File

@ -0,0 +1,74 @@
{
"config": {
"abort": {
"already_configured": "\uc7a5\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4."
},
"create_entry": {
"default": "AlarmDecoder\uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4."
},
"error": {
"service_unavailable": "\uc5f0\uacb0 \uc2e4\ud328"
},
"step": {
"protocol": {
"data": {
"device_baudrate": "\uc7a5\uce58 \uc804\uc1a1 \uc18d\ub3c4",
"device_path": "\uc7a5\uce58 \uacbd\ub85c",
"host": "\ud638\uc2a4\ud2b8",
"port": "\ud3ec\ud2b8"
},
"title": "\uc5f0\uacb0 \uc124\uc815 \uad6c\uc131"
},
"user": {
"data": {
"protocol": "\ud504\ub85c\ud1a0\ucf5c"
},
"title": "AlarmDecoder \ud504\ub85c\ud1a0\ucf5c \uc120\ud0dd"
}
}
},
"options": {
"error": {
"int": "\uc544\ub798 \ud544\ub4dc\ub294 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.",
"loop_range": "RF \ub8e8\ud504\ub294 1\uc5d0\uc11c 4 \uc0ac\uc774\uc758 \uc815\uc218\uc5ec\uc57c \ud569\ub2c8\ub2e4.",
"loop_rfid": "RF \ub8e8\ud504\ub294 RF \uc2dc\ub9ac\uc5bc\uc5c6\uc774 \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
"relay_inclusive": "\ub9b4\ub808\uc774 \uc8fc\uc18c\uc640 \ub9b4\ub808\uc774 \ucc44\ub110\uc740 \uc11c\ub85c \uc758\uc874\uc801\uc774\uba70 \ud568\uaed8 \ud3ec\ud568\ub418\uc5b4\uc57c\ud569\ub2c8\ub2e4."
},
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "\ub300\uccb4 \uc57c\uac04 \ubaa8\ub4dc",
"auto_bypass": "\uacbd\ube44\uc911 \uc790\ub3d9 \uc6b0\ud68c",
"code_arm_required": "\uacbd\ube44\uc5d0 \ud544\uc694\ud55c \ucf54\ub4dc"
},
"title": "AlarmDecoder \uad6c\uc131"
},
"init": {
"data": {
"edit_select": "\ud3b8\uc9d1"
},
"description": "\ubb34\uc5c7\uc744 \ud3b8\uc9d1 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
"title": "AlarmDecoder \uad6c\uc131"
},
"zone_details": {
"data": {
"zone_loop": "RF \ub8e8\ud504",
"zone_name": "\uc601\uc5ed \uc774\ub984",
"zone_relayaddr": "\ub9b4\ub808\uc774 \uc8fc\uc18c",
"zone_relaychan": "\ub9b4\ub808\uc774 \ucc44\ub110",
"zone_rfid": "RF \uc2dc\ub9ac\uc5bc",
"zone_type": "\uc601\uc5ed \uc720\ud615"
},
"description": "{zone_number} \uc601\uc5ed\uc5d0 \ub300\ud55c \uc138\ubd80 \uc815\ubcf4\ub97c \uc785\ub825\ud569\ub2c8\ub2e4. {zone_number} \uc601\uc5ed\uc744 \uc0ad\uc81c\ud558\ub824\uba74 \uc601\uc5ed \uc774\ub984\uc744 \ube44\uc6cc \ub461\ub2c8\ub2e4.",
"title": "AlarmDecoder \uad6c\uc131"
},
"zone_select": {
"data": {
"zone_number": "\uad6c\uc5ed \ubc88\ud638"
},
"description": "\ucd94\uac00, \ud3b8\uc9d1 \ub610\ub294 \uc81c\uac70\ud560 \uc601\uc5ed \ubc88\ud638\ub97c \uc785\ub825\ud569\ub2c8\ub2e4.",
"title": "AlarmDecoder \uad6c\uc131"
}
}
}
}

View File

@ -0,0 +1,64 @@
{
"config": {
"abort": {
"already_configured": "Apparat ass scho konfigur\u00e9iert"
},
"error": {
"service_unavailable": "Feeler beim verbannen"
},
"step": {
"protocol": {
"data": {
"device_baudrate": "Apparat Baudrate",
"device_path": "Pad vum Apparat",
"host": "Host",
"port": "Port"
}
},
"user": {
"data": {
"protocol": "Protokoll"
}
}
}
},
"options": {
"error": {
"int": "D'Feld hei \u00ebnnen muss eng ganz Zuel sinn.",
"relay_inclusive": "Relais Adress a Relais Kanal sin vuneneen ofh\u00e4ngeg a musse mat abegraff sinn."
},
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "Alternative Nuecht Modus",
"auto_bypass": "Auto Bypass beim aktiv\u00e9ieren",
"code_arm_required": "Code erfuerderlech fir d'Aktiv\u00e9ierung"
},
"title": "AlarmDecoder konfigur\u00e9ieren"
},
"init": {
"data": {
"edit_select": "\u00c4nneren"
},
"description": "Wat w\u00eblls du \u00e4nneren?",
"title": "AlarmDecoder konfigur\u00e9ieren"
},
"zone_details": {
"data": {
"zone_loop": "RF Schleef",
"zone_name": "Numm vun der Zone",
"zone_relayaddr": "Relais Adresse",
"zone_relaychan": "Relais Kanal",
"zone_rfid": "RF Serielle",
"zone_type": "Type vun der Zone"
},
"title": "AlarmDecoder konfigur\u00e9ieren"
},
"zone_select": {
"data": {
"zone_number": "Zone Nummer"
}
}
}
}
}

View File

@ -0,0 +1,73 @@
{
"config": {
"abort": {
"already_configured": "AlarmDecoder-apparaat is al geconfigureerd."
},
"create_entry": {
"default": "Succesvol verbonden met AlarmDecoder."
},
"error": {
"service_unavailable": "Kon niet verbinden"
},
"step": {
"protocol": {
"data": {
"device_baudrate": "Baudrate van apparaat",
"device_path": "Apparaatpad",
"host": "Host",
"port": "Poort"
},
"title": "Configureer de verbindingsinstellingen"
},
"user": {
"data": {
"protocol": "Protocol"
},
"title": "Kies AlarmDecoder Protocol"
}
}
},
"options": {
"error": {
"int": "Het onderstaande veld moet een geheel getal zijn.",
"loop_range": "RF Lus moet een geheel getal zijn tussen 1 en 4.",
"loop_rfid": "RF Lus kan niet worden gebruikt zonder RF Serieel.",
"relay_inclusive": "Het relais-adres en het relais-kanaal zijn codeafhankelijk en moeten samen worden opgenomen."
},
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "Alternatieve nachtmodus",
"auto_bypass": "Automatische bypass bij inschakelen",
"code_arm_required": "Code vereist voor inschakelen"
},
"title": "Configureer AlarmDecoder"
},
"init": {
"data": {
"edit_select": "Bewerk"
},
"description": "Wat wilt u bewerken?",
"title": "Configureer AlarmDecoder"
},
"zone_details": {
"data": {
"zone_loop": "RF Lus",
"zone_name": "Zone naam",
"zone_relayaddr": "Relais Adres",
"zone_relaychan": "Relais Kanaal",
"zone_rfid": "RF Serieel",
"zone_type": "Zone Type"
},
"title": "Configureer AlarmDecoder"
},
"zone_select": {
"data": {
"zone_number": "Zone nummer"
},
"description": "Voer het zone nummer in dat u wilt toevoegen, bewerken of verwijderen.",
"title": "Configureer AlarmDecoder"
}
}
}
}

View File

@ -0,0 +1,74 @@
{
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert"
},
"create_entry": {
"default": "Vellykket koblet til AlarmDecoder."
},
"error": {
"service_unavailable": "Tilkobling mislyktes."
},
"step": {
"protocol": {
"data": {
"device_baudrate": "Baud-hastighet for enhet",
"device_path": "Bane til enheten",
"host": "Vert",
"port": "Port"
},
"title": "Konfigurer tilkoblingsinnstillinger"
},
"user": {
"data": {
"protocol": "Protokoll"
},
"title": "Velg AlarmDecoder Protokoll"
}
}
},
"options": {
"error": {
"int": "Feltet nedenfor m\u00e5 v\u00e6re et helt tall.",
"loop_range": "RF Loop m\u00e5 v\u00e6re et heltall mellom 1 og 4.",
"loop_rfid": "RF Loop kan ikke brukes uten RF Serial.",
"relay_inclusive": "Rel\u00e9adresse og rel\u00e9kanal er kodeavhengige og m\u00e5 inkluderes sammen."
},
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "Alternativ nattmodus",
"auto_bypass": "Auto bypass p\u00e5 Arm",
"code_arm_required": "Kode kreves for tilkobling"
},
"title": "Konfigurer AlarmDecoder"
},
"init": {
"data": {
"edit_select": "Rediger"
},
"description": "Hva \u00f8nsker du \u00e5 redigere?",
"title": "Konfigurer AlarmDecoder"
},
"zone_details": {
"data": {
"zone_loop": "RF Loop",
"zone_name": "Sonenavn",
"zone_relayaddr": "Rel\u00e9 adresse",
"zone_relaychan": "Rel\u00e9 kanal",
"zone_rfid": "RF seriell",
"zone_type": "Sone type"
},
"description": "Angi detaljer for sonen {zone_number}. Hvis du vil slette sonen {zone_number}, lar du Sonenavn st\u00e5 tomt.",
"title": "Konfigurer AlarmDecoder"
},
"zone_select": {
"data": {
"zone_number": "Sone nummer"
},
"description": "Angi sonenummeret du vil legge til, redigere eller fjerne.",
"title": "Konfigurer AlarmDecoder"
}
}
}
}

View File

@ -0,0 +1,37 @@
{
"config": {
"abort": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
},
"step": {
"protocol": {
"data": {
"host": "Nazwa hosta lub adres IP",
"port": "Port"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"edit_select": "Edytuj"
}
},
"zone_details": {
"data": {
"zone_relaychan": "Kana\u0142 przeka\u017anika"
}
},
"zone_select": {
"data": {
"zone_number": "Numer strefy"
}
}
}
}
}

View File

@ -0,0 +1,74 @@
{
"config": {
"abort": {
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
},
"create_entry": {
"default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a AlarmDecoder."
},
"error": {
"service_unavailable": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
},
"step": {
"protocol": {
"data": {
"device_baudrate": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430",
"device_path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443",
"host": "\u0425\u043e\u0441\u0442",
"port": "\u041f\u043e\u0440\u0442"
},
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f"
},
"user": {
"data": {
"protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b"
},
"title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b AlarmDecoder"
}
}
},
"options": {
"error": {
"int": "\u041f\u043e\u043b\u0435 \u043d\u0438\u0436\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u0446\u0435\u043b\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c.",
"loop_range": "RF Loop \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0446\u0435\u043b\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c \u043e\u0442 1 \u0434\u043e 4.",
"loop_rfid": "RF Loop \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0431\u0435\u0437 RF Serial.",
"relay_inclusive": "\u0410\u0434\u0440\u0435\u0441 \u0440\u0435\u043b\u0435 \u0438 \u043a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435 \u0432\u0437\u0430\u0438\u043c\u043e\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u044b \u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u043c\u0435\u0441\u0442\u0435."
},
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u043d\u043e\u0447\u043d\u043e\u0439 \u0440\u0435\u0436\u0438\u043c",
"auto_bypass": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u0440\u0438 \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443",
"code_arm_required": "\u041a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443"
},
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder"
},
"init": {
"data": {
"edit_select": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c"
},
"description": "\u0427\u0442\u043e \u0431\u044b \u0412\u044b \u0445\u043e\u0442\u0435\u043b\u0438 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c?",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder"
},
"zone_details": {
"data": {
"zone_loop": "RF Loop",
"zone_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0437\u043e\u043d\u044b",
"zone_relayaddr": "\u0410\u0434\u0440\u0435\u0441 \u0440\u0435\u043b\u0435",
"zone_relaychan": "\u041a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435",
"zone_rfid": "RF Serial",
"zone_type": "\u0422\u0438\u043f \u0437\u043e\u043d\u044b"
},
"description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0437\u043e\u043d\u044b {zone_number}. \u0427\u0442\u043e\u0431\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0437\u043e\u043d\u0443 {zone_number}, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \"\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0437\u043e\u043d\u044b\" \u043f\u0443\u0441\u0442\u044b\u043c.",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder"
},
"zone_select": {
"data": {
"zone_number": "\u041d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u044b"
},
"description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u044b, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c, \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0438\u043b\u0438 \u0443\u0434\u0430\u043b\u0438\u0442\u044c.",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AlarmDecoder"
}
}
}
}

View File

@ -0,0 +1,27 @@
{
"config": {
"step": {
"protocol": {
"data": {
"device_path": "Enhetsv\u00e4g"
},
"title": "Konfigurera anslutningsinst\u00e4llningar"
},
"user": {
"data": {
"protocol": "Protokoll"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"edit_select": "Redigera"
},
"description": "Vad vill du redigera?"
}
}
}
}

View File

@ -0,0 +1,74 @@
{
"config": {
"abort": {
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"create_entry": {
"default": "\u6210\u529f\u9023\u7dda\u81f3 AlarmDecoder\u3002"
},
"error": {
"service_unavailable": "\u9023\u7dda\u5931\u6557"
},
"step": {
"protocol": {
"data": {
"device_baudrate": "\u8a2d\u5099\u901a\u8a0a\u7387",
"device_path": "\u8a2d\u5099\u8def\u5f91",
"host": "\u4e3b\u6a5f\u7aef",
"port": "\u901a\u8a0a\u57e0"
},
"title": "\u8a2d\u5b9a\u9023\u7dda\u8a2d\u5b9a"
},
"user": {
"data": {
"protocol": "\u901a\u8a0a\u5354\u5b9a"
},
"title": "\u9078\u64c7 AlarmDecoder \u901a\u8a0a\u5354\u5b9a"
}
}
},
"options": {
"error": {
"int": "\u4e0b\u65b9\u6b04\u4f4d\u5fc5\u9808\u70ba\u6574\u6578\u3002",
"loop_range": "RF \u8ff4\u8def\u5fc5\u9808\u70ba\u4ecb\u65bc 1 \u81f3 4 \u9593\u7684\u6574\u6578\u3002",
"loop_rfid": "\u5982\u679c\u6c92\u6709 RF \u5e8f\u5217\u5247\u7121\u6cd5\u4f7f\u7528 RF \u8ff4\u8def\u3002",
"relay_inclusive": "\u4e2d\u7e7c\u5730\u5740\u8207\u4e2d\u7e7c\u983b\u9053\u70ba\u76f8\u4e92\u4f9d\u8cf4\uff0c\u4e26\u5fc5\u9808\u4e00\u8d77\u5305\u542b\u3002"
},
"step": {
"arm_settings": {
"data": {
"alt_night_mode": "\u66ff\u4ee3\u591c\u9593\u6a21\u5f0f",
"auto_bypass": "\u81ea\u52d5\u5ffd\u7565\u8b66\u6212",
"code_arm_required": "\u8b66\u6212\u9700\u8981\u4ee3\u78bc"
},
"title": "\u8a2d\u5b9a AlarmDecoder"
},
"init": {
"data": {
"edit_select": "\u7de8\u8f2f"
},
"description": "\u662f\u5426\u8981\u9032\u884c\u7de8\u8f2f\uff1f",
"title": "\u8a2d\u5b9a AlarmDecoder"
},
"zone_details": {
"data": {
"zone_loop": "RF \u8ff4\u8def",
"zone_name": "\u5340\u57df\u540d\u7a31",
"zone_relayaddr": "\u4e2d\u7e7c\u4f4d\u5740",
"zone_relaychan": "\u4e2d\u7e7c\u983b\u9053",
"zone_rfid": "RF \u5e8f\u5217",
"zone_type": "\u5340\u57df\u985e\u578b"
},
"description": "\u8f38\u5165\u5340\u57df {zone_number} \u8a73\u7d30\u8cc7\u6599\u3002\u6b32\u522a\u9664\u5340\u57df {zone_number}\uff0c\u4fdd\u6301\u5340\u57df\u540d\u7a31\u7a7a\u767d\u3002",
"title": "\u8a2d\u5b9a AlarmDecoder"
},
"zone_select": {
"data": {
"zone_number": "\u5340\u57df\u78bc"
},
"description": "\u8f38\u5165\u6240\u8981\u65b0\u589e\u3001\u7de8\u8f2f\u6216\u79fb\u9664\u7684\u5340\u57df\u78bc\u3002",
"title": "\u8a2d\u5b9a AlarmDecoder"
}
}
}
}

View File

@ -33,6 +33,7 @@ from homeassistant.const import (
CONF_NAME,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
__version__,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import network
@ -286,6 +287,12 @@ class AlexaEntity:
"friendlyName": self.friendly_name(),
"description": self.description(),
"manufacturerName": "Home Assistant",
"additionalAttributes": {
"manufacturer": "Home Assistant",
"model": self.entity.domain,
"softwareVersion": __version__,
"customIdentifier": self.entity_id,
},
}
locale = self.config.locale

View File

@ -17,6 +17,7 @@ from homeassistant.components import (
from homeassistant.components.climate import const as climate
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_ENTITY_PICTURE,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
SERVICE_ALARM_ARM_AWAY,
@ -1532,7 +1533,7 @@ async def async_api_initialize_camera_stream(hass, config, directive, context):
"""Process a InitializeCameraStreams request."""
entity = directive.entity
stream_source = await camera.async_request_stream(hass, entity.entity_id, fmt="hls")
camera_image = hass.states.get(entity.entity_id).attributes["entity_picture"]
camera_image = hass.states.get(entity.entity_id).attributes[ATTR_ENTITY_PICTURE]
try:
external_url = network.get_url(

View File

@ -6,7 +6,7 @@ import logging
import aiohttp
import async_timeout
from homeassistant.const import MATCH_ALL, STATE_ON
from homeassistant.const import HTTP_ACCEPTED, MATCH_ALL, STATE_ON
import homeassistant.util.dt as dt_util
from .const import API_CHANGE, Cause
@ -109,7 +109,7 @@ async def async_send_changereport_message(
_LOGGER.debug("Sent: %s", json.dumps(message_serialized))
_LOGGER.debug("Received (%s): %s", response.status, response_text)
if response.status == 202:
if response.status == HTTP_ACCEPTED:
return
response_json = json.loads(response_text)
@ -240,7 +240,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity):
_LOGGER.debug("Sent: %s", json.dumps(message_serialized))
_LOGGER.debug("Received (%s): %s", response.status, response_text)
if response.status == 202:
if response.status == HTTP_ACCEPTED:
return
response_json = json.loads(response_text)

View File

@ -1,7 +1,7 @@
{
"config": {
"step": {
"pick_implementation": { "title": "Pick Authentication Method" },
"pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" },
"hassio_confirm": {
"title": "Almond via Hass.io add-on",
"description": "Do you want to configure Home Assistant to connect to Almond provided by the Hass.io add-on: {addon}?"

View File

@ -3,7 +3,8 @@
"abort": {
"already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Almond.",
"cannot_connect": "No es pot connectar amb el servidor d'Almond.",
"missing_configuration": "Consulta la documentaci\u00f3 sobre com configurar Almond."
"missing_configuration": "Consulta la documentaci\u00f3 sobre com configurar Almond.",
"no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})"
},
"step": {
"hassio_confirm": {

View File

@ -3,7 +3,8 @@
"abort": {
"already_setup": "You can only configure one Almond account.",
"cannot_connect": "Unable to connect to the Almond server.",
"missing_configuration": "Please check the documentation on how to set up Almond."
"missing_configuration": "Please check the documentation on how to set up Almond.",
"no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})"
},
"step": {
"hassio_confirm": {

View File

@ -3,7 +3,8 @@
"abort": {
"already_setup": "S\u00f3lo puede configurar una cuenta de Almond.",
"cannot_connect": "No se puede conectar al servidor Almond.",
"missing_configuration": "Consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond."
"missing_configuration": "Consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond.",
"no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})"
},
"step": {
"hassio_confirm": {

View File

@ -3,7 +3,8 @@
"abort": {
"already_setup": "Vous ne pouvez configurer qu'un seul compte Almond",
"cannot_connect": "Impossible de se connecter au serveur Almond",
"missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond."
"missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond.",
"no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )"
},
"step": {
"hassio_confirm": {

View File

@ -3,7 +3,8 @@
"abort": {
"already_setup": "\u00c8 possibile configurare un solo account Almond.",
"cannot_connect": "Impossibile connettersi al server Almond.",
"missing_configuration": "Si prega di controllare la documentazione su come impostare Almond."
"missing_configuration": "Si prega di controllare la documentazione su come impostare Almond.",
"no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})"
},
"step": {
"hassio_confirm": {
@ -11,7 +12,7 @@
"title": "Almond tramite il componente aggiuntivo di Hass.io"
},
"pick_implementation": {
"title": "Seleziona metodo di autenticazione"
"title": "Scegli il metodo di autenticazione"
}
}
}

View File

@ -3,7 +3,8 @@
"abort": {
"already_setup": "\ud558\ub098\uc758 Almond \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
"missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694."
"missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.",
"no_url_available": "\uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc5d0\ub7ec\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 \ub3c4\uc6c0\ub9d0 \uc139\uc158\uc744 \ud655\uc778\ud558\uc138\uc694({docs_url})"
},
"step": {
"hassio_confirm": {

View File

@ -3,7 +3,8 @@
"abort": {
"already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Almond Kont konfigur\u00e9ieren.",
"cannot_connect": "Kann sech net mam Almond Server verbannen.",
"missing_configuration": "Kuckt w.e.g. Dokumentatioun iwwert d'ariichten vun Almond."
"missing_configuration": "Kuckt w.e.g. Dokumentatioun iwwert d'ariichten vun Almond.",
"no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})"
},
"step": {
"hassio_confirm": {

View File

@ -3,7 +3,8 @@
"abort": {
"already_setup": "U kunt slechts \u00e9\u00e9n Almond-account configureren.",
"cannot_connect": "Kan geen verbinding maken met de Almond-server.",
"missing_configuration": "Raadpleeg de documentatie over het instellen van Almond."
"missing_configuration": "Raadpleeg de documentatie over het instellen van Almond.",
"no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})"
},
"step": {
"hassio_confirm": {

View File

@ -3,11 +3,13 @@
"abort": {
"already_setup": "Du kan bare konfigurere en Almond konto.",
"cannot_connect": "Kan ikke koble til Almond-serveren.",
"missing_configuration": "Vennligst sjekk dokumentasjonen om hvordan du setter opp Almond."
"missing_configuration": "Vennligst sjekk dokumentasjonen om hvordan du setter opp Almond.",
"no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk {docs_url} ] ( {docs_url} )"
},
"step": {
"hassio_confirm": {
"description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io add-on: {addon}?"
"description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io add-on: {addon}?",
"title": ""
},
"pick_implementation": {
"title": "Velg godkjenningsmetode"

View File

@ -3,7 +3,8 @@
"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.",
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Almond.",
"missing_configuration": "\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 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 Almond."
"missing_configuration": "\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 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 Almond.",
"no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435."
},
"step": {
"hassio_confirm": {

View File

@ -3,7 +3,8 @@
"abort": {
"already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Almond \u5e33\u865f\u3002",
"cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Almond \u4f3a\u670d\u5668\u3002",
"missing_configuration": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a Almond\u3002"
"missing_configuration": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a Almond\u3002",
"no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})"
},
"step": {
"hassio_confirm": {

View File

@ -6,8 +6,10 @@ from aioambient import Client
from aioambient.errors import WebsocketError
import voluptuous as vol
from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
AREA_SQUARE_METERS,
ATTR_LOCATION,
ATTR_NAME,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@ -15,8 +17,10 @@ from homeassistant.const import (
CONF_API_KEY,
DEGREE,
EVENT_HOMEASSISTANT_STOP,
LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
PRESSURE_INHG,
SPEED_MILES_PER_HOUR,
TEMP_FAHRENHEIT,
)
@ -141,8 +145,8 @@ TYPE_WINDSPEEDMPH = "windspeedmph"
TYPE_YEARLYRAININ = "yearlyrainin"
SENSOR_TYPES = {
TYPE_24HOURRAININ: ("24 Hr Rain", "in", TYPE_SENSOR, None),
TYPE_BAROMABSIN: ("Abs Pressure", "inHg", TYPE_SENSOR, "pressure"),
TYPE_BAROMRELIN: ("Rel Pressure", "inHg", TYPE_SENSOR, "pressure"),
TYPE_BAROMABSIN: ("Abs Pressure", PRESSURE_INHG, TYPE_SENSOR, "pressure"),
TYPE_BAROMRELIN: ("Rel Pressure", PRESSURE_INHG, TYPE_SENSOR, "pressure"),
TYPE_BATT10: ("Battery 10", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATT1: ("Battery 1", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATT2: ("Battery 2", None, TYPE_BINARY_SENSOR, "battery"),
@ -175,16 +179,16 @@ SENSOR_TYPES = {
TYPE_LASTRAIN: ("Last Rain", None, TYPE_SENSOR, "timestamp"),
TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None),
TYPE_MONTHLYRAININ: ("Monthly Rain", "in", TYPE_SENSOR, None),
TYPE_RELAY10: ("Relay 10", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY1: ("Relay 1", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY2: ("Relay 2", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY3: ("Relay 3", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY4: ("Relay 4", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY5: ("Relay 5", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY6: ("Relay 6", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY7: ("Relay 7", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY8: ("Relay 8", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY9: ("Relay 9", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY10: ("Relay 10", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
TYPE_RELAY1: ("Relay 1", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
TYPE_RELAY2: ("Relay 2", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
TYPE_RELAY3: ("Relay 3", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
TYPE_RELAY4: ("Relay 4", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
TYPE_RELAY5: ("Relay 5", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
TYPE_RELAY6: ("Relay 6", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
TYPE_RELAY7: ("Relay 7", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
TYPE_RELAY8: ("Relay 8", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
TYPE_RELAY9: ("Relay 9", None, TYPE_BINARY_SENSOR, DEVICE_CLASS_CONNECTIVITY),
TYPE_SOILHUM10: ("Soil Humidity 10", PERCENTAGE, TYPE_SENSOR, "humidity"),
TYPE_SOILHUM1: ("Soil Humidity 1", PERCENTAGE, TYPE_SENSOR, "humidity"),
TYPE_SOILHUM2: ("Soil Humidity 2", PERCENTAGE, TYPE_SENSOR, "humidity"),
@ -205,8 +209,13 @@ SENSOR_TYPES = {
TYPE_SOILTEMP7F: ("Soil Temp 7", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
TYPE_SOILTEMP8F: ("Soil Temp 8", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
TYPE_SOILTEMP9F: ("Soil Temp 9", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
TYPE_SOLARRADIATION: ("Solar Rad", f"{POWER_WATT}/m^2", TYPE_SENSOR, None),
TYPE_SOLARRADIATION_LX: ("Solar Rad (lx)", "lx", TYPE_SENSOR, "illuminance"),
TYPE_SOLARRADIATION: (
"Solar Rad",
f"{POWER_WATT}/{AREA_SQUARE_METERS}",
TYPE_SENSOR,
None,
),
TYPE_SOLARRADIATION_LX: ("Solar Rad (lx)", LIGHT_LUX, TYPE_SENSOR, "illuminance"),
TYPE_TEMP10F: ("Temp 10", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
TYPE_TEMP1F: ("Temp 1", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
TYPE_TEMP2F: ("Temp 2", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),

View File

@ -1,5 +1,8 @@
"""Support for Android IP Webcam binary sensors."""
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_MOTION,
BinarySensorEntity,
)
from . import CONF_HOST, CONF_NAME, DATA_IP_WEBCAM, KEY_MAP, AndroidIPCamEntity
@ -47,4 +50,4 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorEntity):
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return "motion"
return DEVICE_CLASS_MOTION

View File

@ -502,14 +502,23 @@ class ADBDevice(MediaPlayerEntity):
return self._unique_id
@adb_decorator()
async def _adb_screencap(self):
"""Take a screen capture from the device."""
return await self.aftv.adb_screencap()
async def async_get_media_image(self):
"""Fetch current playing image."""
if not self._screencap or self.state in [STATE_OFF, None] or not self.available:
return None, None
media_data = await self.aftv.adb_screencap()
media_data = await self._adb_screencap()
if media_data:
return media_data, "image/png"
# If an exception occurred and the device is no longer available, write the state
if not self.available:
self.async_write_ha_state()
return None, None
@adb_decorator()

View File

@ -38,6 +38,7 @@ from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.state import AsyncTrackStates
from homeassistant.helpers.system_info import async_get_system_info
_LOGGER = logging.getLogger(__name__)
@ -45,6 +46,7 @@ ATTR_BASE_URL = "base_url"
ATTR_EXTERNAL_URL = "external_url"
ATTR_INTERNAL_URL = "internal_url"
ATTR_LOCATION_NAME = "location_name"
ATTR_INSTALLATION_TYPE = "installation_type"
ATTR_REQUIRES_API_PASSWORD = "requires_api_password"
ATTR_UUID = "uuid"
ATTR_VERSION = "version"
@ -181,6 +183,7 @@ class APIDiscoveryView(HomeAssistantView):
"""Get discovery information."""
hass = request.app["hass"]
uuid = await hass.helpers.instance_id.async_get()
system_info = await async_get_system_info(hass)
data = {
ATTR_UUID: uuid,
@ -188,6 +191,7 @@ class APIDiscoveryView(HomeAssistantView):
ATTR_EXTERNAL_URL: None,
ATTR_INTERNAL_URL: None,
ATTR_LOCATION_NAME: hass.config.location_name,
ATTR_INSTALLATION_TYPE: system_info[ATTR_INSTALLATION_TYPE],
# always needs authentication
ATTR_REQUIRES_API_PASSWORD: True,
ATTR_VERSION: __version__,

View File

@ -2,6 +2,6 @@
"domain": "apprise",
"name": "Apprise",
"documentation": "https://www.home-assistant.io/integrations/apprise",
"requirements": ["apprise==0.8.8"],
"requirements": ["apprise==0.8.9"],
"codeowners": ["@caronc"]
}

View File

@ -28,12 +28,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_service(hass, config, discovery_info=None):
"""Get the Apprise notification service."""
# Create our Apprise Asset Object
asset = apprise.AppriseAsset(async_mode=False)
# Create our Apprise Instance (reference our asset)
a_obj = apprise.Apprise(asset=asset)
a_obj = apprise.Apprise()
if config.get(CONF_FILE):
# Sourced from a Configuration File

View File

@ -12,7 +12,8 @@
},
"user": {
"data": {
"host": "Vert"
"host": "Vert",
"port": ""
},
"description": "Vennligst skriv inn vertsnavnet eller IP-adressen til enheten."
}

View File

@ -1,7 +1,7 @@
{
"config": {
"abort": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"step": {
"user": {

View File

@ -23,6 +23,12 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the Arduino component."""
_LOGGER.warning(
"The %s integration has been deprecated. Please move your "
"configuration to the firmata integration. "
"https://www.home-assistant.io/integrations/firmata",
DOMAIN,
)
port = config[DOMAIN][CONF_PORT]

View File

@ -3,6 +3,6 @@
"name": "Atag",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/atag/",
"requirements": ["pyatag==0.3.3.4"],
"requirements": ["pyatag==0.3.4.4"],
"codeowners": ["@MatsNL"]
}

View File

@ -16,5 +16,6 @@
"title": "Verbinding maken met het apparaat"
}
}
}
},
"title": ""
}

View File

@ -11,10 +11,12 @@
"user": {
"data": {
"email": "E-post (valgfritt)",
"host": "Vert"
"host": "Vert",
"port": ""
},
"title": "Koble til enheten"
}
}
}
},
"title": ""
}

View File

@ -3,13 +3,18 @@ import asyncio
import itertools
import logging
from aiohttp import ClientError
from aiohttp import ClientError, ClientResponseError
from august.authenticator import ValidationResult
from august.exceptions import AugustApiAIOHTTPError
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from homeassistant.const import (
CONF_PASSWORD,
CONF_TIMEOUT,
CONF_USERNAME,
HTTP_UNAUTHORIZED,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
import homeassistant.helpers.config_validation as cv
@ -29,7 +34,7 @@ from .const import (
MIN_TIME_BETWEEN_DETAIL_UPDATES,
VERIFICATION_CODE_KEY,
)
from .exceptions import InvalidAuth, RequireValidation
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
from .gateway import AugustGateway
from .subscriber import AugustSubscriberMixin
@ -113,10 +118,7 @@ async def async_setup_august(hass, config_entry, august_gateway):
await august_gateway.async_authenticate()
except RequireValidation:
await async_request_validation(hass, config_entry, august_gateway)
return False
except InvalidAuth:
_LOGGER.error("Password is no longer valid. Please set up August again")
return False
raise
# We still use the configurator to get a new 2fa code
# when needed since config_flow doesn't have a way
@ -171,8 +173,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
try:
await august_gateway.async_setup(entry.data)
return await async_setup_august(hass, entry, august_gateway)
except asyncio.TimeoutError as err:
except ClientResponseError as err:
if err.status == HTTP_UNAUTHORIZED:
_async_start_reauth(hass, entry)
return False
raise ConfigEntryNotReady from err
except InvalidAuth:
_async_start_reauth(hass, entry)
return False
except RequireValidation:
return False
except (CannotConnect, asyncio.TimeoutError) as err:
raise ConfigEntryNotReady from err
def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "reauth"},
data=entry.data,
)
)
_LOGGER.error("Password is no longer valid. Please reauthenticate")
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):

View File

@ -4,7 +4,7 @@ import logging
from august.authenticator import ValidationResult
import voluptuous as vol
from homeassistant import config_entries, core
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from .const import (
@ -19,18 +19,8 @@ from .gateway import AugustGateway
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_LOGIN_METHOD, default="phone"): vol.In(LOGIN_METHODS),
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
}
)
async def async_validate_input(
hass: core.HomeAssistant,
data,
august_gateway,
):
@ -79,6 +69,7 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Store an AugustGateway()."""
self._august_gateway = None
self.user_auth_details = {}
self._needs_reset = False
super().__init__()
async def async_step_user(self, user_input=None):
@ -87,30 +78,45 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._august_gateway = AugustGateway(self.hass)
errors = {}
if user_input is not None:
await self._august_gateway.async_setup(user_input)
combined_inputs = {**self.user_auth_details, **user_input}
await self._august_gateway.async_setup(combined_inputs)
if self._needs_reset:
self._needs_reset = False
await self._august_gateway.async_reset_authentication()
try:
info = await async_validate_input(
self.hass,
user_input,
combined_inputs,
self._august_gateway,
)
await self.async_set_unique_id(user_input[CONF_USERNAME])
return self.async_create_entry(title=info["title"], data=info["data"])
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except RequireValidation:
self.user_auth_details = user_input
self.user_auth_details.update(user_input)
return await self.async_step_validation()
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if not errors:
self.user_auth_details.update(user_input)
existing_entry = await self.async_set_unique_id(
combined_inputs[CONF_USERNAME]
)
if existing_entry:
self.hass.config_entries.async_update_entry(
existing_entry, data=info["data"]
)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=info["title"], data=info["data"])
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
step_id="user", data_schema=self._async_build_schema(), errors=errors
)
async def async_step_validation(self, user_input=None):
@ -135,3 +141,23 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
return await self.async_step_user(user_input)
async def async_step_reauth(self, data):
"""Handle configuration by re-auth."""
self.user_auth_details = dict(data)
self._needs_reset = True
return await self.async_step_user()
def _async_build_schema(self):
"""Generate the config flow schema."""
base_schema = {
vol.Required(CONF_LOGIN_METHOD, default="phone"): vol.In(LOGIN_METHODS),
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
}
for key in self.user_auth_details:
if key == CONF_PASSWORD or key not in base_schema:
continue
del base_schema[key]
return vol.Schema(base_schema)

View File

@ -2,12 +2,18 @@
import asyncio
import logging
import os
from aiohttp import ClientError
from aiohttp import ClientError, ClientResponseError
from august.api_async import ApiAsync
from august.authenticator_async import AuthenticationState, AuthenticatorAsync
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from homeassistant.const import (
CONF_PASSWORD,
CONF_TIMEOUT,
CONF_USERNAME,
HTTP_UNAUTHORIZED,
)
from homeassistant.helpers import aiohttp_client
from .const import (
@ -32,29 +38,14 @@ class AugustGateway:
self._access_token_cache_file = None
self._hass = hass
self._config = None
self._api = None
self._authenticator = None
self._authentication = None
@property
def authenticator(self):
"""August authentication object from py-august."""
return self._authenticator
@property
def authentication(self):
"""August authentication object from py-august."""
return self._authentication
self.api = None
self.authenticator = None
self.authentication = None
@property
def access_token(self):
"""Access token for the api."""
return self._authentication.access_token
@property
def api(self):
"""August api object from py-august."""
return self._api
return self.authentication.access_token
def config_entry(self):
"""Config entry."""
@ -78,12 +69,12 @@ class AugustGateway:
)
self._config = conf
self._api = ApiAsync(
self.api = ApiAsync(
self._aiohttp_session, timeout=self._config.get(CONF_TIMEOUT)
)
self._authenticator = AuthenticatorAsync(
self._api,
self.authenticator = AuthenticatorAsync(
self.api,
self._config[CONF_LOGIN_METHOD],
self._config[CONF_USERNAME],
self._config[CONF_PASSWORD],
@ -93,30 +84,47 @@ class AugustGateway:
),
)
await self._authenticator.async_setup_authentication()
await self.authenticator.async_setup_authentication()
async def async_authenticate(self):
"""Authenticate with the details provided to setup."""
self._authentication = None
self.authentication = None
try:
self._authentication = await self.authenticator.async_authenticate()
self.authentication = await self.authenticator.async_authenticate()
if self.authentication.state == AuthenticationState.AUTHENTICATED:
# Call the locks api to verify we are actually
# authenticated because we can be authenticated
# by have no access
await self.api.async_get_operable_locks(self.access_token)
except ClientResponseError as ex:
if ex.status == HTTP_UNAUTHORIZED:
raise InvalidAuth from ex
raise CannotConnect from ex
except ClientError as ex:
_LOGGER.error("Unable to connect to August service: %s", str(ex))
raise CannotConnect from ex
if self._authentication.state == AuthenticationState.BAD_PASSWORD:
if self.authentication.state == AuthenticationState.BAD_PASSWORD:
raise InvalidAuth
if self._authentication.state == AuthenticationState.REQUIRES_VALIDATION:
if self.authentication.state == AuthenticationState.REQUIRES_VALIDATION:
raise RequireValidation
if self._authentication.state != AuthenticationState.AUTHENTICATED:
_LOGGER.error(
"Unknown authentication state: %s", self._authentication.state
)
if self.authentication.state != AuthenticationState.AUTHENTICATED:
_LOGGER.error("Unknown authentication state: %s", self.authentication.state)
raise InvalidAuth
return self._authentication
return self.authentication
async def async_reset_authentication(self):
"""Remove the cache file."""
await self._hass.async_add_executor_job(self._reset_authentication)
def _reset_authentication(self):
"""Remove the cache file."""
if os.path.exists(self._access_token_cache_file):
os.unlink(self._access_token_cache_file)
async def async_refresh_access_token_if_needed(self):
"""Refresh the august access token if needed."""
@ -130,4 +138,4 @@ class AugustGateway:
self.authentication.access_token_expires,
refreshed_authentication.access_token_expires,
)
self._authentication = refreshed_authentication
self.authentication = refreshed_authentication

View File

@ -6,7 +6,8 @@
"invalid_auth": "Invalid authentication"
},
"abort": {
"already_configured": "Account is already configured"
"already_configured": "Account is already configured",
"reauth_successful": "Re-authentication was successful"
},
"step": {
"validation": {

View File

@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "El compte ja ha estat configurat"
"already_configured": "El compte ja ha estat configurat",
"reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
},
"error": {
"cannot_connect": "No s'ha pogut connectar, torna-ho a provar",

View File

@ -0,0 +1,7 @@
{
"config": {
"abort": {
"reauth_successful": "\u0397 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2"
}
}
}

View File

@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "Account is already configured"
"already_configured": "Account is already configured",
"reauth_successful": "Re-authentication was successful"
},
"error": {
"cannot_connect": "Failed to connect, please try again",

View File

@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "La cuenta ya est\u00e1 configurada"
"already_configured": "La cuenta ya est\u00e1 configurada",
"reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente"
},
"error": {
"cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.",

View File

@ -0,0 +1,7 @@
{
"config": {
"abort": {
"reauth_successful": "Taasautentimine \u00f5nnestus"
}
}
}

View File

@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9"
"already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9",
"reauth_successful": "La r\u00e9-authentification a r\u00e9ussi"
},
"error": {
"cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer",

View File

@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "L'account \u00e8 gi\u00e0 configurato"
"already_configured": "L'account \u00e8 gi\u00e0 configurato",
"reauth_successful": "La riautenticazione ha avuto successo"
},
"error": {
"cannot_connect": "Impossibile connettersi, si prega di riprovare.",

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