mirror of
https://github.com/home-assistant/core.git
synced 2025-08-07 14:45:09 +02:00
Merge remote-tracking branch 'refs/remotes/home-assistant/dev' into dev
This commit is contained in:
49
.github/ISSUE_TEMPLATE.md
vendored
Normal file
49
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
<!-- READ THIS FIRST:
|
||||
- If you need additional help with this template, please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/core/releases
|
||||
- Do not report issues for integrations if you are using custom components or integrations.
|
||||
- Provide as many details as possible. Paste logs, configuration samples and code into the backticks.
|
||||
DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment.
|
||||
-->
|
||||
## The problem
|
||||
<!--
|
||||
Describe the issue you are experiencing here to communicate to the
|
||||
maintainers. Tell us what you were trying to do and what happened.
|
||||
-->
|
||||
|
||||
|
||||
## Environment
|
||||
<!--
|
||||
Provide details about the versions you are using, which helps us to reproduce
|
||||
and find the issue quicker. Version information is found in the
|
||||
Home Assistant frontend: Developer tools -> Info.
|
||||
-->
|
||||
|
||||
- Home Assistant Core release with the issue:
|
||||
- Last working Home Assistant Core release (if known):
|
||||
- Operating environment (Home Assistant/Supervised/Docker/venv):
|
||||
- Integration causing this issue:
|
||||
- Link to integration documentation on our website:
|
||||
|
||||
## Problem-relevant `configuration.yaml`
|
||||
<!--
|
||||
An example configuration that caused the problem for you. Fill this out even
|
||||
if it seems unimportant to you. Please be sure to remove personal information
|
||||
like passwords, private URLs and other credentials.
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
## Traceback/Error logs
|
||||
<!--
|
||||
If you come across any trace or error logs, please provide them.
|
||||
-->
|
||||
|
||||
```txt
|
||||
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
14
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
14
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@@ -1,10 +1,10 @@
|
||||
---
|
||||
name: Report a bug with Home Assistant
|
||||
about: Report an issue with Home Assistant
|
||||
name: Report a bug with Home Assistant Core
|
||||
about: Report an issue with Home Assistant Core
|
||||
---
|
||||
<!-- READ THIS FIRST:
|
||||
- If you need additional help with this template, please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/core/releases
|
||||
- Do not report issues for integrations if you are using custom components or integrations.
|
||||
- Provide as many details as possible. Paste logs, configuration samples and code into the backticks.
|
||||
DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment.
|
||||
@@ -12,7 +12,7 @@ about: Report an issue with Home Assistant
|
||||
## The problem
|
||||
<!--
|
||||
Describe the issue you are experiencing here to communicate to the
|
||||
maintainers. Tell us what you were trying to do and what happened instead.
|
||||
maintainers. Tell us what you were trying to do and what happened.
|
||||
-->
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ about: Report an issue with Home Assistant
|
||||
Home Assistant frontend: Developer tools -> Info.
|
||||
-->
|
||||
|
||||
- Home Assistant release with the issue:
|
||||
- Last working Home Assistant release (if known):
|
||||
- Operating environment (Hass.io/Docker/Windows/etc.):
|
||||
- Home Assistant Core release with the issue:
|
||||
- Last working Home Assistant Core release (if known):
|
||||
- Operating environment (Home Assistant/Supervised/Docker/venv):
|
||||
- Integration causing this issue:
|
||||
- Link to integration documentation on our website:
|
||||
|
||||
|
@@ -187,6 +187,7 @@ homeassistant/components/intesishome/* @jnimmo
|
||||
homeassistant/components/ios/* @robbiet480
|
||||
homeassistant/components/iperf3/* @rohankapoorcom
|
||||
homeassistant/components/ipma/* @dgomes @abmantis
|
||||
homeassistant/components/ipp/* @ctalkington
|
||||
homeassistant/components/iqvia/* @bachya
|
||||
homeassistant/components/irish_rail_transport/* @ttroy50
|
||||
homeassistant/components/izone/* @Swamp-Ig
|
||||
@@ -369,7 +370,7 @@ homeassistant/components/switchmate/* @danielhiversen
|
||||
homeassistant/components/syncthru/* @nielstron
|
||||
homeassistant/components/synology_srm/* @aerialls
|
||||
homeassistant/components/syslog/* @fabaff
|
||||
homeassistant/components/tado/* @michaelarnauts
|
||||
homeassistant/components/tado/* @michaelarnauts @bdraco
|
||||
homeassistant/components/tahoma/* @philklei
|
||||
homeassistant/components/tankerkoenig/* @guillempages
|
||||
homeassistant/components/tautulli/* @ludeeus
|
||||
|
@@ -215,12 +215,14 @@ class AuthManager:
|
||||
|
||||
return user
|
||||
|
||||
async def async_create_user(self, name: str) -> models.User:
|
||||
async def async_create_user(
|
||||
self, name: str, group_ids: Optional[List[str]] = None
|
||||
) -> models.User:
|
||||
"""Create a user."""
|
||||
kwargs: Dict[str, Any] = {
|
||||
"name": name,
|
||||
"is_active": True,
|
||||
"group_ids": [GROUP_ID_ADMIN],
|
||||
"group_ids": group_ids or [],
|
||||
}
|
||||
|
||||
if await self._user_should_be_owner():
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Questa chiave API \u00e8 gi\u00e0 in uso."
|
||||
"already_configured": "Queste coordinate sono gi\u00e0 state registrate."
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "Chiave API non valida"
|
||||
|
@@ -41,7 +41,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DATA_CONFIG = "config"
|
||||
|
||||
DEFAULT_SOCKET_MIN_RETRY = 15
|
||||
DEFAULT_WATCHDOG_SECONDS = 5 * 60
|
||||
|
||||
TYPE_24HOURRAININ = "24hourrainin"
|
||||
TYPE_BAROMABSIN = "baromabsin"
|
||||
@@ -342,7 +341,6 @@ class AmbientStation:
|
||||
self._config_entry = config_entry
|
||||
self._entry_setup_complete = False
|
||||
self._hass = hass
|
||||
self._watchdog_listener = None
|
||||
self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
|
||||
self.client = client
|
||||
self.stations = {}
|
||||
@@ -359,21 +357,9 @@ class AmbientStation:
|
||||
async def ws_connect(self):
|
||||
"""Register handlers and connect to the websocket."""
|
||||
|
||||
async def _ws_reconnect(event_time):
|
||||
"""Forcibly disconnect from and reconnect to the websocket."""
|
||||
_LOGGER.debug("Watchdog expired; forcing socket reconnection")
|
||||
await self.client.websocket.disconnect()
|
||||
await self._attempt_connect()
|
||||
|
||||
def on_connect():
|
||||
"""Define a handler to fire when the websocket is connected."""
|
||||
_LOGGER.info("Connected to websocket")
|
||||
_LOGGER.debug("Watchdog starting")
|
||||
if self._watchdog_listener is not None:
|
||||
self._watchdog_listener()
|
||||
self._watchdog_listener = async_call_later(
|
||||
self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect
|
||||
)
|
||||
|
||||
def on_data(data):
|
||||
"""Define a handler to fire when the data is received."""
|
||||
@@ -385,12 +371,6 @@ class AmbientStation:
|
||||
self._hass, f"ambient_station_data_update_{mac_address}"
|
||||
)
|
||||
|
||||
_LOGGER.debug("Resetting watchdog")
|
||||
self._watchdog_listener()
|
||||
self._watchdog_listener = async_call_later(
|
||||
self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect
|
||||
)
|
||||
|
||||
def on_disconnect():
|
||||
"""Define a handler to fire when the websocket is disconnected."""
|
||||
_LOGGER.info("Disconnected from websocket")
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"name": "Ambient Weather Station",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ambient_station",
|
||||
"requirements": ["aioambient==1.0.4"],
|
||||
"requirements": ["aioambient==1.1.0"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@bachya"]
|
||||
}
|
||||
|
@@ -1021,16 +1021,16 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
async def async_volume_up(self):
|
||||
"""Volume up the media player."""
|
||||
current_vol = self.volume_level
|
||||
if not current_vol or current_vol < 0:
|
||||
if not current_vol or current_vol >= 1:
|
||||
return
|
||||
return self.async_set_volume_level(((current_vol * 100) + 1) / 100)
|
||||
return await self.async_set_volume_level(current_vol + 0.01)
|
||||
|
||||
async def async_volume_down(self):
|
||||
"""Volume down the media player."""
|
||||
current_vol = self.volume_level
|
||||
if not current_vol or current_vol < 0:
|
||||
if not current_vol or current_vol <= 0:
|
||||
return
|
||||
return self.async_set_volume_level(((current_vol * 100) - 1) / 100)
|
||||
return await self.async_set_volume_level(current_vol - 0.01)
|
||||
|
||||
async def async_set_volume_level(self, volume):
|
||||
"""Send volume_up command to media player."""
|
||||
|
@@ -13,11 +13,6 @@ SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||
{vol.Required("type"): WS_TYPE_DELETE, vol.Required("user_id"): str}
|
||||
)
|
||||
|
||||
WS_TYPE_CREATE = "config/auth/create"
|
||||
SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||
{vol.Required("type"): WS_TYPE_CREATE, vol.Required("name"): str}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Enable the Home Assistant views."""
|
||||
@@ -27,9 +22,7 @@ async def async_setup(hass):
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_DELETE, websocket_delete, SCHEMA_WS_DELETE
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_CREATE, websocket_create, SCHEMA_WS_CREATE
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(websocket_create)
|
||||
hass.components.websocket_api.async_register_command(websocket_update)
|
||||
return True
|
||||
|
||||
@@ -70,9 +63,16 @@ async def websocket_delete(hass, connection, msg):
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "config/auth/create",
|
||||
vol.Required("name"): str,
|
||||
vol.Optional("group_ids"): [str],
|
||||
}
|
||||
)
|
||||
async def websocket_create(hass, connection, msg):
|
||||
"""Create a user."""
|
||||
user = await hass.auth.async_create_user(msg["name"])
|
||||
user = await hass.auth.async_create_user(msg["name"], msg.get("group_ids"))
|
||||
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg["id"], {"user": _user_info(user)})
|
||||
|
@@ -2,7 +2,11 @@
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"close": "Schlie\u00dfe {entity_name}",
|
||||
"open": "\u00d6ffne {entity_name}"
|
||||
"close_tilt": "{entity_name} gekippt schlie\u00dfen",
|
||||
"open": "\u00d6ffne {entity_name}",
|
||||
"open_tilt": "{entity_name} gekippt \u00f6ffnen",
|
||||
"set_position": "Position von {entity_name} setzen",
|
||||
"set_tilt_position": "Neigeposition von {entity_name} einstellen"
|
||||
},
|
||||
"condition_type": {
|
||||
"is_closed": "{entity_name} ist geschlossen",
|
||||
|
@@ -15,7 +15,8 @@
|
||||
"one": "eins",
|
||||
"other": "andere"
|
||||
},
|
||||
"description": "M\u00f6chten Sie {name} einrichten?"
|
||||
"description": "M\u00f6chten Sie {name} einrichten?",
|
||||
"title": "Stellen Sie eine Verbindung zum DirecTV-Empf\u00e4nger her"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Dieser DoorBird ist bereits konfiguriert"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
|
||||
"invalid_auth": "Ung\u00fcltige Authentifizierung",
|
||||
@@ -23,7 +26,8 @@
|
||||
"init": {
|
||||
"data": {
|
||||
"events": "Durch Kommas getrennte Liste von Ereignissen."
|
||||
}
|
||||
},
|
||||
"description": "F\u00fcgen Sie f\u00fcr jedes Ereignis, das Sie verfolgen m\u00f6chten, einen durch Kommas getrennten Ereignisnamen hinzu. Nachdem Sie sie hier eingegeben haben, verwenden Sie die DoorBird-App, um sie einem bestimmten Ereignis zuzuweisen. Weitere Informationen finden Sie in der Dokumentation unter https://www.home-assistant.io/integrations/doorbird/#events. Beispiel: jemand_hat_den_knopf_gedr\u00fcckt, bewegung"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
34
homeassistant/components/doorbird/.translations/it.json
Normal file
34
homeassistant/components/doorbird/.translations/it.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Questo DoorBird \u00e8 gi\u00e0 configurato"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi, si prega di riprovare",
|
||||
"invalid_auth": "Autenticazione non valida",
|
||||
"unknown": "Errore imprevisto"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host (indirizzo IP)",
|
||||
"name": "Nome del dispositivo",
|
||||
"password": "Password",
|
||||
"username": "Nome utente"
|
||||
},
|
||||
"title": "Connetti a DoorBird"
|
||||
}
|
||||
},
|
||||
"title": "DoorBird"
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"events": "Elenco di eventi separati da virgole."
|
||||
},
|
||||
"description": "Aggiungere un nome di evento separato da virgola per ogni evento che si desidera monitorare. Dopo averli inseriti qui, usa l'applicazione DoorBird per assegnarli a un evento specifico. Consultare la documentazione su https://www.home-assistant.io/integrations/doorbird/#events. Esempio: qualcuno_premuto_il_pulsante, movimento"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,6 +4,6 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecobee",
|
||||
"dependencies": [],
|
||||
"requirements": ["python-ecobee-api==0.2.3"],
|
||||
"requirements": ["python-ecobee-api==0.2.5"],
|
||||
"codeowners": ["@marthoc"]
|
||||
}
|
||||
|
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"address_already_configured": "Ein ElkM1 mit dieser Adresse ist bereits konfiguriert",
|
||||
"already_configured": "Ein ElkM1 mit diesem Pr\u00e4fix ist bereits konfiguriert"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
|
||||
"invalid_auth": "Ung\u00fcltige Authentifizierung",
|
||||
@@ -8,9 +12,14 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"address": "Die IP-Adresse, die Domain oder der serielle Port bei einer seriellen Verbindung.",
|
||||
"password": "Passwort (Nur sicher).",
|
||||
"prefix": "Ein eindeutiges Pr\u00e4fix (leer lassen, wenn Sie nur einen ElkM1 haben).",
|
||||
"protocol": "Protokoll",
|
||||
"temperature_unit": "Die von ElkM1 verwendete Temperatureinheit."
|
||||
"temperature_unit": "Die von ElkM1 verwendete Temperatureinheit.",
|
||||
"username": "Benutzername (Nur sicher)."
|
||||
},
|
||||
"description": "Die Adresszeichenfolge muss in der Form 'adresse[:port]' f\u00fcr 'sicher' und 'nicht sicher' vorliegen. Beispiel: '192.168.1.1'. Der Port ist optional und standardm\u00e4\u00dfig 2101 f\u00fcr \"nicht sicher\" und 2601 f\u00fcr \"sicher\". F\u00fcr das serielle Protokoll muss die Adresse die Form 'tty[:baud]' haben. Beispiel: '/dev/ttyS1'. Der Baudrate ist optional und standardm\u00e4\u00dfig 115200.",
|
||||
"title": "Stellen Sie eine Verbindung zur Elk-M1-Steuerung her"
|
||||
}
|
||||
},
|
||||
|
16
homeassistant/components/elkm1/.translations/es.json
Normal file
16
homeassistant/components/elkm1/.translations/es.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo",
|
||||
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
|
||||
"unknown": "Error inesperado"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"protocol": "Protocolo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
homeassistant/components/elkm1/.translations/it.json
Normal file
28
homeassistant/components/elkm1/.translations/it.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"address_already_configured": "Un ElkM1 con questo indirizzo \u00e8 gi\u00e0 configurato",
|
||||
"already_configured": "Un ElkM1 con questo prefisso \u00e8 gi\u00e0 configurato"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi, si prega di riprovare",
|
||||
"invalid_auth": "Autenticazione non valida",
|
||||
"unknown": "Errore imprevisto"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"address": "L'indirizzo IP o il dominio o la porta seriale se ci si connette tramite seriale.",
|
||||
"password": "Password (solo sicura).",
|
||||
"prefix": "Un prefisso univoco (lasciare vuoto se si dispone di un solo ElkM1).",
|
||||
"protocol": "Protocollo",
|
||||
"temperature_unit": "L'unit\u00e0 di temperatura utilizzata da ElkM1.",
|
||||
"username": "Nome utente (solo sicuro)."
|
||||
},
|
||||
"description": "La stringa di indirizzi deve essere nella forma \"address[:port]\" per \"secure\" e \"non secure\". Esempio: '192.168.1.1.1'. La porta \u00e8 facoltativa e il valore predefinito \u00e8 2101 per 'non sicuro' e 2601 per 'sicuro'. Per il protocollo seriale, l'indirizzo deve essere nella forma 'tty[:baud]'. Esempio: '/dev/ttyS1'. Il baud \u00e8 opzionale e il valore predefinito \u00e8 115200.",
|
||||
"title": "Collegamento al controllo Elk-M1"
|
||||
}
|
||||
},
|
||||
"title": "Controllo Elk-M1"
|
||||
}
|
||||
}
|
28
homeassistant/components/elkm1/.translations/no.json
Normal file
28
homeassistant/components/elkm1/.translations/no.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"address_already_configured": "En ElkM1 med denne adressen er allerede konfigurert",
|
||||
"already_configured": "En ElkM1 med dette prefikset er allerede konfigurert"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen",
|
||||
"invalid_auth": "Ugyldig godkjenning",
|
||||
"unknown": "Uventet feil"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"address": "IP-adressen eller domenet eller seriell port hvis du kobler til via seriell.",
|
||||
"password": "Passord (bare sikkert).",
|
||||
"prefix": "Et unikt prefiks (la v\u00e6re tomt hvis du bare har en ElkM1).",
|
||||
"protocol": "protokoll",
|
||||
"temperature_unit": "Temperaturenheten ElkM1 bruker.",
|
||||
"username": "Brukernavn (bare sikkert)."
|
||||
},
|
||||
"description": "Adressestrengen m\u00e5 v\u00e6re i formen 'adresse [: port]' for 'sikker' og 'ikke-sikker'. Eksempel: '192.168.1.1'. Porten er valgfri og er standard til 2101 for 'ikke-sikker' og 2601 for 'sikker'. For den serielle protokollen m\u00e5 adressen v\u00e6re i formen 'tty [: baud]'. Eksempel: '/ dev / ttyS1'. Baud er valgfri og er standard til 115200.",
|
||||
"title": "Koble til Elk-M1-kontroll"
|
||||
}
|
||||
},
|
||||
"title": "Elk-M1 kontroll"
|
||||
}
|
||||
}
|
10
homeassistant/components/elkm1/.translations/ru.json
Normal file
10
homeassistant/components/elkm1/.translations/ru.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Elk-M1 Control"
|
||||
}
|
||||
},
|
||||
"title": "Elk-M1 Control"
|
||||
}
|
||||
}
|
@@ -11,11 +11,7 @@ from homeassistant.components.climate.const import (
|
||||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
from homeassistant.components.modbus import (
|
||||
CONF_HUB,
|
||||
DEFAULT_HUB,
|
||||
DOMAIN as MODBUS_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.modbus.const import CONF_HUB, DEFAULT_HUB, MODBUS_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_NAME,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "fortios",
|
||||
"name": "Home Assistant Device Tracker to support FortiOS",
|
||||
"name": "FortiOS",
|
||||
"documentation": "https://www.home-assistant.io/integrations/fortios/",
|
||||
"requirements": ["fortiosapi==0.10.8"],
|
||||
"dependencies": [],
|
||||
|
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"step": {
|
||||
"link": {
|
||||
"description": "Klicken Sie auf \"Senden\" und ber\u00fchren Sie dann den Pfeil nach rechts auf dem Router, um Freebox bei Home Assistant zu registrieren. \n\n ![Position der Schaltfl\u00e4che am Router]\n (/static/images/config_freebox.png)",
|
||||
"title": "Link Freebox Router"
|
||||
},
|
||||
"user": {
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20200318.1"
|
||||
"home-assistant-frontend==20200330.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
@@ -96,7 +96,8 @@ class GeonetnzQuakesEvent(GeolocationEvent):
|
||||
self._remove_signal_update()
|
||||
# Remove from entity registry.
|
||||
entity_registry = await async_get_registry(self.hass)
|
||||
entity_registry.async_remove(self.entity_id)
|
||||
if self.entity_id in entity_registry.entities:
|
||||
entity_registry.async_remove(self.entity_id)
|
||||
|
||||
@callback
|
||||
def _delete_callback(self):
|
||||
|
@@ -9,6 +9,10 @@
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"loadzone": "Ladezone (Abwicklungspunkt)"
|
||||
},
|
||||
"description": "Ihre Ladezone befindet sich in Ihrem Griddy-Konto unter \"Konto > Messger\u00e4t > Ladezone\".",
|
||||
"title": "Richten Sie Ihre Griddy Ladezone ein"
|
||||
}
|
||||
},
|
||||
|
@@ -39,7 +39,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SIGNIFICANT_DOMAINS = ("thermostat", "climate", "water_heater")
|
||||
SIGNIFICANT_DOMAINS = ("climate", "device_tracker", "thermostat", "water_heater")
|
||||
IGNORE_DOMAINS = ("zone", "scene")
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ def get_significant_states(
|
||||
entity_ids=None,
|
||||
filters=None,
|
||||
include_start_time_state=True,
|
||||
significant_changes_only=True,
|
||||
):
|
||||
"""
|
||||
Return states changes during UTC period start_time - end_time.
|
||||
@@ -61,13 +62,16 @@ def get_significant_states(
|
||||
timer_start = time.perf_counter()
|
||||
|
||||
with session_scope(hass=hass) as session:
|
||||
query = session.query(States).filter(
|
||||
(
|
||||
States.domain.in_(SIGNIFICANT_DOMAINS)
|
||||
| (States.last_changed == States.last_updated)
|
||||
if significant_changes_only:
|
||||
query = session.query(States).filter(
|
||||
(
|
||||
States.domain.in_(SIGNIFICANT_DOMAINS)
|
||||
| (States.last_changed == States.last_updated)
|
||||
)
|
||||
& (States.last_updated > start_time)
|
||||
)
|
||||
& (States.last_updated > start_time)
|
||||
)
|
||||
else:
|
||||
query = session.query(States).filter(States.last_updated > start_time)
|
||||
|
||||
if filters:
|
||||
query = filters.apply(query, entity_ids)
|
||||
@@ -327,6 +331,9 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
if entity_ids:
|
||||
entity_ids = entity_ids.lower().split(",")
|
||||
include_start_time_state = "skip_initial_state" not in request.query
|
||||
significant_changes_only = (
|
||||
request.query.get("significant_changes_only", "1") != "0"
|
||||
)
|
||||
|
||||
hass = request.app["hass"]
|
||||
|
||||
@@ -338,6 +345,7 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
entity_ids,
|
||||
self.filters,
|
||||
include_start_time_state,
|
||||
significant_changes_only,
|
||||
)
|
||||
result = list(result.values())
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
|
@@ -1,10 +1,12 @@
|
||||
"""Constants used be the HomeKit component."""
|
||||
# #### Misc ####
|
||||
DEBOUNCE_TIMEOUT = 0.5
|
||||
DEVICE_PRECISION_LEEWAY = 6
|
||||
DOMAIN = "homekit"
|
||||
HOMEKIT_FILE = ".homekit.state"
|
||||
HOMEKIT_NOTIFY_ID = 4663548
|
||||
|
||||
|
||||
# #### Attributes ####
|
||||
ATTR_DISPLAY_NAME = "display_name"
|
||||
ATTR_VALUE = "value"
|
||||
@@ -106,6 +108,7 @@ CHAR_CURRENT_POSITION = "CurrentPosition"
|
||||
CHAR_CURRENT_HUMIDITY = "CurrentRelativeHumidity"
|
||||
CHAR_CURRENT_SECURITY_STATE = "SecuritySystemCurrentState"
|
||||
CHAR_CURRENT_TEMPERATURE = "CurrentTemperature"
|
||||
CHAR_CURRENT_TILT_ANGLE = "CurrentHorizontalTiltAngle"
|
||||
CHAR_CURRENT_VISIBILITY_STATE = "CurrentVisibilityState"
|
||||
CHAR_FIRMWARE_REVISION = "FirmwareRevision"
|
||||
CHAR_HEATING_THRESHOLD_TEMPERATURE = "HeatingThresholdTemperature"
|
||||
@@ -141,6 +144,7 @@ CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState"
|
||||
CHAR_TARGET_POSITION = "TargetPosition"
|
||||
CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState"
|
||||
CHAR_TARGET_TEMPERATURE = "TargetTemperature"
|
||||
CHAR_TARGET_TILT_ANGLE = "TargetHorizontalTiltAngle"
|
||||
CHAR_TEMP_DISPLAY_UNITS = "TemperatureDisplayUnits"
|
||||
CHAR_VALVE_TYPE = "ValveType"
|
||||
CHAR_VOLUME = "Volume"
|
||||
|
@@ -5,8 +5,11 @@ from pyhap.const import CATEGORY_GARAGE_DOOR_OPENER, CATEGORY_WINDOW_COVERING
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_CURRENT_POSITION,
|
||||
ATTR_CURRENT_TILT_POSITION,
|
||||
ATTR_POSITION,
|
||||
ATTR_TILT_POSITION,
|
||||
DOMAIN,
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
SUPPORT_STOP,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
@@ -15,6 +18,7 @@ from homeassistant.const import (
|
||||
SERVICE_CLOSE_COVER,
|
||||
SERVICE_OPEN_COVER,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
SERVICE_SET_COVER_TILT_POSITION,
|
||||
SERVICE_STOP_COVER,
|
||||
STATE_CLOSED,
|
||||
STATE_CLOSING,
|
||||
@@ -27,9 +31,12 @@ from .accessories import HomeAccessory, debounce
|
||||
from .const import (
|
||||
CHAR_CURRENT_DOOR_STATE,
|
||||
CHAR_CURRENT_POSITION,
|
||||
CHAR_CURRENT_TILT_ANGLE,
|
||||
CHAR_POSITION_STATE,
|
||||
CHAR_TARGET_DOOR_STATE,
|
||||
CHAR_TARGET_POSITION,
|
||||
CHAR_TARGET_TILT_ANGLE,
|
||||
DEVICE_PRECISION_LEEWAY,
|
||||
SERV_GARAGE_DOOR_OPENER,
|
||||
SERV_WINDOW_COVERING,
|
||||
)
|
||||
@@ -94,9 +101,28 @@ class WindowCovering(HomeAccessory):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a WindowCovering accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
|
||||
self._homekit_target = None
|
||||
|
||||
serv_cover = self.add_preload_service(SERV_WINDOW_COVERING)
|
||||
self._homekit_target = None
|
||||
self._homekit_target_tilt = None
|
||||
|
||||
serv_cover = self.add_preload_service(
|
||||
SERV_WINDOW_COVERING,
|
||||
chars=[CHAR_TARGET_TILT_ANGLE, CHAR_CURRENT_TILT_ANGLE],
|
||||
)
|
||||
|
||||
features = self.hass.states.get(self.entity_id).attributes.get(
|
||||
ATTR_SUPPORTED_FEATURES, 0
|
||||
)
|
||||
|
||||
self._supports_tilt = features & SUPPORT_SET_TILT_POSITION
|
||||
if self._supports_tilt:
|
||||
self.char_target_tilt = serv_cover.configure_char(
|
||||
CHAR_TARGET_TILT_ANGLE, setter_callback=self.set_tilt
|
||||
)
|
||||
self.char_current_tilt = serv_cover.configure_char(
|
||||
CHAR_CURRENT_TILT_ANGLE, value=0
|
||||
)
|
||||
|
||||
self.char_current_position = serv_cover.configure_char(
|
||||
CHAR_CURRENT_POSITION, value=0
|
||||
)
|
||||
@@ -107,6 +133,20 @@ class WindowCovering(HomeAccessory):
|
||||
CHAR_POSITION_STATE, value=2
|
||||
)
|
||||
|
||||
@debounce
|
||||
def set_tilt(self, value):
|
||||
"""Set tilt to value if call came from HomeKit."""
|
||||
self._homekit_target_tilt = value
|
||||
_LOGGER.info("%s: Set tilt to %d", self.entity_id, value)
|
||||
|
||||
# HomeKit sends values between -90 and 90.
|
||||
# We'll have to normalize to [0,100]
|
||||
value = round((value + 90) / 180.0 * 100.0)
|
||||
|
||||
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_TILT_POSITION: value}
|
||||
|
||||
self.call_service(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, params, value)
|
||||
|
||||
@debounce
|
||||
def move_cover(self, value):
|
||||
"""Move cover to value if call came from HomeKit."""
|
||||
@@ -117,14 +157,20 @@ class WindowCovering(HomeAccessory):
|
||||
self.call_service(DOMAIN, SERVICE_SET_COVER_POSITION, params, value)
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update cover position after state changed."""
|
||||
"""Update cover position and tilt after state changed."""
|
||||
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
|
||||
if isinstance(current_position, (float, int)):
|
||||
current_position = int(current_position)
|
||||
self.char_current_position.set_value(current_position)
|
||||
|
||||
# We have to assume that the device has worse precision than HomeKit.
|
||||
# If it reports back a state that is only _close_ to HK's requested
|
||||
# state, we'll "fix" what HomeKit requested so that it won't appear
|
||||
# out of sync.
|
||||
if (
|
||||
self._homekit_target is None
|
||||
or abs(current_position - self._homekit_target) < 6
|
||||
or abs(current_position - self._homekit_target)
|
||||
< DEVICE_PRECISION_LEEWAY
|
||||
):
|
||||
self.char_target_position.set_value(current_position)
|
||||
self._homekit_target = None
|
||||
@@ -135,6 +181,25 @@ class WindowCovering(HomeAccessory):
|
||||
else:
|
||||
self.char_position_state.set_value(2)
|
||||
|
||||
# update tilt
|
||||
current_tilt = new_state.attributes.get(ATTR_CURRENT_TILT_POSITION)
|
||||
if isinstance(current_tilt, (float, int)):
|
||||
# HomeKit sends values between -90 and 90.
|
||||
# We'll have to normalize to [0,100]
|
||||
current_tilt = (current_tilt / 100.0 * 180.0) - 90.0
|
||||
current_tilt = int(current_tilt)
|
||||
self.char_current_tilt.set_value(current_tilt)
|
||||
|
||||
# We have to assume that the device has worse precision than HomeKit.
|
||||
# If it reports back a state that is only _close_ to HK's requested
|
||||
# state, we'll "fix" what HomeKit requested so that it won't appear
|
||||
# out of sync.
|
||||
if self._homekit_target_tilt is None or abs(
|
||||
current_tilt - self._homekit_target_tilt < DEVICE_PRECISION_LEEWAY
|
||||
):
|
||||
self.char_target_tilt.set_value(current_tilt)
|
||||
self._homekit_target_tilt = None
|
||||
|
||||
|
||||
@TYPES.register("WindowCoveringBasic")
|
||||
class WindowCoveringBasic(HomeAccessory):
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"name": "HomeKit Controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": ["aiohomekit[IP]==0.2.35"],
|
||||
"requirements": ["aiohomekit[IP]==0.2.37"],
|
||||
"dependencies": [],
|
||||
"zeroconf": ["_hap._tcp.local."],
|
||||
"codeowners": ["@Jc2k"]
|
||||
|
@@ -166,12 +166,19 @@ class HomematicipGenericDevice(Entity):
|
||||
def name(self) -> str:
|
||||
"""Return the name of the generic device."""
|
||||
name = self._device.label
|
||||
if self._home.name is not None and self._home.name != "":
|
||||
if name and self._home.name:
|
||||
name = f"{self._home.name} {name}"
|
||||
if self.post is not None and self.post != "":
|
||||
if name and self.post:
|
||||
name = f"{name} {self.post}"
|
||||
return name
|
||||
|
||||
def _get_label_by_channel(self, channel: int) -> str:
|
||||
"""Return the name of the channel."""
|
||||
name = self._device.functionalChannels[channel].label
|
||||
if name and self._home.name:
|
||||
name = f"{self._home.name} {name}"
|
||||
return name
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""No polling needed."""
|
||||
|
@@ -71,6 +71,14 @@ class HomematicipLight(HomematicipGenericDevice, Light):
|
||||
"""Initialize the light device."""
|
||||
super().__init__(hap, device)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the multi switch channel."""
|
||||
label = self._get_label_by_channel(1)
|
||||
if label:
|
||||
return label
|
||||
return super().name
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if device is on."""
|
||||
@@ -193,6 +201,9 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the generic device."""
|
||||
label = self._get_label_by_channel(self.channel)
|
||||
if label:
|
||||
return label
|
||||
return f"{super().name} Notification"
|
||||
|
||||
@property
|
||||
|
@@ -153,6 +153,14 @@ class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice):
|
||||
self.channel = channel
|
||||
super().__init__(hap, device, f"Channel{channel}")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the multi switch channel."""
|
||||
label = self._get_label_by_channel(self.channel)
|
||||
if label:
|
||||
return label
|
||||
return super().name
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
|
32
homeassistant/components/ipp/.translations/en.json
Normal file
32
homeassistant/components/ipp/.translations/en.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This printer is already configured.",
|
||||
"connection_error": "Failed to connect to printer.",
|
||||
"connection_upgrade": "Failed to connect to printer due to connection upgrade being required."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Failed to connect to printer.",
|
||||
"connection_upgrade": "Failed to connect to printer. Please try again with SSL/TLS option checked."
|
||||
},
|
||||
"flow_title": "Printer: {name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"base_path": "Relative path to the printer",
|
||||
"host": "Host or IP address",
|
||||
"port": "Port",
|
||||
"ssl": "Printer supports communication over SSL/TLS",
|
||||
"verify_ssl": "Printer uses a proper SSL certificate"
|
||||
},
|
||||
"description": "Set up your printer via Internet Printing Protocol (IPP) to integrate with Home Assistant.",
|
||||
"title": "Link your printer"
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Do you want to add the printer named `{name}` to Home Assistant?",
|
||||
"title": "Discovered printer"
|
||||
}
|
||||
},
|
||||
"title": "Internet Printing Protocol (IPP)"
|
||||
}
|
||||
}
|
190
homeassistant/components/ipp/__init__.py
Normal file
190
homeassistant/components/ipp/__init__.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""The Internet Printing Protocol (IPP) integration."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from pyipp import IPP, IPPError, Printer as IPPPrinter
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_NAME,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
ATTR_IDENTIFIERS,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_SOFTWARE_VERSION,
|
||||
CONF_BASE_PATH,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
PLATFORMS = [SENSOR_DOMAIN]
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
|
||||
"""Set up the IPP component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up IPP from a config entry."""
|
||||
|
||||
# Create IPP instance for this entry
|
||||
coordinator = IPPDataUpdateCoordinator(
|
||||
hass,
|
||||
host=entry.data[CONF_HOST],
|
||||
port=entry.data[CONF_PORT],
|
||||
base_path=entry.data[CONF_BASE_PATH],
|
||||
tls=entry.data[CONF_SSL],
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
if not coordinator.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
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: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class IPPDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching IPP data from single endpoint."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
host: str,
|
||||
port: int,
|
||||
base_path: str,
|
||||
tls: bool,
|
||||
verify_ssl: bool,
|
||||
):
|
||||
"""Initialize global IPP data updater."""
|
||||
self.ipp = IPP(
|
||||
host=host,
|
||||
port=port,
|
||||
base_path=base_path,
|
||||
tls=tls,
|
||||
verify_ssl=verify_ssl,
|
||||
session=async_get_clientsession(hass, verify_ssl),
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> IPPPrinter:
|
||||
"""Fetch data from IPP."""
|
||||
try:
|
||||
return await self.ipp.printer()
|
||||
except IPPError as error:
|
||||
raise UpdateFailed(f"Invalid response from API: {error}")
|
||||
|
||||
|
||||
class IPPEntity(Entity):
|
||||
"""Defines a base IPP entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
entry_id: str,
|
||||
coordinator: IPPDataUpdateCoordinator,
|
||||
name: str,
|
||||
icon: str,
|
||||
enabled_default: bool = True,
|
||||
) -> None:
|
||||
"""Initialize the IPP entity."""
|
||||
self._enabled_default = enabled_default
|
||||
self._entry_id = entry_id
|
||||
self._icon = icon
|
||||
self._name = name
|
||||
self._unsub_dispatcher = None
|
||||
self.coordinator = coordinator
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the mdi icon of the entity."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self.coordinator.last_update_success
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return self._enabled_default
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return the polling requirement of the entity."""
|
||||
return False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Connect to dispatcher listening for entity data notifications."""
|
||||
self.coordinator.async_add_listener(self.async_write_ha_state)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect from update signal."""
|
||||
self.coordinator.async_remove_listener(self.async_write_ha_state)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update an IPP entity."""
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@property
|
||||
def device_info(self) -> Dict[str, Any]:
|
||||
"""Return device information about this IPP device."""
|
||||
return {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.uuid)},
|
||||
ATTR_NAME: self.coordinator.data.info.name,
|
||||
ATTR_MANUFACTURER: self.coordinator.data.info.manufacturer,
|
||||
ATTR_MODEL: self.coordinator.data.info.model,
|
||||
ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version,
|
||||
}
|
144
homeassistant/components/ipp/config_flow.py
Normal file
144
homeassistant/components/ipp/config_flow.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Config flow to configure the IPP integration."""
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pyipp import IPP, IPPConnectionError, IPPConnectionUpgradeRequired
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
|
||||
from .const import CONF_BASE_PATH, CONF_UUID
|
||||
from .const import DOMAIN # pylint: disable=unused-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
session = async_get_clientsession(hass)
|
||||
ipp = IPP(
|
||||
host=data[CONF_HOST],
|
||||
port=data[CONF_PORT],
|
||||
base_path=data[CONF_BASE_PATH],
|
||||
tls=data[CONF_SSL],
|
||||
verify_ssl=data[CONF_VERIFY_SSL],
|
||||
session=session,
|
||||
)
|
||||
|
||||
printer = await ipp.printer()
|
||||
|
||||
return {CONF_UUID: printer.info.uuid}
|
||||
|
||||
|
||||
class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle an IPP config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Set up the instance."""
|
||||
self.discovery_info = {}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Optional[ConfigType] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle a flow initiated by the user."""
|
||||
if user_input is None:
|
||||
return self._show_setup_form()
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except IPPConnectionUpgradeRequired:
|
||||
return self._show_setup_form({"base": "connection_upgrade"})
|
||||
except IPPConnectionError:
|
||||
return self._show_setup_form({"base": "connection_error"})
|
||||
user_input[CONF_UUID] = info[CONF_UUID]
|
||||
|
||||
await self.async_set_unique_id(user_input[CONF_UUID])
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]})
|
||||
|
||||
return self.async_create_entry(title=user_input[CONF_HOST], data=user_input)
|
||||
|
||||
async def async_step_zeroconf(self, discovery_info: ConfigType) -> Dict[str, Any]:
|
||||
"""Handle zeroconf discovery."""
|
||||
# Hostname is format: EPSON123456.local.
|
||||
host = discovery_info["hostname"].rstrip(".")
|
||||
port = discovery_info["port"]
|
||||
name, _ = host.rsplit(".")
|
||||
tls = discovery_info["type"] == "_ipps._tcp.local."
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context.update({"title_placeholders": {"name": name}})
|
||||
|
||||
self.discovery_info.update(
|
||||
{
|
||||
CONF_HOST: host,
|
||||
CONF_PORT: port,
|
||||
CONF_SSL: tls,
|
||||
CONF_VERIFY_SSL: False,
|
||||
CONF_BASE_PATH: "/"
|
||||
+ discovery_info["properties"].get("rp", "ipp/print"),
|
||||
CONF_NAME: name,
|
||||
CONF_UUID: discovery_info["properties"].get("UUID"),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, self.discovery_info)
|
||||
except IPPConnectionUpgradeRequired:
|
||||
return self.async_abort(reason="connection_upgrade")
|
||||
except IPPConnectionError:
|
||||
return self.async_abort(reason="connection_error")
|
||||
|
||||
self.discovery_info[CONF_UUID] = info[CONF_UUID]
|
||||
|
||||
await self.async_set_unique_id(self.discovery_info[CONF_UUID])
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: self.discovery_info[CONF_HOST]}
|
||||
)
|
||||
|
||||
return await self.async_step_zeroconf_confirm()
|
||||
|
||||
async def async_step_zeroconf_confirm(
|
||||
self, user_input: ConfigType = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle a confirmation flow initiated by zeroconf."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
description_placeholders={"name": self.discovery_info[CONF_NAME]},
|
||||
errors={},
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=self.discovery_info[CONF_NAME], data=self.discovery_info,
|
||||
)
|
||||
|
||||
def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Show the setup form to the user."""
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=631): int,
|
||||
vol.Required(CONF_BASE_PATH, default="/ipp/print"): str,
|
||||
vol.Required(CONF_SSL, default=False): bool,
|
||||
vol.Required(CONF_VERIFY_SSL, default=False): bool,
|
||||
}
|
||||
),
|
||||
errors=errors or {},
|
||||
)
|
25
homeassistant/components/ipp/const.py
Normal file
25
homeassistant/components/ipp/const.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Constants for the IPP integration."""
|
||||
|
||||
# Integration domain
|
||||
DOMAIN = "ipp"
|
||||
|
||||
# Attributes
|
||||
ATTR_COMMAND_SET = "command_set"
|
||||
ATTR_IDENTIFIERS = "identifiers"
|
||||
ATTR_INFO = "info"
|
||||
ATTR_LOCATION = "location"
|
||||
ATTR_MANUFACTURER = "manufacturer"
|
||||
ATTR_MARKER_TYPE = "marker_type"
|
||||
ATTR_MARKER_LOW_LEVEL = "marker_low_level"
|
||||
ATTR_MARKER_HIGH_LEVEL = "marker_high_level"
|
||||
ATTR_MODEL = "model"
|
||||
ATTR_SERIAL = "serial"
|
||||
ATTR_SOFTWARE_VERSION = "sw_version"
|
||||
ATTR_STATE_MESSAGE = "state_message"
|
||||
ATTR_STATE_REASON = "state_reason"
|
||||
ATTR_URI_SUPPORTED = "uri_supported"
|
||||
|
||||
# Config Keys
|
||||
CONF_BASE_PATH = "base_path"
|
||||
CONF_TLS = "tls"
|
||||
CONF_UUID = "uuid"
|
11
homeassistant/components/ipp/manifest.json
Normal file
11
homeassistant/components/ipp/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "ipp",
|
||||
"name": "Internet Printing Protocol (IPP)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/ipp",
|
||||
"requirements": ["pyipp==0.8.1"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@ctalkington"],
|
||||
"config_flow": true,
|
||||
"quality_scale": "platinum",
|
||||
"zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."]
|
||||
}
|
178
homeassistant/components/ipp/sensor.py
Normal file
178
homeassistant/components/ipp/sensor.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""Support for IPP sensors."""
|
||||
from datetime import timedelta
|
||||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import DEVICE_CLASS_TIMESTAMP, UNIT_PERCENTAGE
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import IPPDataUpdateCoordinator, IPPEntity
|
||||
from .const import (
|
||||
ATTR_COMMAND_SET,
|
||||
ATTR_INFO,
|
||||
ATTR_LOCATION,
|
||||
ATTR_MARKER_HIGH_LEVEL,
|
||||
ATTR_MARKER_LOW_LEVEL,
|
||||
ATTR_MARKER_TYPE,
|
||||
ATTR_SERIAL,
|
||||
ATTR_STATE_MESSAGE,
|
||||
ATTR_STATE_REASON,
|
||||
ATTR_URI_SUPPORTED,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: Callable[[List[Entity], bool], None],
|
||||
) -> None:
|
||||
"""Set up IPP sensor based on a config entry."""
|
||||
coordinator: IPPDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
sensors = []
|
||||
|
||||
sensors.append(IPPPrinterSensor(entry.entry_id, coordinator))
|
||||
sensors.append(IPPUptimeSensor(entry.entry_id, coordinator))
|
||||
|
||||
for marker_index in range(len(coordinator.data.markers)):
|
||||
sensors.append(IPPMarkerSensor(entry.entry_id, coordinator, marker_index))
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
|
||||
|
||||
class IPPSensor(IPPEntity):
|
||||
"""Defines an IPP sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
coordinator: IPPDataUpdateCoordinator,
|
||||
enabled_default: bool = True,
|
||||
entry_id: str,
|
||||
icon: str,
|
||||
key: str,
|
||||
name: str,
|
||||
unit_of_measurement: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Initialize IPP sensor."""
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._key = key
|
||||
|
||||
super().__init__(
|
||||
entry_id=entry_id,
|
||||
coordinator=coordinator,
|
||||
name=name,
|
||||
icon=icon,
|
||||
enabled_default=enabled_default,
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID for this sensor."""
|
||||
return f"{self.coordinator.data.info.uuid}_{self._key}"
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self) -> str:
|
||||
"""Return the unit this state is expressed in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
|
||||
class IPPMarkerSensor(IPPSensor):
|
||||
"""Defines an IPP marker sensor."""
|
||||
|
||||
def __init__(
|
||||
self, entry_id: str, coordinator: IPPDataUpdateCoordinator, marker_index: int
|
||||
) -> None:
|
||||
"""Initialize IPP marker sensor."""
|
||||
self.marker_index = marker_index
|
||||
|
||||
super().__init__(
|
||||
coordinator=coordinator,
|
||||
entry_id=entry_id,
|
||||
icon="mdi:water",
|
||||
key=f"marker_{marker_index}",
|
||||
name=f"{coordinator.data.info.name} {coordinator.data.markers[marker_index].name}",
|
||||
unit_of_measurement=UNIT_PERCENTAGE,
|
||||
)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> Optional[Dict[str, Any]]:
|
||||
"""Return the state attributes of the entity."""
|
||||
return {
|
||||
ATTR_MARKER_HIGH_LEVEL: self.coordinator.data.markers[
|
||||
self.marker_index
|
||||
].high_level,
|
||||
ATTR_MARKER_LOW_LEVEL: self.coordinator.data.markers[
|
||||
self.marker_index
|
||||
].low_level,
|
||||
ATTR_MARKER_TYPE: self.coordinator.data.markers[
|
||||
self.marker_index
|
||||
].marker_type,
|
||||
}
|
||||
|
||||
@property
|
||||
def state(self) -> Union[None, str, int, float]:
|
||||
"""Return the state of the sensor."""
|
||||
return self.coordinator.data.markers[self.marker_index].level
|
||||
|
||||
|
||||
class IPPPrinterSensor(IPPSensor):
|
||||
"""Defines an IPP printer sensor."""
|
||||
|
||||
def __init__(self, entry_id: str, coordinator: IPPDataUpdateCoordinator) -> None:
|
||||
"""Initialize IPP printer sensor."""
|
||||
super().__init__(
|
||||
coordinator=coordinator,
|
||||
entry_id=entry_id,
|
||||
icon="mdi:printer",
|
||||
key="printer",
|
||||
name=coordinator.data.info.name,
|
||||
unit_of_measurement=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> Optional[Dict[str, Any]]:
|
||||
"""Return the state attributes of the entity."""
|
||||
return {
|
||||
ATTR_INFO: self.coordinator.data.info.printer_info,
|
||||
ATTR_SERIAL: self.coordinator.data.info.serial,
|
||||
ATTR_LOCATION: self.coordinator.data.info.location,
|
||||
ATTR_STATE_MESSAGE: self.coordinator.data.state.message,
|
||||
ATTR_STATE_REASON: self.coordinator.data.state.reasons,
|
||||
ATTR_COMMAND_SET: self.coordinator.data.info.command_set,
|
||||
ATTR_URI_SUPPORTED: self.coordinator.data.info.printer_uri_supported,
|
||||
}
|
||||
|
||||
@property
|
||||
def state(self) -> Union[None, str, int, float]:
|
||||
"""Return the state of the sensor."""
|
||||
return self.coordinator.data.state.printer_state
|
||||
|
||||
|
||||
class IPPUptimeSensor(IPPSensor):
|
||||
"""Defines a IPP uptime sensor."""
|
||||
|
||||
def __init__(self, entry_id: str, coordinator: IPPDataUpdateCoordinator) -> None:
|
||||
"""Initialize IPP uptime sensor."""
|
||||
super().__init__(
|
||||
coordinator=coordinator,
|
||||
enabled_default=False,
|
||||
entry_id=entry_id,
|
||||
icon="mdi:clock-outline",
|
||||
key="uptime",
|
||||
name=f"{coordinator.data.info.name} Uptime",
|
||||
)
|
||||
|
||||
@property
|
||||
def state(self) -> Union[None, str, int, float]:
|
||||
"""Return the state of the sensor."""
|
||||
uptime = utcnow() - timedelta(seconds=self.coordinator.data.info.uptime)
|
||||
return uptime.replace(microsecond=0).isoformat()
|
||||
|
||||
@property
|
||||
def device_class(self) -> Optional[str]:
|
||||
"""Return the class of this sensor."""
|
||||
return DEVICE_CLASS_TIMESTAMP
|
32
homeassistant/components/ipp/strings.json
Normal file
32
homeassistant/components/ipp/strings.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Internet Printing Protocol (IPP)",
|
||||
"flow_title": "Printer: {name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Link your printer",
|
||||
"description": "Set up your printer via Internet Printing Protocol (IPP) to integrate with Home Assistant.",
|
||||
"data": {
|
||||
"host": "Host or IP address",
|
||||
"port": "Port",
|
||||
"base_path": "Relative path to the printer",
|
||||
"ssl": "Printer supports communication over SSL/TLS",
|
||||
"verify_ssl": "Printer uses a proper SSL certificate"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Do you want to add the printer named `{name}` to Home Assistant?",
|
||||
"title": "Discovered printer"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Failed to connect to printer.",
|
||||
"connection_upgrade": "Failed to connect to printer. Please try again with SSL/TLS option checked."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This printer is already configured.",
|
||||
"connection_error": "Failed to connect to printer.",
|
||||
"connection_upgrade": "Failed to connect to printer due to connection upgrade being required."
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,5 +4,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/kef",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@basnijholt"],
|
||||
"requirements": ["aiokef==0.2.7", "getmac==0.8.1"]
|
||||
"requirements": ["aiokef==0.2.9", "getmac==0.8.1"]
|
||||
}
|
||||
|
@@ -1,11 +1,13 @@
|
||||
"""Platform for the KEF Wireless Speakers."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import ipaddress
|
||||
import logging
|
||||
|
||||
from aiokef import AsyncKefSpeaker
|
||||
from aiokef.aiokef import DSP_OPTION_MAPPING
|
||||
from getmac import get_mac_address
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -31,7 +33,8 @@ from homeassistant.const import (
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,6 +58,17 @@ CONF_INVERSE_SPEAKER_MODE = "inverse_speaker_mode"
|
||||
CONF_SUPPORTS_ON = "supports_on"
|
||||
CONF_STANDBY_TIME = "standby_time"
|
||||
|
||||
SERVICE_MODE = "set_mode"
|
||||
SERVICE_DESK_DB = "set_desk_db"
|
||||
SERVICE_WALL_DB = "set_wall_db"
|
||||
SERVICE_TREBLE_DB = "set_treble_db"
|
||||
SERVICE_HIGH_HZ = "set_high_hz"
|
||||
SERVICE_LOW_HZ = "set_low_hz"
|
||||
SERVICE_SUB_DB = "set_sub_db"
|
||||
SERVICE_UPDATE_DSP = "update_dsp"
|
||||
|
||||
DSP_SCAN_INTERVAL = 3600
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
@@ -118,6 +132,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
inverse_speaker_mode,
|
||||
supports_on,
|
||||
sources,
|
||||
speaker_type,
|
||||
ioloop=hass.loop,
|
||||
unique_id=unique_id,
|
||||
)
|
||||
@@ -128,6 +143,36 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
hass.data[DOMAIN][host] = media_player
|
||||
async_add_entities([media_player], update_before_add=True)
|
||||
|
||||
platform = entity_platform.current_platform.get()
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_MODE,
|
||||
{
|
||||
vol.Optional("desk_mode"): cv.boolean,
|
||||
vol.Optional("wall_mode"): cv.boolean,
|
||||
vol.Optional("phase_correction"): cv.boolean,
|
||||
vol.Optional("high_pass"): cv.boolean,
|
||||
vol.Optional("sub_polarity"): vol.In(["-", "+"]),
|
||||
vol.Optional("bass_extension"): vol.In(["Less", "Standard", "Extra"]),
|
||||
},
|
||||
"set_mode",
|
||||
)
|
||||
platform.async_register_entity_service(SERVICE_UPDATE_DSP, {}, "update_dsp")
|
||||
|
||||
def add_service(name, which, option):
|
||||
platform.async_register_entity_service(
|
||||
name,
|
||||
{vol.Required(option): vol.In(DSP_OPTION_MAPPING[which])},
|
||||
f"set_{which}",
|
||||
)
|
||||
|
||||
add_service(SERVICE_DESK_DB, "desk_db", "db_value")
|
||||
add_service(SERVICE_WALL_DB, "wall_db", "db_value")
|
||||
add_service(SERVICE_TREBLE_DB, "treble_db", "db_value")
|
||||
add_service(SERVICE_HIGH_HZ, "high_hz", "hz_value")
|
||||
add_service(SERVICE_LOW_HZ, "low_hz", "hz_value")
|
||||
add_service(SERVICE_SUB_DB, "sub_db", "db_value")
|
||||
|
||||
|
||||
class KefMediaPlayer(MediaPlayerDevice):
|
||||
"""Kef Player Object."""
|
||||
@@ -143,6 +188,7 @@ class KefMediaPlayer(MediaPlayerDevice):
|
||||
inverse_speaker_mode,
|
||||
supports_on,
|
||||
sources,
|
||||
speaker_type,
|
||||
ioloop,
|
||||
unique_id,
|
||||
):
|
||||
@@ -160,12 +206,15 @@ class KefMediaPlayer(MediaPlayerDevice):
|
||||
)
|
||||
self._unique_id = unique_id
|
||||
self._supports_on = supports_on
|
||||
self._speaker_type = speaker_type
|
||||
|
||||
self._state = None
|
||||
self._muted = None
|
||||
self._source = None
|
||||
self._volume = None
|
||||
self._is_online = None
|
||||
self._dsp = None
|
||||
self._update_dsp_task_remover = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -190,6 +239,9 @@ class KefMediaPlayer(MediaPlayerDevice):
|
||||
state = await self._speaker.get_state()
|
||||
self._source = state.source
|
||||
self._state = STATE_ON if state.is_on else STATE_OFF
|
||||
if self._dsp is None:
|
||||
# Only do this when necessary because it is a slow operation
|
||||
await self.update_dsp()
|
||||
else:
|
||||
self._muted = None
|
||||
self._source = None
|
||||
@@ -291,11 +343,11 @@ class KefMediaPlayer(MediaPlayerDevice):
|
||||
|
||||
async def async_media_play(self):
|
||||
"""Send play command."""
|
||||
await self._speaker.play_pause()
|
||||
await self._speaker.set_play_pause()
|
||||
|
||||
async def async_media_pause(self):
|
||||
"""Send pause command."""
|
||||
await self._speaker.play_pause()
|
||||
await self._speaker.set_play_pause()
|
||||
|
||||
async def async_media_previous_track(self):
|
||||
"""Send previous track command."""
|
||||
@@ -304,3 +356,87 @@ class KefMediaPlayer(MediaPlayerDevice):
|
||||
async def async_media_next_track(self):
|
||||
"""Send next track command."""
|
||||
await self._speaker.next_track()
|
||||
|
||||
async def update_dsp(self) -> None:
|
||||
"""Update the DSP settings."""
|
||||
if self._speaker_type == "LS50" and self._state == STATE_OFF:
|
||||
# The LSX is able to respond when off the LS50 has to be on.
|
||||
return
|
||||
|
||||
(mode, *rest) = await asyncio.gather(
|
||||
self._speaker.get_mode(),
|
||||
self._speaker.get_desk_db(),
|
||||
self._speaker.get_wall_db(),
|
||||
self._speaker.get_treble_db(),
|
||||
self._speaker.get_high_hz(),
|
||||
self._speaker.get_low_hz(),
|
||||
self._speaker.get_sub_db(),
|
||||
)
|
||||
keys = ["desk_db", "wall_db", "treble_db", "high_hz", "low_hz", "sub_db"]
|
||||
self._dsp = dict(zip(keys, rest), **mode._asdict())
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to DSP updates."""
|
||||
self._update_dsp_task_remover = async_track_time_interval(
|
||||
self.hass, self.update_dsp, DSP_SCAN_INTERVAL
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Unsubscribe to DSP updates."""
|
||||
self._update_dsp_task_remover()
|
||||
self._update_dsp_task_remover = None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the DSP settings of the KEF device."""
|
||||
return self._dsp or {}
|
||||
|
||||
async def set_mode(
|
||||
self,
|
||||
desk_mode=None,
|
||||
wall_mode=None,
|
||||
phase_correction=None,
|
||||
high_pass=None,
|
||||
sub_polarity=None,
|
||||
bass_extension=None,
|
||||
):
|
||||
"""Set the speaker mode."""
|
||||
await self._speaker.set_mode(
|
||||
desk_mode=desk_mode,
|
||||
wall_mode=wall_mode,
|
||||
phase_correction=phase_correction,
|
||||
high_pass=high_pass,
|
||||
sub_polarity=sub_polarity,
|
||||
bass_extension=bass_extension,
|
||||
)
|
||||
self._dsp = None
|
||||
|
||||
async def set_desk_db(self, db_value):
|
||||
"""Set desk_db of the KEF speakers."""
|
||||
await self._speaker.set_desk_db(db_value)
|
||||
self._dsp = None
|
||||
|
||||
async def set_wall_db(self, db_value):
|
||||
"""Set wall_db of the KEF speakers."""
|
||||
await self._speaker.set_wall_db(db_value)
|
||||
self._dsp = None
|
||||
|
||||
async def set_treble_db(self, db_value):
|
||||
"""Set treble_db of the KEF speakers."""
|
||||
await self._speaker.set_treble_db(db_value)
|
||||
self._dsp = None
|
||||
|
||||
async def set_high_hz(self, hz_value):
|
||||
"""Set high_hz of the KEF speakers."""
|
||||
await self._speaker.set_high_hz(hz_value)
|
||||
self._dsp = None
|
||||
|
||||
async def set_low_hz(self, hz_value):
|
||||
"""Set low_hz of the KEF speakers."""
|
||||
await self._speaker.set_low_hz(hz_value)
|
||||
self._dsp = None
|
||||
|
||||
async def set_sub_db(self, db_value):
|
||||
"""Set sub_db of the KEF speakers."""
|
||||
await self._speaker.set_sub_db(db_value)
|
||||
self._dsp = None
|
||||
|
97
homeassistant/components/kef/services.yaml
Normal file
97
homeassistant/components/kef/services.yaml
Normal file
@@ -0,0 +1,97 @@
|
||||
update_dsp:
|
||||
description: Update all DSP settings.
|
||||
fields:
|
||||
entity_id:
|
||||
description: The entity_id of the KEF speaker.
|
||||
example: media_player.kef_lsx
|
||||
|
||||
set_mode:
|
||||
description: Set the mode of the speaker.
|
||||
fields:
|
||||
entity_id:
|
||||
description: The entity_id of the KEF speaker.
|
||||
example: media_player.kef_lsx
|
||||
desk_mode:
|
||||
description: >
|
||||
"Desk mode" (true or false)
|
||||
example: true
|
||||
wall_mode:
|
||||
description: >
|
||||
"Wall mode" (true or false)
|
||||
example: true
|
||||
phase_correction:
|
||||
description: >
|
||||
"Phase correction" (true or false)
|
||||
example: true
|
||||
high_pass:
|
||||
description: >
|
||||
"High-pass mode" (true or false)
|
||||
example: true
|
||||
sub_polarity:
|
||||
description: >
|
||||
"Sub polarity" ("-" or "+")
|
||||
example: "+"
|
||||
bass_extension:
|
||||
description: >
|
||||
"Bass extension" selector ("Less", "Standard", or "Extra")
|
||||
example: "Extra"
|
||||
|
||||
set_desk_db:
|
||||
description: Set the "Desk mode" slider of the speaker in dB.
|
||||
fields:
|
||||
entity_id:
|
||||
description: The entity_id of the KEF speaker.
|
||||
example: media_player.kef_lsx
|
||||
db_value:
|
||||
description: Value of the slider (-6 to 0 with steps of 0.5)
|
||||
example: 0.0
|
||||
|
||||
set_wall_db:
|
||||
description: Set the "Wall mode" slider of the speaker in dB.
|
||||
fields:
|
||||
entity_id:
|
||||
description: The entity_id of the KEF speaker.
|
||||
example: media_player.kef_lsx
|
||||
db_value:
|
||||
description: Value of the slider (-6 to 0 with steps of 0.5)
|
||||
example: 0.0
|
||||
|
||||
set_treble_db:
|
||||
description: Set desk the "Treble trim" slider of the speaker in dB.
|
||||
fields:
|
||||
entity_id:
|
||||
description: The entity_id of the KEF speaker.
|
||||
example: media_player.kef_lsx
|
||||
db_value:
|
||||
description: Value of the slider (-2 to 2 with steps of 0.5)
|
||||
example: 0.0
|
||||
|
||||
set_high_hz:
|
||||
description: Set the "High-pass mode" slider of the speaker in Hz.
|
||||
fields:
|
||||
entity_id:
|
||||
description: The entity_id of the KEF speaker.
|
||||
example: media_player.kef_lsx
|
||||
hz_value:
|
||||
description: Value of the slider (50 to 120 with steps of 5)
|
||||
example: 95
|
||||
|
||||
set_low_hz:
|
||||
description: Set the "Sub out low-pass frequency" slider of the speaker in Hz.
|
||||
fields:
|
||||
entity_id:
|
||||
description: The entity_id of the KEF speaker.
|
||||
example: media_player.kef_lsx
|
||||
hz_value:
|
||||
description: Value of the slider (40 to 250 with steps of 5)
|
||||
example: 80
|
||||
|
||||
set_sub_db:
|
||||
description: Set the "Sub gain" slider of the speaker in dB.
|
||||
fields:
|
||||
entity_id:
|
||||
description: The entity_id of the KEF speaker.
|
||||
example: media_player.kef_lsx
|
||||
db_value:
|
||||
description: Value of the slider (-10 to 10 with steps of 1)
|
||||
example: 0
|
@@ -91,11 +91,12 @@
|
||||
"data": {
|
||||
"activation": "Ausgabe, wenn eingeschaltet",
|
||||
"momentary": "Impulsdauer (ms) (optional)",
|
||||
"more_states": "Konfigurieren Sie zus\u00e4tzliche Zust\u00e4nde f\u00fcr diese Zone",
|
||||
"name": "Name (optional)",
|
||||
"pause": "Pause zwischen Impulsen (ms) (optional)",
|
||||
"repeat": "Zeit zum Wiederholen (-1 = unendlich) (optional)"
|
||||
},
|
||||
"description": "Bitte w\u00e4hlen Sie die Ausgabeoptionen f\u00fcr {zone}"
|
||||
"description": "Bitte w\u00e4hlen Sie die Ausgabeoptionen f\u00fcr {zone} : Status {state}"
|
||||
}
|
||||
},
|
||||
"title": "Konnected Alarm Panel-Optionen"
|
||||
|
@@ -91,11 +91,12 @@
|
||||
"data": {
|
||||
"activation": "Output when on",
|
||||
"momentary": "Pulse duration (ms) (optional)",
|
||||
"more_states": "Configure additional states for this zone",
|
||||
"name": "Name (optional)",
|
||||
"pause": "Pause between pulses (ms) (optional)",
|
||||
"repeat": "Times to repeat (-1=infinite) (optional)"
|
||||
},
|
||||
"description": "Please select the output options for {zone}",
|
||||
"description": "Please select the output options for {zone}: state {state}",
|
||||
"title": "Configure Switchable Output"
|
||||
}
|
||||
},
|
||||
|
@@ -91,11 +91,12 @@
|
||||
"data": {
|
||||
"activation": "Utgang n\u00e5r den er p\u00e5",
|
||||
"momentary": "Pulsvarighet (ms) (valgfritt)",
|
||||
"more_states": "Konfigurere flere tilstander for denne sonen",
|
||||
"name": "Navn (valgfritt)",
|
||||
"pause": "Pause mellom pulser (ms) (valgfritt)",
|
||||
"repeat": "Tider \u00e5 gjenta (-1 = uendelig) (valgfritt)"
|
||||
},
|
||||
"description": "Velg outputalternativer for {zone}",
|
||||
"description": "Velg outputalternativer for {zone} : state {state}",
|
||||
"title": "Konfigurere Valgbare Utgang"
|
||||
}
|
||||
},
|
||||
|
@@ -91,6 +91,7 @@
|
||||
"data": {
|
||||
"activation": "\u958b\u555f\u6642\u8f38\u51fa",
|
||||
"momentary": "\u6301\u7e8c\u6642\u9593\uff08ms\uff09\uff08\u9078\u9805\uff09",
|
||||
"more_states": "\u8a2d\u5b9a\u6b64\u5340\u57df\u7684\u9644\u52a0\u72c0\u614b",
|
||||
"name": "\u540d\u7a31\uff08\u9078\u9805\uff09",
|
||||
"pause": "\u66ab\u505c\u9593\u8ddd\uff08ms\uff09\uff08\u9078\u9805\uff09",
|
||||
"repeat": "\u91cd\u8907\u6642\u9593\uff08-1=\u7121\u9650\uff09\uff08\u9078\u9805\uff09"
|
||||
|
@@ -57,6 +57,10 @@ CONF_IO_BIN = "Binary Sensor"
|
||||
CONF_IO_DIG = "Digital Sensor"
|
||||
CONF_IO_SWI = "Switchable Output"
|
||||
|
||||
CONF_MORE_STATES = "more_states"
|
||||
CONF_YES = "Yes"
|
||||
CONF_NO = "No"
|
||||
|
||||
KONN_MANUFACTURER = "konnected.io"
|
||||
KONN_PANEL_MODEL_NAMES = {
|
||||
KONN_MODEL: "Konnected Alarm Panel",
|
||||
@@ -117,7 +121,7 @@ SWITCH_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_ZONE): vol.In(ZONES),
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): vol.All(
|
||||
vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)
|
||||
vol.Lower, vol.In([STATE_HIGH, STATE_LOW])
|
||||
),
|
||||
vol.Optional(CONF_MOMENTARY): vol.All(vol.Coerce(int), vol.Range(min=10)),
|
||||
vol.Optional(CONF_PAUSE): vol.All(vol.Coerce(int), vol.Range(min=10)),
|
||||
@@ -361,6 +365,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
self.new_opt = {CONF_IO: {}}
|
||||
self.active_cfg = None
|
||||
self.io_cfg = {}
|
||||
self.current_states = []
|
||||
self.current_state = 1
|
||||
|
||||
@callback
|
||||
def get_current_cfg(self, io_type, zone):
|
||||
@@ -666,12 +672,21 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
if user_input is not None:
|
||||
zone = {"zone": self.active_cfg}
|
||||
zone.update(user_input)
|
||||
del zone[CONF_MORE_STATES]
|
||||
self.new_opt[CONF_SWITCHES] = self.new_opt.get(CONF_SWITCHES, []) + [zone]
|
||||
self.io_cfg.pop(self.active_cfg)
|
||||
self.active_cfg = None
|
||||
|
||||
# iterate through multiple switch states
|
||||
if self.current_states:
|
||||
self.current_states.pop(0)
|
||||
|
||||
# only go to next zone if all states are entered
|
||||
self.current_state += 1
|
||||
if user_input[CONF_MORE_STATES] == CONF_NO:
|
||||
self.io_cfg.pop(self.active_cfg)
|
||||
self.active_cfg = None
|
||||
|
||||
if self.active_cfg:
|
||||
current_cfg = self.get_current_cfg(CONF_SWITCHES, self.active_cfg)
|
||||
current_cfg = next(iter(self.current_states), {})
|
||||
return self.async_show_form(
|
||||
step_id="options_switch",
|
||||
data_schema=vol.Schema(
|
||||
@@ -682,7 +697,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
vol.Optional(
|
||||
CONF_ACTIVATION,
|
||||
default=current_cfg.get(CONF_ACTIVATION, STATE_HIGH),
|
||||
): vol.All(vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)),
|
||||
): vol.All(vol.Lower, vol.In([STATE_HIGH, STATE_LOW])),
|
||||
vol.Optional(
|
||||
CONF_MOMENTARY,
|
||||
default=current_cfg.get(CONF_MOMENTARY, vol.UNDEFINED),
|
||||
@@ -695,12 +710,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
CONF_REPEAT,
|
||||
default=current_cfg.get(CONF_REPEAT, vol.UNDEFINED),
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=-1)),
|
||||
vol.Required(
|
||||
CONF_MORE_STATES,
|
||||
default=CONF_YES
|
||||
if len(self.current_states) > 1
|
||||
else CONF_NO,
|
||||
): vol.In([CONF_YES, CONF_NO]),
|
||||
}
|
||||
),
|
||||
description_placeholders={
|
||||
"zone": f"Zone {self.active_cfg}"
|
||||
if len(self.active_cfg) < 3
|
||||
else self.active_cfg.upper()
|
||||
else self.active_cfg.upper(),
|
||||
"state": str(self.current_state),
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
@@ -709,7 +731,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
for key, value in self.io_cfg.items():
|
||||
if value == CONF_IO_SWI:
|
||||
self.active_cfg = key
|
||||
current_cfg = self.get_current_cfg(CONF_SWITCHES, self.active_cfg)
|
||||
self.current_states = [
|
||||
cfg
|
||||
for cfg in self.current_opt.get(CONF_SWITCHES, [])
|
||||
if cfg[CONF_ZONE] == self.active_cfg
|
||||
]
|
||||
current_cfg = next(iter(self.current_states), {})
|
||||
self.current_state = 1
|
||||
return self.async_show_form(
|
||||
step_id="options_switch",
|
||||
data_schema=vol.Schema(
|
||||
@@ -720,7 +748,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_ACTIVATION,
|
||||
default=current_cfg.get(CONF_ACTIVATION, "high"),
|
||||
default=current_cfg.get(CONF_ACTIVATION, STATE_HIGH),
|
||||
): vol.In(["low", "high"]),
|
||||
vol.Optional(
|
||||
CONF_MOMENTARY,
|
||||
@@ -734,12 +762,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
CONF_REPEAT,
|
||||
default=current_cfg.get(CONF_REPEAT, vol.UNDEFINED),
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=-1)),
|
||||
vol.Required(
|
||||
CONF_MORE_STATES,
|
||||
default=CONF_YES
|
||||
if len(self.current_states) > 1
|
||||
else CONF_NO,
|
||||
): vol.In([CONF_YES, CONF_NO]),
|
||||
}
|
||||
),
|
||||
description_placeholders={
|
||||
"zone": f"Zone {self.active_cfg}"
|
||||
if len(self.active_cfg) < 3
|
||||
else self.active_cfg.upper()
|
||||
else self.active_cfg.upper(),
|
||||
"state": str(self.current_state),
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
@@ -80,13 +80,14 @@
|
||||
},
|
||||
"options_switch": {
|
||||
"title": "Configure Switchable Output",
|
||||
"description": "Please select the output options for {zone}",
|
||||
"description": "Please select the output options for {zone}: state {state}",
|
||||
"data": {
|
||||
"name": "Name (optional)",
|
||||
"activation": "Output when on",
|
||||
"momentary": "Pulse duration (ms) (optional)",
|
||||
"pause": "Pause between pulses (ms) (optional)",
|
||||
"repeat": "Times to repeat (-1=infinite) (optional)"
|
||||
"repeat": "Times to repeat (-1=infinite) (optional)",
|
||||
"more_states": "Configure additional states for this zone"
|
||||
}
|
||||
},
|
||||
"options_misc": {
|
||||
|
@@ -72,6 +72,7 @@ async def websocket_lovelace_config(hass, connection, msg, config):
|
||||
return await config.async_load(msg["force"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
@@ -86,6 +87,7 @@ async def websocket_lovelace_save_config(hass, connection, msg, config):
|
||||
await config.async_save(msg["config"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
|
@@ -83,9 +83,14 @@ SENSOR_TYPES = {
|
||||
}
|
||||
|
||||
CONDITION_CLASSES = {
|
||||
"clear-night": ["Nuit Claire"],
|
||||
"clear-night": ["Nuit Claire", "Nuit claire"],
|
||||
"cloudy": ["Très nuageux"],
|
||||
"fog": ["Brume ou bancs de brouillard", "Brouillard", "Brouillard givrant"],
|
||||
"fog": [
|
||||
"Brume ou bancs de brouillard",
|
||||
"Brume",
|
||||
"Brouillard",
|
||||
"Brouillard givrant",
|
||||
],
|
||||
"hail": ["Risque de grêle"],
|
||||
"lightning": ["Risque d'orages", "Orages"],
|
||||
"lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"],
|
||||
|
@@ -25,7 +25,7 @@ from homeassistant.const import (
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import ( # DEFAULT_HUB,
|
||||
from .const import (
|
||||
ATTR_ADDRESS,
|
||||
ATTR_HUB,
|
||||
ATTR_UNIT,
|
||||
@@ -34,16 +34,12 @@ from .const import ( # DEFAULT_HUB,
|
||||
CONF_BYTESIZE,
|
||||
CONF_PARITY,
|
||||
CONF_STOPBITS,
|
||||
DEFAULT_HUB,
|
||||
MODBUS_DOMAIN,
|
||||
SERVICE_WRITE_COIL,
|
||||
SERVICE_WRITE_REGISTER,
|
||||
)
|
||||
|
||||
# Kept for compatibility with other integrations, TO BE REMOVED
|
||||
CONF_HUB = "hub"
|
||||
DEFAULT_HUB = "default"
|
||||
DOMAIN = MODBUS_DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string})
|
||||
|
@@ -1,11 +1,16 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
|
||||
"unknown": "Unerwarteter Fehler"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"port": "Serielle Schnittstelle",
|
||||
"source_1": "Name der Quelle #1",
|
||||
"source_2": "Name der Quelle #2",
|
||||
"source_3": "Name der Quelle #3",
|
||||
@@ -15,7 +20,8 @@
|
||||
},
|
||||
"title": "Stellen Sie eine Verbindung zum Ger\u00e4t her"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Monoprice 6-Zonen-Verst\u00e4rker"
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
|
41
homeassistant/components/monoprice/.translations/it.json
Normal file
41
homeassistant/components/monoprice/.translations/it.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi, si prega di riprovare",
|
||||
"unknown": "Errore imprevisto"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"port": "Porta seriale",
|
||||
"source_1": "Nome della fonte n. 1",
|
||||
"source_2": "Nome della fonte n. 2",
|
||||
"source_3": "Nome della fonte n. 3",
|
||||
"source_4": "Nome della fonte n. 4",
|
||||
"source_5": "Nome della fonte n. 5",
|
||||
"source_6": "Nome della fonte n. 6"
|
||||
},
|
||||
"title": "Connettersi al dispositivo"
|
||||
}
|
||||
},
|
||||
"title": "Amplificatore a 6 zone Monoprice"
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"source_1": "Nome della fonte n. 1",
|
||||
"source_2": "Nome della fonte n. 2",
|
||||
"source_3": "Nome della fonte n. 3",
|
||||
"source_4": "Nome della fonte n. 4",
|
||||
"source_5": "Nome della fonte n. 5",
|
||||
"source_6": "Nome della fonte n. 6"
|
||||
},
|
||||
"title": "Configurare le sorgenti"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -56,7 +56,7 @@ from .const import (
|
||||
DEFAULT_QOS,
|
||||
PROTOCOL_311,
|
||||
)
|
||||
from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash
|
||||
from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash, set_discovery_hash
|
||||
from .models import Message, MessageCallbackType, PublishPayloadType
|
||||
from .subscription import async_subscribe_topics, async_unsubscribe_topics
|
||||
|
||||
@@ -1181,10 +1181,12 @@ class MqttDiscoveryUpdate(Entity):
|
||||
self._discovery_data = discovery_data
|
||||
self._discovery_update = discovery_update
|
||||
self._remove_signal = None
|
||||
self._removed_from_hass = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to discovery updates."""
|
||||
await super().async_added_to_hass()
|
||||
self._removed_from_hass = False
|
||||
discovery_hash = (
|
||||
self._discovery_data[ATTR_DISCOVERY_HASH] if self._discovery_data else None
|
||||
)
|
||||
@@ -1217,6 +1219,8 @@ class MqttDiscoveryUpdate(Entity):
|
||||
await self._discovery_update(payload)
|
||||
|
||||
if discovery_hash:
|
||||
# Set in case the entity has been removed and is re-added
|
||||
set_discovery_hash(self.hass, discovery_hash)
|
||||
self._remove_signal = async_dispatcher_connect(
|
||||
self.hass,
|
||||
MQTT_DISCOVERY_UPDATED.format(discovery_hash),
|
||||
@@ -1225,7 +1229,7 @@ class MqttDiscoveryUpdate(Entity):
|
||||
|
||||
async def async_removed_from_registry(self) -> None:
|
||||
"""Clear retained discovery topic in broker."""
|
||||
if self._discovery_data:
|
||||
if not self._removed_from_hass:
|
||||
discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC]
|
||||
publish(
|
||||
self.hass, discovery_topic, "", retain=True,
|
||||
@@ -1237,9 +1241,9 @@ class MqttDiscoveryUpdate(Entity):
|
||||
|
||||
def _cleanup_on_remove(self) -> None:
|
||||
"""Stop listening to signal and cleanup discovery data."""
|
||||
if self._discovery_data:
|
||||
if self._discovery_data and not self._removed_from_hass:
|
||||
clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH])
|
||||
self._discovery_data = None
|
||||
self._removed_from_hass = True
|
||||
|
||||
if self._remove_signal:
|
||||
self._remove_signal()
|
||||
|
@@ -64,6 +64,11 @@ def clear_discovery_hash(hass, discovery_hash):
|
||||
del hass.data[ALREADY_DISCOVERED][discovery_hash]
|
||||
|
||||
|
||||
def set_discovery_hash(hass, discovery_hash):
|
||||
"""Clear entry in ALREADY_DISCOVERED list."""
|
||||
hass.data[ALREADY_DISCOVERED][discovery_hash] = {}
|
||||
|
||||
|
||||
class MQTTConfig(dict):
|
||||
"""Dummy class to allow adding attributes."""
|
||||
|
||||
|
22
homeassistant/components/myq/.translations/it.json
Normal file
22
homeassistant/components/myq/.translations/it.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "MyQ \u00e8 gi\u00e0 configurato"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi, si prega di riprovare",
|
||||
"invalid_auth": "Autenticazione non valida",
|
||||
"unknown": "Errore imprevisto"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
"username": "Nome utente"
|
||||
},
|
||||
"title": "Connettersi al gateway MyQ"
|
||||
}
|
||||
},
|
||||
"title": "MyQ"
|
||||
}
|
||||
}
|
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Dieses Nexia Home ist bereits konfiguriert"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut",
|
||||
"invalid_auth": "Ung\u00fcltige Authentifizierung",
|
||||
|
22
homeassistant/components/nexia/.translations/it.json
Normal file
22
homeassistant/components/nexia/.translations/it.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Questo Nexia Home \u00e8 gi\u00e0 configurato"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi, si prega di riprovare",
|
||||
"invalid_auth": "Autenticazione non valida",
|
||||
"unknown": "Errore imprevisto"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
"username": "Nome utente"
|
||||
},
|
||||
"title": "Connettersi a mynexia.com"
|
||||
}
|
||||
},
|
||||
"title": "Nexia"
|
||||
}
|
||||
}
|
@@ -380,7 +380,10 @@ class LeafDataStore:
|
||||
)
|
||||
return server_info
|
||||
except CarwingsError:
|
||||
_LOGGER.error("An error occurred getting battery status.")
|
||||
_LOGGER.error("An error occurred getting battery status")
|
||||
return None
|
||||
except KeyError:
|
||||
_LOGGER.error("An error occurred parsing response from server")
|
||||
return None
|
||||
|
||||
async def async_get_climate(self):
|
||||
|
@@ -15,7 +15,9 @@
|
||||
"password": "Passwort",
|
||||
"serial_number": "Seriennummer des Thermostats.",
|
||||
"username": "Benutzername"
|
||||
}
|
||||
},
|
||||
"description": "Sie m\u00fcssen die numerische Seriennummer oder ID Ihres Thermostats erhalten, indem Sie sich bei https://MyNuHeat.com anmelden und Ihre Thermostate ausw\u00e4hlen.",
|
||||
"title": "Stellen Sie eine Verbindung zu NuHeat her"
|
||||
}
|
||||
},
|
||||
"title": "NuHeat"
|
||||
|
25
homeassistant/components/nuheat/.translations/it.json
Normal file
25
homeassistant/components/nuheat/.translations/it.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Il termostato \u00e8 gi\u00e0 configurato"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi, si prega di riprovare",
|
||||
"invalid_auth": "Autenticazione non valida",
|
||||
"invalid_thermostat": "Il numero di serie del termostato non \u00e8 valido.",
|
||||
"unknown": "Errore imprevisto"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
"serial_number": "Numero di serie del termostato.",
|
||||
"username": "Nome utente"
|
||||
},
|
||||
"description": "\u00c8 necessario ottenere il numero di serie o l'ID numerico del termostato accedendo a https://MyNuHeat.com e selezionando il termostato.",
|
||||
"title": "Connettersi al NuHeat"
|
||||
}
|
||||
},
|
||||
"title": "NuHeat"
|
||||
}
|
||||
}
|
@@ -220,6 +220,8 @@ class NUTSensor(Entity):
|
||||
self._name = "{} {}".format(name, SENSOR_TYPES[sensor_type][0])
|
||||
self._unit = SENSOR_TYPES[sensor_type][1]
|
||||
self._state = None
|
||||
self._display_state = None
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -241,38 +243,44 @@ class NUTSensor(Entity):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if the device is polling successfully."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the sensor attributes."""
|
||||
attr = dict()
|
||||
attr[ATTR_STATE] = self.display_state()
|
||||
return attr
|
||||
|
||||
def display_state(self):
|
||||
"""Return UPS display state."""
|
||||
if self._data.status is None:
|
||||
return STATE_TYPES["OFF"]
|
||||
try:
|
||||
return " ".join(
|
||||
STATE_TYPES[state] for state in self._data.status[KEY_STATUS].split()
|
||||
)
|
||||
except KeyError:
|
||||
return STATE_UNKNOWN
|
||||
return {ATTR_STATE: self._display_state}
|
||||
|
||||
def update(self):
|
||||
"""Get the latest status and use it to update our sensor state."""
|
||||
if self._data.status is None:
|
||||
self._state = None
|
||||
status = self._data.status
|
||||
|
||||
if status is None:
|
||||
self._available = False
|
||||
return
|
||||
|
||||
self._available = True
|
||||
self._display_state = _format_display_state(status)
|
||||
# In case of the display status sensor, keep a human-readable form
|
||||
# as the sensor state.
|
||||
if self.type == KEY_STATUS_DISPLAY:
|
||||
self._state = self.display_state()
|
||||
elif self.type not in self._data.status:
|
||||
self._state = self._display_state
|
||||
elif self.type not in status:
|
||||
self._state = None
|
||||
else:
|
||||
self._state = self._data.status[self.type]
|
||||
self._state = status[self.type]
|
||||
|
||||
|
||||
def _format_display_state(status):
|
||||
"""Return UPS display state."""
|
||||
if status is None:
|
||||
return STATE_TYPES["OFF"]
|
||||
try:
|
||||
return " ".join(STATE_TYPES[state] for state in status[KEY_STATUS].split())
|
||||
except KeyError:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
|
||||
class PyNUTData:
|
||||
|
@@ -3,6 +3,7 @@ import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.core import callback
|
||||
@@ -99,7 +100,7 @@ class UserOnboardingView(_BaseOnboardingView):
|
||||
provider = _async_get_hass_provider(hass)
|
||||
await provider.async_initialize()
|
||||
|
||||
user = await hass.auth.async_create_user(data["name"])
|
||||
user = await hass.auth.async_create_user(data["name"], [GROUP_ID_ADMIN])
|
||||
await hass.async_add_executor_job(
|
||||
provider.data.add_auth, data["username"], data["password"]
|
||||
)
|
||||
|
@@ -2,7 +2,10 @@
|
||||
"domain": "opencv",
|
||||
"name": "OpenCV",
|
||||
"documentation": "https://www.home-assistant.io/integrations/opencv",
|
||||
"requirements": ["numpy==1.18.1", "opencv-python-headless==4.1.2.30"],
|
||||
"requirements": [
|
||||
"numpy==1.18.1",
|
||||
"opencv-python-headless==4.2.0.32"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Die Integration ist bereits mit einem vorhandenen Sensor mit diesem Tarif konfiguriert"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "Sensorname",
|
||||
"tariff": "Vertragstarif (1, 2 oder 3 Perioden)"
|
||||
},
|
||||
"description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. \nWeitere Informationen finden Sie in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nW\u00e4hlen Sie den vertraglich vereinbarten Tarif basierend auf der Anzahl der Abrechnungsperioden pro Tag aus: \n - 1 Periode: Normal \n - 2 Perioden: Diskriminierung (Nachttarif) \n - 3 Perioden: Elektroauto (Nachttarif von 3 Perioden)",
|
||||
"title": "Tarifauswahl"
|
||||
}
|
||||
},
|
||||
|
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "L'integrazione \u00e8 gi\u00e0 configurata con un sensore esistente con quella tariffa"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "Nome del sensore",
|
||||
"tariff": "Tariffa contrattuale (1, 2 o 3 periodi)"
|
||||
},
|
||||
"description": "Questo sensore utilizza l'API ufficiale per ottenere [prezzi orari dell'elettricit\u00e0 (PVPC)](https://www.esios.ree.es/es/pvpc) in Spagna.\nPer una spiegazione pi\u00f9 precisa, visitare la [documentazione di integrazione](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelezionare la tariffa contrattuale in base al numero di periodi di fatturazione al giorno:\n- 1 periodo: normale\n- 2 periodi: discriminazione (tariffa notturna)\n- 3 periodi: auto elettrica (tariffa notturna di 3 periodi)",
|
||||
"title": "Selezione della tariffa"
|
||||
}
|
||||
},
|
||||
"title": "Prezzo orario dell'elettricit\u00e0 in Spagna (PVPC)"
|
||||
}
|
||||
}
|
@@ -13,9 +13,19 @@
|
||||
"data": {
|
||||
"api_key": "Der API-Schl\u00fcssel f\u00fcr das Rachio-Konto."
|
||||
},
|
||||
"description": "Sie ben\u00f6tigen den API-Schl\u00fcssel von https://app.rach.io/. W\u00e4hlen Sie \"Kontoeinstellungen\" und klicken Sie dann auf \"API-SCHL\u00dcSSEL ERHALTEN\".",
|
||||
"title": "Stellen Sie eine Verbindung zu Ihrem Rachio-Ger\u00e4t her"
|
||||
}
|
||||
},
|
||||
"title": "Rachio"
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"manual_run_mins": "Wie lange, in Minuten, um eine Station einzuschalten, wenn der Schalter aktiviert ist."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@
|
||||
"domain": "slack",
|
||||
"name": "Slack",
|
||||
"documentation": "https://www.home-assistant.io/integrations/slack",
|
||||
"requirements": ["slacker==0.14.0"],
|
||||
"requirements": ["slackclient==2.5.0"],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
"""Slack platform for notify component."""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
import slacker
|
||||
from slacker import Slacker
|
||||
from slack import WebClient
|
||||
from slack.errors import SlackApiError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
@@ -15,157 +16,138 @@ from homeassistant.components.notify import (
|
||||
BaseNotificationService,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CHANNEL = "default_channel"
|
||||
CONF_TIMEOUT = 15
|
||||
|
||||
# Top level attributes in 'data'
|
||||
ATTR_ATTACHMENTS = "attachments"
|
||||
ATTR_BLOCKS = "blocks"
|
||||
ATTR_FILE = "file"
|
||||
# Attributes contained in file
|
||||
ATTR_FILE_URL = "url"
|
||||
ATTR_FILE_PATH = "path"
|
||||
ATTR_FILE_USERNAME = "username"
|
||||
ATTR_FILE_PASSWORD = "password"
|
||||
ATTR_FILE_AUTH = "auth"
|
||||
# Any other value or absence of 'auth' lead to basic authentication being used
|
||||
ATTR_FILE_AUTH_DIGEST = "digest"
|
||||
|
||||
CONF_DEFAULT_CHANNEL = "default_channel"
|
||||
|
||||
DEFAULT_TIMEOUT_SECONDS = 15
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_CHANNEL): cv.string,
|
||||
vol.Required(CONF_DEFAULT_CHANNEL): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.string,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_service(hass, config, discovery_info=None):
|
||||
"""Get the Slack notification service."""
|
||||
|
||||
channel = config.get(CONF_CHANNEL)
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
username = config.get(CONF_USERNAME)
|
||||
icon = config.get(CONF_ICON)
|
||||
async def async_get_service(hass, config, discovery_info=None):
|
||||
"""Set up the Slack notification service."""
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
client = WebClient(token=config[CONF_API_KEY], run_async=True, session=session)
|
||||
|
||||
try:
|
||||
return SlackNotificationService(
|
||||
channel, api_key, username, icon, hass.config.is_allowed_path
|
||||
)
|
||||
await client.auth_test()
|
||||
except SlackApiError as err:
|
||||
_LOGGER.error("Error while setting up integration: %s", err)
|
||||
return
|
||||
|
||||
except slacker.Error:
|
||||
_LOGGER.exception("Authentication failed")
|
||||
return None
|
||||
return SlackNotificationService(
|
||||
hass,
|
||||
client,
|
||||
config[CONF_DEFAULT_CHANNEL],
|
||||
username=config.get(CONF_USERNAME),
|
||||
icon=config.get(CONF_ICON),
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_sanitize_channel_names(channel_list):
|
||||
"""Remove any # symbols from a channel list."""
|
||||
return [channel.lstrip("#") for channel in channel_list]
|
||||
|
||||
|
||||
class SlackNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for Slack."""
|
||||
|
||||
def __init__(self, default_channel, api_token, username, icon, is_allowed_path):
|
||||
"""Initialize the service."""
|
||||
"""Define the Slack notification logic."""
|
||||
|
||||
def __init__(self, hass, client, default_channel, username, icon):
|
||||
"""Initialize."""
|
||||
self._client = client
|
||||
self._default_channel = default_channel
|
||||
self._api_token = api_token
|
||||
self._username = username
|
||||
self._hass = hass
|
||||
self._icon = icon
|
||||
if self._username or self._icon:
|
||||
|
||||
if username or self._icon:
|
||||
self._as_user = False
|
||||
else:
|
||||
self._as_user = True
|
||||
|
||||
self.is_allowed_path = is_allowed_path
|
||||
self.slack = Slacker(self._api_token)
|
||||
self.slack.auth.test()
|
||||
async def _async_send_local_file_message(self, path, targets, message, title):
|
||||
"""Upload a local file (with message) to Slack."""
|
||||
if not self._hass.config.is_allowed_path(path):
|
||||
_LOGGER.error("Path does not exist or is not allowed: %s", path)
|
||||
return
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
parsed_url = urlparse(path)
|
||||
filename = os.path.basename(parsed_url.path)
|
||||
|
||||
if kwargs.get(ATTR_TARGET) is None:
|
||||
targets = [self._default_channel]
|
||||
else:
|
||||
targets = kwargs.get(ATTR_TARGET)
|
||||
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
attachments = data.get(ATTR_ATTACHMENTS) if data else None
|
||||
file = data.get(ATTR_FILE) if data else None
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
|
||||
for target in targets:
|
||||
try:
|
||||
if file is not None:
|
||||
# Load from file or URL
|
||||
file_as_bytes = self.load_file(
|
||||
url=file.get(ATTR_FILE_URL),
|
||||
local_path=file.get(ATTR_FILE_PATH),
|
||||
username=file.get(ATTR_FILE_USERNAME),
|
||||
password=file.get(ATTR_FILE_PASSWORD),
|
||||
auth=file.get(ATTR_FILE_AUTH),
|
||||
)
|
||||
# Choose filename
|
||||
if file.get(ATTR_FILE_URL):
|
||||
filename = file.get(ATTR_FILE_URL)
|
||||
else:
|
||||
filename = file.get(ATTR_FILE_PATH)
|
||||
# Prepare structure for Slack API
|
||||
data = {
|
||||
"content": None,
|
||||
"filetype": None,
|
||||
"filename": filename,
|
||||
# If optional title is none use the filename
|
||||
"title": title if title else filename,
|
||||
"initial_comment": message,
|
||||
"channels": target,
|
||||
}
|
||||
# Post to slack
|
||||
self.slack.files.post(
|
||||
"files.upload", data=data, files={"file": file_as_bytes}
|
||||
)
|
||||
else:
|
||||
self.slack.chat.post_message(
|
||||
target,
|
||||
message,
|
||||
as_user=self._as_user,
|
||||
username=self._username,
|
||||
icon_emoji=self._icon,
|
||||
attachments=attachments,
|
||||
link_names=True,
|
||||
)
|
||||
except slacker.Error as err:
|
||||
_LOGGER.error("Could not send notification. Error: %s", err)
|
||||
|
||||
def load_file(
|
||||
self, url=None, local_path=None, username=None, password=None, auth=None
|
||||
):
|
||||
"""Load image/document/etc from a local path or URL."""
|
||||
try:
|
||||
if url:
|
||||
# Check whether authentication parameters are provided
|
||||
if username:
|
||||
# Use digest or basic authentication
|
||||
if ATTR_FILE_AUTH_DIGEST == auth:
|
||||
auth_ = HTTPDigestAuth(username, password)
|
||||
else:
|
||||
auth_ = HTTPBasicAuth(username, password)
|
||||
# Load file from URL with authentication
|
||||
req = requests.get(url, auth=auth_, timeout=CONF_TIMEOUT)
|
||||
else:
|
||||
# Load file from URL without authentication
|
||||
req = requests.get(url, timeout=CONF_TIMEOUT)
|
||||
return req.content
|
||||
await self._client.files_upload(
|
||||
channels=",".join(targets),
|
||||
file=path,
|
||||
filename=filename,
|
||||
initial_comment=message,
|
||||
title=title or filename,
|
||||
)
|
||||
except SlackApiError as err:
|
||||
_LOGGER.error("Error while uploading file-based message: %s", err)
|
||||
|
||||
if local_path:
|
||||
# Check whether path is whitelisted in configuration.yaml
|
||||
if self.is_allowed_path(local_path):
|
||||
return open(local_path, "rb")
|
||||
_LOGGER.warning("'%s' is not secure to load data from!", local_path)
|
||||
else:
|
||||
_LOGGER.warning("Neither URL nor local path found in parameters!")
|
||||
async def _async_send_text_only_message(
|
||||
self, targets, message, title, attachments, blocks
|
||||
):
|
||||
"""Send a text-only message."""
|
||||
tasks = {
|
||||
target: self._client.chat_postMessage(
|
||||
channel=target,
|
||||
text=message,
|
||||
as_user=self._as_user,
|
||||
attachments=attachments,
|
||||
blocks=blocks,
|
||||
icon_emoji=self._icon,
|
||||
link_names=True,
|
||||
)
|
||||
for target in targets
|
||||
}
|
||||
|
||||
except OSError as error:
|
||||
_LOGGER.error("Can't load from URL or local path: %s", error)
|
||||
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
|
||||
for target, result in zip(tasks, results):
|
||||
if isinstance(result, SlackApiError):
|
||||
_LOGGER.error(
|
||||
"There was a Slack API error while sending to %s: %s",
|
||||
target,
|
||||
result,
|
||||
)
|
||||
|
||||
return None
|
||||
async def async_send_message(self, message, **kwargs):
|
||||
"""Send a message to Slack."""
|
||||
data = kwargs[ATTR_DATA] or {}
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
targets = _async_sanitize_channel_names(
|
||||
kwargs.get(ATTR_TARGET, [self._default_channel])
|
||||
)
|
||||
|
||||
if ATTR_FILE in data:
|
||||
return await self._async_send_local_file_message(
|
||||
data[ATTR_FILE], targets, message, title
|
||||
)
|
||||
|
||||
attachments = data.get(ATTR_ATTACHMENTS, {})
|
||||
if attachments:
|
||||
_LOGGER.warning(
|
||||
"Attachments are deprecated and part of Slack's legacy API; support "
|
||||
"for them will be dropped in 0.114.0. In most cases, Blocks should be "
|
||||
"used instead: https://www.home-assistant.io/integrations/slack/"
|
||||
)
|
||||
blocks = data.get(ATTR_BLOCKS, {})
|
||||
|
||||
return await self._async_send_text_only_message(
|
||||
targets, message, title, attachments, blocks
|
||||
)
|
||||
|
@@ -437,15 +437,25 @@ class SoundTouchDevice(MediaPlayerDevice):
|
||||
# slaves for some reason. To compensate for this shortcoming we have to fetch
|
||||
# the zone info from the master when the current device is a slave until this is
|
||||
# fixed in the SoundTouch API or libsoundtouch, or of course until somebody has a
|
||||
# better idea on how to fix this
|
||||
if zone_status.is_master:
|
||||
# better idea on how to fix this.
|
||||
# In addition to this shortcoming, libsoundtouch seems to report the "is_master"
|
||||
# property wrong on some slaves, so the only reliable way to detect if the current
|
||||
# devices is the master, is by comparing the master_id of the zone with the device_id
|
||||
if zone_status.master_id == self._device.config.device_id:
|
||||
return self._build_zone_info(self.entity_id, zone_status.slaves)
|
||||
|
||||
master_instance = self._get_instance_by_ip(zone_status.master_ip)
|
||||
master_zone_status = master_instance.device.zone_status()
|
||||
return self._build_zone_info(
|
||||
master_instance.entity_id, master_zone_status.slaves
|
||||
)
|
||||
# The master device has to be searched by it's ID and not IP since libsoundtouch / BOSE API
|
||||
# do not return the IP of the master for some slave objects/responses
|
||||
master_instance = self._get_instance_by_id(zone_status.master_id)
|
||||
if master_instance is not None:
|
||||
master_zone_status = master_instance.device.zone_status()
|
||||
return self._build_zone_info(
|
||||
master_instance.entity_id, master_zone_status.slaves
|
||||
)
|
||||
|
||||
# We should never end up here since this means we haven't found a master device to get the
|
||||
# correct zone info from. In this case, assume current device is master
|
||||
return self._build_zone_info(self.entity_id, zone_status.slaves)
|
||||
|
||||
def _get_instance_by_ip(self, ip_address):
|
||||
"""Search and return a SoundTouchDevice instance by it's IP address."""
|
||||
@@ -454,6 +464,13 @@ class SoundTouchDevice(MediaPlayerDevice):
|
||||
return instance
|
||||
return None
|
||||
|
||||
def _get_instance_by_id(self, instance_id):
|
||||
"""Search and return a SoundTouchDevice instance by it's ID (aka MAC address)."""
|
||||
for instance in self.hass.data[DATA_SOUNDTOUCH]:
|
||||
if instance and instance.device.config.device_id == instance_id:
|
||||
return instance
|
||||
return None
|
||||
|
||||
def _build_zone_info(self, master, zone_slaves):
|
||||
"""Build the exposed zone attributes."""
|
||||
slaves = []
|
||||
|
@@ -5,11 +5,7 @@ import logging
|
||||
from pystiebeleltron import pystiebeleltron
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.modbus import (
|
||||
CONF_HUB,
|
||||
DEFAULT_HUB,
|
||||
DOMAIN as MODBUS_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.modbus.const import CONF_HUB, DEFAULT_HUB, MODBUS_DOMAIN
|
||||
from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@@ -1,9 +1,9 @@
|
||||
"""Support for the (unofficial) Tado API."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
from PyTado.interface import Tado
|
||||
from requests import RequestException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate.const import PRESET_AWAY, PRESET_HOME
|
||||
@@ -110,7 +110,7 @@ class TadoConnector:
|
||||
"""Connect to Tado and fetch the zones."""
|
||||
try:
|
||||
self.tado = Tado(self._username, self._password)
|
||||
except (RuntimeError, urllib.error.HTTPError) as exc:
|
||||
except (RuntimeError, RequestException) as exc:
|
||||
_LOGGER.error("Unable to connect: %s", exc)
|
||||
return False
|
||||
|
||||
@@ -135,9 +135,14 @@ class TadoConnector:
|
||||
_LOGGER.debug("Updating %s %s", sensor_type, sensor)
|
||||
try:
|
||||
if sensor_type == "zone":
|
||||
data = self.tado.getState(sensor)
|
||||
data = self.tado.getZoneState(sensor)
|
||||
elif sensor_type == "device":
|
||||
data = self.tado.getDevices()[0]
|
||||
devices_data = self.tado.getDevices()
|
||||
if not devices_data:
|
||||
_LOGGER.info("There are no devices to setup on this tado account.")
|
||||
return
|
||||
|
||||
data = devices_data[0]
|
||||
else:
|
||||
_LOGGER.debug("Unknown sensor: %s", sensor_type)
|
||||
return
|
||||
@@ -174,29 +179,40 @@ class TadoConnector:
|
||||
|
||||
def set_zone_overlay(
|
||||
self,
|
||||
zone_id,
|
||||
overlay_mode,
|
||||
zone_id=None,
|
||||
overlay_mode=None,
|
||||
temperature=None,
|
||||
duration=None,
|
||||
device_type="HEATING",
|
||||
mode=None,
|
||||
fan_speed=None,
|
||||
):
|
||||
"""Set a zone overlay."""
|
||||
_LOGGER.debug(
|
||||
"Set overlay for zone %s: mode=%s, temp=%s, duration=%s, type=%s, mode=%s",
|
||||
"Set overlay for zone %s: overlay_mode=%s, temp=%s, duration=%s, type=%s, mode=%s fan_speed=%s",
|
||||
zone_id,
|
||||
overlay_mode,
|
||||
temperature,
|
||||
duration,
|
||||
device_type,
|
||||
mode,
|
||||
fan_speed,
|
||||
)
|
||||
|
||||
try:
|
||||
self.tado.setZoneOverlay(
|
||||
zone_id, overlay_mode, temperature, duration, device_type, "ON", mode
|
||||
zone_id,
|
||||
overlay_mode,
|
||||
temperature,
|
||||
duration,
|
||||
device_type,
|
||||
"ON",
|
||||
mode,
|
||||
fan_speed,
|
||||
)
|
||||
except urllib.error.HTTPError as exc:
|
||||
_LOGGER.error("Could not set zone overlay: %s", exc.read())
|
||||
|
||||
except RequestException as exc:
|
||||
_LOGGER.error("Could not set zone overlay: %s", exc)
|
||||
|
||||
self.update_sensor("zone", zone_id)
|
||||
|
||||
@@ -206,7 +222,7 @@ class TadoConnector:
|
||||
self.tado.setZoneOverlay(
|
||||
zone_id, overlay_mode, None, None, device_type, "OFF"
|
||||
)
|
||||
except urllib.error.HTTPError as exc:
|
||||
_LOGGER.error("Could not set zone overlay: %s", exc.read())
|
||||
except RequestException as exc:
|
||||
_LOGGER.error("Could not set zone overlay: %s", exc)
|
||||
|
||||
self.update_sensor("zone", zone_id)
|
||||
|
@@ -3,21 +3,13 @@ import logging
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.climate.const import (
|
||||
CURRENT_HVAC_COOL,
|
||||
CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_IDLE,
|
||||
CURRENT_HVAC_OFF,
|
||||
FAN_HIGH,
|
||||
FAN_LOW,
|
||||
FAN_MIDDLE,
|
||||
FAN_OFF,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_COOL,
|
||||
FAN_AUTO,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_OFF,
|
||||
PRESET_AWAY,
|
||||
PRESET_HOME,
|
||||
SUPPORT_FAN_MODE,
|
||||
SUPPORT_PRESET_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
@@ -27,49 +19,30 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
|
||||
from .const import (
|
||||
CONST_FAN_AUTO,
|
||||
CONST_FAN_OFF,
|
||||
CONST_MODE_AUTO,
|
||||
CONST_MODE_COOL,
|
||||
CONST_MODE_HEAT,
|
||||
CONST_MODE_OFF,
|
||||
CONST_MODE_SMART_SCHEDULE,
|
||||
CONST_OVERLAY_MANUAL,
|
||||
CONST_OVERLAY_TADO_MODE,
|
||||
CONST_OVERLAY_TIMER,
|
||||
DATA,
|
||||
HA_TO_TADO_FAN_MODE_MAP,
|
||||
HA_TO_TADO_HVAC_MODE_MAP,
|
||||
ORDERED_KNOWN_TADO_MODES,
|
||||
SUPPORT_PRESET,
|
||||
TADO_HVAC_ACTION_TO_HA_HVAC_ACTION,
|
||||
TADO_MODES_WITH_NO_TEMP_SETTING,
|
||||
TADO_TO_HA_FAN_MODE_MAP,
|
||||
TADO_TO_HA_HVAC_MODE_MAP,
|
||||
TYPE_AIR_CONDITIONING,
|
||||
TYPE_HEATING,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FAN_MAP_TADO = {"HIGH": FAN_HIGH, "MIDDLE": FAN_MIDDLE, "LOW": FAN_LOW}
|
||||
|
||||
HVAC_MAP_TADO_HEAT = {
|
||||
CONST_OVERLAY_MANUAL: HVAC_MODE_HEAT,
|
||||
CONST_OVERLAY_TIMER: HVAC_MODE_HEAT,
|
||||
CONST_OVERLAY_TADO_MODE: HVAC_MODE_HEAT,
|
||||
CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO,
|
||||
CONST_MODE_OFF: HVAC_MODE_OFF,
|
||||
}
|
||||
HVAC_MAP_TADO_COOL = {
|
||||
CONST_OVERLAY_MANUAL: HVAC_MODE_COOL,
|
||||
CONST_OVERLAY_TIMER: HVAC_MODE_COOL,
|
||||
CONST_OVERLAY_TADO_MODE: HVAC_MODE_COOL,
|
||||
CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO,
|
||||
CONST_MODE_OFF: HVAC_MODE_OFF,
|
||||
}
|
||||
HVAC_MAP_TADO_HEAT_COOL = {
|
||||
CONST_OVERLAY_MANUAL: HVAC_MODE_HEAT_COOL,
|
||||
CONST_OVERLAY_TIMER: HVAC_MODE_HEAT_COOL,
|
||||
CONST_OVERLAY_TADO_MODE: HVAC_MODE_HEAT_COOL,
|
||||
CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO,
|
||||
CONST_MODE_OFF: HVAC_MODE_OFF,
|
||||
}
|
||||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
|
||||
SUPPORT_HVAC_HEAT = [HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF]
|
||||
SUPPORT_HVAC_COOL = [HVAC_MODE_COOL, HVAC_MODE_AUTO, HVAC_MODE_OFF]
|
||||
SUPPORT_HVAC_HEAT_COOL = [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO, HVAC_MODE_OFF]
|
||||
SUPPORT_FAN = [FAN_HIGH, FAN_MIDDLE, FAN_LOW, FAN_OFF]
|
||||
SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Tado climate platform."""
|
||||
@@ -96,29 +69,80 @@ def create_climate_entity(tado, name: str, zone_id: int):
|
||||
_LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities)
|
||||
|
||||
zone_type = capabilities["type"]
|
||||
support_flags = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE
|
||||
supported_hvac_modes = [
|
||||
TADO_TO_HA_HVAC_MODE_MAP[CONST_MODE_OFF],
|
||||
TADO_TO_HA_HVAC_MODE_MAP[CONST_MODE_SMART_SCHEDULE],
|
||||
]
|
||||
supported_fan_modes = None
|
||||
heat_temperatures = None
|
||||
cool_temperatures = None
|
||||
|
||||
ac_support_heat = False
|
||||
if zone_type == TYPE_AIR_CONDITIONING:
|
||||
# Only use heat if available
|
||||
# (you don't have to setup a heat mode, but cool is required)
|
||||
# Heat is preferred as it generally has a lower minimum temperature
|
||||
if "HEAT" in capabilities:
|
||||
temperatures = capabilities["HEAT"]["temperatures"]
|
||||
ac_support_heat = True
|
||||
else:
|
||||
temperatures = capabilities["COOL"]["temperatures"]
|
||||
elif "temperatures" in capabilities:
|
||||
temperatures = capabilities["temperatures"]
|
||||
for mode in ORDERED_KNOWN_TADO_MODES:
|
||||
if mode not in capabilities:
|
||||
continue
|
||||
|
||||
supported_hvac_modes.append(TADO_TO_HA_HVAC_MODE_MAP[mode])
|
||||
if not capabilities[mode].get("fanSpeeds"):
|
||||
continue
|
||||
|
||||
support_flags |= SUPPORT_FAN_MODE
|
||||
|
||||
if supported_fan_modes:
|
||||
continue
|
||||
|
||||
supported_fan_modes = [
|
||||
TADO_TO_HA_FAN_MODE_MAP[speed]
|
||||
for speed in capabilities[mode]["fanSpeeds"]
|
||||
]
|
||||
|
||||
cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"]
|
||||
else:
|
||||
_LOGGER.debug("Not adding zone %s since it has no temperature", name)
|
||||
supported_hvac_modes.append(HVAC_MODE_HEAT)
|
||||
|
||||
if CONST_MODE_HEAT in capabilities:
|
||||
heat_temperatures = capabilities[CONST_MODE_HEAT]["temperatures"]
|
||||
|
||||
if heat_temperatures is None and "temperatures" in capabilities:
|
||||
heat_temperatures = capabilities["temperatures"]
|
||||
|
||||
if cool_temperatures is None and heat_temperatures is None:
|
||||
_LOGGER.debug("Not adding zone %s since it has no temperatures", name)
|
||||
return None
|
||||
|
||||
min_temp = float(temperatures["celsius"]["min"])
|
||||
max_temp = float(temperatures["celsius"]["max"])
|
||||
step = temperatures["celsius"].get("step", PRECISION_TENTHS)
|
||||
heat_min_temp = None
|
||||
heat_max_temp = None
|
||||
heat_step = None
|
||||
cool_min_temp = None
|
||||
cool_max_temp = None
|
||||
cool_step = None
|
||||
|
||||
if heat_temperatures is not None:
|
||||
heat_min_temp = float(heat_temperatures["celsius"]["min"])
|
||||
heat_max_temp = float(heat_temperatures["celsius"]["max"])
|
||||
heat_step = heat_temperatures["celsius"].get("step", PRECISION_TENTHS)
|
||||
|
||||
if cool_temperatures is not None:
|
||||
cool_min_temp = float(cool_temperatures["celsius"]["min"])
|
||||
cool_max_temp = float(cool_temperatures["celsius"]["max"])
|
||||
cool_step = cool_temperatures["celsius"].get("step", PRECISION_TENTHS)
|
||||
|
||||
entity = TadoClimate(
|
||||
tado, name, zone_id, zone_type, min_temp, max_temp, step, ac_support_heat,
|
||||
tado,
|
||||
name,
|
||||
zone_id,
|
||||
zone_type,
|
||||
heat_min_temp,
|
||||
heat_max_temp,
|
||||
heat_step,
|
||||
cool_min_temp,
|
||||
cool_max_temp,
|
||||
cool_step,
|
||||
supported_hvac_modes,
|
||||
supported_fan_modes,
|
||||
support_flags,
|
||||
)
|
||||
return entity
|
||||
|
||||
@@ -132,10 +156,15 @@ class TadoClimate(ClimateDevice):
|
||||
zone_name,
|
||||
zone_id,
|
||||
zone_type,
|
||||
min_temp,
|
||||
max_temp,
|
||||
step,
|
||||
ac_support_heat,
|
||||
heat_min_temp,
|
||||
heat_max_temp,
|
||||
heat_step,
|
||||
cool_min_temp,
|
||||
cool_max_temp,
|
||||
cool_step,
|
||||
supported_hvac_modes,
|
||||
supported_fan_modes,
|
||||
support_flags,
|
||||
):
|
||||
"""Initialize of Tado climate entity."""
|
||||
self._tado = tado
|
||||
@@ -146,49 +175,51 @@ class TadoClimate(ClimateDevice):
|
||||
self._unique_id = f"{zone_type} {zone_id} {tado.device_id}"
|
||||
|
||||
self._ac_device = zone_type == TYPE_AIR_CONDITIONING
|
||||
self._ac_support_heat = ac_support_heat
|
||||
self._cooling = False
|
||||
self._supported_hvac_modes = supported_hvac_modes
|
||||
self._supported_fan_modes = supported_fan_modes
|
||||
self._support_flags = support_flags
|
||||
|
||||
self._active = False
|
||||
self._device_is_active = False
|
||||
self._available = False
|
||||
|
||||
self._cur_temp = None
|
||||
self._cur_humidity = None
|
||||
self._is_away = False
|
||||
self._min_temp = min_temp
|
||||
self._max_temp = max_temp
|
||||
self._step = step
|
||||
|
||||
self._heat_min_temp = heat_min_temp
|
||||
self._heat_max_temp = heat_max_temp
|
||||
self._heat_step = heat_step
|
||||
|
||||
self._cool_min_temp = cool_min_temp
|
||||
self._cool_max_temp = cool_max_temp
|
||||
self._cool_step = cool_step
|
||||
|
||||
self._target_temp = None
|
||||
|
||||
if tado.fallback:
|
||||
# Fallback to Smart Schedule at next Schedule switch
|
||||
self._default_overlay = CONST_OVERLAY_TADO_MODE
|
||||
else:
|
||||
# Don't fallback to Smart Schedule, but keep in manual mode
|
||||
self._default_overlay = CONST_OVERLAY_MANUAL
|
||||
self._current_tado_fan_speed = CONST_FAN_OFF
|
||||
self._current_tado_hvac_mode = CONST_MODE_OFF
|
||||
self._current_tado_hvac_action = CURRENT_HVAC_OFF
|
||||
|
||||
self._current_fan = CONST_MODE_OFF
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._undo_dispatcher = None
|
||||
self._tado_zone_data = None
|
||||
self._async_update_zone_data()
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""When entity will be removed from hass."""
|
||||
if self._undo_dispatcher:
|
||||
self._undo_dispatcher()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register for sensor updates."""
|
||||
|
||||
@callback
|
||||
def async_update_callback():
|
||||
"""Schedule an entity update."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async_dispatcher_connect(
|
||||
self._undo_dispatcher = async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id),
|
||||
async_update_callback,
|
||||
self._async_update_callback,
|
||||
)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -208,12 +239,12 @@ class TadoClimate(ClimateDevice):
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
return self._cur_humidity
|
||||
return self._tado_zone_data.current_humidity
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the sensor temperature."""
|
||||
return self._cur_temp
|
||||
return self._tado_zone_data.current_temp
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
@@ -221,11 +252,7 @@ class TadoClimate(ClimateDevice):
|
||||
|
||||
Need to be one of HVAC_MODE_*.
|
||||
"""
|
||||
if self._ac_device and self._ac_support_heat:
|
||||
return HVAC_MAP_TADO_HEAT_COOL.get(self._current_operation)
|
||||
if self._ac_device and not self._ac_support_heat:
|
||||
return HVAC_MAP_TADO_COOL.get(self._current_operation)
|
||||
return HVAC_MAP_TADO_HEAT.get(self._current_operation)
|
||||
return TADO_TO_HA_HVAC_MODE_MAP.get(self._current_tado_hvac_mode, HVAC_MODE_OFF)
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
@@ -233,11 +260,7 @@ class TadoClimate(ClimateDevice):
|
||||
|
||||
Need to be a subset of HVAC_MODES.
|
||||
"""
|
||||
if self._ac_device:
|
||||
if self._ac_support_heat:
|
||||
return SUPPORT_HVAC_HEAT_COOL
|
||||
return SUPPORT_HVAC_COOL
|
||||
return SUPPORT_HVAC_HEAT
|
||||
return self._supported_hvac_modes
|
||||
|
||||
@property
|
||||
def hvac_action(self):
|
||||
@@ -245,40 +268,30 @@ class TadoClimate(ClimateDevice):
|
||||
|
||||
Need to be one of CURRENT_HVAC_*.
|
||||
"""
|
||||
if not self._device_is_active:
|
||||
return CURRENT_HVAC_OFF
|
||||
if self._ac_device:
|
||||
if self._active:
|
||||
if self._ac_support_heat and not self._cooling:
|
||||
return CURRENT_HVAC_HEAT
|
||||
return CURRENT_HVAC_COOL
|
||||
return CURRENT_HVAC_IDLE
|
||||
if self._active:
|
||||
return CURRENT_HVAC_HEAT
|
||||
return CURRENT_HVAC_IDLE
|
||||
return TADO_HVAC_ACTION_TO_HA_HVAC_ACTION.get(
|
||||
self._tado_zone_data.current_hvac_action, CURRENT_HVAC_OFF
|
||||
)
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
if self._ac_device:
|
||||
return FAN_MAP_TADO.get(self._current_fan)
|
||||
return TADO_TO_HA_FAN_MODE_MAP.get(self._current_tado_fan_speed, FAN_AUTO)
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_modes(self):
|
||||
"""List of available fan modes."""
|
||||
if self._ac_device:
|
||||
return SUPPORT_FAN
|
||||
return None
|
||||
return self._supported_fan_modes
|
||||
|
||||
def set_fan_mode(self, fan_mode: str):
|
||||
"""Turn fan on/off."""
|
||||
pass
|
||||
self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode])
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Return the current preset mode (home, away)."""
|
||||
if self._is_away:
|
||||
if self._tado_zone_data.is_away:
|
||||
return PRESET_AWAY
|
||||
return PRESET_HOME
|
||||
|
||||
@@ -299,12 +312,18 @@ class TadoClimate(ClimateDevice):
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return self._step
|
||||
if self._tado_zone_data.current_hvac_mode == CONST_MODE_COOL:
|
||||
return self._cool_step or self._heat_step
|
||||
return self._heat_step or self._cool_step
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
# If the target temperature will be None
|
||||
# if the device is performing an action
|
||||
# that does not affect the temperature or
|
||||
# the device is switching states
|
||||
return self._tado_zone_data.target_temp or self._tado_zone_data.current_temp
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -312,174 +331,149 @@ class TadoClimate(ClimateDevice):
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
self._current_operation = self._default_overlay
|
||||
self._overlay_mode = None
|
||||
self._target_temp = temperature
|
||||
self._control_heating()
|
||||
if self._current_tado_hvac_mode not in (
|
||||
CONST_MODE_OFF,
|
||||
CONST_MODE_AUTO,
|
||||
CONST_MODE_SMART_SCHEDULE,
|
||||
):
|
||||
self._control_hvac(target_temp=temperature)
|
||||
return
|
||||
|
||||
new_hvac_mode = CONST_MODE_COOL if self._ac_device else CONST_MODE_HEAT
|
||||
self._control_hvac(target_temp=temperature, hvac_mode=new_hvac_mode)
|
||||
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set new target hvac mode."""
|
||||
mode = None
|
||||
|
||||
if hvac_mode == HVAC_MODE_OFF:
|
||||
mode = CONST_MODE_OFF
|
||||
elif hvac_mode == HVAC_MODE_AUTO:
|
||||
mode = CONST_MODE_SMART_SCHEDULE
|
||||
elif hvac_mode == HVAC_MODE_HEAT:
|
||||
mode = self._default_overlay
|
||||
elif hvac_mode == HVAC_MODE_COOL:
|
||||
mode = self._default_overlay
|
||||
elif hvac_mode == HVAC_MODE_HEAT_COOL:
|
||||
mode = self._default_overlay
|
||||
self._control_hvac(hvac_mode=HA_TO_TADO_HVAC_MODE_MAP[hvac_mode])
|
||||
|
||||
self._current_operation = mode
|
||||
self._overlay_mode = None
|
||||
|
||||
# Set a target temperature if we don't have any
|
||||
# This can happen when we switch from Off to On
|
||||
if self._target_temp is None:
|
||||
if self._ac_device:
|
||||
self._target_temp = self.max_temp
|
||||
else:
|
||||
self._target_temp = self.min_temp
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self._control_heating()
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if the device is available."""
|
||||
return self._tado_zone_data.available
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._min_temp
|
||||
if (
|
||||
self._current_tado_hvac_mode == CONST_MODE_COOL
|
||||
and self._cool_min_temp is not None
|
||||
):
|
||||
return self._cool_min_temp
|
||||
if self._heat_min_temp is not None:
|
||||
return self._heat_min_temp
|
||||
|
||||
return self._cool_min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._max_temp
|
||||
|
||||
def update(self):
|
||||
"""Handle update callbacks."""
|
||||
_LOGGER.debug("Updating climate platform for zone %d", self.zone_id)
|
||||
data = self._tado.data["zone"][self.zone_id]
|
||||
|
||||
if "sensorDataPoints" in data:
|
||||
sensor_data = data["sensorDataPoints"]
|
||||
|
||||
if "insideTemperature" in sensor_data:
|
||||
temperature = float(sensor_data["insideTemperature"]["celsius"])
|
||||
self._cur_temp = temperature
|
||||
|
||||
if "humidity" in sensor_data:
|
||||
humidity = float(sensor_data["humidity"]["percentage"])
|
||||
self._cur_humidity = humidity
|
||||
|
||||
# temperature setting will not exist when device is off
|
||||
if (
|
||||
"temperature" in data["setting"]
|
||||
and data["setting"]["temperature"] is not None
|
||||
self._current_tado_hvac_mode == CONST_MODE_HEAT
|
||||
and self._heat_max_temp is not None
|
||||
):
|
||||
setting = float(data["setting"]["temperature"]["celsius"])
|
||||
self._target_temp = setting
|
||||
return self._heat_max_temp
|
||||
if self._heat_max_temp is not None:
|
||||
return self._heat_max_temp
|
||||
|
||||
if "tadoMode" in data:
|
||||
mode = data["tadoMode"]
|
||||
self._is_away = mode == "AWAY"
|
||||
return self._heat_max_temp
|
||||
|
||||
if "setting" in data:
|
||||
power = data["setting"]["power"]
|
||||
if power == "OFF":
|
||||
self._current_operation = CONST_MODE_OFF
|
||||
self._current_fan = CONST_MODE_OFF
|
||||
# There is no overlay, the mode will always be
|
||||
# "SMART_SCHEDULE"
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._device_is_active = False
|
||||
else:
|
||||
self._device_is_active = True
|
||||
@callback
|
||||
def _async_update_zone_data(self):
|
||||
"""Load tado data into zone."""
|
||||
self._tado_zone_data = self._tado.data["zone"][self.zone_id]
|
||||
self._current_tado_fan_speed = self._tado_zone_data.current_fan_speed
|
||||
self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode
|
||||
self._current_tado_hvac_action = self._tado_zone_data.current_hvac_action
|
||||
|
||||
active = False
|
||||
if "activityDataPoints" in data:
|
||||
activity_data = data["activityDataPoints"]
|
||||
if self._ac_device:
|
||||
if "acPower" in activity_data and activity_data["acPower"] is not None:
|
||||
if not activity_data["acPower"]["value"] == "OFF":
|
||||
active = True
|
||||
else:
|
||||
if (
|
||||
"heatingPower" in activity_data
|
||||
and activity_data["heatingPower"] is not None
|
||||
):
|
||||
if float(activity_data["heatingPower"]["percentage"]) > 0.0:
|
||||
active = True
|
||||
self._active = active
|
||||
@callback
|
||||
def _async_update_callback(self):
|
||||
"""Load tado data and update state."""
|
||||
self._async_update_zone_data()
|
||||
self.async_write_ha_state()
|
||||
|
||||
overlay = False
|
||||
overlay_data = None
|
||||
termination = CONST_MODE_SMART_SCHEDULE
|
||||
cooling = False
|
||||
fan_speed = CONST_MODE_OFF
|
||||
def _normalize_target_temp_for_hvac_mode(self):
|
||||
# Set a target temperature if we don't have any
|
||||
# This can happen when we switch from Off to On
|
||||
if self._target_temp is None:
|
||||
self._target_temp = self._tado_zone_data.current_temp
|
||||
elif self._current_tado_hvac_mode == CONST_MODE_COOL:
|
||||
if self._target_temp > self._cool_max_temp:
|
||||
self._target_temp = self._cool_max_temp
|
||||
elif self._target_temp < self._cool_min_temp:
|
||||
self._target_temp = self._cool_min_temp
|
||||
elif self._current_tado_hvac_mode == CONST_MODE_HEAT:
|
||||
if self._target_temp > self._heat_max_temp:
|
||||
self._target_temp = self._heat_max_temp
|
||||
elif self._target_temp < self._heat_min_temp:
|
||||
self._target_temp = self._heat_min_temp
|
||||
|
||||
if "overlay" in data:
|
||||
overlay_data = data["overlay"]
|
||||
overlay = overlay_data is not None
|
||||
|
||||
if overlay:
|
||||
termination = overlay_data["termination"]["type"]
|
||||
setting = False
|
||||
setting_data = None
|
||||
|
||||
if "setting" in overlay_data:
|
||||
setting_data = overlay_data["setting"]
|
||||
setting = setting_data is not None
|
||||
|
||||
if setting:
|
||||
if "mode" in setting_data:
|
||||
cooling = setting_data["mode"] == "COOL"
|
||||
|
||||
if "fanSpeed" in setting_data:
|
||||
fan_speed = setting_data["fanSpeed"]
|
||||
|
||||
if self._device_is_active:
|
||||
# If you set mode manually to off, there will be an overlay
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
self._overlay_mode = termination
|
||||
self._current_operation = termination
|
||||
|
||||
self._cooling = cooling
|
||||
self._current_fan = fan_speed
|
||||
|
||||
def _control_heating(self):
|
||||
def _control_hvac(self, hvac_mode=None, target_temp=None, fan_mode=None):
|
||||
"""Send new target temperature to Tado."""
|
||||
if self._current_operation == CONST_MODE_SMART_SCHEDULE:
|
||||
|
||||
if hvac_mode:
|
||||
self._current_tado_hvac_mode = hvac_mode
|
||||
|
||||
if target_temp:
|
||||
self._target_temp = target_temp
|
||||
|
||||
if fan_mode:
|
||||
self._current_tado_fan_speed = fan_mode
|
||||
|
||||
self._normalize_target_temp_for_hvac_mode()
|
||||
|
||||
# tado does not permit setting the fan speed to
|
||||
# off, you must turn off the device
|
||||
if (
|
||||
self._current_tado_fan_speed == CONST_FAN_OFF
|
||||
and self._current_tado_hvac_mode != CONST_MODE_OFF
|
||||
):
|
||||
self._current_tado_fan_speed = CONST_FAN_AUTO
|
||||
|
||||
if self._current_tado_hvac_mode == CONST_MODE_OFF:
|
||||
_LOGGER.debug(
|
||||
"Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id
|
||||
)
|
||||
self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, self.zone_type)
|
||||
return
|
||||
|
||||
if self._current_tado_hvac_mode == CONST_MODE_SMART_SCHEDULE:
|
||||
_LOGGER.debug(
|
||||
"Switching to SMART_SCHEDULE for zone %s (%d)",
|
||||
self.zone_name,
|
||||
self.zone_id,
|
||||
)
|
||||
self._tado.reset_zone_overlay(self.zone_id)
|
||||
self._overlay_mode = self._current_operation
|
||||
return
|
||||
|
||||
if self._current_operation == CONST_MODE_OFF:
|
||||
_LOGGER.debug(
|
||||
"Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id
|
||||
)
|
||||
self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, self.zone_type)
|
||||
self._overlay_mode = self._current_operation
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"Switching to %s for zone %s (%d) with temperature %s °C",
|
||||
self._current_operation,
|
||||
self._current_tado_hvac_mode,
|
||||
self.zone_name,
|
||||
self.zone_id,
|
||||
self._target_temp,
|
||||
)
|
||||
self._tado.set_zone_overlay(
|
||||
self.zone_id,
|
||||
self._current_operation,
|
||||
self._target_temp,
|
||||
None,
|
||||
self.zone_type,
|
||||
"COOL" if self._ac_device else None,
|
||||
|
||||
# Fallback to Smart Schedule at next Schedule switch if we have fallback enabled
|
||||
overlay_mode = (
|
||||
CONST_OVERLAY_TADO_MODE if self._tado.fallback else CONST_OVERLAY_MANUAL
|
||||
)
|
||||
|
||||
temperature_to_send = self._target_temp
|
||||
if self._current_tado_hvac_mode in TADO_MODES_WITH_NO_TEMP_SETTING:
|
||||
# A temperature cannot be passed with these modes
|
||||
temperature_to_send = None
|
||||
|
||||
self._tado.set_zone_overlay(
|
||||
zone_id=self.zone_id,
|
||||
overlay_mode=overlay_mode, # What to do when the period ends
|
||||
temperature=temperature_to_send,
|
||||
duration=None,
|
||||
device_type=self.zone_type,
|
||||
mode=self._current_tado_hvac_mode,
|
||||
fan_speed=(
|
||||
self._current_tado_fan_speed
|
||||
if (self._support_flags & SUPPORT_FAN_MODE)
|
||||
else None
|
||||
), # api defaults to not sending fanSpeed if not specified
|
||||
)
|
||||
self._overlay_mode = self._current_operation
|
||||
|
@@ -1,5 +1,48 @@
|
||||
"""Constant values for the Tado component."""
|
||||
|
||||
from PyTado.const import (
|
||||
CONST_HVAC_COOL,
|
||||
CONST_HVAC_DRY,
|
||||
CONST_HVAC_FAN,
|
||||
CONST_HVAC_HEAT,
|
||||
CONST_HVAC_HOT_WATER,
|
||||
CONST_HVAC_IDLE,
|
||||
CONST_HVAC_OFF,
|
||||
)
|
||||
|
||||
from homeassistant.components.climate.const import (
|
||||
CURRENT_HVAC_COOL,
|
||||
CURRENT_HVAC_DRY,
|
||||
CURRENT_HVAC_FAN,
|
||||
CURRENT_HVAC_HEAT,
|
||||
CURRENT_HVAC_IDLE,
|
||||
CURRENT_HVAC_OFF,
|
||||
FAN_AUTO,
|
||||
FAN_HIGH,
|
||||
FAN_LOW,
|
||||
FAN_MEDIUM,
|
||||
FAN_OFF,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_DRY,
|
||||
HVAC_MODE_FAN_ONLY,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_HEAT_COOL,
|
||||
HVAC_MODE_OFF,
|
||||
PRESET_AWAY,
|
||||
PRESET_HOME,
|
||||
)
|
||||
|
||||
TADO_HVAC_ACTION_TO_HA_HVAC_ACTION = {
|
||||
CONST_HVAC_HEAT: CURRENT_HVAC_HEAT,
|
||||
CONST_HVAC_DRY: CURRENT_HVAC_DRY,
|
||||
CONST_HVAC_FAN: CURRENT_HVAC_FAN,
|
||||
CONST_HVAC_COOL: CURRENT_HVAC_COOL,
|
||||
CONST_HVAC_IDLE: CURRENT_HVAC_IDLE,
|
||||
CONST_HVAC_OFF: CURRENT_HVAC_OFF,
|
||||
CONST_HVAC_HOT_WATER: CURRENT_HVAC_HEAT,
|
||||
}
|
||||
|
||||
# Configuration
|
||||
CONF_FALLBACK = "fallback"
|
||||
DATA = "data"
|
||||
@@ -10,10 +53,81 @@ TYPE_HEATING = "HEATING"
|
||||
TYPE_HOT_WATER = "HOT_WATER"
|
||||
|
||||
# Base modes
|
||||
CONST_MODE_OFF = "OFF"
|
||||
CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Use the schedule
|
||||
CONST_MODE_OFF = "OFF" # Switch off heating in a zone
|
||||
CONST_MODE_AUTO = "AUTO"
|
||||
CONST_MODE_COOL = "COOL"
|
||||
CONST_MODE_HEAT = "HEAT"
|
||||
CONST_MODE_DRY = "DRY"
|
||||
CONST_MODE_FAN = "FAN"
|
||||
|
||||
CONST_LINK_OFFLINE = "OFFLINE"
|
||||
|
||||
CONST_FAN_OFF = "OFF"
|
||||
CONST_FAN_AUTO = "AUTO"
|
||||
CONST_FAN_LOW = "LOW"
|
||||
CONST_FAN_MIDDLE = "MIDDLE"
|
||||
CONST_FAN_HIGH = "HIGH"
|
||||
|
||||
|
||||
# When we change the temperature setting, we need an overlay mode
|
||||
CONST_OVERLAY_TADO_MODE = "TADO_MODE" # wait until tado changes the mode automatic
|
||||
CONST_OVERLAY_MANUAL = "MANUAL" # the user has change the temperature or mode manually
|
||||
CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan
|
||||
|
||||
|
||||
# Heat always comes first since we get the
|
||||
# min and max tempatures for the zone from
|
||||
# it.
|
||||
# Heat is preferred as it generally has a lower minimum temperature
|
||||
ORDERED_KNOWN_TADO_MODES = [
|
||||
CONST_MODE_HEAT,
|
||||
CONST_MODE_COOL,
|
||||
CONST_MODE_AUTO,
|
||||
CONST_MODE_DRY,
|
||||
CONST_MODE_FAN,
|
||||
]
|
||||
|
||||
TADO_MODES_TO_HA_CURRENT_HVAC_ACTION = {
|
||||
CONST_MODE_HEAT: CURRENT_HVAC_HEAT,
|
||||
CONST_MODE_DRY: CURRENT_HVAC_DRY,
|
||||
CONST_MODE_FAN: CURRENT_HVAC_FAN,
|
||||
CONST_MODE_COOL: CURRENT_HVAC_COOL,
|
||||
}
|
||||
|
||||
# These modes will not allow a temp to be set
|
||||
TADO_MODES_WITH_NO_TEMP_SETTING = [CONST_MODE_AUTO, CONST_MODE_DRY, CONST_MODE_FAN]
|
||||
#
|
||||
# HVAC_MODE_HEAT_COOL is mapped to CONST_MODE_AUTO
|
||||
# This lets tado decide on a temp
|
||||
#
|
||||
# HVAC_MODE_AUTO is mapped to CONST_MODE_SMART_SCHEDULE
|
||||
# This runs the smart schedule
|
||||
#
|
||||
HA_TO_TADO_HVAC_MODE_MAP = {
|
||||
HVAC_MODE_OFF: CONST_MODE_OFF,
|
||||
HVAC_MODE_HEAT_COOL: CONST_MODE_AUTO,
|
||||
HVAC_MODE_AUTO: CONST_MODE_SMART_SCHEDULE,
|
||||
HVAC_MODE_HEAT: CONST_MODE_HEAT,
|
||||
HVAC_MODE_COOL: CONST_MODE_COOL,
|
||||
HVAC_MODE_DRY: CONST_MODE_DRY,
|
||||
HVAC_MODE_FAN_ONLY: CONST_MODE_FAN,
|
||||
}
|
||||
|
||||
HA_TO_TADO_FAN_MODE_MAP = {
|
||||
FAN_AUTO: CONST_FAN_AUTO,
|
||||
FAN_OFF: CONST_FAN_OFF,
|
||||
FAN_LOW: CONST_FAN_LOW,
|
||||
FAN_MEDIUM: CONST_FAN_MIDDLE,
|
||||
FAN_HIGH: CONST_FAN_HIGH,
|
||||
}
|
||||
|
||||
TADO_TO_HA_HVAC_MODE_MAP = {
|
||||
value: key for key, value in HA_TO_TADO_HVAC_MODE_MAP.items()
|
||||
}
|
||||
|
||||
TADO_TO_HA_FAN_MODE_MAP = {value: key for key, value in HA_TO_TADO_FAN_MODE_MAP.items()}
|
||||
|
||||
DEFAULT_TADO_PRECISION = 0.1
|
||||
|
||||
SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME]
|
||||
|
@@ -7,6 +7,6 @@
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@michaelarnauts"
|
||||
"@michaelarnauts", "@bdraco"
|
||||
]
|
||||
}
|
||||
|
@@ -31,6 +31,7 @@ ZONE_SENSORS = {
|
||||
"ac",
|
||||
"tado mode",
|
||||
"overlay",
|
||||
"open window",
|
||||
],
|
||||
TYPE_HOT_WATER: ["power", "link", "tado mode", "overlay"],
|
||||
}
|
||||
@@ -46,20 +47,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
for tado in api_list:
|
||||
# Create zone sensors
|
||||
zones = tado.zones
|
||||
devices = tado.devices
|
||||
|
||||
for zone in zones:
|
||||
zone_type = zone["type"]
|
||||
if zone_type not in ZONE_SENSORS:
|
||||
_LOGGER.warning("Unknown zone type skipped: %s", zone_type)
|
||||
continue
|
||||
|
||||
for zone in tado.zones:
|
||||
entities.extend(
|
||||
[
|
||||
create_zone_sensor(tado, zone["name"], zone["id"], variable)
|
||||
for variable in ZONE_SENSORS.get(zone["type"])
|
||||
TadoZoneSensor(tado, zone["name"], zone["id"], variable)
|
||||
for variable in ZONE_SENSORS[zone_type]
|
||||
]
|
||||
)
|
||||
|
||||
# Create device sensors
|
||||
for home in tado.devices:
|
||||
for device in devices:
|
||||
entities.extend(
|
||||
[
|
||||
create_device_sensor(tado, home["name"], home["id"], variable)
|
||||
TadoDeviceSensor(tado, device["name"], device["id"], variable)
|
||||
for variable in DEVICE_SENSORS
|
||||
]
|
||||
)
|
||||
@@ -67,46 +75,38 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
add_entities(entities, True)
|
||||
|
||||
|
||||
def create_zone_sensor(tado, name, zone_id, variable):
|
||||
"""Create a zone sensor."""
|
||||
return TadoSensor(tado, name, "zone", zone_id, variable)
|
||||
|
||||
|
||||
def create_device_sensor(tado, name, device_id, variable):
|
||||
"""Create a device sensor."""
|
||||
return TadoSensor(tado, name, "device", device_id, variable)
|
||||
|
||||
|
||||
class TadoSensor(Entity):
|
||||
class TadoZoneSensor(Entity):
|
||||
"""Representation of a tado Sensor."""
|
||||
|
||||
def __init__(self, tado, zone_name, sensor_type, zone_id, zone_variable):
|
||||
def __init__(self, tado, zone_name, zone_id, zone_variable):
|
||||
"""Initialize of the Tado Sensor."""
|
||||
self._tado = tado
|
||||
|
||||
self.zone_name = zone_name
|
||||
self.zone_id = zone_id
|
||||
self.zone_variable = zone_variable
|
||||
self.sensor_type = sensor_type
|
||||
|
||||
self._unique_id = f"{zone_variable} {zone_id} {tado.device_id}"
|
||||
|
||||
self._state = None
|
||||
self._state_attributes = None
|
||||
self._tado_zone_data = None
|
||||
self._undo_dispatcher = None
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""When entity will be removed from hass."""
|
||||
if self._undo_dispatcher:
|
||||
self._undo_dispatcher()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register for sensor updates."""
|
||||
|
||||
@callback
|
||||
def async_update_callback():
|
||||
"""Schedule an entity update."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async_dispatcher_connect(
|
||||
self._undo_dispatcher = async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format(self.sensor_type, self.zone_id),
|
||||
async_update_callback,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id),
|
||||
self._async_update_callback,
|
||||
)
|
||||
self._async_update_zone_data()
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -138,7 +138,7 @@ class TadoSensor(Entity):
|
||||
if self.zone_variable == "heating":
|
||||
return UNIT_PERCENTAGE
|
||||
if self.zone_variable == "ac":
|
||||
return ""
|
||||
return None
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
@@ -149,97 +149,143 @@ class TadoSensor(Entity):
|
||||
return "mdi:water-percent"
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
def should_poll(self):
|
||||
"""Do not poll."""
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
@callback
|
||||
def _async_update_callback(self):
|
||||
"""Update and write state."""
|
||||
self._async_update_zone_data()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_zone_data(self):
|
||||
"""Handle update callbacks."""
|
||||
try:
|
||||
data = self._tado.data[self.sensor_type][self.zone_id]
|
||||
self._tado_zone_data = self._tado.data["zone"][self.zone_id]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
|
||||
if self.zone_variable == "temperature":
|
||||
if "sensorDataPoints" in data:
|
||||
sensor_data = data["sensorDataPoints"]
|
||||
temperature = float(sensor_data["insideTemperature"]["celsius"])
|
||||
|
||||
self._state = self.hass.config.units.temperature(temperature, unit)
|
||||
self._state_attributes = {
|
||||
"time": sensor_data["insideTemperature"]["timestamp"],
|
||||
"setting": 0, # setting is used in climate device
|
||||
}
|
||||
|
||||
# temperature setting will not exist when device is off
|
||||
if (
|
||||
"temperature" in data["setting"]
|
||||
and data["setting"]["temperature"] is not None
|
||||
):
|
||||
temperature = float(data["setting"]["temperature"]["celsius"])
|
||||
|
||||
self._state_attributes[
|
||||
"setting"
|
||||
] = self.hass.config.units.temperature(temperature, unit)
|
||||
self._state = self.hass.config.units.temperature(
|
||||
self._tado_zone_data.current_temp, TEMP_CELSIUS
|
||||
)
|
||||
self._state_attributes = {
|
||||
"time": self._tado_zone_data.current_temp_timestamp,
|
||||
"setting": 0, # setting is used in climate device
|
||||
}
|
||||
|
||||
elif self.zone_variable == "humidity":
|
||||
if "sensorDataPoints" in data:
|
||||
sensor_data = data["sensorDataPoints"]
|
||||
self._state = float(sensor_data["humidity"]["percentage"])
|
||||
self._state_attributes = {"time": sensor_data["humidity"]["timestamp"]}
|
||||
self._state = self._tado_zone_data.current_humidity
|
||||
self._state_attributes = {
|
||||
"time": self._tado_zone_data.current_humidity_timestamp
|
||||
}
|
||||
|
||||
elif self.zone_variable == "power":
|
||||
if "setting" in data:
|
||||
self._state = data["setting"]["power"]
|
||||
self._state = self._tado_zone_data.power
|
||||
|
||||
elif self.zone_variable == "link":
|
||||
if "link" in data:
|
||||
self._state = data["link"]["state"]
|
||||
self._state = self._tado_zone_data.link
|
||||
|
||||
elif self.zone_variable == "heating":
|
||||
if "activityDataPoints" in data:
|
||||
activity_data = data["activityDataPoints"]
|
||||
|
||||
if (
|
||||
"heatingPower" in activity_data
|
||||
and activity_data["heatingPower"] is not None
|
||||
):
|
||||
self._state = float(activity_data["heatingPower"]["percentage"])
|
||||
self._state_attributes = {
|
||||
"time": activity_data["heatingPower"]["timestamp"]
|
||||
}
|
||||
self._state = self._tado_zone_data.heating_power_percentage
|
||||
self._state_attributes = {
|
||||
"time": self._tado_zone_data.heating_power_timestamp
|
||||
}
|
||||
|
||||
elif self.zone_variable == "ac":
|
||||
if "activityDataPoints" in data:
|
||||
activity_data = data["activityDataPoints"]
|
||||
|
||||
if "acPower" in activity_data and activity_data["acPower"] is not None:
|
||||
self._state = activity_data["acPower"]["value"]
|
||||
self._state_attributes = {
|
||||
"time": activity_data["acPower"]["timestamp"]
|
||||
}
|
||||
self._state = self._tado_zone_data.ac_power
|
||||
self._state_attributes = {"time": self._tado_zone_data.ac_power_timestamp}
|
||||
|
||||
elif self.zone_variable == "tado bridge status":
|
||||
if "connectionState" in data:
|
||||
self._state = data["connectionState"]["value"]
|
||||
self._state = self._tado_zone_data.connection
|
||||
|
||||
elif self.zone_variable == "tado mode":
|
||||
if "tadoMode" in data:
|
||||
self._state = data["tadoMode"]
|
||||
self._state = self._tado_zone_data.tado_mode
|
||||
|
||||
elif self.zone_variable == "overlay":
|
||||
self._state = "overlay" in data and data["overlay"] is not None
|
||||
self._state = self._tado_zone_data.overlay_active
|
||||
self._state_attributes = (
|
||||
{"termination": data["overlay"]["termination"]["type"]}
|
||||
if self._state
|
||||
{"termination": self._tado_zone_data.overlay_termination_type}
|
||||
if self._tado_zone_data.overlay_active
|
||||
else {}
|
||||
)
|
||||
|
||||
elif self.zone_variable == "early start":
|
||||
self._state = "preparation" in data and data["preparation"] is not None
|
||||
self._state = self._tado_zone_data.preparation
|
||||
|
||||
elif self.zone_variable == "open window":
|
||||
self._state = "openWindow" in data and data["openWindow"] is not None
|
||||
self._state_attributes = data["openWindow"] if self._state else {}
|
||||
self._state = self._tado_zone_data.open_window
|
||||
self._state_attributes = self._tado_zone_data.open_window_attr
|
||||
|
||||
|
||||
class TadoDeviceSensor(Entity):
|
||||
"""Representation of a tado Sensor."""
|
||||
|
||||
def __init__(self, tado, device_name, device_id, device_variable):
|
||||
"""Initialize of the Tado Sensor."""
|
||||
self._tado = tado
|
||||
|
||||
self.device_name = device_name
|
||||
self.device_id = device_id
|
||||
self.device_variable = device_variable
|
||||
|
||||
self._unique_id = f"{device_variable} {device_id} {tado.device_id}"
|
||||
|
||||
self._state = None
|
||||
self._state_attributes = None
|
||||
self._tado_device_data = None
|
||||
self._undo_dispatcher = None
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""When entity will be removed from hass."""
|
||||
if self._undo_dispatcher:
|
||||
self._undo_dispatcher()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register for sensor updates."""
|
||||
|
||||
self._undo_dispatcher = async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format("device", self.device_id),
|
||||
self._async_update_callback,
|
||||
)
|
||||
self._async_update_device_data()
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return f"{self.device_name} {self.device_variable}"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Do not poll."""
|
||||
return False
|
||||
|
||||
@callback
|
||||
def _async_update_callback(self):
|
||||
"""Update and write state."""
|
||||
self._async_update_device_data()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_device_data(self):
|
||||
"""Handle update callbacks."""
|
||||
try:
|
||||
data = self._tado.data["device"][self.device_id]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
if self.device_variable == "tado bridge status":
|
||||
self._state = data.get("connectionState", {}).get("value", False)
|
||||
|
@@ -12,6 +12,9 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
|
||||
from .const import (
|
||||
CONST_HVAC_HEAT,
|
||||
CONST_MODE_AUTO,
|
||||
CONST_MODE_HEAT,
|
||||
CONST_MODE_OFF,
|
||||
CONST_MODE_SMART_SCHEDULE,
|
||||
CONST_OVERLAY_MANUAL,
|
||||
@@ -33,6 +36,7 @@ WATER_HEATER_MAP_TADO = {
|
||||
CONST_OVERLAY_MANUAL: MODE_HEAT,
|
||||
CONST_OVERLAY_TIMER: MODE_HEAT,
|
||||
CONST_OVERLAY_TADO_MODE: MODE_HEAT,
|
||||
CONST_HVAC_HEAT: MODE_HEAT,
|
||||
CONST_MODE_SMART_SCHEDULE: MODE_AUTO,
|
||||
CONST_MODE_OFF: MODE_OFF,
|
||||
}
|
||||
@@ -50,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
for tado in api_list:
|
||||
for zone in tado.zones:
|
||||
if zone["type"] in [TYPE_HOT_WATER]:
|
||||
if zone["type"] == TYPE_HOT_WATER:
|
||||
entity = create_water_heater_entity(tado, zone["name"], zone["id"])
|
||||
entities.append(entity)
|
||||
|
||||
@@ -61,6 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
def create_water_heater_entity(tado, name: str, zone_id: int):
|
||||
"""Create a Tado water heater device."""
|
||||
capabilities = tado.get_capabilities(zone_id)
|
||||
|
||||
supports_temperature_control = capabilities["canSetTemperature"]
|
||||
|
||||
if supports_temperature_control and "temperatures" in capabilities:
|
||||
@@ -98,7 +103,6 @@ class TadoWaterHeater(WaterHeaterDevice):
|
||||
self._unique_id = f"{zone_id} {tado.device_id}"
|
||||
|
||||
self._device_is_active = False
|
||||
self._is_away = False
|
||||
|
||||
self._supports_temperature_control = supports_temperature_control
|
||||
self._min_temperature = min_temp
|
||||
@@ -110,29 +114,25 @@ class TadoWaterHeater(WaterHeaterDevice):
|
||||
if self._supports_temperature_control:
|
||||
self._supported_features |= SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
if tado.fallback:
|
||||
# Fallback to Smart Schedule at next Schedule switch
|
||||
self._default_overlay = CONST_OVERLAY_TADO_MODE
|
||||
else:
|
||||
# Don't fallback to Smart Schedule, but keep in manual mode
|
||||
self._default_overlay = CONST_OVERLAY_MANUAL
|
||||
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
self._current_tado_hvac_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._tado_zone_data = None
|
||||
self._undo_dispatcher = None
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""When entity will be removed from hass."""
|
||||
if self._undo_dispatcher:
|
||||
self._undo_dispatcher()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register for sensor updates."""
|
||||
|
||||
@callback
|
||||
def async_update_callback():
|
||||
"""Schedule an entity update."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async_dispatcher_connect(
|
||||
self._undo_dispatcher = async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id),
|
||||
async_update_callback,
|
||||
self._async_update_callback,
|
||||
)
|
||||
self._async_update_data()
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@@ -157,17 +157,17 @@ class TadoWaterHeater(WaterHeaterDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current readable operation mode."""
|
||||
return WATER_HEATER_MAP_TADO.get(self._current_operation)
|
||||
return WATER_HEATER_MAP_TADO.get(self._current_tado_hvac_mode)
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
return self._tado_zone_data.target_temp
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._is_away
|
||||
return self._tado_zone_data.is_away
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
@@ -198,16 +198,9 @@ class TadoWaterHeater(WaterHeaterDevice):
|
||||
elif operation_mode == MODE_AUTO:
|
||||
mode = CONST_MODE_SMART_SCHEDULE
|
||||
elif operation_mode == MODE_HEAT:
|
||||
mode = self._default_overlay
|
||||
mode = CONST_MODE_HEAT
|
||||
|
||||
self._current_operation = mode
|
||||
self._overlay_mode = None
|
||||
|
||||
# Set a target temperature if we don't have any
|
||||
if mode == CONST_OVERLAY_TADO_MODE and self._target_temp is None:
|
||||
self._target_temp = self.min_temp
|
||||
|
||||
self._control_heater()
|
||||
self._control_heater(hvac_mode=mode)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@@ -215,88 +208,75 @@ class TadoWaterHeater(WaterHeaterDevice):
|
||||
if not self._supports_temperature_control or temperature is None:
|
||||
return
|
||||
|
||||
self._current_operation = self._default_overlay
|
||||
self._overlay_mode = None
|
||||
self._target_temp = temperature
|
||||
self._control_heater()
|
||||
|
||||
def update(self):
|
||||
"""Handle update callbacks."""
|
||||
_LOGGER.debug("Updating water_heater platform for zone %d", self.zone_id)
|
||||
data = self._tado.data["zone"][self.zone_id]
|
||||
|
||||
if "tadoMode" in data:
|
||||
mode = data["tadoMode"]
|
||||
self._is_away = mode == "AWAY"
|
||||
|
||||
if "setting" in data:
|
||||
power = data["setting"]["power"]
|
||||
if power == "OFF":
|
||||
self._current_operation = CONST_MODE_OFF
|
||||
# There is no overlay, the mode will always be
|
||||
# "SMART_SCHEDULE"
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._device_is_active = False
|
||||
else:
|
||||
self._device_is_active = True
|
||||
|
||||
# temperature setting will not exist when device is off
|
||||
if (
|
||||
"temperature" in data["setting"]
|
||||
and data["setting"]["temperature"] is not None
|
||||
if self._current_tado_hvac_mode not in (
|
||||
CONST_MODE_OFF,
|
||||
CONST_MODE_AUTO,
|
||||
CONST_MODE_SMART_SCHEDULE,
|
||||
):
|
||||
setting = float(data["setting"]["temperature"]["celsius"])
|
||||
self._target_temp = setting
|
||||
self._control_heater(target_temp=temperature)
|
||||
return
|
||||
|
||||
overlay = False
|
||||
overlay_data = None
|
||||
termination = CONST_MODE_SMART_SCHEDULE
|
||||
self._control_heater(target_temp=temperature, hvac_mode=CONST_MODE_HEAT)
|
||||
|
||||
if "overlay" in data:
|
||||
overlay_data = data["overlay"]
|
||||
overlay = overlay_data is not None
|
||||
@callback
|
||||
def _async_update_callback(self):
|
||||
"""Load tado data and update state."""
|
||||
self._async_update_data()
|
||||
self.async_write_ha_state()
|
||||
|
||||
if overlay:
|
||||
termination = overlay_data["termination"]["type"]
|
||||
@callback
|
||||
def _async_update_data(self):
|
||||
"""Load tado data."""
|
||||
_LOGGER.debug("Updating water_heater platform for zone %d", self.zone_id)
|
||||
self._tado_zone_data = self._tado.data["zone"][self.zone_id]
|
||||
self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode
|
||||
|
||||
if self._device_is_active:
|
||||
# If you set mode manually to off, there will be an overlay
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
self._overlay_mode = termination
|
||||
self._current_operation = termination
|
||||
|
||||
def _control_heater(self):
|
||||
def _control_heater(self, hvac_mode=None, target_temp=None):
|
||||
"""Send new target temperature."""
|
||||
if self._current_operation == CONST_MODE_SMART_SCHEDULE:
|
||||
|
||||
if hvac_mode:
|
||||
self._current_tado_hvac_mode = hvac_mode
|
||||
|
||||
if target_temp:
|
||||
self._target_temp = target_temp
|
||||
|
||||
# Set a target temperature if we don't have any
|
||||
if self._target_temp is None:
|
||||
self._target_temp = self.min_temp
|
||||
|
||||
if self._current_tado_hvac_mode == CONST_MODE_SMART_SCHEDULE:
|
||||
_LOGGER.debug(
|
||||
"Switching to SMART_SCHEDULE for zone %s (%d)",
|
||||
self.zone_name,
|
||||
self.zone_id,
|
||||
)
|
||||
self._tado.reset_zone_overlay(self.zone_id)
|
||||
self._overlay_mode = self._current_operation
|
||||
return
|
||||
|
||||
if self._current_operation == CONST_MODE_OFF:
|
||||
if self._current_tado_hvac_mode == CONST_MODE_OFF:
|
||||
_LOGGER.debug(
|
||||
"Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id
|
||||
)
|
||||
self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, TYPE_HOT_WATER)
|
||||
self._overlay_mode = self._current_operation
|
||||
return
|
||||
|
||||
# Fallback to Smart Schedule at next Schedule switch if we have fallback enabled
|
||||
overlay_mode = (
|
||||
CONST_OVERLAY_TADO_MODE if self._tado.fallback else CONST_OVERLAY_MANUAL
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Switching to %s for zone %s (%d) with temperature %s",
|
||||
self._current_operation,
|
||||
self._current_tado_hvac_mode,
|
||||
self.zone_name,
|
||||
self.zone_id,
|
||||
self._target_temp,
|
||||
)
|
||||
self._tado.set_zone_overlay(
|
||||
self.zone_id,
|
||||
self._current_operation,
|
||||
self._target_temp,
|
||||
None,
|
||||
TYPE_HOT_WATER,
|
||||
zone_id=self.zone_id,
|
||||
overlay_mode=overlay_mode,
|
||||
temperature=self._target_temp,
|
||||
duration=None,
|
||||
device_type=TYPE_HOT_WATER,
|
||||
)
|
||||
self._overlay_mode = self._current_operation
|
||||
self._overlay_mode = self._current_tado_hvac_mode
|
||||
|
@@ -22,6 +22,7 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"enable_wake_on_start": "Forza il risveglio delle auto all'avvio",
|
||||
"scan_interval": "Secondi tra le scansioni"
|
||||
}
|
||||
}
|
||||
|
@@ -71,6 +71,7 @@
|
||||
"timeout": "API Request Timeout (Sekunden)",
|
||||
"volume_step": "Lautst\u00e4rken-Schrittgr\u00f6\u00dfe"
|
||||
},
|
||||
"description": "Wenn Sie \u00fcber ein Smart-TV-Ger\u00e4t verf\u00fcgen, k\u00f6nnen Sie Ihre Quellliste optional filtern, indem Sie ausw\u00e4hlen, welche Apps in Ihre Quellliste aufgenommen oder ausgeschlossen werden sollen.",
|
||||
"title": "Aktualisieren Sie die Vizo SmartCast-Optionen"
|
||||
}
|
||||
},
|
||||
|
@@ -950,6 +950,15 @@ def async_load_api(hass):
|
||||
schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND],
|
||||
)
|
||||
|
||||
def _get_ias_wd_channel(zha_device):
|
||||
"""Get the IASWD channel for a device."""
|
||||
cluster_channels = {
|
||||
ch.name: ch
|
||||
for pool in zha_device.channels.pools
|
||||
for ch in pool.claimed_channels.values()
|
||||
}
|
||||
return cluster_channels.get(CHANNEL_IAS_WD)
|
||||
|
||||
async def warning_device_squawk(service):
|
||||
"""Issue the squawk command for an IAS warning device."""
|
||||
ieee = service.data[ATTR_IEEE]
|
||||
@@ -959,9 +968,9 @@ def async_load_api(hass):
|
||||
|
||||
zha_device = zha_gateway.get_device(ieee)
|
||||
if zha_device is not None:
|
||||
channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD)
|
||||
channel = _get_ias_wd_channel(zha_device)
|
||||
if channel:
|
||||
await channel.squawk(mode, strobe, level)
|
||||
await channel.issue_squawk(mode, strobe, level)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Squawking IASWD: %s: [%s] is missing the required IASWD channel!",
|
||||
@@ -1003,9 +1012,9 @@ def async_load_api(hass):
|
||||
|
||||
zha_device = zha_gateway.get_device(ieee)
|
||||
if zha_device is not None:
|
||||
channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD)
|
||||
channel = _get_ias_wd_channel(zha_device)
|
||||
if channel:
|
||||
await channel.start_warning(
|
||||
await channel.issue_start_warning(
|
||||
mode, strobe, level, duration, duty_mode, intensity
|
||||
)
|
||||
else:
|
||||
|
@@ -51,7 +51,7 @@ class IasWd(ZigbeeChannel):
|
||||
"""Get the specified bit from the value."""
|
||||
return (value & (1 << bit)) != 0
|
||||
|
||||
async def squawk(
|
||||
async def issue_squawk(
|
||||
self,
|
||||
mode=WARNING_DEVICE_SQUAWK_MODE_ARMED,
|
||||
strobe=WARNING_DEVICE_STROBE_YES,
|
||||
@@ -76,7 +76,7 @@ class IasWd(ZigbeeChannel):
|
||||
|
||||
await self.squawk(value)
|
||||
|
||||
async def start_warning(
|
||||
async def issue_start_warning(
|
||||
self,
|
||||
mode=WARNING_DEVICE_MODE_EMERGENCY,
|
||||
strobe=WARNING_DEVICE_STROBE_YES,
|
||||
|
@@ -474,7 +474,6 @@ class ZHAGateway:
|
||||
"""Update the devices in the store."""
|
||||
for device in self.devices.values():
|
||||
self.zha_storage.async_update_device(device)
|
||||
await self.zha_storage.async_save()
|
||||
|
||||
async def async_device_initialized(self, device: zha_typing.ZigpyDeviceType):
|
||||
"""Handle device joined and basic information discovered (async)."""
|
||||
|
@@ -6,7 +6,7 @@
|
||||
"requirements": [
|
||||
"bellows-homeassistant==0.14.0",
|
||||
"zha-quirks==0.0.37",
|
||||
"zigpy-cc==0.3.0",
|
||||
"zigpy-cc==0.3.1",
|
||||
"zigpy-deconz==0.7.0",
|
||||
"zigpy-homeassistant==0.16.0",
|
||||
"zigpy-xbee-homeassistant==0.10.0",
|
||||
|
@@ -184,6 +184,7 @@ EVENT_CORE_CONFIG_UPDATE = "core_config_updated"
|
||||
EVENT_HOMEASSISTANT_CLOSE = "homeassistant_close"
|
||||
EVENT_HOMEASSISTANT_START = "homeassistant_start"
|
||||
EVENT_HOMEASSISTANT_STOP = "homeassistant_stop"
|
||||
EVENT_HOMEASSISTANT_FINAL_WRITE = "homeassistant_final_write"
|
||||
EVENT_LOGBOOK_ENTRY = "logbook_entry"
|
||||
EVENT_PLATFORM_DISCOVERED = "platform_discovered"
|
||||
EVENT_SCRIPT_STARTED = "script_started"
|
||||
|
@@ -47,6 +47,7 @@ from homeassistant.const import (
|
||||
EVENT_CALL_SERVICE,
|
||||
EVENT_CORE_CONFIG_UPDATE,
|
||||
EVENT_HOMEASSISTANT_CLOSE,
|
||||
EVENT_HOMEASSISTANT_FINAL_WRITE,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_SERVICE_REGISTERED,
|
||||
@@ -151,6 +152,7 @@ class CoreState(enum.Enum):
|
||||
starting = "STARTING"
|
||||
running = "RUNNING"
|
||||
stopping = "STOPPING"
|
||||
writing_data = "WRITING_DATA"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return the event."""
|
||||
@@ -412,7 +414,7 @@ class HomeAssistant:
|
||||
# regardless of the state of the loop.
|
||||
if self.state == CoreState.not_running: # just ignore
|
||||
return
|
||||
if self.state == CoreState.stopping:
|
||||
if self.state == CoreState.stopping or self.state == CoreState.writing_data:
|
||||
_LOGGER.info("async_stop called twice: ignored")
|
||||
return
|
||||
if self.state == CoreState.starting:
|
||||
@@ -426,6 +428,11 @@ class HomeAssistant:
|
||||
await self.async_block_till_done()
|
||||
|
||||
# stage 2
|
||||
self.state = CoreState.writing_data
|
||||
self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE)
|
||||
await self.async_block_till_done()
|
||||
|
||||
# stage 3
|
||||
self.state = CoreState.not_running
|
||||
self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE)
|
||||
await self.async_block_till_done()
|
||||
|
@@ -54,6 +54,7 @@ FLOWS = [
|
||||
"ifttt",
|
||||
"ios",
|
||||
"ipma",
|
||||
"ipp",
|
||||
"iqvia",
|
||||
"izone",
|
||||
"konnected",
|
||||
|
@@ -25,6 +25,12 @@ ZEROCONF = {
|
||||
"_hap._tcp.local.": [
|
||||
"homekit_controller"
|
||||
],
|
||||
"_ipp._tcp.local.": [
|
||||
"ipp"
|
||||
],
|
||||
"_ipps._tcp.local.": [
|
||||
"ipp"
|
||||
],
|
||||
"_printer._tcp.local.": [
|
||||
"brother"
|
||||
],
|
||||
|
@@ -4,7 +4,10 @@ from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any, Awaitable, Dict, List, Optional, Set, cast
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_FINAL_WRITE,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
CoreState,
|
||||
HomeAssistant,
|
||||
@@ -184,7 +187,9 @@ class RestoreStateData:
|
||||
async_track_time_interval(self.hass, _async_dump_states, STATE_DUMP_INTERVAL)
|
||||
|
||||
# Dump states when stopping hass
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_dump_states)
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_FINAL_WRITE, _async_dump_states
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_restore_entity_added(self, entity_id: str) -> None:
|
||||
|
@@ -5,7 +5,7 @@ import logging
|
||||
import os
|
||||
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.loader import bind_hass
|
||||
@@ -153,7 +153,7 @@ class Store:
|
||||
"""Ensure that we write if we quit before delay has passed."""
|
||||
if self._unsub_stop_listener is None:
|
||||
self._unsub_stop_listener = self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, self._async_callback_stop_write
|
||||
EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_callback_stop_write
|
||||
)
|
||||
|
||||
@callback
|
||||
|
@@ -12,7 +12,7 @@ cryptography==2.8
|
||||
defusedxml==0.6.0
|
||||
distro==1.4.0
|
||||
hass-nabucasa==0.32.2
|
||||
home-assistant-frontend==20200318.1
|
||||
home-assistant-frontend==20200330.0
|
||||
importlib-metadata==1.5.0
|
||||
jinja2>=2.11.1
|
||||
netdisco==2.6.0
|
||||
|
@@ -140,7 +140,7 @@ aio_geojson_nsw_rfs_incidents==0.3
|
||||
aio_georss_gdacs==0.3
|
||||
|
||||
# homeassistant.components.ambient_station
|
||||
aioambient==1.0.4
|
||||
aioambient==1.1.0
|
||||
|
||||
# homeassistant.components.asuswrt
|
||||
aioasuswrt==1.2.3
|
||||
@@ -168,7 +168,7 @@ aioftp==0.12.0
|
||||
aioharmony==0.1.13
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit[IP]==0.2.35
|
||||
aiohomekit[IP]==0.2.37
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -184,7 +184,7 @@ aioimaplib==0.7.15
|
||||
aiokafka==0.5.1
|
||||
|
||||
# homeassistant.components.kef
|
||||
aiokef==0.2.7
|
||||
aiokef==0.2.9
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx==0.6.7
|
||||
@@ -704,7 +704,7 @@ hole==0.5.1
|
||||
holidays==0.10.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20200318.1
|
||||
home-assistant-frontend==20200330.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -964,7 +964,7 @@ onkyo-eiscp==1.2.7
|
||||
onvif-zeep-async==0.2.0
|
||||
|
||||
# homeassistant.components.opencv
|
||||
# opencv-python-headless==4.1.2.30
|
||||
# opencv-python-headless==4.2.0.32
|
||||
|
||||
# homeassistant.components.openevse
|
||||
openevsewifi==0.4
|
||||
@@ -1335,6 +1335,9 @@ pyintesishome==1.7.1
|
||||
# homeassistant.components.ipma
|
||||
pyipma==2.0.5
|
||||
|
||||
# homeassistant.components.ipp
|
||||
pyipp==0.8.1
|
||||
|
||||
# homeassistant.components.iqvia
|
||||
pyiqvia==0.2.1
|
||||
|
||||
@@ -1596,7 +1599,7 @@ python-clementine-remote==1.0.1
|
||||
python-digitalocean==1.13.2
|
||||
|
||||
# homeassistant.components.ecobee
|
||||
python-ecobee-api==0.2.3
|
||||
python-ecobee-api==0.2.5
|
||||
|
||||
# homeassistant.components.eq3btsmart
|
||||
# python-eq3bt==0.1.11
|
||||
@@ -1876,7 +1879,7 @@ sisyphus-control==2.2.1
|
||||
skybellpy==0.4.0
|
||||
|
||||
# homeassistant.components.slack
|
||||
slacker==0.14.0
|
||||
slackclient==2.5.0
|
||||
|
||||
# homeassistant.components.sleepiq
|
||||
sleepyq==0.7
|
||||
@@ -2182,7 +2185,7 @@ zhong_hong_hvac==1.0.9
|
||||
ziggo-mediabox-xl==1.1.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-cc==0.3.0
|
||||
zigpy-cc==0.3.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.7.0
|
||||
|
@@ -47,7 +47,7 @@ aio_geojson_nsw_rfs_incidents==0.3
|
||||
aio_georss_gdacs==0.3
|
||||
|
||||
# homeassistant.components.ambient_station
|
||||
aioambient==1.0.4
|
||||
aioambient==1.1.0
|
||||
|
||||
# homeassistant.components.asuswrt
|
||||
aioasuswrt==1.2.3
|
||||
@@ -72,7 +72,7 @@ aiofreepybox==0.0.8
|
||||
aioharmony==0.1.13
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit[IP]==0.2.35
|
||||
aiohomekit[IP]==0.2.37
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -282,7 +282,7 @@ hole==0.5.1
|
||||
holidays==0.10.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20200318.1
|
||||
home-assistant-frontend==20200330.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -518,6 +518,9 @@ pyicloud==0.9.6.1
|
||||
# homeassistant.components.ipma
|
||||
pyipma==2.0.5
|
||||
|
||||
# homeassistant.components.ipp
|
||||
pyipp==0.8.1
|
||||
|
||||
# homeassistant.components.iqvia
|
||||
pyiqvia==0.2.1
|
||||
|
||||
@@ -602,7 +605,7 @@ pysonos==0.0.25
|
||||
pyspcwebgw==0.4.0
|
||||
|
||||
# homeassistant.components.ecobee
|
||||
python-ecobee-api==0.2.3
|
||||
python-ecobee-api==0.2.5
|
||||
|
||||
# homeassistant.components.darksky
|
||||
python-forecastio==1.4.0
|
||||
@@ -616,6 +619,9 @@ python-miio==0.4.8
|
||||
# homeassistant.components.nest
|
||||
python-nest==4.1.0
|
||||
|
||||
# homeassistant.components.tado
|
||||
python-tado==0.5.0
|
||||
|
||||
# homeassistant.components.twitch
|
||||
python-twitch-client==0.6.0
|
||||
|
||||
@@ -792,7 +798,7 @@ zeroconf==0.24.5
|
||||
zha-quirks==0.0.37
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-cc==0.3.0
|
||||
zigpy-cc==0.3.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.7.0
|
||||
|
@@ -899,8 +899,8 @@ async def test_async_remove_user(hass):
|
||||
assert events[0].data["user_id"] == user.id
|
||||
|
||||
|
||||
async def test_new_users_admin(mock_hass):
|
||||
"""Test newly created users are admin."""
|
||||
async def test_new_users(mock_hass):
|
||||
"""Test newly created users."""
|
||||
manager = await auth.auth_manager_from_config(
|
||||
mock_hass,
|
||||
[
|
||||
@@ -911,7 +911,17 @@ async def test_new_users_admin(mock_hass):
|
||||
"username": "test-user",
|
||||
"password": "test-pass",
|
||||
"name": "Test Name",
|
||||
}
|
||||
},
|
||||
{
|
||||
"username": "test-user-2",
|
||||
"password": "test-pass",
|
||||
"name": "Test Name",
|
||||
},
|
||||
{
|
||||
"username": "test-user-3",
|
||||
"password": "test-pass",
|
||||
"name": "Test Name",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
@@ -920,7 +930,18 @@ async def test_new_users_admin(mock_hass):
|
||||
ensure_auth_manager_loaded(manager)
|
||||
|
||||
user = await manager.async_create_user("Hello")
|
||||
# first user in the system is owner and admin
|
||||
assert user.is_owner
|
||||
assert user.is_admin
|
||||
assert user.groups == []
|
||||
|
||||
user = await manager.async_create_user("Hello 2")
|
||||
assert not user.is_admin
|
||||
assert user.groups == []
|
||||
|
||||
user = await manager.async_create_user("Hello 3", ["system-admin"])
|
||||
assert user.is_admin
|
||||
assert user.groups[0].id == "system-admin"
|
||||
|
||||
user_cred = await manager.async_get_or_create_user(
|
||||
auth_models.Credentials(
|
||||
|
@@ -156,8 +156,35 @@ async def test_create(hass, hass_ws_client, hass_access_token):
|
||||
|
||||
assert len(await hass.auth.async_get_users()) == 1
|
||||
|
||||
await client.send_json({"id": 5, "type": "config/auth/create", "name": "Paulus"})
|
||||
|
||||
result = await client.receive_json()
|
||||
assert result["success"], result
|
||||
assert len(await hass.auth.async_get_users()) == 2
|
||||
data_user = result["result"]["user"]
|
||||
user = await hass.auth.async_get_user(data_user["id"])
|
||||
assert user is not None
|
||||
assert user.name == data_user["name"]
|
||||
assert user.is_active
|
||||
assert user.groups == []
|
||||
assert not user.is_admin
|
||||
assert not user.is_owner
|
||||
assert not user.system_generated
|
||||
|
||||
|
||||
async def test_create_user_group(hass, hass_ws_client, hass_access_token):
|
||||
"""Test create user with a group."""
|
||||
client = await hass_ws_client(hass, hass_access_token)
|
||||
|
||||
assert len(await hass.auth.async_get_users()) == 1
|
||||
|
||||
await client.send_json(
|
||||
{"id": 5, "type": auth_config.WS_TYPE_CREATE, "name": "Paulus"}
|
||||
{
|
||||
"id": 5,
|
||||
"type": "config/auth/create",
|
||||
"name": "Paulus",
|
||||
"group_ids": ["system-admin"],
|
||||
}
|
||||
)
|
||||
|
||||
result = await client.receive_json()
|
||||
@@ -168,6 +195,8 @@ async def test_create(hass, hass_ws_client, hass_access_token):
|
||||
assert user is not None
|
||||
assert user.name == data_user["name"]
|
||||
assert user.is_active
|
||||
assert user.groups[0].id == "system-admin"
|
||||
assert user.is_admin
|
||||
assert not user.is_owner
|
||||
assert not user.system_generated
|
||||
|
||||
@@ -176,7 +205,7 @@ async def test_create_requires_admin(hass, hass_ws_client, hass_read_only_access
|
||||
"""Test create command requires an admin."""
|
||||
client = await hass_ws_client(hass, hass_read_only_access_token)
|
||||
|
||||
await client.send_json({"id": 5, "type": auth_config.WS_TYPE_CREATE, "name": "YO"})
|
||||
await client.send_json({"id": 5, "type": "config/auth/create", "name": "YO"})
|
||||
|
||||
result = await client.receive_json()
|
||||
assert not result["success"], result
|
||||
|
@@ -522,6 +522,62 @@ class TestComponentHistory(unittest.TestCase):
|
||||
)
|
||||
assert list(hist.keys()) == entity_ids
|
||||
|
||||
def test_get_significant_states_only(self):
|
||||
"""Test significant states when significant_states_only is set."""
|
||||
self.init_recorder()
|
||||
entity_id = "sensor.test"
|
||||
|
||||
def set_state(state, **kwargs):
|
||||
"""Set the state."""
|
||||
self.hass.states.set(entity_id, state, **kwargs)
|
||||
wait_recording_done(self.hass)
|
||||
return self.hass.states.get(entity_id)
|
||||
|
||||
start = dt_util.utcnow() - timedelta(minutes=4)
|
||||
points = []
|
||||
for i in range(1, 4):
|
||||
points.append(start + timedelta(minutes=i))
|
||||
|
||||
states = []
|
||||
with patch(
|
||||
"homeassistant.components.recorder.dt_util.utcnow", return_value=start
|
||||
):
|
||||
set_state("123", attributes={"attribute": 10.64})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.recorder.dt_util.utcnow", return_value=points[0]
|
||||
):
|
||||
# Attributes are different, state not
|
||||
states.append(set_state("123", attributes={"attribute": 21.42}))
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.recorder.dt_util.utcnow", return_value=points[1]
|
||||
):
|
||||
# state is different, attributes not
|
||||
states.append(set_state("32", attributes={"attribute": 21.42}))
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.recorder.dt_util.utcnow", return_value=points[2]
|
||||
):
|
||||
# everything is different
|
||||
states.append(set_state("412", attributes={"attribute": 54.23}))
|
||||
|
||||
hist = history.get_significant_states(
|
||||
self.hass, start, significant_changes_only=True
|
||||
)
|
||||
|
||||
assert len(hist[entity_id]) == 2
|
||||
assert states[0] not in hist[entity_id]
|
||||
assert states[1] in hist[entity_id]
|
||||
assert states[2] in hist[entity_id]
|
||||
|
||||
hist = history.get_significant_states(
|
||||
self.hass, start, significant_changes_only=False
|
||||
)
|
||||
|
||||
assert len(hist[entity_id]) == 3
|
||||
assert states == hist[entity_id]
|
||||
|
||||
def check_significant_states(self, zero, four, states, config):
|
||||
"""Check if significant states are retrieved."""
|
||||
filters = history.Filters()
|
||||
|
@@ -5,8 +5,11 @@ import pytest
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_CURRENT_POSITION,
|
||||
ATTR_CURRENT_TILT_POSITION,
|
||||
ATTR_POSITION,
|
||||
ATTR_TILT_POSITION,
|
||||
DOMAIN,
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
SUPPORT_STOP,
|
||||
)
|
||||
from homeassistant.components.homekit.const import ATTR_VALUE
|
||||
@@ -14,6 +17,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
SERVICE_SET_COVER_TILT_POSITION,
|
||||
STATE_CLOSED,
|
||||
STATE_CLOSING,
|
||||
STATE_OPEN,
|
||||
@@ -193,6 +197,72 @@ async def test_window_set_cover_position(hass, hk_driver, cls, events):
|
||||
assert events[-1].data[ATTR_VALUE] == 75
|
||||
|
||||
|
||||
async def test_window_cover_set_tilt(hass, hk_driver, cls, events):
|
||||
"""Test if accessory and HA update slat tilt accordingly."""
|
||||
entity_id = "cover.window"
|
||||
|
||||
hass.states.async_set(
|
||||
entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_TILT_POSITION}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
acc = cls.window(hass, hk_driver, "Cover", entity_id, 2, None)
|
||||
await hass.async_add_job(acc.run)
|
||||
|
||||
assert acc.aid == 2
|
||||
assert acc.category == 14 # CATEGORY_WINDOW_COVERING
|
||||
|
||||
assert acc.char_current_tilt.value == 0
|
||||
assert acc.char_target_tilt.value == 0
|
||||
|
||||
hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_TILT_POSITION: None})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_current_tilt.value == 0
|
||||
assert acc.char_target_tilt.value == 0
|
||||
|
||||
hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_TILT_POSITION: 100})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_current_tilt.value == 90
|
||||
assert acc.char_target_tilt.value == 90
|
||||
|
||||
hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_TILT_POSITION: 50})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_current_tilt.value == 0
|
||||
assert acc.char_target_tilt.value == 0
|
||||
|
||||
hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_TILT_POSITION: 0})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_current_tilt.value == -90
|
||||
assert acc.char_target_tilt.value == -90
|
||||
|
||||
# set from HomeKit
|
||||
call_set_tilt_position = async_mock_service(
|
||||
hass, DOMAIN, SERVICE_SET_COVER_TILT_POSITION
|
||||
)
|
||||
|
||||
# HomeKit sets tilts between -90 and 90 (degrees), whereas
|
||||
# Homeassistant expects a % between 0 and 100. Keep that in mind
|
||||
# when comparing
|
||||
await hass.async_add_job(acc.char_target_tilt.client_update_value, 90)
|
||||
await hass.async_block_till_done()
|
||||
assert call_set_tilt_position[0]
|
||||
assert call_set_tilt_position[0].data[ATTR_ENTITY_ID] == entity_id
|
||||
assert call_set_tilt_position[0].data[ATTR_TILT_POSITION] == 100
|
||||
assert acc.char_current_tilt.value == -90
|
||||
assert acc.char_target_tilt.value == 90
|
||||
assert len(events) == 1
|
||||
assert events[-1].data[ATTR_VALUE] == 100
|
||||
|
||||
await hass.async_add_job(acc.char_target_tilt.client_update_value, 45)
|
||||
await hass.async_block_till_done()
|
||||
assert call_set_tilt_position[1]
|
||||
assert call_set_tilt_position[1].data[ATTR_ENTITY_ID] == entity_id
|
||||
assert call_set_tilt_position[1].data[ATTR_TILT_POSITION] == 75
|
||||
assert acc.char_current_tilt.value == -90
|
||||
assert acc.char_target_tilt.value == 45
|
||||
assert len(events) == 2
|
||||
assert events[-1].data[ATTR_VALUE] == 75
|
||||
|
||||
|
||||
async def test_window_open_close(hass, hk_driver, cls, events):
|
||||
"""Test if accessory and HA are updated accordingly."""
|
||||
entity_id = "cover.window"
|
||||
|
@@ -26,11 +26,11 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory):
|
||||
|
||||
async def test_hmip_remove_device(hass, default_mock_hap_factory):
|
||||
"""Test Remove of hmip device."""
|
||||
entity_id = "light.treppe"
|
||||
entity_name = "Treppe"
|
||||
entity_id = "light.treppe_ch"
|
||||
entity_name = "Treppe CH"
|
||||
device_model = "HmIP-BSL"
|
||||
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
|
||||
test_devices=[entity_name]
|
||||
test_devices=["Treppe"]
|
||||
)
|
||||
|
||||
ha_state, hmip_device = get_and_check_entity_basics(
|
||||
@@ -58,11 +58,11 @@ async def test_hmip_remove_device(hass, default_mock_hap_factory):
|
||||
|
||||
async def test_hmip_add_device(hass, default_mock_hap_factory, hmip_config_entry):
|
||||
"""Test Remove of hmip device."""
|
||||
entity_id = "light.treppe"
|
||||
entity_name = "Treppe"
|
||||
entity_id = "light.treppe_ch"
|
||||
entity_name = "Treppe CH"
|
||||
device_model = "HmIP-BSL"
|
||||
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
|
||||
test_devices=[entity_name]
|
||||
test_devices=["Treppe"]
|
||||
)
|
||||
|
||||
ha_state, hmip_device = get_and_check_entity_basics(
|
||||
@@ -137,11 +137,11 @@ async def test_all_devices_unavailable_when_hap_not_connected(
|
||||
hass, default_mock_hap_factory
|
||||
):
|
||||
"""Test make all devices unavaulable when hap is not connected."""
|
||||
entity_id = "light.treppe"
|
||||
entity_name = "Treppe"
|
||||
entity_id = "light.treppe_ch"
|
||||
entity_name = "Treppe CH"
|
||||
device_model = "HmIP-BSL"
|
||||
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
|
||||
test_devices=[entity_name]
|
||||
test_devices=["Treppe"]
|
||||
)
|
||||
|
||||
ha_state, hmip_device = get_and_check_entity_basics(
|
||||
@@ -161,11 +161,11 @@ async def test_all_devices_unavailable_when_hap_not_connected(
|
||||
|
||||
async def test_hap_reconnected(hass, default_mock_hap_factory):
|
||||
"""Test reconnect hap."""
|
||||
entity_id = "light.treppe"
|
||||
entity_name = "Treppe"
|
||||
entity_id = "light.treppe_ch"
|
||||
entity_name = "Treppe CH"
|
||||
device_model = "HmIP-BSL"
|
||||
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
|
||||
test_devices=[entity_name]
|
||||
test_devices=["Treppe"]
|
||||
)
|
||||
|
||||
ha_state, hmip_device = get_and_check_entity_basics(
|
||||
@@ -192,8 +192,8 @@ async def test_hap_reconnected(hass, default_mock_hap_factory):
|
||||
async def test_hap_with_name(hass, mock_connection, hmip_config_entry):
|
||||
"""Test hap with name."""
|
||||
home_name = "TestName"
|
||||
entity_id = f"light.{home_name.lower()}_treppe"
|
||||
entity_name = f"{home_name} Treppe"
|
||||
entity_id = f"light.{home_name.lower()}_treppe_ch"
|
||||
entity_name = f"{home_name} Treppe CH"
|
||||
device_model = "HmIP-BSL"
|
||||
|
||||
hmip_config_entry.data = {**hmip_config_entry.data, "name": home_name}
|
||||
|
@@ -27,11 +27,11 @@ async def test_manually_configured_platform(hass):
|
||||
|
||||
async def test_hmip_light(hass, default_mock_hap_factory):
|
||||
"""Test HomematicipLight."""
|
||||
entity_id = "light.treppe"
|
||||
entity_name = "Treppe"
|
||||
entity_id = "light.treppe_ch"
|
||||
entity_name = "Treppe CH"
|
||||
device_model = "HmIP-BSL"
|
||||
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
|
||||
test_devices=[entity_name]
|
||||
test_devices=["Treppe"]
|
||||
)
|
||||
|
||||
ha_state, hmip_device = get_and_check_entity_basics(
|
||||
@@ -66,8 +66,8 @@ async def test_hmip_light(hass, default_mock_hap_factory):
|
||||
|
||||
async def test_hmip_notification_light(hass, default_mock_hap_factory):
|
||||
"""Test HomematicipNotificationLight."""
|
||||
entity_id = "light.treppe_top_notification"
|
||||
entity_name = "Treppe Top Notification"
|
||||
entity_id = "light.alarm_status"
|
||||
entity_name = "Alarm Status"
|
||||
device_model = "HmIP-BSL"
|
||||
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
|
||||
test_devices=["Treppe"]
|
||||
|
95
tests/components/ipp/__init__.py
Normal file
95
tests/components/ipp/__init__.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Tests for the IPP integration."""
|
||||
import os
|
||||
|
||||
from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_TYPE,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
ATTR_HOSTNAME = "hostname"
|
||||
ATTR_PROPERTIES = "properties"
|
||||
|
||||
IPP_ZEROCONF_SERVICE_TYPE = "_ipp._tcp.local."
|
||||
IPPS_ZEROCONF_SERVICE_TYPE = "_ipps._tcp.local."
|
||||
|
||||
ZEROCONF_NAME = "EPSON123456"
|
||||
ZEROCONF_HOST = "1.2.3.4"
|
||||
ZEROCONF_HOSTNAME = "EPSON123456.local."
|
||||
ZEROCONF_PORT = 631
|
||||
|
||||
|
||||
MOCK_USER_INPUT = {
|
||||
CONF_HOST: "EPSON123456.local",
|
||||
CONF_PORT: 361,
|
||||
CONF_SSL: False,
|
||||
CONF_VERIFY_SSL: False,
|
||||
CONF_BASE_PATH: "/ipp/print",
|
||||
}
|
||||
|
||||
MOCK_ZEROCONF_IPP_SERVICE_INFO = {
|
||||
CONF_TYPE: IPP_ZEROCONF_SERVICE_TYPE,
|
||||
CONF_NAME: ZEROCONF_NAME,
|
||||
CONF_HOST: ZEROCONF_HOST,
|
||||
ATTR_HOSTNAME: ZEROCONF_HOSTNAME,
|
||||
CONF_PORT: ZEROCONF_PORT,
|
||||
ATTR_PROPERTIES: {"rp": "ipp/print"},
|
||||
}
|
||||
|
||||
MOCK_ZEROCONF_IPPS_SERVICE_INFO = {
|
||||
CONF_TYPE: IPPS_ZEROCONF_SERVICE_TYPE,
|
||||
CONF_NAME: ZEROCONF_NAME,
|
||||
CONF_HOST: ZEROCONF_HOST,
|
||||
ATTR_HOSTNAME: ZEROCONF_HOSTNAME,
|
||||
CONF_PORT: ZEROCONF_PORT,
|
||||
ATTR_PROPERTIES: {"rp": "ipp/print"},
|
||||
}
|
||||
|
||||
|
||||
def load_fixture_binary(filename):
|
||||
"""Load a binary fixture."""
|
||||
path = os.path.join(os.path.dirname(__file__), "..", "..", "fixtures", filename)
|
||||
with open(path, "rb") as fptr:
|
||||
return fptr.read()
|
||||
|
||||
|
||||
async def init_integration(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the IPP integration in Home Assistant."""
|
||||
|
||||
fixture = "ipp/get-printer-attributes.bin"
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print",
|
||||
content=load_fixture_binary(fixture),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="cfe92100-67c4-11d4-a45f-f8d027761251",
|
||||
data={
|
||||
CONF_HOST: "EPSON123456.local",
|
||||
CONF_PORT: 631,
|
||||
CONF_SSL: False,
|
||||
CONF_VERIFY_SSL: True,
|
||||
CONF_BASE_PATH: "/ipp/print",
|
||||
CONF_UUID: "cfe92100-67c4-11d4-a45f-f8d027761251",
|
||||
},
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
if not skip_setup:
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
306
tests/components/ipp/test_config_flow.py
Normal file
306
tests/components/ipp/test_config_flow.py
Normal file
@@ -0,0 +1,306 @@
|
||||
"""Tests for the IPP config flow."""
|
||||
import aiohttp
|
||||
from pyipp import IPPConnectionUpgradeRequired
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.ipp import config_flow
|
||||
from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import (
|
||||
MOCK_USER_INPUT,
|
||||
MOCK_ZEROCONF_IPP_SERVICE_INFO,
|
||||
MOCK_ZEROCONF_IPPS_SERVICE_INFO,
|
||||
init_integration,
|
||||
load_fixture_binary,
|
||||
)
|
||||
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
async def test_show_user_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the user set up form is served."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
|
||||
async def test_show_zeroconf_confirm_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the zeroconf confirmation form is served."""
|
||||
flow = config_flow.IPPFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
flow.discovery_info = {CONF_NAME: "EPSON123456"}
|
||||
|
||||
result = await flow.async_step_zeroconf_confirm()
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"}
|
||||
|
||||
|
||||
async def test_show_zeroconf_form(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test that the zeroconf confirmation form is served."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print",
|
||||
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
|
||||
flow = config_flow.IPPFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await flow.async_step_zeroconf(discovery_info)
|
||||
|
||||
assert flow.discovery_info[CONF_HOST] == "EPSON123456.local"
|
||||
assert flow.discovery_info[CONF_NAME] == "EPSON123456"
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"}
|
||||
|
||||
|
||||
async def test_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we show user form on IPP connection error."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print", exc=aiohttp.ClientError
|
||||
)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input,
|
||||
)
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "connection_error"}
|
||||
|
||||
|
||||
async def test_zeroconf_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on IPP connection error."""
|
||||
aioclient_mock.post("http://EPSON123456.local/ipp/print", exc=aiohttp.ClientError)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "connection_error"
|
||||
|
||||
|
||||
async def test_zeroconf_confirm_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on IPP connection error."""
|
||||
aioclient_mock.post("http://EPSON123456.local/ipp/print", exc=aiohttp.ClientError)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_ZEROCONF,
|
||||
CONF_HOST: "EPSON123456.local",
|
||||
CONF_NAME: "EPSON123456",
|
||||
},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "connection_error"
|
||||
|
||||
|
||||
async def test_user_connection_upgrade_required(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we show the user form if connection upgrade required by server."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print", exc=IPPConnectionUpgradeRequired
|
||||
)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input,
|
||||
)
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "connection_upgrade"}
|
||||
|
||||
|
||||
async def test_zeroconf_connection_upgrade_required(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on IPP connection error."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local/ipp/print", exc=IPPConnectionUpgradeRequired
|
||||
)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "connection_upgrade"
|
||||
|
||||
|
||||
async def test_user_device_exists_abort(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort user flow if printer already configured."""
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_device_exists_abort(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow if printer already configured."""
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_with_uuid_device_exists_abort(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow if printer already configured."""
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
discovery_info["properties"]["UUID"] = "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_full_user_flow_implementation(
|
||||
hass: HomeAssistant, aioclient_mock
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print",
|
||||
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "EPSON123456.local", CONF_BASE_PATH: "/ipp/print"},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "EPSON123456.local"
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == "EPSON123456.local"
|
||||
assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||
|
||||
|
||||
async def test_full_zeroconf_flow_implementation(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print",
|
||||
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
|
||||
flow = config_flow.IPPFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await flow.async_step_zeroconf(discovery_info)
|
||||
|
||||
assert flow.discovery_info
|
||||
assert flow.discovery_info[CONF_HOST] == "EPSON123456.local"
|
||||
assert flow.discovery_info[CONF_NAME] == "EPSON123456"
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await flow.async_step_zeroconf_confirm(
|
||||
user_input={CONF_HOST: "EPSON123456.local"}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "EPSON123456"
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == "EPSON123456.local"
|
||||
assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||
assert not result["data"][CONF_SSL]
|
||||
|
||||
|
||||
async def test_full_zeroconf_tls_flow_implementation(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
aioclient_mock.post(
|
||||
"https://EPSON123456.local:631/ipp/print",
|
||||
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
|
||||
flow = config_flow.IPPFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPPS_SERVICE_INFO.copy()
|
||||
result = await flow.async_step_zeroconf(discovery_info)
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"}
|
||||
|
||||
result = await flow.async_step_zeroconf_confirm(
|
||||
user_input={CONF_HOST: "EPSON123456.local"}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "EPSON123456"
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == "EPSON123456.local"
|
||||
assert result["data"][CONF_NAME] == "EPSON123456"
|
||||
assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||
assert result["data"][CONF_SSL]
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user