mirror of
https://github.com/home-assistant/core.git
synced 2025-08-05 05:35:11 +02:00
Merge branch 'dev' into tesla-mfa
This commit is contained in:
13
.coveragerc
13
.coveragerc
@@ -20,6 +20,8 @@ omit =
|
||||
homeassistant/components/acmeda/helpers.py
|
||||
homeassistant/components/acmeda/hub.py
|
||||
homeassistant/components/acmeda/sensor.py
|
||||
homeassistant/components/adax/__init__.py
|
||||
homeassistant/components/adax/climate.py
|
||||
homeassistant/components/adguard/__init__.py
|
||||
homeassistant/components/adguard/const.py
|
||||
homeassistant/components/adguard/sensor.py
|
||||
@@ -73,6 +75,12 @@ omit =
|
||||
homeassistant/components/asuswrt/router.py
|
||||
homeassistant/components/aten_pe/*
|
||||
homeassistant/components/atome/*
|
||||
homeassistant/components/automate/__init__.py
|
||||
homeassistant/components/automate/base.py
|
||||
homeassistant/components/automate/const.py
|
||||
homeassistant/components/automate/cover.py
|
||||
homeassistant/components/automate/helpers.py
|
||||
homeassistant/components/automate/hub.py
|
||||
homeassistant/components/aurora/__init__.py
|
||||
homeassistant/components/aurora/binary_sensor.py
|
||||
homeassistant/components/aurora/const.py
|
||||
@@ -130,9 +138,7 @@ omit =
|
||||
homeassistant/components/brottsplatskartan/sensor.py
|
||||
homeassistant/components/browser/*
|
||||
homeassistant/components/brunt/cover.py
|
||||
homeassistant/components/bsblan/__init__.py
|
||||
homeassistant/components/bsblan/climate.py
|
||||
homeassistant/components/bsblan/const.py
|
||||
homeassistant/components/bt_home_hub_5/device_tracker.py
|
||||
homeassistant/components/bt_smarthub/device_tracker.py
|
||||
homeassistant/components/buienradar/sensor.py
|
||||
@@ -622,6 +628,7 @@ omit =
|
||||
homeassistant/components/mill/__init__.py
|
||||
homeassistant/components/mill/climate.py
|
||||
homeassistant/components/mill/const.py
|
||||
homeassistant/components/mill/sensor.py
|
||||
homeassistant/components/minecraft_server/__init__.py
|
||||
homeassistant/components/minecraft_server/binary_sensor.py
|
||||
homeassistant/components/minecraft_server/const.py
|
||||
@@ -690,6 +697,7 @@ omit =
|
||||
homeassistant/components/neurio_energy/sensor.py
|
||||
homeassistant/components/nexia/climate.py
|
||||
homeassistant/components/nextcloud/*
|
||||
homeassistant/components/nfandroidtv/__init__.py
|
||||
homeassistant/components/nfandroidtv/notify.py
|
||||
homeassistant/components/niko_home_control/light.py
|
||||
homeassistant/components/nilu/air_quality.py
|
||||
@@ -967,7 +975,6 @@ omit =
|
||||
homeassistant/components/sonos/*
|
||||
homeassistant/components/sony_projector/switch.py
|
||||
homeassistant/components/spc/*
|
||||
homeassistant/components/speedtestdotnet/*
|
||||
homeassistant/components/spider/*
|
||||
homeassistant/components/splunk/*
|
||||
homeassistant/components/spotify/__init__.py
|
||||
|
@@ -9,10 +9,12 @@ homeassistant.components.actiontec.*
|
||||
homeassistant.components.aftership.*
|
||||
homeassistant.components.air_quality.*
|
||||
homeassistant.components.airly.*
|
||||
homeassistant.components.airvisual.*
|
||||
homeassistant.components.aladdin_connect.*
|
||||
homeassistant.components.alarm_control_panel.*
|
||||
homeassistant.components.amazon_polly.*
|
||||
homeassistant.components.ambee.*
|
||||
homeassistant.components.ambient_station.*
|
||||
homeassistant.components.ampio.*
|
||||
homeassistant.components.automation.*
|
||||
homeassistant.components.binary_sensor.*
|
||||
@@ -41,6 +43,7 @@ homeassistant.components.fritz.*
|
||||
homeassistant.components.geo_location.*
|
||||
homeassistant.components.gios.*
|
||||
homeassistant.components.group.*
|
||||
homeassistant.components.guardian.*
|
||||
homeassistant.components.history.*
|
||||
homeassistant.components.homeassistant.triggers.event
|
||||
homeassistant.components.http.*
|
||||
@@ -58,6 +61,7 @@ homeassistant.components.mailbox.*
|
||||
homeassistant.components.media_player.*
|
||||
homeassistant.components.mysensors.*
|
||||
homeassistant.components.nam.*
|
||||
homeassistant.components.netatmo.*
|
||||
homeassistant.components.network.*
|
||||
homeassistant.components.no_ip.*
|
||||
homeassistant.components.notify.*
|
||||
@@ -73,6 +77,7 @@ homeassistant.components.remote.*
|
||||
homeassistant.components.scene.*
|
||||
homeassistant.components.select.*
|
||||
homeassistant.components.sensor.*
|
||||
homeassistant.components.shelly.*
|
||||
homeassistant.components.slack.*
|
||||
homeassistant.components.sonos.media_player
|
||||
homeassistant.components.ssdp.*
|
||||
|
13
.vscode/tasks.json
vendored
13
.vscode/tasks.json
vendored
@@ -5,10 +5,7 @@
|
||||
"label": "Run Home Assistant Core",
|
||||
"type": "shell",
|
||||
"command": "hass -c ./config",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"group": "test",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
@@ -19,7 +16,9 @@
|
||||
"label": "Pytest",
|
||||
"type": "shell",
|
||||
"command": "pytest --timeout=10 tests",
|
||||
"dependsOn": ["Install all Test Requirements"],
|
||||
"dependsOn": [
|
||||
"Install all Test Requirements"
|
||||
],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
@@ -48,7 +47,9 @@
|
||||
"label": "Pylint",
|
||||
"type": "shell",
|
||||
"command": "pylint homeassistant",
|
||||
"dependsOn": ["Install all Requirements"],
|
||||
"dependsOn": [
|
||||
"Install all Requirements"
|
||||
],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
|
@@ -22,6 +22,7 @@ homeassistant/scripts/check_config.py @kellerza
|
||||
homeassistant/components/abode/* @shred86
|
||||
homeassistant/components/accuweather/* @bieniu
|
||||
homeassistant/components/acmeda/* @atmurray
|
||||
homeassistant/components/adax/* @danielhiversen
|
||||
homeassistant/components/adguard/* @frenck
|
||||
homeassistant/components/advantage_air/* @Bre77
|
||||
homeassistant/components/aemet/* @noltari
|
||||
@@ -55,6 +56,7 @@ homeassistant/components/august/* @bdraco
|
||||
homeassistant/components/aurora/* @djtimca
|
||||
homeassistant/components/aurora_abb_powerone/* @davet2001
|
||||
homeassistant/components/auth/* @home-assistant/core
|
||||
homeassistant/components/automate/* @sillyfrog
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/avea/* @pattyland
|
||||
homeassistant/components/awair/* @ahayworth @danielsjf
|
||||
@@ -160,6 +162,7 @@ homeassistant/components/fireservicerota/* @cyberjunky
|
||||
homeassistant/components/firmata/* @DaAwesomeP
|
||||
homeassistant/components/fixer/* @fabaff
|
||||
homeassistant/components/flick_electric/* @ZephireNZ
|
||||
homeassistant/components/flipr/* @cnico
|
||||
homeassistant/components/flo/* @dmulcahey
|
||||
homeassistant/components/flock/* @fabaff
|
||||
homeassistant/components/flume/* @ChrisMandich @bdraco
|
||||
@@ -331,6 +334,7 @@ homeassistant/components/netdata/* @fabaff
|
||||
homeassistant/components/nexia/* @bdraco
|
||||
homeassistant/components/nextbus/* @vividboarder
|
||||
homeassistant/components/nextcloud/* @meichthys
|
||||
homeassistant/components/nfandroidtv/* @tkdrob
|
||||
homeassistant/components/nightscout/* @marciogranzotto
|
||||
homeassistant/components/nilu/* @hfurubotten
|
||||
homeassistant/components/nissan_leaf/* @filcole
|
||||
@@ -562,6 +566,7 @@ homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/wemo/* @esev
|
||||
homeassistant/components/wiffi/* @mampfes
|
||||
homeassistant/components/wilight/* @leofig-rj
|
||||
homeassistant/components/wirelesstag/* @sergeymaysak
|
||||
homeassistant/components/withings/* @vangorra
|
||||
homeassistant/components/wled/* @frenck
|
||||
homeassistant/components/wolflink/* @adamkrol93
|
||||
|
@@ -26,7 +26,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Passwort",
|
||||
"username": "E-Mail-Adresse"
|
||||
"username": "E-Mail"
|
||||
},
|
||||
"title": "Gib deine Abode-Anmeldeinformationen ein"
|
||||
}
|
||||
|
18
homeassistant/components/adax/__init__.py
Normal file
18
homeassistant/components/adax/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""The Adax integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
PLATFORMS = ["climate"]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Adax from a config entry."""
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
152
homeassistant/components/adax/climate.py
Normal file
152
homeassistant/components/adax/climate.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""Support for Adax wifi-enabled home heaters."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from adax import Adax
|
||||
|
||||
from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.components.climate.const import (
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_OFF,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_PASSWORD,
|
||||
PRECISION_WHOLE,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import ACCOUNT_ID
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Adax thermostat with config flow."""
|
||||
adax_data_handler = Adax(
|
||||
entry.data[ACCOUNT_ID],
|
||||
entry.data[CONF_PASSWORD],
|
||||
websession=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
AdaxDevice(room, adax_data_handler)
|
||||
for room in await adax_data_handler.get_rooms()
|
||||
)
|
||||
|
||||
|
||||
class AdaxDevice(ClimateEntity):
|
||||
"""Representation of a heater."""
|
||||
|
||||
def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None:
|
||||
"""Initialize the heater."""
|
||||
self._heater_data = heater_data
|
||||
self._adax_data_handler = adax_data_handler
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
return f"{self._heater_data['homeId']}_{self._heater_data['id']}"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the device, if any."""
|
||||
return self._heater_data["name"]
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> str:
|
||||
"""Return hvac operation ie. heat, cool mode."""
|
||||
if self._heater_data["heatingEnabled"]:
|
||||
return HVAC_MODE_HEAT
|
||||
return HVAC_MODE_OFF
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return nice icon for heater."""
|
||||
if self.hvac_mode == HVAC_MODE_HEAT:
|
||||
return "mdi:radiator"
|
||||
return "mdi:radiator-off"
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> list[str]:
|
||||
"""Return the list of available hvac operation modes."""
|
||||
return [HVAC_MODE_HEAT, HVAC_MODE_OFF]
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
|
||||
"""Set hvac mode."""
|
||||
if hvac_mode == HVAC_MODE_HEAT:
|
||||
temperature = max(
|
||||
self.min_temp, self._heater_data.get("targetTemperature", self.min_temp)
|
||||
)
|
||||
await self._adax_data_handler.set_room_target_temperature(
|
||||
self._heater_data["id"], temperature, True
|
||||
)
|
||||
elif hvac_mode == HVAC_MODE_OFF:
|
||||
await self._adax_data_handler.set_room_target_temperature(
|
||||
self._heater_data["id"], self.min_temp, False
|
||||
)
|
||||
else:
|
||||
return
|
||||
await self._adax_data_handler.update()
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of measurement which this device uses."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def min_temp(self) -> int:
|
||||
"""Return the minimum temperature."""
|
||||
return 5
|
||||
|
||||
@property
|
||||
def max_temp(self) -> int:
|
||||
"""Return the maximum temperature."""
|
||||
return 35
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
return self._heater_data.get("temperature")
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> int | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._heater_data.get("targetTemperature")
|
||||
|
||||
@property
|
||||
def target_temperature_step(self) -> int:
|
||||
"""Return the supported step of target temperature."""
|
||||
return PRECISION_WHOLE
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
await self._adax_data_handler.set_room_target_temperature(
|
||||
self._heater_data["id"], temperature, True
|
||||
)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data."""
|
||||
for room in await self._adax_data_handler.get_rooms():
|
||||
if room["id"] == self._heater_data["id"]:
|
||||
self._heater_data = room
|
||||
return
|
71
homeassistant/components/adax/config_flow.py
Normal file
71
homeassistant/components/adax/config_flow.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Config flow for Adax integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import adax
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import ACCOUNT_ID, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(ACCOUNT_ID): int, vol.Required(CONF_PASSWORD): str}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||
"""Validate the user input allows us to connect."""
|
||||
account_id = data[ACCOUNT_ID]
|
||||
password = data[CONF_PASSWORD].replace(" ", "")
|
||||
|
||||
token = await adax.get_adax_token(
|
||||
async_get_clientsession(hass), account_id, password
|
||||
)
|
||||
if token is None:
|
||||
_LOGGER.info("Adax: Failed to login to retrieve token")
|
||||
raise CannotConnect
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Adax."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
self._async_abort_entries_match({ACCOUNT_ID: user_input[ACCOUNT_ID]})
|
||||
await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input[ACCOUNT_ID], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
5
homeassistant/components/adax/const.py
Normal file
5
homeassistant/components/adax/const.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Constants for the Adax integration."""
|
||||
from typing import Final
|
||||
|
||||
ACCOUNT_ID: Final = "account_id"
|
||||
DOMAIN: Final = "adax"
|
13
homeassistant/components/adax/manifest.json
Normal file
13
homeassistant/components/adax/manifest.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "adax",
|
||||
"name": "Adax",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/adax",
|
||||
"requirements": [
|
||||
"adax==0.0.1"
|
||||
],
|
||||
"codeowners": [
|
||||
"@danielhiversen"
|
||||
],
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
20
homeassistant/components/adax/strings.json
Normal file
20
homeassistant/components/adax/strings.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"account_id": "Account ID",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
}
|
||||
}
|
20
homeassistant/components/adax/translations/ca.json
Normal file
20
homeassistant/components/adax/translations/ca.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "El dispositiu ja est\u00e0 configurat"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Ha fallat la connexi\u00f3",
|
||||
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"account_id": "ID del compte",
|
||||
"host": "Amfitri\u00f3",
|
||||
"password": "Contrasenya"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
homeassistant/components/adax/translations/de.json
Normal file
20
homeassistant/components/adax/translations/de.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen",
|
||||
"invalid_auth": "Ung\u00fcltige Authentifizierung"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"account_id": "Konto-ID",
|
||||
"host": "Host",
|
||||
"password": "Passwort"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
homeassistant/components/adax/translations/en.json
Normal file
20
homeassistant/components/adax/translations/en.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"account_id": "Account ID",
|
||||
"host": "Host",
|
||||
"password": "Password"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
homeassistant/components/adax/translations/et.json
Normal file
20
homeassistant/components/adax/translations/et.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Seade on juba h\u00e4\u00e4lestatud"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u00dchendamine nurjus",
|
||||
"invalid_auth": "Tuvastamise viga"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"account_id": "Konto ID",
|
||||
"host": "Host",
|
||||
"password": "Salas\u00f5na"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
homeassistant/components/adax/translations/ru.json
Normal file
20
homeassistant/components/adax/translations/ru.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
|
||||
"invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"account_id": "ID \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438",
|
||||
"host": "\u0425\u043e\u0441\u0442",
|
||||
"password": "\u041f\u0430\u0440\u043e\u043b\u044c"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -17,7 +17,7 @@
|
||||
"host": "Host",
|
||||
"password": "Passwort",
|
||||
"port": "Port",
|
||||
"ssl": "AdGuard Home verwendet ein SSL-Zertifikat",
|
||||
"ssl": "Verwendet ein SSL-Zertifikat",
|
||||
"username": "Benutzername",
|
||||
"verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
|
||||
},
|
||||
|
@@ -2,7 +2,7 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
|
||||
from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
@@ -25,9 +25,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
entities.append(AdvantageAirTimeTo(instance, ac_key, "On"))
|
||||
entities.append(AdvantageAirTimeTo(instance, ac_key, "Off"))
|
||||
for zone_key, zone in ac_device["zones"].items():
|
||||
# Only show damper sensors when zone is in temperature control
|
||||
# Only show damper and temp sensors when zone is in temperature control
|
||||
if zone["type"] != 0:
|
||||
entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key))
|
||||
entities.append(AdvantageAirZoneTemp(instance, ac_key, zone_key))
|
||||
# Only show wireless signal strength sensors when using wireless sensors
|
||||
if zone["rssi"] > 0:
|
||||
entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key))
|
||||
@@ -144,3 +145,23 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity):
|
||||
if self._zone["rssi"] >= 20:
|
||||
return "mdi:wifi-strength-1"
|
||||
return "mdi:wifi-strength-outline"
|
||||
|
||||
|
||||
class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity):
|
||||
"""Representation of Advantage Air Zone wireless signal sensor."""
|
||||
|
||||
_attr_unit_of_measurement = TEMP_CELSIUS
|
||||
_attr_state_class = STATE_CLASS_MEASUREMENT
|
||||
_attr_icon = "mdi:thermometer"
|
||||
_attr_entity_registry_enabled_default = False
|
||||
|
||||
def __init__(self, instance, ac_key, zone_key):
|
||||
"""Initialize an Advantage Air Zone Temp Sensor."""
|
||||
super().__init__(instance, ac_key, zone_key)
|
||||
self._attr_name = f'{self._zone["name"]} Temperature'
|
||||
self._attr_unique_id = f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-temp'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current value of the measured temperature."""
|
||||
return self._zone["measuredTemp"]
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "IP Adresse",
|
||||
"ip_address": "IP-Adresse",
|
||||
"port": "Port"
|
||||
},
|
||||
"description": "Anschluss an die API deines Advantage Air Wandtabletts.",
|
||||
|
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"title": "AirNow",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
|
@@ -1,6 +1,10 @@
|
||||
"""The airvisual component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, MutableMapping
|
||||
from datetime import timedelta
|
||||
from math import ceil
|
||||
from typing import Any, Dict, cast
|
||||
|
||||
from pyairvisual import CloudAPI, NodeSamba
|
||||
from pyairvisual.errors import (
|
||||
@@ -10,6 +14,7 @@ from pyairvisual.errors import (
|
||||
NodeProError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
CONF_API_KEY,
|
||||
@@ -20,7 +25,7 @@ from homeassistant.const import (
|
||||
CONF_SHOW_ON_MAP,
|
||||
CONF_STATE,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
@@ -57,11 +62,8 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_geography_id(geography_dict):
|
||||
def async_get_geography_id(geography_dict: Mapping[str, Any]) -> str:
|
||||
"""Generate a unique ID from a geography dict."""
|
||||
if not geography_dict:
|
||||
return
|
||||
|
||||
if CONF_CITY in geography_dict:
|
||||
return ", ".join(
|
||||
(
|
||||
@@ -76,7 +78,9 @@ def async_get_geography_id(geography_dict):
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_cloud_api_update_interval(hass, api_key, num_consumers):
|
||||
def async_get_cloud_api_update_interval(
|
||||
hass: HomeAssistant, api_key: str, num_consumers: int
|
||||
) -> timedelta:
|
||||
"""Get a leveled scan interval for a particular cloud API key.
|
||||
|
||||
This will shift based on the number of active consumers, thus keeping the user
|
||||
@@ -97,18 +101,22 @@ def async_get_cloud_api_update_interval(hass, api_key, num_consumers):
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_cloud_coordinators_by_api_key(hass, api_key):
|
||||
def async_get_cloud_coordinators_by_api_key(
|
||||
hass: HomeAssistant, api_key: str
|
||||
) -> list[DataUpdateCoordinator]:
|
||||
"""Get all DataUpdateCoordinator objects related to a particular API key."""
|
||||
coordinators = []
|
||||
for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items():
|
||||
config_entry = hass.config_entries.async_get_entry(entry_id)
|
||||
if config_entry.data.get(CONF_API_KEY) == api_key:
|
||||
if config_entry and config_entry.data.get(CONF_API_KEY) == api_key:
|
||||
coordinators.append(coordinator)
|
||||
return coordinators
|
||||
|
||||
|
||||
@callback
|
||||
def async_sync_geo_coordinator_update_intervals(hass, api_key):
|
||||
def async_sync_geo_coordinator_update_intervals(
|
||||
hass: HomeAssistant, api_key: str
|
||||
) -> None:
|
||||
"""Sync the update interval for geography-based data coordinators (by API key)."""
|
||||
coordinators = async_get_cloud_coordinators_by_api_key(hass, api_key)
|
||||
|
||||
@@ -129,7 +137,9 @@ def async_sync_geo_coordinator_update_intervals(hass, api_key):
|
||||
|
||||
|
||||
@callback
|
||||
def _standardize_geography_config_entry(hass, config_entry):
|
||||
def _standardize_geography_config_entry(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Ensure that geography config entries have appropriate properties."""
|
||||
entry_updates = {}
|
||||
|
||||
@@ -162,9 +172,11 @@ def _standardize_geography_config_entry(hass, config_entry):
|
||||
|
||||
|
||||
@callback
|
||||
def _standardize_node_pro_config_entry(hass, config_entry):
|
||||
def _standardize_node_pro_config_entry(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Ensure that Node/Pro config entries have appropriate properties."""
|
||||
entry_updates = {}
|
||||
entry_updates: dict[str, Any] = {}
|
||||
|
||||
if CONF_INTEGRATION_TYPE not in config_entry.data:
|
||||
# If the config entry data doesn't contain the integration type, add it:
|
||||
@@ -179,7 +191,7 @@ def _standardize_node_pro_config_entry(hass, config_entry):
|
||||
hass.config_entries.async_update_entry(config_entry, **entry_updates)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up AirVisual as config entry."""
|
||||
hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_LISTENER: {}})
|
||||
|
||||
@@ -189,7 +201,7 @@ async def async_setup_entry(hass, config_entry):
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession)
|
||||
|
||||
async def async_update_data():
|
||||
async def async_update_data() -> dict[str, Any]:
|
||||
"""Get new data from the API."""
|
||||
if CONF_CITY in config_entry.data:
|
||||
api_coro = cloud_api.air_quality.city(
|
||||
@@ -204,7 +216,8 @@ async def async_setup_entry(hass, config_entry):
|
||||
)
|
||||
|
||||
try:
|
||||
return await api_coro
|
||||
data = await api_coro
|
||||
return cast(Dict[str, Any], data)
|
||||
except (InvalidKeyError, KeyExpiredError) as ex:
|
||||
raise ConfigEntryAuthFailed from ex
|
||||
except AirVisualError as err:
|
||||
@@ -242,13 +255,14 @@ async def async_setup_entry(hass, config_entry):
|
||||
|
||||
_standardize_node_pro_config_entry(hass, config_entry)
|
||||
|
||||
async def async_update_data():
|
||||
async def async_update_data() -> dict[str, Any]:
|
||||
"""Get new data from the API."""
|
||||
try:
|
||||
async with NodeSamba(
|
||||
config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD]
|
||||
) as node:
|
||||
return await node.async_get_latest_measurements()
|
||||
data = await node.async_get_latest_measurements()
|
||||
return cast(Dict[str, Any], data)
|
||||
except NodeProError as err:
|
||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||
|
||||
@@ -275,7 +289,7 @@ async def async_setup_entry(hass, config_entry):
|
||||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(hass, config_entry):
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate an old config entry."""
|
||||
version = config_entry.version
|
||||
|
||||
@@ -317,7 +331,7 @@ async def async_migrate_entry(hass, config_entry):
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, config_entry):
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload an AirVisual config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
@@ -338,7 +352,7 @@ async def async_unload_entry(hass, config_entry):
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_reload_entry(hass, config_entry):
|
||||
async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Handle an options update."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
@@ -346,16 +360,19 @@ async def async_reload_entry(hass, config_entry):
|
||||
class AirVisualEntity(CoordinatorEntity):
|
||||
"""Define a generic AirVisual entity."""
|
||||
|
||||
def __init__(self, coordinator):
|
||||
def __init__(self, coordinator: DataUpdateCoordinator) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
self._attr_extra_state_attributes: MutableMapping[str, Any] = {
|
||||
ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION
|
||||
}
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
|
||||
@callback
|
||||
def update():
|
||||
def update() -> None:
|
||||
"""Update the state."""
|
||||
self.update_from_latest_data()
|
||||
self.async_write_ha_state()
|
||||
@@ -365,6 +382,6 @@ class AirVisualEntity(CoordinatorEntity):
|
||||
self.update_from_latest_data()
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self):
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the entity from the latest data."""
|
||||
raise NotImplementedError
|
||||
|
@@ -1,4 +1,6 @@
|
||||
"""Define a config flow manager for AirVisual."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from pyairvisual import CloudAPI, NodeSamba
|
||||
@@ -11,6 +13,7 @@ from pyairvisual.errors import (
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigEntry, OptionsFlow
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_IP_ADDRESS,
|
||||
@@ -21,6 +24,7 @@ from homeassistant.const import (
|
||||
CONF_STATE,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
|
||||
from . import async_get_geography_id
|
||||
@@ -64,13 +68,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 2
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._entry_data_for_reauth = None
|
||||
self._geo_id = None
|
||||
self._entry_data_for_reauth: dict[str, str] = {}
|
||||
self._geo_id: str | None = None
|
||||
|
||||
@property
|
||||
def geography_coords_schema(self):
|
||||
def geography_coords_schema(self) -> vol.Schema:
|
||||
"""Return the data schema for the cloud API."""
|
||||
return API_KEY_DATA_SCHEMA.extend(
|
||||
{
|
||||
@@ -83,7 +87,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
)
|
||||
|
||||
async def _async_finish_geography(self, user_input, integration_type):
|
||||
async def _async_finish_geography(
|
||||
self, user_input: dict[str, str], integration_type: str
|
||||
) -> FlowResult:
|
||||
"""Validate a Cloud API key."""
|
||||
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||
cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession)
|
||||
@@ -142,25 +148,29 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
data={**user_input, CONF_INTEGRATION_TYPE: integration_type},
|
||||
)
|
||||
|
||||
async def _async_init_geography(self, user_input, integration_type):
|
||||
async def _async_init_geography(
|
||||
self, user_input: dict[str, str], integration_type: str
|
||||
) -> FlowResult:
|
||||
"""Handle the initialization of the integration via the cloud API."""
|
||||
self._geo_id = async_get_geography_id(user_input)
|
||||
await self._async_set_unique_id(self._geo_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return await self._async_finish_geography(user_input, integration_type)
|
||||
|
||||
async def _async_set_unique_id(self, unique_id):
|
||||
async def _async_set_unique_id(self, unique_id: str) -> None:
|
||||
"""Set the unique ID of the config flow and abort if it already exists."""
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
"""Define the config flow to handle options."""
|
||||
return AirVisualOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_geography_by_coords(self, user_input=None):
|
||||
async def async_step_geography_by_coords(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initialization of the cloud API based on latitude/longitude."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
@@ -171,7 +181,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
user_input, INTEGRATION_TYPE_GEOGRAPHY_COORDS
|
||||
)
|
||||
|
||||
async def async_step_geography_by_name(self, user_input=None):
|
||||
async def async_step_geography_by_name(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initialization of the cloud API based on city/state/country."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
@@ -182,7 +194,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
user_input, INTEGRATION_TYPE_GEOGRAPHY_NAME
|
||||
)
|
||||
|
||||
async def async_step_node_pro(self, user_input=None):
|
||||
async def async_step_node_pro(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initialization of the integration with a Node/Pro."""
|
||||
if not user_input:
|
||||
return self.async_show_form(step_id="node_pro", data_schema=NODE_PRO_SCHEMA)
|
||||
@@ -208,13 +222,15 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO},
|
||||
)
|
||||
|
||||
async def async_step_reauth(self, data):
|
||||
async def async_step_reauth(self, data: dict[str, str]) -> FlowResult:
|
||||
"""Handle configuration by re-auth."""
|
||||
self._entry_data_for_reauth = data
|
||||
self._geo_id = async_get_geography_id(data)
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(self, user_input=None):
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle re-auth completion."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
@@ -227,7 +243,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
conf, self._entry_data_for_reauth[CONF_INTEGRATION_TYPE]
|
||||
)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the start of the config flow."""
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
@@ -244,11 +262,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
class AirVisualOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle an AirVisual options flow."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
@@ -1,5 +1,8 @@
|
||||
"""Support for AirVisual air quality sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
@@ -18,7 +21,10 @@ from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from . import AirVisualEntity
|
||||
from .const import (
|
||||
@@ -141,10 +147,15 @@ POLLUTANT_UNITS = {
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AirVisual sensors based on a config entry."""
|
||||
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
|
||||
|
||||
sensors: list[AirVisualGeographySensor | AirVisualNodeProSensor]
|
||||
if config_entry.data[CONF_INTEGRATION_TYPE] in [
|
||||
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
|
||||
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||
@@ -174,7 +185,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
|
||||
"""Define an AirVisual sensor related to geography data via the Cloud API."""
|
||||
|
||||
def __init__(self, coordinator, config_entry, kind, name, icon, unit, locale):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
config_entry: ConfigEntry,
|
||||
kind: str,
|
||||
name: str,
|
||||
icon: str,
|
||||
unit: str | None,
|
||||
locale: str,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
@@ -203,7 +223,7 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
|
||||
return super().available and self.coordinator.data["current"]["pollution"]
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self):
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the entity from the latest data."""
|
||||
try:
|
||||
data = self.coordinator.data["current"]["pollution"]
|
||||
@@ -260,18 +280,29 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
|
||||
class AirVisualNodeProSensor(AirVisualEntity, SensorEntity):
|
||||
"""Define an AirVisual sensor related to a Node/Pro unit."""
|
||||
|
||||
def __init__(self, coordinator, kind, name, device_class, icon, unit):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
kind: str,
|
||||
name: str,
|
||||
device_class: str | None,
|
||||
icon: str | None,
|
||||
unit: str,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_device_class = device_class
|
||||
self._attr_icon = icon
|
||||
self._attr_name = (
|
||||
f"{coordinator.data['settings']['node_name']} Node/Pro: {name}"
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.data['serial_number']}_{kind}"
|
||||
self._attr_unit_of_measurement = unit
|
||||
self._kind = kind
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device registry information for this entity."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.coordinator.data["serial_number"])},
|
||||
@@ -284,19 +315,8 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity):
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name."""
|
||||
node_name = self.coordinator.data["settings"]["node_name"]
|
||||
return f"{node_name} Node/Pro: {self._name}"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
return f"{self.coordinator.data['serial_number']}_{self._kind}"
|
||||
|
||||
@callback
|
||||
def update_from_latest_data(self):
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the entity from the latest data."""
|
||||
if self._kind == SENSOR_KIND_AQI:
|
||||
if self.coordinator.data["settings"]["is_aqi_usa"]:
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Diese Koordinaten oder Node/Pro ID sind bereits registriert.",
|
||||
"already_configured": "Diese Node/Pro ID oder Standort ist bereits konfiguriert.",
|
||||
"reauth_successful": "Die erneute Authentifizierung war erfolgreich"
|
||||
},
|
||||
"error": {
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API-Key"
|
||||
"api_key": "API-Schl\u00fcssel"
|
||||
},
|
||||
"title": "AirVisual erneut authentifizieren"
|
||||
},
|
||||
|
@@ -17,7 +17,7 @@
|
||||
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
|
||||
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430"
|
||||
},
|
||||
"description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0448\u0438\u0440\u043e\u0442\u044b/\u0434\u043e\u043b\u0433\u043e\u0442\u044b.",
|
||||
"description": "\u0414\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u043f\u043e \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual.",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
|
||||
},
|
||||
"geography_by_name": {
|
||||
@@ -25,9 +25,9 @@
|
||||
"api_key": "\u041a\u043b\u044e\u0447 API",
|
||||
"city": "\u0413\u043e\u0440\u043e\u0434",
|
||||
"country": "\u0421\u0442\u0440\u0430\u043d\u0430",
|
||||
"state": "\u0448\u0442\u0430\u0442"
|
||||
"state": "\u0428\u0442\u0430\u0442"
|
||||
},
|
||||
"description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b.",
|
||||
"description": "\u0414\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual.",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
|
||||
},
|
||||
"node_pro": {
|
||||
|
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"state": {
|
||||
"airvisual__pollutant_label": {
|
||||
"co": "Mon\u00f2xid de carboni",
|
||||
"n2": "Di\u00f2xid de nitrogen",
|
||||
"o3": "Oz\u00f3",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2.5",
|
||||
"s2": "Di\u00f2xid de sofre"
|
||||
},
|
||||
"airvisual__pollutant_level": {
|
||||
"good": "Bo",
|
||||
"hazardous": "Perill\u00f3s",
|
||||
"moderate": "Moderat",
|
||||
"unhealthy": "Poc saludable",
|
||||
"unhealthy_sensitive": "Poc saludable per a grups sensibles",
|
||||
"very_unhealthy": "Molt poc saludable"
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"state": {
|
||||
"airvisual__pollutant_label": {
|
||||
"co": "Kohlenmonoxid",
|
||||
"n2": "Stickstoffdioxid",
|
||||
"o3": "Ozon",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2,5",
|
||||
"s2": "Schwefeldioxid"
|
||||
},
|
||||
"airvisual__pollutant_level": {
|
||||
"good": "Gut",
|
||||
"hazardous": "Gef\u00e4hrlich",
|
||||
"moderate": "M\u00e4\u00dfig",
|
||||
"unhealthy": "Ungesund",
|
||||
"unhealthy_sensitive": "Ungesund f\u00fcr sensible Gruppen",
|
||||
"very_unhealthy": "Sehr ungesund"
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"state": {
|
||||
"airvisual__pollutant_label": {
|
||||
"co": "Vingugaas",
|
||||
"n2": "L\u00e4mmastikdioksiid",
|
||||
"o3": "Osoon",
|
||||
"p1": "PM10 osakesed",
|
||||
"p2": "PM2.5 osakesed",
|
||||
"s2": "V\u00e4\u00e4veldioksiid"
|
||||
},
|
||||
"airvisual__pollutant_level": {
|
||||
"good": "Hea",
|
||||
"hazardous": "Ohtlik",
|
||||
"moderate": "M\u00f5\u00f5dukas",
|
||||
"unhealthy": "Ebatervislik",
|
||||
"unhealthy_sensitive": "Ebatervislik riskir\u00fchmale",
|
||||
"very_unhealthy": "V\u00e4ga ebatervislik"
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"state": {
|
||||
"airvisual__pollutant_label": {
|
||||
"co": "Koolmonoxide",
|
||||
"n2": "Stikstofdioxide",
|
||||
"o3": "Ozon",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2.5",
|
||||
"s2": "Zwaveldioxide"
|
||||
},
|
||||
"airvisual__pollutant_level": {
|
||||
"good": "Goed",
|
||||
"hazardous": "Gevaarlijk",
|
||||
"moderate": "Matig",
|
||||
"unhealthy": "Ongezond",
|
||||
"unhealthy_sensitive": "Ongezond voor gevoelige groepen",
|
||||
"very_unhealthy": "Heel ongezond"
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"state": {
|
||||
"airvisual__pollutant_label": {
|
||||
"co": "\u0423\u0433\u0430\u0440\u043d\u044b\u0439 \u0433\u0430\u0437",
|
||||
"n2": "\u0414\u0438\u043e\u043a\u0441\u0438\u0434 \u0430\u0437\u043e\u0442\u0430",
|
||||
"o3": "\u041e\u0437\u043e\u043d",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2.5",
|
||||
"s2": "\u0414\u0438\u043e\u043a\u0441\u0438\u0434 \u0441\u0435\u0440\u044b"
|
||||
},
|
||||
"airvisual__pollutant_level": {
|
||||
"good": "\u0425\u043e\u0440\u043e\u0448\u043e",
|
||||
"hazardous": "\u041e\u043f\u0430\u0441\u043d\u043e",
|
||||
"moderate": "\u0421\u0440\u0435\u0434\u043d\u0435",
|
||||
"unhealthy": "\u0412\u0440\u0435\u0434\u043d\u043e",
|
||||
"unhealthy_sensitive": "\u0412\u0440\u0435\u0434\u043d\u043e \u0434\u043b\u044f \u0443\u044f\u0437\u0432\u0438\u043c\u044b\u0445 \u0433\u0440\u0443\u043f\u043f",
|
||||
"very_unhealthy": "\u041e\u0447\u0435\u043d\u044c \u0432\u0440\u0435\u0434\u043d\u043e"
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"state": {
|
||||
"airvisual__pollutant_label": {
|
||||
"co": "\u4e00\u6c27\u5316\u78b3",
|
||||
"n2": "\u4e8c\u6c27\u5316\u6c2e",
|
||||
"o3": "\u81ed\u6c27",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2.5",
|
||||
"s2": "\u4e8c\u6c27\u5316\u786b"
|
||||
},
|
||||
"airvisual__pollutant_level": {
|
||||
"good": "\u826f\u597d",
|
||||
"hazardous": "\u5371\u96aa",
|
||||
"moderate": "\u4e2d\u7b49",
|
||||
"unhealthy": "\u4e0d\u5065\u5eb7",
|
||||
"unhealthy_sensitive": "\u5c0d\u654f\u611f\u65cf\u7fa4\u4e0d\u5065\u5eb7",
|
||||
"very_unhealthy": "\u975e\u5e38\u4e0d\u5065\u5eb7"
|
||||
}
|
||||
}
|
||||
}
|
@@ -48,7 +48,12 @@ ALERT_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
|
||||
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
|
||||
vol.Required(CONF_REPEAT): vol.All(
|
||||
cv.ensure_list,
|
||||
[vol.Coerce(float)],
|
||||
# Minimum delay is 1 second = 0.016 minutes
|
||||
[vol.Range(min=0.016)],
|
||||
),
|
||||
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
|
||||
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
|
||||
vol.Optional(CONF_ALERT_MESSAGE): cv.template,
|
||||
|
@@ -19,6 +19,7 @@ from homeassistant.components.alarm_control_panel.const import (
|
||||
SUPPORT_ALARM_ARM_NIGHT,
|
||||
)
|
||||
import homeassistant.components.climate.const as climate
|
||||
from homeassistant.components.lock import STATE_LOCKING, STATE_UNLOCKING
|
||||
import homeassistant.components.media_player.const as media_player
|
||||
from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
@@ -446,9 +447,11 @@ class AlexaLockController(AlexaCapability):
|
||||
if name != "lockState":
|
||||
raise UnsupportedProperty(name)
|
||||
|
||||
if self.entity.state == STATE_LOCKED:
|
||||
# If its unlocking its still locked and not unlocked yet
|
||||
if self.entity.state in (STATE_UNLOCKING, STATE_LOCKED):
|
||||
return "LOCKED"
|
||||
if self.entity.state == STATE_UNLOCKED:
|
||||
# If its locking its still unlocked and not locked yet
|
||||
if self.entity.state in (STATE_LOCKING, STATE_UNLOCKED):
|
||||
return "UNLOCKED"
|
||||
return "JAMMED"
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
"""Support for Ambiclimate ac."""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import ambiclimate
|
||||
import voluptuous as vol
|
||||
@@ -146,24 +147,24 @@ class AmbiclimateEntity(ClimateEntity):
|
||||
"""Initialize the thermostat."""
|
||||
self._heater = heater
|
||||
self._store = store
|
||||
self._attr_unique_id = self._heater.device_id
|
||||
self._attr_name = self._heater.name
|
||||
self._attr_unique_id = heater.device_id
|
||||
self._attr_name = heater.name
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, self.unique_id)},
|
||||
"name": self.name,
|
||||
"manufacturer": "Ambiclimate",
|
||||
}
|
||||
self._attr_min_temp = self._heater.get_min_temp()
|
||||
self._attr_max_temp = self._heater.get_max_temp()
|
||||
self._attr_min_temp = heater.get_min_temp()
|
||||
self._attr_max_temp = heater.get_max_temp()
|
||||
|
||||
async def async_set_temperature(self, **kwargs) -> None:
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
await self._heater.set_target_temperature(temperature)
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode) -> None:
|
||||
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
if hvac_mode == HVAC_MODE_HEAT:
|
||||
await self._heater.turn_on()
|
||||
|
@@ -1,6 +1,7 @@
|
||||
"""Config flow for Ambiclimate."""
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
import ambiclimate
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -139,7 +140,7 @@ class AmbiclimateAuthCallbackView(HomeAssistantView):
|
||||
url = AUTH_CALLBACK_PATH
|
||||
name = AUTH_CALLBACK_NAME
|
||||
|
||||
async def get(self, request) -> str:
|
||||
async def get(self, request: web.Request) -> str:
|
||||
"""Receive authorization token."""
|
||||
code = request.query.get("code")
|
||||
if code is None:
|
||||
|
@@ -6,7 +6,7 @@
|
||||
"missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Erfolgreiche Authentifizierung mit Ambiclimate"
|
||||
"default": "Erfolgreich authentifiziert"
|
||||
},
|
||||
"error": {
|
||||
"follow_link": "Bitte folge dem Link und authentifizieren dich, bevor du auf Senden klickst",
|
||||
|
@@ -28,11 +28,13 @@ from homeassistant.const import (
|
||||
IRRADIATION_WATTS_PER_SQUARE_METER,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
PRECIPITATION_INCHES,
|
||||
PRECIPITATION_INCHES_PER_HOUR,
|
||||
PRESSURE_INHG,
|
||||
SPEED_MILES_PER_HOUR,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
@@ -156,7 +158,7 @@ TYPE_WINDSPDMPH_AVG2M = "windspdmph_avg2m"
|
||||
TYPE_WINDSPEEDMPH = "windspeedmph"
|
||||
TYPE_YEARLYRAININ = "yearlyrainin"
|
||||
SENSOR_TYPES = {
|
||||
TYPE_24HOURRAININ: ("24 Hr Rain", "in", SENSOR, None),
|
||||
TYPE_24HOURRAININ: ("24 Hr Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
TYPE_BAROMABSIN: ("Abs Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE),
|
||||
TYPE_BAROMRELIN: ("Rel Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE),
|
||||
TYPE_BATT10: ("Battery 10", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
@@ -172,11 +174,16 @@ SENSOR_TYPES = {
|
||||
TYPE_BATTOUT: ("Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_BATT_CO2: ("CO2 Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
|
||||
TYPE_CO2: ("co2", CONCENTRATION_PARTS_PER_MILLION, SENSOR, DEVICE_CLASS_CO2),
|
||||
TYPE_DAILYRAININ: ("Daily Rain", "in", SENSOR, None),
|
||||
TYPE_DAILYRAININ: ("Daily Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
TYPE_DEWPOINT: ("Dew Point", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_EVENTRAININ: ("Event Rain", "in", SENSOR, None),
|
||||
TYPE_EVENTRAININ: ("Event Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
TYPE_FEELSLIKE: ("Feels Like", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_HOURLYRAININ: ("Hourly Rain Rate", "in/hr", SENSOR, None),
|
||||
TYPE_HOURLYRAININ: (
|
||||
"Hourly Rain Rate",
|
||||
PRECIPITATION_INCHES_PER_HOUR,
|
||||
SENSOR,
|
||||
None,
|
||||
),
|
||||
TYPE_HUMIDITY10: ("Humidity 10", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_HUMIDITY2: ("Humidity 2", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
@@ -191,7 +198,7 @@ SENSOR_TYPES = {
|
||||
TYPE_HUMIDITYIN: ("Humidity In", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
|
||||
TYPE_LASTRAIN: ("Last Rain", None, SENSOR, DEVICE_CLASS_TIMESTAMP),
|
||||
TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, SENSOR, None),
|
||||
TYPE_MONTHLYRAININ: ("Monthly Rain", "in", SENSOR, None),
|
||||
TYPE_MONTHLYRAININ: ("Monthly Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
TYPE_PM25_24H: (
|
||||
"PM25 24h Avg",
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
@@ -277,9 +284,9 @@ SENSOR_TYPES = {
|
||||
TYPE_TEMP9F: ("Temp 9", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TEMPF: ("Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TEMPINF: ("Inside Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
|
||||
TYPE_TOTALRAININ: ("Lifetime Rain", "in", SENSOR, None),
|
||||
TYPE_TOTALRAININ: ("Lifetime Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
TYPE_UV: ("uv", "Index", SENSOR, None),
|
||||
TYPE_WEEKLYRAININ: ("Weekly Rain", "in", SENSOR, None),
|
||||
TYPE_WEEKLYRAININ: ("Weekly Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
TYPE_WINDDIR: ("Wind Dir", DEGREE, SENSOR, None),
|
||||
TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", DEGREE, SENSOR, None),
|
||||
TYPE_WINDDIR_AVG2M: ("Wind Dir Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None),
|
||||
@@ -288,7 +295,7 @@ SENSOR_TYPES = {
|
||||
TYPE_WINDSPDMPH_AVG10M: ("Wind Avg 10m", SPEED_MILES_PER_HOUR, SENSOR, None),
|
||||
TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None),
|
||||
TYPE_WINDSPEEDMPH: ("Wind Speed", SPEED_MILES_PER_HOUR, SENSOR, None),
|
||||
TYPE_YEARLYRAININ: ("Yearly Rain", "in", SENSOR, None),
|
||||
TYPE_YEARLYRAININ: ("Yearly Rain", PRECIPITATION_INCHES, SENSOR, None),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
|
||||
@@ -320,7 +327,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
LOGGER.error("Config entry failed: %s", err)
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
async def _async_disconnect_websocket(*_):
|
||||
async def _async_disconnect_websocket(_: Event) -> None:
|
||||
await ambient.client.websocket.disconnect()
|
||||
|
||||
config_entry.async_on_unload(
|
||||
@@ -378,7 +385,7 @@ class AmbientStation:
|
||||
async def _attempt_connect(self) -> None:
|
||||
"""Attempt to connect to the socket (retrying later on fail)."""
|
||||
|
||||
async def connect(timestamp: int | None = None):
|
||||
async def connect(timestamp: int | None = None) -> None:
|
||||
"""Connect."""
|
||||
await self.client.websocket.connect()
|
||||
|
||||
|
@@ -10,7 +10,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API Schl\u00fcssel",
|
||||
"api_key": "API-Schl\u00fcssel",
|
||||
"app_key": "Anwendungsschl\u00fcssel"
|
||||
},
|
||||
"title": "Gib deine Informationen ein"
|
||||
|
@@ -8,15 +8,15 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
from homeassistant.const import (
|
||||
CONF_RESOURCES,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ELECTRICAL_CURRENT_AMPERE,
|
||||
ELECTRICAL_VOLT_AMPERE,
|
||||
ELECTRIC_CURRENT_AMPERE,
|
||||
ELECTRIC_POTENTIAL_VOLT,
|
||||
FREQUENCY_HERTZ,
|
||||
PERCENTAGE,
|
||||
POWER_VOLT_AMPERE,
|
||||
POWER_WATT,
|
||||
TEMP_CELSIUS,
|
||||
TIME_MINUTES,
|
||||
TIME_SECONDS,
|
||||
VOLT,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@@ -33,7 +33,7 @@ SENSOR_TYPES = {
|
||||
"badbatts": ["Bad Batteries", "", "mdi:information-outline", None],
|
||||
"battdate": ["Battery Replaced", "", "mdi:calendar-clock", None],
|
||||
"battstat": ["Battery Status", "", "mdi:information-outline", None],
|
||||
"battv": ["Battery Voltage", VOLT, "mdi:flash", None],
|
||||
"battv": ["Battery Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"bcharge": ["Battery", PERCENTAGE, "mdi:battery", None],
|
||||
"cable": ["Cable Type", "", "mdi:ethernet-cable", None],
|
||||
"cumonbatt": ["Total Time on Battery", "", "mdi:timer-outline", None],
|
||||
@@ -46,33 +46,33 @@ SENSOR_TYPES = {
|
||||
"endapc": ["Date and Time", "", "mdi:calendar-clock", None],
|
||||
"extbatts": ["External Batteries", "", "mdi:information-outline", None],
|
||||
"firmware": ["Firmware Version", "", "mdi:information-outline", None],
|
||||
"hitrans": ["Transfer High", VOLT, "mdi:flash", None],
|
||||
"hitrans": ["Transfer High", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"hostname": ["Hostname", "", "mdi:information-outline", None],
|
||||
"humidity": ["Ambient Humidity", PERCENTAGE, "mdi:water-percent", None],
|
||||
"itemp": ["Internal Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE],
|
||||
"lastxfer": ["Last Transfer", "", "mdi:transfer", None],
|
||||
"linefail": ["Input Voltage Status", "", "mdi:information-outline", None],
|
||||
"linefreq": ["Line Frequency", FREQUENCY_HERTZ, "mdi:information-outline", None],
|
||||
"linev": ["Input Voltage", VOLT, "mdi:flash", None],
|
||||
"linev": ["Input Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"loadpct": ["Load", PERCENTAGE, "mdi:gauge", None],
|
||||
"loadapnt": ["Load Apparent Power", PERCENTAGE, "mdi:gauge", None],
|
||||
"lotrans": ["Transfer Low", VOLT, "mdi:flash", None],
|
||||
"lotrans": ["Transfer Low", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"mandate": ["Manufacture Date", "", "mdi:calendar", None],
|
||||
"masterupd": ["Master Update", "", "mdi:information-outline", None],
|
||||
"maxlinev": ["Input Voltage High", VOLT, "mdi:flash", None],
|
||||
"maxlinev": ["Input Voltage High", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"maxtime": ["Battery Timeout", "", "mdi:timer-off-outline", None],
|
||||
"mbattchg": ["Battery Shutdown", PERCENTAGE, "mdi:battery-alert", None],
|
||||
"minlinev": ["Input Voltage Low", VOLT, "mdi:flash", None],
|
||||
"minlinev": ["Input Voltage Low", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"mintimel": ["Shutdown Time", "", "mdi:timer-outline", None],
|
||||
"model": ["Model", "", "mdi:information-outline", None],
|
||||
"nombattv": ["Battery Nominal Voltage", VOLT, "mdi:flash", None],
|
||||
"nominv": ["Nominal Input Voltage", VOLT, "mdi:flash", None],
|
||||
"nomoutv": ["Nominal Output Voltage", VOLT, "mdi:flash", None],
|
||||
"nombattv": ["Battery Nominal Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"nominv": ["Nominal Input Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"nomoutv": ["Nominal Output Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"nompower": ["Nominal Output Power", POWER_WATT, "mdi:flash", None],
|
||||
"nomapnt": ["Nominal Apparent Power", ELECTRICAL_VOLT_AMPERE, "mdi:flash", None],
|
||||
"nomapnt": ["Nominal Apparent Power", POWER_VOLT_AMPERE, "mdi:flash", None],
|
||||
"numxfers": ["Transfer Count", "", "mdi:counter", None],
|
||||
"outcurnt": ["Output Current", ELECTRICAL_CURRENT_AMPERE, "mdi:flash", None],
|
||||
"outputv": ["Output Voltage", VOLT, "mdi:flash", None],
|
||||
"outcurnt": ["Output Current", ELECTRIC_CURRENT_AMPERE, "mdi:flash", None],
|
||||
"outputv": ["Output Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
|
||||
"reg1": ["Register 1 Fault", "", "mdi:information-outline", None],
|
||||
"reg2": ["Register 2 Fault", "", "mdi:information-outline", None],
|
||||
"reg3": ["Register 3 Fault", "", "mdi:information-outline", None],
|
||||
@@ -99,9 +99,9 @@ INFERRED_UNITS = {
|
||||
" Minutes": TIME_MINUTES,
|
||||
" Seconds": TIME_SECONDS,
|
||||
" Percent": PERCENTAGE,
|
||||
" Volts": VOLT,
|
||||
" Ampere": ELECTRICAL_CURRENT_AMPERE,
|
||||
" Volt-Ampere": ELECTRICAL_VOLT_AMPERE,
|
||||
" Volts": ELECTRIC_POTENTIAL_VOLT,
|
||||
" Ampere": ELECTRIC_CURRENT_AMPERE,
|
||||
" Volt-Ampere": POWER_VOLT_AMPERE,
|
||||
" Watts": POWER_WATT,
|
||||
" Hz": FREQUENCY_HERTZ,
|
||||
" C": TEMP_CELSIUS,
|
||||
|
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"title": "Apple TV",
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
|
@@ -35,24 +35,11 @@ class ArduinoSensor(SensorEntity):
|
||||
def __init__(self, name, pin, pin_type, board):
|
||||
"""Initialize the sensor."""
|
||||
self._pin = pin
|
||||
self._name = name
|
||||
self.pin_type = pin_type
|
||||
self.direction = "in"
|
||||
self._value = None
|
||||
self._attr_name = name
|
||||
|
||||
board.set_mode(self._pin, self.direction, self.pin_type)
|
||||
board.set_mode(self._pin, "in", pin_type)
|
||||
self._board = board
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._value
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Get the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
def update(self):
|
||||
"""Get the latest value from the pin."""
|
||||
self._value = self._board.get_analog_inputs()[self._pin][1]
|
||||
self._attr_state = self._board.get_analog_inputs()[self._pin][1]
|
||||
|
@@ -43,11 +43,9 @@ class ArduinoSwitch(SwitchEntity):
|
||||
def __init__(self, pin, options, board):
|
||||
"""Initialize the Pin."""
|
||||
self._pin = pin
|
||||
self._name = options[CONF_NAME]
|
||||
self.pin_type = CONF_TYPE
|
||||
self.direction = "out"
|
||||
self._attr_name = options[CONF_NAME]
|
||||
|
||||
self._state = options[CONF_INITIAL]
|
||||
self._attr_is_on = options[CONF_INITIAL]
|
||||
|
||||
if options[CONF_NEGATE]:
|
||||
self.turn_on_handler = board.set_digital_out_low
|
||||
@@ -56,25 +54,15 @@ class ArduinoSwitch(SwitchEntity):
|
||||
self.turn_on_handler = board.set_digital_out_high
|
||||
self.turn_off_handler = board.set_digital_out_low
|
||||
|
||||
board.set_mode(self._pin, self.direction, self.pin_type)
|
||||
(self.turn_on_handler if self._state else self.turn_off_handler)(pin)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Get the name of the pin."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if pin is high/on."""
|
||||
return self._state
|
||||
board.set_mode(pin, "out", CONF_TYPE)
|
||||
(self.turn_on_handler if self.is_on else self.turn_off_handler)(pin)
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the pin to high/on."""
|
||||
self._state = True
|
||||
self._attr_is_on = True
|
||||
self.turn_on_handler(self._pin)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the pin to low/off."""
|
||||
self._state = False
|
||||
self._attr_is_on = False
|
||||
self.turn_off_handler(self._pin)
|
||||
|
@@ -73,34 +73,18 @@ class ArestBinarySensor(BinarySensorEntity):
|
||||
def __init__(self, arest, resource, name, device_class, pin):
|
||||
"""Initialize the aREST device."""
|
||||
self.arest = arest
|
||||
self._resource = resource
|
||||
self._name = name
|
||||
self._device_class = device_class
|
||||
self._pin = pin
|
||||
self._attr_name = name
|
||||
self._attr_device_class = device_class
|
||||
|
||||
if self._pin is not None:
|
||||
request = requests.get(f"{self._resource}/mode/{self._pin}/i", timeout=10)
|
||||
if pin is not None:
|
||||
request = requests.get(f"{resource}/mode/{pin}/i", timeout=10)
|
||||
if request.status_code != HTTP_OK:
|
||||
_LOGGER.error("Can't set mode of %s", self._resource)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return bool(self.arest.data.get("state"))
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._device_class
|
||||
_LOGGER.error("Can't set mode of %s", resource)
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from aREST API."""
|
||||
self.arest.update()
|
||||
self._attr_is_on = bool(self.arest.data.get("state"))
|
||||
|
||||
|
||||
class ArestData:
|
||||
|
@@ -139,48 +139,27 @@ class ArestSensor(SensorEntity):
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
self.arest = arest
|
||||
self._resource = resource
|
||||
self._name = f"{location.title()} {name.title()}"
|
||||
self._attr_name = f"{location.title()} {name.title()}"
|
||||
self._variable = variable
|
||||
self._pin = pin
|
||||
self._state = None
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._attr_unit_of_measurement = unit_of_measurement
|
||||
self._renderer = renderer
|
||||
|
||||
if self._pin is not None:
|
||||
request = requests.get(f"{self._resource}/mode/{self._pin}/i", timeout=10)
|
||||
if pin is not None:
|
||||
request = requests.get(f"{resource}/mode/{pin}/i", timeout=10)
|
||||
if request.status_code != HTTP_OK:
|
||||
_LOGGER.error("Can't set mode of %s", self._resource)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
values = self.arest.data
|
||||
|
||||
if "error" in values:
|
||||
return values["error"]
|
||||
|
||||
value = self._renderer(values.get("value", values.get(self._variable, None)))
|
||||
return value
|
||||
_LOGGER.error("Can't set mode of %s", resource)
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from aREST API."""
|
||||
self.arest.update()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Could the device be accessed during the last update call."""
|
||||
return self.arest.available
|
||||
self._attr_available = self.arest.available
|
||||
values = self.arest.data
|
||||
if "error" in values:
|
||||
self._attr_state = values["error"]
|
||||
else:
|
||||
self._attr_state = self._renderer(
|
||||
values.get("value", values.get(self._variable, None))
|
||||
)
|
||||
|
||||
|
||||
class ArestData:
|
||||
@@ -191,7 +170,7 @@ class ArestData:
|
||||
self._resource = resource
|
||||
self._pin = pin
|
||||
self.data = {}
|
||||
self.available = True
|
||||
self._attr_available = True
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
@@ -212,7 +191,7 @@ class ArestData:
|
||||
f"{self._resource}/digital/{self._pin}", timeout=10
|
||||
)
|
||||
self.data = {"value": response.json()["return_value"]}
|
||||
self.available = True
|
||||
self._attr_available = True
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to device %s", self._resource)
|
||||
self.available = False
|
||||
self._attr_available = False
|
||||
|
@@ -86,24 +86,8 @@ class ArestSwitchBase(SwitchEntity):
|
||||
def __init__(self, resource, location, name):
|
||||
"""Initialize the switch."""
|
||||
self._resource = resource
|
||||
self._name = f"{location.title()} {name.title()}"
|
||||
self._state = None
|
||||
self._available = True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the switch."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Could the device be accessed during the last update call."""
|
||||
return self._available
|
||||
self._attr_name = f"{location.title()} {name.title()}"
|
||||
self._attr_available = True
|
||||
|
||||
|
||||
class ArestSwitchFunction(ArestSwitchBase):
|
||||
@@ -134,7 +118,7 @@ class ArestSwitchFunction(ArestSwitchBase):
|
||||
)
|
||||
|
||||
if request.status_code == HTTP_OK:
|
||||
self._state = True
|
||||
self._attr_is_on = True
|
||||
else:
|
||||
_LOGGER.error("Can't turn on function %s at %s", self._func, self._resource)
|
||||
|
||||
@@ -145,7 +129,7 @@ class ArestSwitchFunction(ArestSwitchBase):
|
||||
)
|
||||
|
||||
if request.status_code == HTTP_OK:
|
||||
self._state = False
|
||||
self._attr_is_on = False
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Can't turn off function %s at %s", self._func, self._resource
|
||||
@@ -155,11 +139,11 @@ class ArestSwitchFunction(ArestSwitchBase):
|
||||
"""Get the latest data from aREST API and update the state."""
|
||||
try:
|
||||
request = requests.get(f"{self._resource}/{self._func}", timeout=10)
|
||||
self._state = request.json()["return_value"] != 0
|
||||
self._available = True
|
||||
self._attr_is_on = request.json()["return_value"] != 0
|
||||
self._attr_available = True
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.warning("No route to device %s", self._resource)
|
||||
self._available = False
|
||||
self._attr_available = False
|
||||
|
||||
|
||||
class ArestSwitchPin(ArestSwitchBase):
|
||||
@@ -171,10 +155,10 @@ class ArestSwitchPin(ArestSwitchBase):
|
||||
self._pin = pin
|
||||
self.invert = invert
|
||||
|
||||
request = requests.get(f"{self._resource}/mode/{self._pin}/o", timeout=10)
|
||||
request = requests.get(f"{resource}/mode/{pin}/o", timeout=10)
|
||||
if request.status_code != HTTP_OK:
|
||||
_LOGGER.error("Can't set mode")
|
||||
self._available = False
|
||||
self._attr_available = False
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
@@ -183,7 +167,7 @@ class ArestSwitchPin(ArestSwitchBase):
|
||||
f"{self._resource}/digital/{self._pin}/{turn_on_payload}", timeout=10
|
||||
)
|
||||
if request.status_code == HTTP_OK:
|
||||
self._state = True
|
||||
self._attr_is_on = True
|
||||
else:
|
||||
_LOGGER.error("Can't turn on pin %s at %s", self._pin, self._resource)
|
||||
|
||||
@@ -194,7 +178,7 @@ class ArestSwitchPin(ArestSwitchBase):
|
||||
f"{self._resource}/digital/{self._pin}/{turn_off_payload}", timeout=10
|
||||
)
|
||||
if request.status_code == HTTP_OK:
|
||||
self._state = False
|
||||
self._attr_is_on = False
|
||||
else:
|
||||
_LOGGER.error("Can't turn off pin %s at %s", self._pin, self._resource)
|
||||
|
||||
@@ -203,8 +187,8 @@ class ArestSwitchPin(ArestSwitchBase):
|
||||
try:
|
||||
request = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10)
|
||||
status_value = int(self.invert)
|
||||
self._state = request.json()["return_value"] != status_value
|
||||
self._available = True
|
||||
self._attr_is_on = request.json()["return_value"] != status_value
|
||||
self._attr_available = True
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.warning("No route to device %s", self._resource)
|
||||
self._available = False
|
||||
self._attr_available = False
|
||||
|
@@ -7,6 +7,7 @@ from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.const import (
|
||||
DEGREE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
PRECIPITATION_INCHES,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
@@ -44,7 +45,11 @@ def discover_sensors(topic, payload):
|
||||
if domain == "rain":
|
||||
if len(parts) >= 3 and parts[2] == "today":
|
||||
return ArwnSensor(
|
||||
topic, "Rain Since Midnight", "since_midnight", "in", "mdi:water"
|
||||
topic,
|
||||
"Rain Since Midnight",
|
||||
"since_midnight",
|
||||
PRECIPITATION_INCHES,
|
||||
"mdi:water",
|
||||
)
|
||||
return (
|
||||
ArwnSensor(topic + "/total", "Total Rainfall", "total", unit, "mdi:water"),
|
||||
|
@@ -75,27 +75,16 @@ class AtagEntity(CoordinatorEntity):
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._id = atag_id
|
||||
self._name = DOMAIN.title()
|
||||
self._attr_name = DOMAIN.title()
|
||||
self._attr_unique_id = f"{coordinator.data.id}-{atag_id}"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return info for device registry."""
|
||||
device = self.coordinator.data.id
|
||||
version = self.coordinator.data.apiversion
|
||||
return {
|
||||
"identifiers": {(DOMAIN, device)},
|
||||
"identifiers": {(DOMAIN, self.coordinator.data.id)},
|
||||
"name": "Atag Thermostat",
|
||||
"model": "Atag One",
|
||||
"sw_version": version,
|
||||
"sw_version": self.coordinator.data.apiversion,
|
||||
"manufacturer": "Atag",
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID to use for this entity."""
|
||||
return f"{self.coordinator.data.id}-{self._id}"
|
||||
|
@@ -37,10 +37,14 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
||||
class AtagThermostat(AtagEntity, ClimateEntity):
|
||||
"""Atag climate device."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
_attr_hvac_modes = HVAC_MODES
|
||||
_attr_preset_modes = list(PRESET_MAP.keys())
|
||||
_attr_supported_features = SUPPORT_FLAGS
|
||||
|
||||
def __init__(self, coordinator, atag_id):
|
||||
"""Initialize an Atag climate device."""
|
||||
super().__init__(coordinator, atag_id)
|
||||
self._attr_temperature_unit = coordinator.data.climate.temp_unit
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> str | None:
|
||||
@@ -49,22 +53,12 @@ class AtagThermostat(AtagEntity, ClimateEntity):
|
||||
return self.coordinator.data.climate.hvac_mode
|
||||
return None
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> list[str]:
|
||||
"""Return the list of available hvac operation modes."""
|
||||
return HVAC_MODES
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> str | None:
|
||||
"""Return the current running hvac operation."""
|
||||
is_active = self.coordinator.data.climate.status
|
||||
return CURRENT_HVAC_HEAT if is_active else CURRENT_HVAC_IDLE
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
return self.coordinator.data.climate.temp_unit
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
@@ -81,11 +75,6 @@ class AtagThermostat(AtagEntity, ClimateEntity):
|
||||
preset = self.coordinator.data.climate.preset_mode
|
||||
return PRESET_INVERTED.get(preset)
|
||||
|
||||
@property
|
||||
def preset_modes(self) -> list[str] | None:
|
||||
"""Return a list of available preset modes."""
|
||||
return list(PRESET_MAP.keys())
|
||||
|
||||
async def async_set_temperature(self, **kwargs) -> None:
|
||||
"""Set new target temperature."""
|
||||
await self.coordinator.data.climate.set_temp(kwargs.get(ATTR_TEMPERATURE))
|
||||
|
@@ -36,7 +36,20 @@ class AtagSensor(AtagEntity, SensorEntity):
|
||||
def __init__(self, coordinator, sensor):
|
||||
"""Initialize Atag sensor."""
|
||||
super().__init__(coordinator, SENSORS[sensor])
|
||||
self._name = sensor
|
||||
self._attr_name = sensor
|
||||
if coordinator.data.report[self._id].sensorclass in [
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
]:
|
||||
self._attr_device_class = coordinator.data.report[self._id].sensorclass
|
||||
if coordinator.data.report[self._id].measure in [
|
||||
PRESSURE_BAR,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
PERCENTAGE,
|
||||
TIME_HOURS,
|
||||
]:
|
||||
self._attr_unit_of_measurement = coordinator.data.report[self._id].measure
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -47,26 +60,3 @@ class AtagSensor(AtagEntity, SensorEntity):
|
||||
def icon(self):
|
||||
"""Return icon."""
|
||||
return self.coordinator.data.report[self._id].icon
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return deviceclass."""
|
||||
if self.coordinator.data.report[self._id].sensorclass in [
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
]:
|
||||
return self.coordinator.data.report[self._id].sensorclass
|
||||
return None
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return measure."""
|
||||
if self.coordinator.data.report[self._id].measure in [
|
||||
PRESSURE_BAR,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
PERCENTAGE,
|
||||
TIME_HOURS,
|
||||
]:
|
||||
return self.coordinator.data.report[self._id].measure
|
||||
return None
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert"
|
||||
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen",
|
||||
|
@@ -22,15 +22,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
|
||||
"""Representation of an ATAG water heater."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_FLAGS_HEATER
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
_attr_operation_list = OPERATION_LIST
|
||||
_attr_supported_features = SUPPORT_FLAGS_HEATER
|
||||
_attr_temperature_unit = TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
@@ -43,11 +37,6 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
|
||||
operation = self.coordinator.data.dhw.current_operation
|
||||
return operation if operation in self.operation_list else STATE_OFF
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return OPERATION_LIST
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
if await self.coordinator.data.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)):
|
||||
|
@@ -1,6 +1,7 @@
|
||||
"""Support for August lock."""
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from yalexs.activity import SOURCE_PUBNUB, ActivityType
|
||||
from yalexs.lock import LockStatus
|
||||
from yalexs.util import update_lock_detail_from_activity
|
||||
@@ -9,12 +10,15 @@ from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import DATA_AUGUST, DOMAIN
|
||||
from .entity import AugustEntityMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LOCK_JAMMED_ERR = 531
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up August locks."""
|
||||
@@ -44,9 +48,17 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
|
||||
await self._call_lock_operation(self._data.async_unlock)
|
||||
|
||||
async def _call_lock_operation(self, lock_operation):
|
||||
activities = await lock_operation(self._device_id)
|
||||
for lock_activity in activities:
|
||||
update_lock_detail_from_activity(self._detail, lock_activity)
|
||||
try:
|
||||
activities = await lock_operation(self._device_id)
|
||||
except ClientResponseError as err:
|
||||
if err.status == LOCK_JAMMED_ERR:
|
||||
self._detail.lock_status = LockStatus.JAMMED
|
||||
self._detail.lock_status_datetime = dt_util.utcnow()
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
for lock_activity in activities:
|
||||
update_lock_detail_from_activity(self._detail, lock_activity)
|
||||
|
||||
if self._update_lock_status_from_detail():
|
||||
_LOGGER.debug(
|
||||
@@ -91,6 +103,10 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
|
||||
else:
|
||||
self._attr_is_locked = self._lock_status is LockStatus.LOCKED
|
||||
|
||||
self._attr_is_jammed = self._lock_status is LockStatus.JAMMED
|
||||
self._attr_is_locking = self._lock_status is LockStatus.LOCKING
|
||||
self._attr_is_unlocking = self._lock_status is LockStatus.UNLOCKING
|
||||
|
||||
self._attr_extra_state_attributes = {
|
||||
ATTR_BATTERY_LEVEL: self._detail.battery_level
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"domain": "august",
|
||||
"name": "August",
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"requirements": ["yalexs==1.1.11"],
|
||||
"requirements": ["yalexs==1.1.12"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"dhcp": [
|
||||
{
|
||||
|
36
homeassistant/components/automate/__init__.py
Normal file
36
homeassistant/components/automate/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""The Automate Pulse Hub v2 integration."""
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .hub import PulseHub
|
||||
|
||||
PLATFORMS = ["cover"]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Automate Pulse Hub v2 from a config entry."""
|
||||
hub = PulseHub(hass, entry)
|
||||
|
||||
if not await hub.async_setup():
|
||||
return False
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = hub
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
hub = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
if not await hub.async_reset():
|
||||
return False
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
93
homeassistant/components/automate/base.py
Normal file
93
homeassistant/components/automate/base.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Base class for Automate Roller Blinds."""
|
||||
import logging
|
||||
|
||||
import aiopulse2
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import entity
|
||||
from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg
|
||||
|
||||
from .const import AUTOMATE_ENTITY_REMOVE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AutomateBase(entity.Entity):
|
||||
"""Base representation of an Automate roller."""
|
||||
|
||||
def __init__(self, roller: aiopulse2.Roller) -> None:
|
||||
"""Initialize the roller."""
|
||||
self.roller = roller
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if roller and hub is available."""
|
||||
return self.roller.online and self.roller.hub.connected
|
||||
|
||||
async def async_remove_and_unregister(self):
|
||||
"""Unregister from entity and device registry and call entity remove function."""
|
||||
_LOGGER.info("Removing %s %s", self.__class__.__name__, self.unique_id)
|
||||
|
||||
ent_registry = await get_ent_reg(self.hass)
|
||||
if self.entity_id in ent_registry.entities:
|
||||
ent_registry.async_remove(self.entity_id)
|
||||
|
||||
dev_registry = await get_dev_reg(self.hass)
|
||||
device = dev_registry.async_get_device(
|
||||
identifiers={(DOMAIN, self.unique_id)}, connections=set()
|
||||
)
|
||||
if device is not None:
|
||||
dev_registry.async_update_device(
|
||||
device.id, remove_config_entry_id=self.registry_entry.config_entry_id
|
||||
)
|
||||
|
||||
await self.async_remove()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Entity has been added to hass."""
|
||||
self.roller.callback_subscribe(self.notify_update)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
AUTOMATE_ENTITY_REMOVE.format(self.roller.id),
|
||||
self.async_remove_and_unregister,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Entity being removed from hass."""
|
||||
self.roller.callback_unsubscribe(self.notify_update)
|
||||
|
||||
@callback
|
||||
def notify_update(self, roller: aiopulse2.Roller):
|
||||
"""Write updated device state information."""
|
||||
_LOGGER.debug(
|
||||
"Device update notification received: %s (%r)", roller.id, roller.name
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Report that Automate entities do not need polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of this roller."""
|
||||
return self.roller.id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of roller."""
|
||||
return self.roller.name
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info."""
|
||||
attrs = {
|
||||
"identifiers": {(DOMAIN, self.roller.id)},
|
||||
}
|
||||
return attrs
|
37
homeassistant/components/automate/config_flow.py
Normal file
37
homeassistant/components/automate/config_flow.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Config flow for Automate Pulse Hub v2 integration."""
|
||||
import logging
|
||||
|
||||
import aiopulse2
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA = vol.Schema({vol.Required("host"): str})
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Automate Pulse Hub v2."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step once we have info from the user."""
|
||||
if user_input is not None:
|
||||
try:
|
||||
hub = aiopulse2.Hub(user_input["host"])
|
||||
await hub.test()
|
||||
title = hub.name
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=DATA_SCHEMA,
|
||||
errors={"base": "cannot_connect"},
|
||||
)
|
||||
|
||||
return self.async_create_entry(title=title, data=user_input)
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
|
6
homeassistant/components/automate/const.py
Normal file
6
homeassistant/components/automate/const.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Constants for the Automate Pulse Hub v2 integration."""
|
||||
|
||||
DOMAIN = "automate"
|
||||
|
||||
AUTOMATE_HUB_UPDATE = "automate_hub_update_{}"
|
||||
AUTOMATE_ENTITY_REMOVE = "automate_entity_remove_{}"
|
147
homeassistant/components/automate/cover.py
Normal file
147
homeassistant/components/automate/cover.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""Support for Automate Roller Blinds."""
|
||||
import aiopulse2
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
DEVICE_CLASS_SHADE,
|
||||
SUPPORT_CLOSE,
|
||||
SUPPORT_CLOSE_TILT,
|
||||
SUPPORT_OPEN,
|
||||
SUPPORT_OPEN_TILT,
|
||||
SUPPORT_SET_POSITION,
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
SUPPORT_STOP,
|
||||
SUPPORT_STOP_TILT,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .base import AutomateBase
|
||||
from .const import AUTOMATE_HUB_UPDATE, DOMAIN
|
||||
from .helpers import async_add_automate_entities
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Automate Rollers from a config entry."""
|
||||
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
current = set()
|
||||
|
||||
@callback
|
||||
def async_add_automate_covers():
|
||||
async_add_automate_entities(
|
||||
hass, AutomateCover, config_entry, current, async_add_entities
|
||||
)
|
||||
|
||||
hub.cleanup_callbacks.append(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
AUTOMATE_HUB_UPDATE.format(config_entry.entry_id),
|
||||
async_add_automate_covers,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class AutomateCover(AutomateBase, CoverEntity):
|
||||
"""Representation of a Automate cover device."""
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of the roller blind.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
position = None
|
||||
if self.roller.closed_percent is not None:
|
||||
position = 100 - self.roller.closed_percent
|
||||
return position
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
"""Return the current tilt of the roller blind.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = 0
|
||||
if self.current_cover_position is not None:
|
||||
supported_features |= (
|
||||
SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION
|
||||
)
|
||||
if self.current_cover_tilt_position is not None:
|
||||
supported_features |= (
|
||||
SUPPORT_OPEN_TILT
|
||||
| SUPPORT_CLOSE_TILT
|
||||
| SUPPORT_STOP_TILT
|
||||
| SUPPORT_SET_TILT_POSITION
|
||||
)
|
||||
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info."""
|
||||
attrs = super().device_info
|
||||
attrs["manufacturer"] = "Automate"
|
||||
attrs["model"] = self.roller.devicetype
|
||||
attrs["sw_version"] = self.roller.version
|
||||
attrs["via_device"] = (DOMAIN, self.roller.hub.id)
|
||||
attrs["name"] = self.name
|
||||
return attrs
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Class of the cover, a shade."""
|
||||
return DEVICE_CLASS_SHADE
|
||||
|
||||
@property
|
||||
def is_opening(self):
|
||||
"""Is cover opening/moving up."""
|
||||
return self.roller.action == aiopulse2.MovingAction.up
|
||||
|
||||
@property
|
||||
def is_closing(self):
|
||||
"""Is cover closing/moving down."""
|
||||
return self.roller.action == aiopulse2.MovingAction.down
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self.roller.closed_percent == 100
|
||||
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Close the roller."""
|
||||
await self.roller.move_down()
|
||||
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Open the roller."""
|
||||
await self.roller.move_up()
|
||||
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Stop the roller."""
|
||||
await self.roller.move_stop()
|
||||
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the roller shutter to a specific position."""
|
||||
await self.roller.move_to(100 - kwargs[ATTR_POSITION])
|
||||
|
||||
async def async_close_cover_tilt(self, **kwargs):
|
||||
"""Close the roller."""
|
||||
await self.roller.move_down()
|
||||
|
||||
async def async_open_cover_tilt(self, **kwargs):
|
||||
"""Open the roller."""
|
||||
await self.roller.move_up()
|
||||
|
||||
async def async_stop_cover_tilt(self, **kwargs):
|
||||
"""Stop the roller."""
|
||||
await self.roller.move_stop()
|
||||
|
||||
async def async_set_cover_tilt(self, **kwargs):
|
||||
"""Tilt the roller shutter to a specific position."""
|
||||
await self.roller.move_to(100 - kwargs[ATTR_POSITION])
|
46
homeassistant/components/automate/helpers.py
Normal file
46
homeassistant/components/automate/helpers.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Helper functions for Automate Pulse."""
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_automate_entities(
|
||||
hass, entity_class, config_entry, current, async_add_entities
|
||||
):
|
||||
"""Add any new entities."""
|
||||
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||
_LOGGER.debug("Looking for new %s on: %s", entity_class.__name__, hub.host)
|
||||
|
||||
api = hub.api.rollers
|
||||
|
||||
new_items = []
|
||||
for unique_id, roller in api.items():
|
||||
if unique_id not in current:
|
||||
_LOGGER.debug("New %s %s", entity_class.__name__, unique_id)
|
||||
new_item = entity_class(roller)
|
||||
current.add(unique_id)
|
||||
new_items.append(new_item)
|
||||
|
||||
async_add_entities(new_items)
|
||||
|
||||
|
||||
async def update_devices(hass, config_entry, api):
|
||||
"""Tell hass that device info has been updated."""
|
||||
dev_registry = await get_dev_reg(hass)
|
||||
|
||||
for api_item in api.values():
|
||||
# Update Device name
|
||||
device = dev_registry.async_get_device(
|
||||
identifiers={(DOMAIN, api_item.id)}, connections=set()
|
||||
)
|
||||
if device is not None:
|
||||
dev_registry.async_update_device(
|
||||
device.id,
|
||||
name=api_item.name,
|
||||
)
|
89
homeassistant/components/automate/hub.py
Normal file
89
homeassistant/components/automate/hub.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Code to handle a Pulse Hub."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiopulse2
|
||||
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import AUTOMATE_ENTITY_REMOVE, AUTOMATE_HUB_UPDATE
|
||||
from .helpers import update_devices
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PulseHub:
|
||||
"""Manages a single Pulse Hub."""
|
||||
|
||||
def __init__(self, hass, config_entry):
|
||||
"""Initialize the system."""
|
||||
self.config_entry = config_entry
|
||||
self.hass = hass
|
||||
self.api: aiopulse2.Hub | None = None
|
||||
self.tasks = []
|
||||
self.current_rollers = {}
|
||||
self.cleanup_callbacks = []
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
"""Return the title of the hub shown in the integrations list."""
|
||||
return f"{self.api.name} ({self.api.host})"
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
"""Return the host of this hub."""
|
||||
return self.config_entry.data["host"]
|
||||
|
||||
async def async_setup(self):
|
||||
"""Set up a hub based on host parameter."""
|
||||
host = self.host
|
||||
|
||||
hub = aiopulse2.Hub(host, propagate_callbacks=True)
|
||||
|
||||
self.api = hub
|
||||
|
||||
hub.callback_subscribe(self.async_notify_update)
|
||||
self.tasks.append(asyncio.create_task(hub.run()))
|
||||
|
||||
_LOGGER.debug("Hub setup complete")
|
||||
return True
|
||||
|
||||
async def async_reset(self):
|
||||
"""Reset this hub to default state."""
|
||||
for cleanup_callback in self.cleanup_callbacks:
|
||||
cleanup_callback()
|
||||
|
||||
# If not setup
|
||||
if self.api is None:
|
||||
return False
|
||||
|
||||
self.api.callback_unsubscribe(self.async_notify_update)
|
||||
await self.api.stop()
|
||||
del self.api
|
||||
self.api = None
|
||||
|
||||
# Wait for any running tasks to complete
|
||||
await asyncio.wait(self.tasks)
|
||||
|
||||
return True
|
||||
|
||||
async def async_notify_update(self, hub=None):
|
||||
"""Evaluate entities when hub reports that update has occurred."""
|
||||
_LOGGER.debug("Hub {self.title} updated")
|
||||
|
||||
await update_devices(self.hass, self.config_entry, self.api.rollers)
|
||||
self.hass.config_entries.async_update_entry(self.config_entry, title=self.title)
|
||||
|
||||
async_dispatcher_send(
|
||||
self.hass, AUTOMATE_HUB_UPDATE.format(self.config_entry.entry_id)
|
||||
)
|
||||
|
||||
for unique_id in list(self.current_rollers):
|
||||
if unique_id not in self.api.rollers:
|
||||
_LOGGER.debug("Notifying remove of %s", unique_id)
|
||||
self.current_rollers.pop(unique_id)
|
||||
async_dispatcher_send(
|
||||
self.hass, AUTOMATE_ENTITY_REMOVE.format(unique_id)
|
||||
)
|
13
homeassistant/components/automate/manifest.json
Normal file
13
homeassistant/components/automate/manifest.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "automate",
|
||||
"name": "Automate Pulse Hub v2",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"documentation": "https://www.home-assistant.io/integrations/automate",
|
||||
"requirements": [
|
||||
"aiopulse2==0.6.0"
|
||||
],
|
||||
"codeowners": [
|
||||
"@sillyfrog"
|
||||
]
|
||||
}
|
19
homeassistant/components/automate/strings.json
Normal file
19
homeassistant/components/automate/strings.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
}
|
||||
}
|
19
homeassistant/components/automate/translations/en.json
Normal file
19
homeassistant/components/automate/translations/en.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -139,8 +139,8 @@
|
||||
"on": "Nass"
|
||||
},
|
||||
"motion": {
|
||||
"off": "Ruhig",
|
||||
"on": "Bewegung erkannt"
|
||||
"off": "Normal",
|
||||
"on": "Erkannt"
|
||||
},
|
||||
"moving": {
|
||||
"off": "Bewegt sich nicht",
|
||||
@@ -171,16 +171,16 @@
|
||||
"on": "Unsicher"
|
||||
},
|
||||
"smoke": {
|
||||
"off": "OK",
|
||||
"on": "Rauch erkannt"
|
||||
"off": "Normal",
|
||||
"on": "Erkannt"
|
||||
},
|
||||
"sound": {
|
||||
"off": "Stille",
|
||||
"on": "Ger\u00e4usch erkannt"
|
||||
"off": "Normal",
|
||||
"on": "Erkannt"
|
||||
},
|
||||
"vibration": {
|
||||
"off": "Normal",
|
||||
"on": "Vibration"
|
||||
"on": "Erkannt"
|
||||
},
|
||||
"window": {
|
||||
"off": "Geschlossen",
|
||||
|
@@ -13,7 +13,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "IP Adresse",
|
||||
"host": "IP-Adresse",
|
||||
"port": "Port"
|
||||
},
|
||||
"description": "Richte deine BleBox f\u00fcr die Integration mit dem Home Assistant ein.",
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"domain": "blinksticklight",
|
||||
"name": "BlinkStick",
|
||||
"documentation": "https://www.home-assistant.io/integrations/blinksticklight",
|
||||
"requirements": ["blinkstick==1.1.8"],
|
||||
"requirements": ["blinkstick==1.2.0"],
|
||||
"codeowners": [],
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ from homeassistant.const import (
|
||||
AREA_SQUARE_METERS,
|
||||
CONF_MONITORED_CONDITIONS,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ELECTRIC_POTENTIAL_MILLIVOLT,
|
||||
PERCENTAGE,
|
||||
PRESSURE_INHG,
|
||||
PRESSURE_MBAR,
|
||||
@@ -32,7 +33,7 @@ SENSOR_UNITS_IMPERIAL = {
|
||||
"Humidity": PERCENTAGE,
|
||||
"Pressure": PRESSURE_INHG,
|
||||
"Luminance": f"cd/{AREA_SQUARE_METERS}",
|
||||
"Voltage": "mV",
|
||||
"Voltage": ELECTRIC_POTENTIAL_MILLIVOLT,
|
||||
}
|
||||
|
||||
# Metric units
|
||||
@@ -41,7 +42,7 @@ SENSOR_UNITS_METRIC = {
|
||||
"Humidity": PERCENTAGE,
|
||||
"Pressure": PRESSURE_MBAR,
|
||||
"Luminance": f"cd/{AREA_SQUARE_METERS}",
|
||||
"Voltage": "mV",
|
||||
"Voltage": ELECTRIC_POTENTIAL_MILLIVOLT,
|
||||
}
|
||||
|
||||
# Device class
|
||||
|
@@ -203,33 +203,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
class BluesoundPlayer(MediaPlayerEntity):
|
||||
"""Representation of a Bluesound Player."""
|
||||
|
||||
def __init__(self, hass, host, port=None, name=None, init_callback=None):
|
||||
_attr_media_content_type = MEDIA_TYPE_MUSIC
|
||||
|
||||
def __init__(self, hass, host, port=DEFAULT_PORT, name=None, init_callback=None):
|
||||
"""Initialize the media player."""
|
||||
self.host = host
|
||||
self._hass = hass
|
||||
self.port = port
|
||||
self._polling_session = async_get_clientsession(hass)
|
||||
self._polling_task = None # The actual polling task.
|
||||
self._name = name
|
||||
self._icon = None
|
||||
self._attr_name = name
|
||||
self._capture_items = []
|
||||
self._services_items = []
|
||||
self._preset_items = []
|
||||
self._sync_status = {}
|
||||
self._status = None
|
||||
self._last_status_update = None
|
||||
self._is_online = False
|
||||
self._is_online = None
|
||||
self._retry_remove = None
|
||||
self._muted = False
|
||||
self._master = None
|
||||
self._is_master = False
|
||||
self._group_name = None
|
||||
self._group_list = []
|
||||
self._bluesound_device_name = None
|
||||
|
||||
self._is_master = False
|
||||
self._group_list = []
|
||||
self._init_callback = init_callback
|
||||
if self.port is None:
|
||||
self.port = DEFAULT_PORT
|
||||
|
||||
class _TimeoutException(Exception):
|
||||
pass
|
||||
@@ -252,12 +248,12 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
return None
|
||||
self._sync_status = resp["SyncStatus"].copy()
|
||||
|
||||
if not self._name:
|
||||
self._name = self._sync_status.get("@name", self.host)
|
||||
if not self.name:
|
||||
self._attr_name = self._sync_status.get("@name", self.host)
|
||||
if not self._bluesound_device_name:
|
||||
self._bluesound_device_name = self._sync_status.get("@name", self.host)
|
||||
if not self._icon:
|
||||
self._icon = self._sync_status.get("@icon", self.host)
|
||||
if not self.icon:
|
||||
self._attr_icon = self._sync_status.get("@icon", self.host)
|
||||
|
||||
master = self._sync_status.get("master")
|
||||
if master is not None:
|
||||
@@ -291,14 +287,14 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
await self.async_update_status()
|
||||
|
||||
except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException):
|
||||
_LOGGER.info("Node %s is offline, retrying later", self._name)
|
||||
_LOGGER.info("Node %s is offline, retrying later", self.name)
|
||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
|
||||
self.start_polling()
|
||||
|
||||
except CancelledError:
|
||||
_LOGGER.debug("Stopping the polling of node %s", self._name)
|
||||
_LOGGER.debug("Stopping the polling of node %s", self.name)
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error in %s", self._name)
|
||||
_LOGGER.exception("Unexpected error in %s", self.name)
|
||||
raise
|
||||
|
||||
def start_polling(self):
|
||||
@@ -402,7 +398,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
if response.status == HTTP_OK:
|
||||
result = await response.text()
|
||||
self._is_online = True
|
||||
self._last_status_update = dt_util.utcnow()
|
||||
self._attr_media_position_updated_at = dt_util.utcnow()
|
||||
self._status = xmltodict.parse(result)["status"].copy()
|
||||
|
||||
group_name = self._status.get("groupName")
|
||||
@@ -438,11 +434,58 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
except (asyncio.TimeoutError, ClientError):
|
||||
self._is_online = False
|
||||
self._last_status_update = None
|
||||
self._attr_media_position_updated_at = None
|
||||
self._status = None
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.info("Client connection error, marking %s as offline", self._name)
|
||||
_LOGGER.info("Client connection error, marking %s as offline", self.name)
|
||||
raise
|
||||
self.update_state_attr()
|
||||
|
||||
def update_state_attr(self):
|
||||
"""Update state attributes."""
|
||||
if self._status is None:
|
||||
self._attr_state = STATE_OFF
|
||||
self._attr_supported_features = 0
|
||||
elif self.is_grouped and not self.is_master:
|
||||
self._attr_state = STATE_GROUPED
|
||||
self._attr_supported_features = (
|
||||
SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
|
||||
)
|
||||
else:
|
||||
status = self._status.get("state")
|
||||
self._attr_state = STATE_IDLE
|
||||
if status in ("pause", "stop"):
|
||||
self._attr_state = STATE_PAUSED
|
||||
elif status in ("stream", "play"):
|
||||
self._attr_state = STATE_PLAYING
|
||||
supported = SUPPORT_CLEAR_PLAYLIST
|
||||
if self._status.get("indexing", "0") == "0":
|
||||
supported = (
|
||||
supported
|
||||
| SUPPORT_PAUSE
|
||||
| SUPPORT_PREVIOUS_TRACK
|
||||
| SUPPORT_NEXT_TRACK
|
||||
| SUPPORT_PLAY_MEDIA
|
||||
| SUPPORT_STOP
|
||||
| SUPPORT_PLAY
|
||||
| SUPPORT_SELECT_SOURCE
|
||||
| SUPPORT_SHUFFLE_SET
|
||||
)
|
||||
if self.volume_level is not None and self.volume_level >= 0:
|
||||
supported = (
|
||||
supported
|
||||
| SUPPORT_VOLUME_STEP
|
||||
| SUPPORT_VOLUME_SET
|
||||
| SUPPORT_VOLUME_MUTE
|
||||
)
|
||||
if self._status.get("canSeek", "") == "1":
|
||||
supported = supported | SUPPORT_SEEK
|
||||
self._attr_supported_features = supported
|
||||
self._attr_extra_state_attributes = {}
|
||||
if self._group_list:
|
||||
self._attr_extra_state_attributes = {ATTR_BLUESOUND_GROUP: self._group_list}
|
||||
self._attr_extra_state_attributes[ATTR_MASTER] = self._is_master
|
||||
self._attr_shuffle = self._status.get("shuffle", "0") == "1"
|
||||
|
||||
async def async_trigger_sync_on_all(self):
|
||||
"""Trigger sync status update on all devices."""
|
||||
@@ -542,27 +585,6 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
return self._services_items
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._status is None:
|
||||
return STATE_OFF
|
||||
|
||||
if self.is_grouped and not self.is_master:
|
||||
return STATE_GROUPED
|
||||
|
||||
status = self._status.get("state")
|
||||
if status in ("pause", "stop"):
|
||||
return STATE_PAUSED
|
||||
if status in ("stream", "play"):
|
||||
return STATE_PLAYING
|
||||
return STATE_IDLE
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
@@ -617,7 +639,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
return None
|
||||
|
||||
mediastate = self.state
|
||||
if self._last_status_update is None or mediastate == STATE_IDLE:
|
||||
if self.media_position_updated_at is None or mediastate == STATE_IDLE:
|
||||
return None
|
||||
|
||||
position = self._status.get("secs")
|
||||
@@ -626,7 +648,9 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
position = float(position)
|
||||
if mediastate == STATE_PLAYING:
|
||||
position += (dt_util.utcnow() - self._last_status_update).total_seconds()
|
||||
position += (
|
||||
dt_util.utcnow() - self.media_position_updated_at
|
||||
).total_seconds()
|
||||
|
||||
return position
|
||||
|
||||
@@ -641,11 +665,6 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
return None
|
||||
return float(duration)
|
||||
|
||||
@property
|
||||
def media_position_updated_at(self):
|
||||
"""Last time status was updated."""
|
||||
return self._last_status_update
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Volume level of the media player (0..1)."""
|
||||
@@ -668,21 +687,11 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
mute = bool(int(mute))
|
||||
return mute
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def bluesound_device_name(self):
|
||||
"""Return the device name as returned by the device."""
|
||||
return self._bluesound_device_name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the device."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
@@ -778,58 +787,15 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
return None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
if self._status is None:
|
||||
return 0
|
||||
|
||||
if self.is_grouped and not self.is_master:
|
||||
return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
|
||||
|
||||
supported = SUPPORT_CLEAR_PLAYLIST
|
||||
|
||||
if self._status.get("indexing", "0") == "0":
|
||||
supported = (
|
||||
supported
|
||||
| SUPPORT_PAUSE
|
||||
| SUPPORT_PREVIOUS_TRACK
|
||||
| SUPPORT_NEXT_TRACK
|
||||
| SUPPORT_PLAY_MEDIA
|
||||
| SUPPORT_STOP
|
||||
| SUPPORT_PLAY
|
||||
| SUPPORT_SELECT_SOURCE
|
||||
| SUPPORT_SHUFFLE_SET
|
||||
)
|
||||
|
||||
current_vol = self.volume_level
|
||||
if current_vol is not None and current_vol >= 0:
|
||||
supported = (
|
||||
supported
|
||||
| SUPPORT_VOLUME_STEP
|
||||
| SUPPORT_VOLUME_SET
|
||||
| SUPPORT_VOLUME_MUTE
|
||||
)
|
||||
|
||||
if self._status.get("canSeek", "") == "1":
|
||||
supported = supported | SUPPORT_SEEK
|
||||
|
||||
return supported
|
||||
|
||||
@property
|
||||
def is_master(self):
|
||||
def is_master(self) -> bool:
|
||||
"""Return true if player is a coordinator."""
|
||||
return self._is_master
|
||||
|
||||
@property
|
||||
def is_grouped(self):
|
||||
def is_grouped(self) -> bool:
|
||||
"""Return true if player is a coordinator."""
|
||||
return self._master is not None or self._is_master
|
||||
|
||||
@property
|
||||
def shuffle(self):
|
||||
"""Return true if shuffle is active."""
|
||||
return self._status.get("shuffle", "0") == "1"
|
||||
|
||||
async def async_join(self, master):
|
||||
"""Join the player to a group."""
|
||||
master_device = [
|
||||
@@ -849,17 +815,6 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
else:
|
||||
_LOGGER.error("Master not found %s", master_device)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""List members in group."""
|
||||
attributes = {}
|
||||
if self._group_list:
|
||||
attributes = {ATTR_BLUESOUND_GROUP: self._group_list}
|
||||
|
||||
attributes[ATTR_MASTER] = self._is_master
|
||||
|
||||
return attributes
|
||||
|
||||
def rebuild_bluesound_group(self):
|
||||
"""Rebuild the list of entities in speaker group."""
|
||||
if self._group_name is None:
|
||||
|
@@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry, discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.util import slugify
|
||||
import homeassistant.util.dt as dt_util
|
||||
@@ -317,6 +317,8 @@ class BMWConnectedDriveAccount:
|
||||
class BMWConnectedDriveBaseEntity(Entity):
|
||||
"""Common base for BMW entities."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, account, vehicle):
|
||||
"""Initialize sensor."""
|
||||
self._account = account
|
||||
@@ -326,15 +328,11 @@ class BMWConnectedDriveBaseEntity(Entity):
|
||||
"vin": self._vehicle.vin,
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
}
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return info for device registry."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._vehicle.vin)},
|
||||
"name": f'{self._vehicle.attributes.get("brand")} {self._vehicle.name}',
|
||||
"model": self._vehicle.name,
|
||||
"manufacturer": self._vehicle.attributes.get("brand"),
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, vehicle.vin)},
|
||||
"name": f'{vehicle.attributes.get("brand")} {vehicle.name}',
|
||||
"model": vehicle.name,
|
||||
"manufacturer": vehicle.attributes.get("brand"),
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -342,14 +340,6 @@ class BMWConnectedDriveBaseEntity(Entity):
|
||||
"""Return the state attributes of the sensor."""
|
||||
return self._attrs
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Do not poll this class.
|
||||
|
||||
Updates are triggered from BMWConnectedDriveAccount.
|
||||
"""
|
||||
return False
|
||||
|
||||
def update_callback(self):
|
||||
"""Schedule a state update."""
|
||||
self.schedule_update_ha_state(True)
|
||||
|
@@ -76,41 +76,45 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity):
|
||||
super().__init__(account, vehicle)
|
||||
|
||||
self._attribute = attribute
|
||||
self._name = f"{self._vehicle.name} {self._attribute}"
|
||||
self._unique_id = f"{self._vehicle.vin}-{self._attribute}"
|
||||
self._attr_name = f"{vehicle.name} {attribute}"
|
||||
self._attr_unique_id = f"{vehicle.vin}-{attribute}"
|
||||
self._sensor_name = sensor_name
|
||||
self._device_class = device_class
|
||||
self._icon = icon
|
||||
self._state = None
|
||||
self._attr_device_class = device_class
|
||||
self._attr_icon = icon
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of the binary sensor."""
|
||||
return self._unique_id
|
||||
def update(self):
|
||||
"""Read new state data from the library."""
|
||||
vehicle_state = self._vehicle.state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
# device class opening: On means open, Off means closed
|
||||
if self._attribute == "lids":
|
||||
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
|
||||
self._attr_state = not vehicle_state.all_lids_closed
|
||||
if self._attribute == "windows":
|
||||
self._attr_state = not vehicle_state.all_windows_closed
|
||||
# device class lock: On means unlocked, Off means locked
|
||||
if self._attribute == "door_lock_state":
|
||||
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
||||
self._attr_state = vehicle_state.door_lock_state not in [
|
||||
LockState.LOCKED,
|
||||
LockState.SECURED,
|
||||
]
|
||||
# device class light: On means light detected, Off means no light
|
||||
if self._attribute == "lights_parking":
|
||||
self._attr_state = vehicle_state.are_parking_lights_on
|
||||
# device class problem: On means problem detected, Off means no problem
|
||||
if self._attribute == "condition_based_services":
|
||||
self._attr_state = not vehicle_state.are_all_cbs_ok
|
||||
if self._attribute == "check_control_messages":
|
||||
self._attr_state = vehicle_state.has_check_control_messages
|
||||
# device class power: On means power detected, Off means no power
|
||||
if self._attribute == "charging_status":
|
||||
self._attr_state = vehicle_state.charging_status in [ChargingState.CHARGING]
|
||||
# device class plug: On means device is plugged in,
|
||||
# Off means device is unplugged
|
||||
if self._attribute == "connection_status":
|
||||
self._attr_state = vehicle_state.connection_status == "CONNECTED"
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the binary sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes of the binary sensor."""
|
||||
vehicle_state = self._vehicle.state
|
||||
result = self._attrs.copy()
|
||||
|
||||
@@ -144,40 +148,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity):
|
||||
elif self._attribute == "connection_status":
|
||||
result["connection_status"] = vehicle_state.connection_status
|
||||
|
||||
return sorted(result.items())
|
||||
|
||||
def update(self):
|
||||
"""Read new state data from the library."""
|
||||
vehicle_state = self._vehicle.state
|
||||
|
||||
# device class opening: On means open, Off means closed
|
||||
if self._attribute == "lids":
|
||||
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
|
||||
self._state = not vehicle_state.all_lids_closed
|
||||
if self._attribute == "windows":
|
||||
self._state = not vehicle_state.all_windows_closed
|
||||
# device class lock: On means unlocked, Off means locked
|
||||
if self._attribute == "door_lock_state":
|
||||
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
||||
self._state = vehicle_state.door_lock_state not in [
|
||||
LockState.LOCKED,
|
||||
LockState.SECURED,
|
||||
]
|
||||
# device class light: On means light detected, Off means no light
|
||||
if self._attribute == "lights_parking":
|
||||
self._state = vehicle_state.are_parking_lights_on
|
||||
# device class problem: On means problem detected, Off means no problem
|
||||
if self._attribute == "condition_based_services":
|
||||
self._state = not vehicle_state.are_all_cbs_ok
|
||||
if self._attribute == "check_control_messages":
|
||||
self._state = vehicle_state.has_check_control_messages
|
||||
# device class power: On means power detected, Off means no power
|
||||
if self._attribute == "charging_status":
|
||||
self._state = vehicle_state.charging_status in [ChargingState.CHARGING]
|
||||
# device class plug: On means device is plugged in,
|
||||
# Off means device is unplugged
|
||||
if self._attribute == "connection_status":
|
||||
self._state = vehicle_state.connection_status == "CONNECTED"
|
||||
self._attr_extra_state_attributes = sorted(result.items())
|
||||
|
||||
def _format_cbs_report(self, report):
|
||||
result = {}
|
||||
|
@@ -29,15 +29,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity):
|
||||
"""BMW Connected Drive device tracker."""
|
||||
|
||||
_attr_force_update = False
|
||||
_attr_icon = "mdi:car"
|
||||
|
||||
def __init__(self, account, vehicle):
|
||||
"""Initialize the Tracker."""
|
||||
super().__init__(account, vehicle)
|
||||
|
||||
self._unique_id = vehicle.vin
|
||||
self._attr_unique_id = vehicle.vin
|
||||
self._location = (
|
||||
vehicle.state.gps_position if vehicle.state.gps_position else (None, None)
|
||||
)
|
||||
self._name = vehicle.name
|
||||
self._attr_name = vehicle.name
|
||||
|
||||
@property
|
||||
def latitude(self):
|
||||
@@ -49,31 +52,11 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity):
|
||||
"""Return longitude value of the device."""
|
||||
return self._location[1] if self._location else None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def source_type(self):
|
||||
"""Return the source type, eg gps or router, of the device."""
|
||||
return SOURCE_TYPE_GPS
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return "mdi:car"
|
||||
|
||||
@property
|
||||
def force_update(self):
|
||||
"""All updates do not need to be written to the state machine."""
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
"""Update state of the decvice tracker."""
|
||||
self._location = (
|
||||
|
@@ -4,7 +4,6 @@ import logging
|
||||
from bimmer_connected.state import LockState
|
||||
|
||||
from homeassistant.components.lock import LockEntity
|
||||
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
|
||||
|
||||
from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity
|
||||
from .const import CONF_ACCOUNT, DATA_ENTRIES
|
||||
@@ -33,50 +32,17 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity):
|
||||
super().__init__(account, vehicle)
|
||||
|
||||
self._attribute = attribute
|
||||
self._name = f"{self._vehicle.name} {self._attribute}"
|
||||
self._unique_id = f"{self._vehicle.vin}-{self._attribute}"
|
||||
self._attr_name = f"{vehicle.name} {attribute}"
|
||||
self._attr_unique_id = f"{vehicle.vin}-{attribute}"
|
||||
self._sensor_name = sensor_name
|
||||
self._state = None
|
||||
self.door_lock_state_available = (
|
||||
DOOR_LOCK_STATE in self._vehicle.available_attributes
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of the lock."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the lock."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes of the lock."""
|
||||
vehicle_state = self._vehicle.state
|
||||
result = self._attrs.copy()
|
||||
|
||||
if self.door_lock_state_available:
|
||||
result["door_lock_state"] = vehicle_state.door_lock_state.value
|
||||
result["last_update_reason"] = vehicle_state.last_update_reason
|
||||
return result
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
"""Return true if lock is locked."""
|
||||
if self.door_lock_state_available:
|
||||
result = self._state == STATE_LOCKED
|
||||
else:
|
||||
result = None
|
||||
return result
|
||||
self.door_lock_state_available = DOOR_LOCK_STATE in vehicle.available_attributes
|
||||
|
||||
def lock(self, **kwargs):
|
||||
"""Lock the car."""
|
||||
_LOGGER.debug("%s: locking doors", self._vehicle.name)
|
||||
# Optimistic state set here because it takes some time before the
|
||||
# update callback response
|
||||
self._state = STATE_LOCKED
|
||||
self._attr_is_locked = True
|
||||
self.schedule_update_ha_state()
|
||||
self._vehicle.remote_services.trigger_remote_door_lock()
|
||||
|
||||
@@ -85,18 +51,23 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity):
|
||||
_LOGGER.debug("%s: unlocking doors", self._vehicle.name)
|
||||
# Optimistic state set here because it takes some time before the
|
||||
# update callback response
|
||||
self._state = STATE_UNLOCKED
|
||||
self._attr_is_locked = False
|
||||
self.schedule_update_ha_state()
|
||||
self._vehicle.remote_services.trigger_remote_door_unlock()
|
||||
|
||||
def update(self):
|
||||
"""Update state of the lock."""
|
||||
_LOGGER.debug("%s: updating data for %s", self._vehicle.name, self._attribute)
|
||||
vehicle_state = self._vehicle.state
|
||||
if self._vehicle.state.door_lock_state in [LockState.LOCKED, LockState.SECURED]:
|
||||
self._attr_is_locked = True
|
||||
else:
|
||||
self._attr_is_locked = False
|
||||
if not self.door_lock_state_available:
|
||||
self._attr_is_locked = None
|
||||
|
||||
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
||||
self._state = (
|
||||
STATE_LOCKED
|
||||
if vehicle_state.door_lock_state in [LockState.LOCKED, LockState.SECURED]
|
||||
else STATE_UNLOCKED
|
||||
)
|
||||
vehicle_state = self._vehicle.state
|
||||
result = self._attrs.copy()
|
||||
if self.door_lock_state_available:
|
||||
result["door_lock_state"] = vehicle_state.door_lock_state.value
|
||||
result["last_update_reason"] = vehicle_state.last_update_reason
|
||||
self._attr_extra_state_attributes = result
|
||||
|
@@ -503,94 +503,46 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity):
|
||||
|
||||
self._attribute = attribute
|
||||
self._service = service
|
||||
self._state = None
|
||||
if self._service:
|
||||
self._name = (
|
||||
f"{self._vehicle.name} {self._service.lower()}_{self._attribute}"
|
||||
)
|
||||
self._unique_id = (
|
||||
f"{self._vehicle.vin}-{self._service.lower()}-{self._attribute}"
|
||||
)
|
||||
if service:
|
||||
self._attr_name = f"{vehicle.name} {service.lower()}_{attribute}"
|
||||
self._attr_unique_id = f"{vehicle.vin}-{service.lower()}-{attribute}"
|
||||
else:
|
||||
self._name = f"{self._vehicle.name} {self._attribute}"
|
||||
self._unique_id = f"{self._vehicle.vin}-{self._attribute}"
|
||||
self._attr_name = f"{vehicle.name} {attribute}"
|
||||
self._attr_unique_id = f"{vehicle.vin}-{attribute}"
|
||||
self._attribute_info = attribute_info
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of the sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
vehicle_state = self._vehicle.state
|
||||
charging_state = vehicle_state.charging_status in [ChargingState.CHARGING]
|
||||
|
||||
if self._attribute == "charging_level_hv":
|
||||
return icon_for_battery_level(
|
||||
battery_level=vehicle_state.charging_level_hv, charging=charging_state
|
||||
)
|
||||
icon = self._attribute_info.get(self._attribute, [None, None, None, None])[0]
|
||||
return icon
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
enabled_default = self._attribute_info.get(
|
||||
self._attribute, [None, None, None, True]
|
||||
self._attr_entity_registry_enabled_default = attribute_info.get(
|
||||
attribute, [None, None, None, True]
|
||||
)[3]
|
||||
return enabled_default
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor.
|
||||
|
||||
The return type of this call depends on the attribute that
|
||||
is configured.
|
||||
"""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_class(self) -> str:
|
||||
"""Get the device class."""
|
||||
clss = self._attribute_info.get(self._attribute, [None, None, None, None])[1]
|
||||
return clss
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self) -> str:
|
||||
"""Get the unit of measurement."""
|
||||
unit = self._attribute_info.get(self._attribute, [None, None, None, None])[2]
|
||||
return unit
|
||||
self._attr_device_class = attribute_info.get(
|
||||
attribute, [None, None, None, None]
|
||||
)[1]
|
||||
self._attr_unit_of_measurement = attribute_info.get(
|
||||
attribute, [None, None, None, None]
|
||||
)[2]
|
||||
|
||||
def update(self) -> None:
|
||||
"""Read new state data from the library."""
|
||||
_LOGGER.debug("Updating %s", self._vehicle.name)
|
||||
vehicle_state = self._vehicle.state
|
||||
if self._attribute == "charging_status":
|
||||
self._state = getattr(vehicle_state, self._attribute).value
|
||||
self._attr_state = getattr(vehicle_state, self._attribute).value
|
||||
elif self.unit_of_measurement == VOLUME_GALLONS:
|
||||
value = getattr(vehicle_state, self._attribute)
|
||||
value_converted = self.hass.config.units.volume(value, VOLUME_LITERS)
|
||||
self._state = round(value_converted)
|
||||
self._attr_state = round(value_converted)
|
||||
elif self.unit_of_measurement == LENGTH_MILES:
|
||||
value = getattr(vehicle_state, self._attribute)
|
||||
value_converted = self.hass.config.units.length(value, LENGTH_KILOMETERS)
|
||||
self._state = round(value_converted)
|
||||
self._attr_state = round(value_converted)
|
||||
elif self._service is None:
|
||||
self._state = getattr(vehicle_state, self._attribute)
|
||||
self._attr_state = getattr(vehicle_state, self._attribute)
|
||||
elif self._service == SERVICE_LAST_TRIP:
|
||||
vehicle_last_trip = self._vehicle.state.last_trip
|
||||
if self._attribute == "date_utc":
|
||||
date_str = getattr(vehicle_last_trip, "date")
|
||||
self._state = dt_util.parse_datetime(date_str).isoformat()
|
||||
self._attr_state = dt_util.parse_datetime(date_str).isoformat()
|
||||
else:
|
||||
self._state = getattr(vehicle_last_trip, self._attribute)
|
||||
self._attr_state = getattr(vehicle_last_trip, self._attribute)
|
||||
elif self._service == SERVICE_ALL_TRIPS:
|
||||
vehicle_all_trips = self._vehicle.state.all_trips
|
||||
for attribute in (
|
||||
@@ -603,10 +555,21 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity):
|
||||
if self._attribute.startswith(f"{attribute}_"):
|
||||
attr = getattr(vehicle_all_trips, attribute)
|
||||
sub_attr = self._attribute.replace(f"{attribute}_", "")
|
||||
self._state = getattr(attr, sub_attr)
|
||||
self._attr_state = getattr(attr, sub_attr)
|
||||
return
|
||||
if self._attribute == "reset_date_utc":
|
||||
date_str = getattr(vehicle_all_trips, "reset_date")
|
||||
self._state = dt_util.parse_datetime(date_str).isoformat()
|
||||
self._attr_state = dt_util.parse_datetime(date_str).isoformat()
|
||||
else:
|
||||
self._state = getattr(vehicle_all_trips, self._attribute)
|
||||
self._attr_state = getattr(vehicle_all_trips, self._attribute)
|
||||
|
||||
vehicle_state = self._vehicle.state
|
||||
charging_state = vehicle_state.charging_status in [ChargingState.CHARGING]
|
||||
|
||||
if self._attribute == "charging_level_hv":
|
||||
self._attr_icon = icon_for_battery_level(
|
||||
battery_level=vehicle_state.charging_level_hv, charging=charging_state
|
||||
)
|
||||
self._attr_icon = self._attribute_info.get(
|
||||
self._attribute, [None, None, None, None]
|
||||
)[0]
|
||||
|
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"access_token": "Zugriffstoken",
|
||||
"access_token": "Zugangstoken",
|
||||
"host": "Host"
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"title": "Bosch SHC",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
@@ -35,4 +34,4 @@
|
||||
},
|
||||
"flow_title": "Bosch SHC: {name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,12 @@
|
||||
"""Broadlink entities."""
|
||||
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class BroadlinkEntity:
|
||||
class BroadlinkEntity(Entity):
|
||||
"""Representation of a Broadlink entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
@@ -127,10 +127,10 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity):
|
||||
self._flags = defaultdict(int)
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
self._attr_name = f"{self._device.name} Remote"
|
||||
self._attr_name = f"{device.name} Remote"
|
||||
self._attr_is_on = True
|
||||
self._attr_supported_features = SUPPORT_LEARN_COMMAND | SUPPORT_DELETE_COMMAND
|
||||
self._attr_unique_id = self._device.unique_id
|
||||
self._attr_unique_id = device.unique_id
|
||||
|
||||
def _extract_codes(self, commands, device=None):
|
||||
"""Extract a list of codes.
|
||||
|
@@ -77,14 +77,12 @@ class BroadlinkSensor(BroadlinkEntity, SensorEntity):
|
||||
self._coordinator = device.update_manager.coordinator
|
||||
self._monitored_condition = monitored_condition
|
||||
|
||||
self._attr_device_class = SENSOR_TYPES[self._monitored_condition][2]
|
||||
self._attr_name = (
|
||||
f"{self._device.name} {SENSOR_TYPES[self._monitored_condition][0]}"
|
||||
)
|
||||
self._attr_state_class = SENSOR_TYPES[self._monitored_condition][3]
|
||||
self._attr_device_class = SENSOR_TYPES[monitored_condition][2]
|
||||
self._attr_name = f"{device.name} {SENSOR_TYPES[monitored_condition][0]}"
|
||||
self._attr_state_class = SENSOR_TYPES[monitored_condition][3]
|
||||
self._attr_state = self._coordinator.data[monitored_condition]
|
||||
self._attr_unique_id = f"{self._device.unique_id}-{self._monitored_condition}"
|
||||
self._attr_unit_of_measurement = SENSOR_TYPES[self._monitored_condition][1]
|
||||
self._attr_unique_id = f"{device.unique_id}-{monitored_condition}"
|
||||
self._attr_unit_of_measurement = SENSOR_TYPES[monitored_condition][1]
|
||||
|
||||
@callback
|
||||
def update_data(self):
|
||||
|
@@ -135,22 +135,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC):
|
||||
"""Representation of a Broadlink switch."""
|
||||
|
||||
_attr_assumed_state = True
|
||||
_attr_device_class = DEVICE_CLASS_SWITCH
|
||||
|
||||
def __init__(self, device, command_on, command_off):
|
||||
"""Initialize the switch."""
|
||||
super().__init__(device)
|
||||
self._command_on = command_on
|
||||
self._command_off = command_off
|
||||
self._coordinator = device.update_manager.coordinator
|
||||
self._state = None
|
||||
|
||||
self._attr_assumed_state = True
|
||||
self._attr_device_class = DEVICE_CLASS_SWITCH
|
||||
self._attr_name = f"{self._device.name} Switch"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the switch is on."""
|
||||
return self._state
|
||||
self._attr_name = f"{device.name} Switch"
|
||||
self._attr_unique_id = device.unique_id
|
||||
|
||||
@callback
|
||||
def update_data(self):
|
||||
@@ -159,9 +157,8 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC):
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when the switch is added to hass."""
|
||||
if self._state is None:
|
||||
state = await self.async_get_last_state()
|
||||
self._state = state is not None and state.state == STATE_ON
|
||||
state = await self.async_get_last_state()
|
||||
self._attr_is_on = state is not None and state.state == STATE_ON
|
||||
self.async_on_remove(self._coordinator.async_add_listener(self.update_data))
|
||||
|
||||
async def async_update(self):
|
||||
@@ -171,13 +168,13 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC):
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn on the switch."""
|
||||
if await self._async_send_packet(self._command_on):
|
||||
self._state = True
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn off the switch."""
|
||||
if await self._async_send_packet(self._command_off):
|
||||
self._state = False
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@abstractmethod
|
||||
@@ -229,46 +226,41 @@ class BroadlinkSP1Switch(BroadlinkSwitch):
|
||||
class BroadlinkSP2Switch(BroadlinkSP1Switch):
|
||||
"""Representation of a Broadlink SP2 switch."""
|
||||
|
||||
_attr_assumed_state = False
|
||||
|
||||
def __init__(self, device, *args, **kwargs):
|
||||
"""Initialize the switch."""
|
||||
super().__init__(device, *args, **kwargs)
|
||||
self._state = self._coordinator.data["pwr"]
|
||||
self._load_power = self._coordinator.data.get("power")
|
||||
|
||||
self._attr_assumed_state = False
|
||||
|
||||
@property
|
||||
def current_power_w(self):
|
||||
"""Return the current power usage in Watt."""
|
||||
return self._load_power
|
||||
self._attr_is_on = self._coordinator.data["pwr"]
|
||||
self._attr_current_power_w = self._coordinator.data.get("power")
|
||||
|
||||
@callback
|
||||
def update_data(self):
|
||||
"""Update data."""
|
||||
if self._coordinator.last_update_success:
|
||||
self._state = self._coordinator.data["pwr"]
|
||||
self._load_power = self._coordinator.data.get("power")
|
||||
self._attr_is_on = self._coordinator.data["pwr"]
|
||||
self._attr_current_power_w = self._coordinator.data.get("power")
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class BroadlinkMP1Slot(BroadlinkSwitch):
|
||||
"""Representation of a Broadlink MP1 slot."""
|
||||
|
||||
_attr_assumed_state = False
|
||||
|
||||
def __init__(self, device, slot):
|
||||
"""Initialize the switch."""
|
||||
super().__init__(device, 1, 0)
|
||||
self._slot = slot
|
||||
self._state = self._coordinator.data[f"s{slot}"]
|
||||
|
||||
self._attr_name = f"{self._device.name} S{self._slot}"
|
||||
self._attr_unique_id = f"{self._device.unique_id}-s{self._slot}"
|
||||
self._attr_assumed_state = False
|
||||
self._attr_is_on = self._coordinator.data[f"s{slot}"]
|
||||
self._attr_name = f"{device.name} S{slot}"
|
||||
self._attr_unique_id = f"{device.unique_id}-s{slot}"
|
||||
|
||||
@callback
|
||||
def update_data(self):
|
||||
"""Update data."""
|
||||
if self._coordinator.last_update_success:
|
||||
self._state = self._coordinator.data[f"s{self._slot}"]
|
||||
self._attr_is_on = self._coordinator.data[f"s{self._slot}"]
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _async_send_packet(self, packet):
|
||||
@@ -286,22 +278,23 @@ class BroadlinkMP1Slot(BroadlinkSwitch):
|
||||
class BroadlinkBG1Slot(BroadlinkSwitch):
|
||||
"""Representation of a Broadlink BG1 slot."""
|
||||
|
||||
_attr_assumed_state = False
|
||||
|
||||
def __init__(self, device, slot):
|
||||
"""Initialize the switch."""
|
||||
super().__init__(device, 1, 0)
|
||||
self._slot = slot
|
||||
self._state = self._coordinator.data[f"pwr{slot}"]
|
||||
self._attr_is_on = self._coordinator.data[f"pwr{slot}"]
|
||||
|
||||
self._attr_name = f"{self._device.name} S{self._slot}"
|
||||
self._attr_name = f"{device.name} S{slot}"
|
||||
self._attr_device_class = DEVICE_CLASS_OUTLET
|
||||
self._attr_unique_id = f"{self._device.unique_id}-s{self._slot}"
|
||||
self._attr_assumed_state = False
|
||||
self._attr_unique_id = f"{device.unique_id}-s{slot}"
|
||||
|
||||
@callback
|
||||
def update_data(self):
|
||||
"""Update data."""
|
||||
if self._coordinator.last_update_success:
|
||||
self._state = self._coordinator.data[f"pwr{self._slot}"]
|
||||
self._attr_is_on = self._coordinator.data[f"pwr{self._slot}"]
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _async_send_packet(self, packet):
|
||||
|
@@ -4,13 +4,13 @@
|
||||
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
|
||||
"already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
|
||||
"cannot_connect": "Verbindung fehlgeschlagen",
|
||||
"invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse",
|
||||
"invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse",
|
||||
"not_supported": "Ger\u00e4t nicht unterst\u00fctzt",
|
||||
"unknown": "Unerwarteter Fehler"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung fehlgeschlagen",
|
||||
"invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse",
|
||||
"invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse",
|
||||
"unknown": "Unerwarteter Fehler"
|
||||
},
|
||||
"flow_title": "{name} ({model} unter {host})",
|
||||
|
@@ -3,15 +3,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ICON,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
PERCENTAGE,
|
||||
)
|
||||
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
|
||||
from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE
|
||||
|
||||
from .model import SensorDescription
|
||||
from .model import BrotherSensorMetadata
|
||||
|
||||
ATTR_BELT_UNIT_REMAINING_LIFE: Final = "belt_unit_remaining_life"
|
||||
ATTR_BLACK_DRUM_COUNTER: Final = "black_drum_counter"
|
||||
@@ -31,9 +26,7 @@ ATTR_DRUM_COUNTER: Final = "drum_counter"
|
||||
ATTR_DRUM_REMAINING_LIFE: Final = "drum_remaining_life"
|
||||
ATTR_DRUM_REMAINING_PAGES: Final = "drum_remaining_pages"
|
||||
ATTR_DUPLEX_COUNTER: Final = "duplex_unit_pages_counter"
|
||||
ATTR_ENABLED: Final = "enabled"
|
||||
ATTR_FUSER_REMAINING_LIFE: Final = "fuser_remaining_life"
|
||||
ATTR_LABEL: Final = "label"
|
||||
ATTR_LASER_REMAINING_LIFE: Final = "laser_remaining_life"
|
||||
ATTR_MAGENTA_DRUM_COUNTER: Final = "magenta_drum_counter"
|
||||
ATTR_MAGENTA_DRUM_REMAINING_LIFE: Final = "magenta_drum_remaining_life"
|
||||
@@ -46,7 +39,6 @@ ATTR_PF_KIT_1_REMAINING_LIFE: Final = "pf_kit_1_remaining_life"
|
||||
ATTR_PF_KIT_MP_REMAINING_LIFE: Final = "pf_kit_mp_remaining_life"
|
||||
ATTR_REMAINING_PAGES: Final = "remaining_pages"
|
||||
ATTR_STATUS: Final = "status"
|
||||
ATTR_UNIT: Final = "unit"
|
||||
ATTR_UPTIME: Final = "uptime"
|
||||
ATTR_YELLOW_DRUM_COUNTER: Final = "yellow_drum_counter"
|
||||
ATTR_YELLOW_DRUM_REMAINING_LIFE: Final = "yellow_drum_remaining_life"
|
||||
@@ -84,174 +76,172 @@ ATTRS_MAP: Final[dict[str, tuple[str, str]]] = {
|
||||
),
|
||||
}
|
||||
|
||||
SENSOR_TYPES: Final[dict[str, SensorDescription]] = {
|
||||
ATTR_STATUS: {
|
||||
ATTR_ICON: "mdi:printer",
|
||||
ATTR_LABEL: ATTR_STATUS.title(),
|
||||
ATTR_UNIT: None,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: None,
|
||||
},
|
||||
ATTR_PAGE_COUNTER: {
|
||||
ATTR_ICON: "mdi:file-document-outline",
|
||||
ATTR_LABEL: ATTR_PAGE_COUNTER.replace("_", " ").title(),
|
||||
ATTR_UNIT: UNIT_PAGES,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_BW_COUNTER: {
|
||||
ATTR_ICON: "mdi:file-document-outline",
|
||||
ATTR_LABEL: ATTR_BW_COUNTER.replace("_", " ").title(),
|
||||
ATTR_UNIT: UNIT_PAGES,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_COLOR_COUNTER: {
|
||||
ATTR_ICON: "mdi:file-document-outline",
|
||||
ATTR_LABEL: ATTR_COLOR_COUNTER.replace("_", " ").title(),
|
||||
ATTR_UNIT: UNIT_PAGES,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_DUPLEX_COUNTER: {
|
||||
ATTR_ICON: "mdi:file-document-outline",
|
||||
ATTR_LABEL: ATTR_DUPLEX_COUNTER.replace("_", " ").title(),
|
||||
ATTR_UNIT: UNIT_PAGES,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_DRUM_REMAINING_LIFE: {
|
||||
ATTR_ICON: "mdi:chart-donut",
|
||||
ATTR_LABEL: ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_BLACK_DRUM_REMAINING_LIFE: {
|
||||
ATTR_ICON: "mdi:chart-donut",
|
||||
ATTR_LABEL: ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_CYAN_DRUM_REMAINING_LIFE: {
|
||||
ATTR_ICON: "mdi:chart-donut",
|
||||
ATTR_LABEL: ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_MAGENTA_DRUM_REMAINING_LIFE: {
|
||||
ATTR_ICON: "mdi:chart-donut",
|
||||
ATTR_LABEL: ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_YELLOW_DRUM_REMAINING_LIFE: {
|
||||
ATTR_ICON: "mdi:chart-donut",
|
||||
ATTR_LABEL: ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_BELT_UNIT_REMAINING_LIFE: {
|
||||
ATTR_ICON: "mdi:current-ac",
|
||||
ATTR_LABEL: ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_FUSER_REMAINING_LIFE: {
|
||||
ATTR_ICON: "mdi:water-outline",
|
||||
ATTR_LABEL: ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_LASER_REMAINING_LIFE: {
|
||||
ATTR_ICON: "mdi:spotlight-beam",
|
||||
ATTR_LABEL: ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_PF_KIT_1_REMAINING_LIFE: {
|
||||
ATTR_ICON: "mdi:printer-3d",
|
||||
ATTR_LABEL: ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_PF_KIT_MP_REMAINING_LIFE: {
|
||||
ATTR_ICON: "mdi:printer-3d",
|
||||
ATTR_LABEL: ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_BLACK_TONER_REMAINING: {
|
||||
ATTR_ICON: "mdi:printer-3d-nozzle",
|
||||
ATTR_LABEL: ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_CYAN_TONER_REMAINING: {
|
||||
ATTR_ICON: "mdi:printer-3d-nozzle",
|
||||
ATTR_LABEL: ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_MAGENTA_TONER_REMAINING: {
|
||||
ATTR_ICON: "mdi:printer-3d-nozzle",
|
||||
ATTR_LABEL: ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_YELLOW_TONER_REMAINING: {
|
||||
ATTR_ICON: "mdi:printer-3d-nozzle",
|
||||
ATTR_LABEL: ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_BLACK_INK_REMAINING: {
|
||||
ATTR_ICON: "mdi:printer-3d-nozzle",
|
||||
ATTR_LABEL: ATTR_BLACK_INK_REMAINING.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_CYAN_INK_REMAINING: {
|
||||
ATTR_ICON: "mdi:printer-3d-nozzle",
|
||||
ATTR_LABEL: ATTR_CYAN_INK_REMAINING.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_MAGENTA_INK_REMAINING: {
|
||||
ATTR_ICON: "mdi:printer-3d-nozzle",
|
||||
ATTR_LABEL: ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_YELLOW_INK_REMAINING: {
|
||||
ATTR_ICON: "mdi:printer-3d-nozzle",
|
||||
ATTR_LABEL: ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(),
|
||||
ATTR_UNIT: PERCENTAGE,
|
||||
ATTR_ENABLED: True,
|
||||
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
},
|
||||
ATTR_UPTIME: {
|
||||
ATTR_ICON: None,
|
||||
ATTR_LABEL: ATTR_UPTIME.title(),
|
||||
ATTR_UNIT: None,
|
||||
ATTR_ENABLED: False,
|
||||
ATTR_STATE_CLASS: None,
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
|
||||
},
|
||||
SENSOR_TYPES: Final[dict[str, BrotherSensorMetadata]] = {
|
||||
ATTR_STATUS: BrotherSensorMetadata(
|
||||
icon="mdi:printer",
|
||||
label=ATTR_STATUS.title(),
|
||||
unit_of_measurement=None,
|
||||
enabled=True,
|
||||
),
|
||||
ATTR_PAGE_COUNTER: BrotherSensorMetadata(
|
||||
icon="mdi:file-document-outline",
|
||||
label=ATTR_PAGE_COUNTER.replace("_", " ").title(),
|
||||
unit_of_measurement=UNIT_PAGES,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_BW_COUNTER: BrotherSensorMetadata(
|
||||
icon="mdi:file-document-outline",
|
||||
label=ATTR_BW_COUNTER.replace("_", " ").title(),
|
||||
unit_of_measurement=UNIT_PAGES,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_COLOR_COUNTER: BrotherSensorMetadata(
|
||||
icon="mdi:file-document-outline",
|
||||
label=ATTR_COLOR_COUNTER.replace("_", " ").title(),
|
||||
unit_of_measurement=UNIT_PAGES,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_DUPLEX_COUNTER: BrotherSensorMetadata(
|
||||
icon="mdi:file-document-outline",
|
||||
label=ATTR_DUPLEX_COUNTER.replace("_", " ").title(),
|
||||
unit_of_measurement=UNIT_PAGES,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_DRUM_REMAINING_LIFE: BrotherSensorMetadata(
|
||||
icon="mdi:chart-donut",
|
||||
label=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_BLACK_DRUM_REMAINING_LIFE: BrotherSensorMetadata(
|
||||
icon="mdi:chart-donut",
|
||||
label=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_CYAN_DRUM_REMAINING_LIFE: BrotherSensorMetadata(
|
||||
icon="mdi:chart-donut",
|
||||
label=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_MAGENTA_DRUM_REMAINING_LIFE: BrotherSensorMetadata(
|
||||
icon="mdi:chart-donut",
|
||||
label=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_YELLOW_DRUM_REMAINING_LIFE: BrotherSensorMetadata(
|
||||
icon="mdi:chart-donut",
|
||||
label=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_BELT_UNIT_REMAINING_LIFE: BrotherSensorMetadata(
|
||||
icon="mdi:current-ac",
|
||||
label=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_FUSER_REMAINING_LIFE: BrotherSensorMetadata(
|
||||
icon="mdi:water-outline",
|
||||
label=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_LASER_REMAINING_LIFE: BrotherSensorMetadata(
|
||||
icon="mdi:spotlight-beam",
|
||||
label=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_PF_KIT_1_REMAINING_LIFE: BrotherSensorMetadata(
|
||||
icon="mdi:printer-3d",
|
||||
label=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_PF_KIT_MP_REMAINING_LIFE: BrotherSensorMetadata(
|
||||
icon="mdi:printer-3d",
|
||||
label=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_BLACK_TONER_REMAINING: BrotherSensorMetadata(
|
||||
icon="mdi:printer-3d-nozzle",
|
||||
label=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_CYAN_TONER_REMAINING: BrotherSensorMetadata(
|
||||
icon="mdi:printer-3d-nozzle",
|
||||
label=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_MAGENTA_TONER_REMAINING: BrotherSensorMetadata(
|
||||
icon="mdi:printer-3d-nozzle",
|
||||
label=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_YELLOW_TONER_REMAINING: BrotherSensorMetadata(
|
||||
icon="mdi:printer-3d-nozzle",
|
||||
label=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_BLACK_INK_REMAINING: BrotherSensorMetadata(
|
||||
icon="mdi:printer-3d-nozzle",
|
||||
label=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_CYAN_INK_REMAINING: BrotherSensorMetadata(
|
||||
icon="mdi:printer-3d-nozzle",
|
||||
label=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_MAGENTA_INK_REMAINING: BrotherSensorMetadata(
|
||||
icon="mdi:printer-3d-nozzle",
|
||||
label=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_YELLOW_INK_REMAINING: BrotherSensorMetadata(
|
||||
icon="mdi:printer-3d-nozzle",
|
||||
label=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(),
|
||||
unit_of_measurement=PERCENTAGE,
|
||||
enabled=True,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
ATTR_UPTIME: BrotherSensorMetadata(
|
||||
icon=None,
|
||||
label=ATTR_UPTIME.title(),
|
||||
unit_of_measurement=None,
|
||||
enabled=False,
|
||||
device_class=DEVICE_CLASS_TIMESTAMP,
|
||||
),
|
||||
}
|
||||
|
@@ -1,15 +1,15 @@
|
||||
"""Type definitions for Brother integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypedDict
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class SensorDescription(TypedDict, total=False):
|
||||
"""Sensor description class."""
|
||||
class BrotherSensorMetadata(NamedTuple):
|
||||
"""Metadata for an individual Brother sensor."""
|
||||
|
||||
icon: str | None
|
||||
label: str
|
||||
unit: str | None
|
||||
unit_of_measurement: str | None
|
||||
enabled: bool
|
||||
state_class: str | None
|
||||
device_class: str | None
|
||||
state_class: str | None = None
|
||||
device_class: str | None = None
|
||||
|
@@ -3,9 +3,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -14,17 +13,15 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from . import BrotherDataUpdateCoordinator
|
||||
from .const import (
|
||||
ATTR_COUNTER,
|
||||
ATTR_ENABLED,
|
||||
ATTR_LABEL,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_REMAINING_PAGES,
|
||||
ATTR_UNIT,
|
||||
ATTR_UPTIME,
|
||||
ATTRS_MAP,
|
||||
DATA_CONFIG_ENTRY,
|
||||
DOMAIN,
|
||||
SENSOR_TYPES,
|
||||
)
|
||||
from .model import BrotherSensorMetadata
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -43,9 +40,11 @@ async def async_setup_entry(
|
||||
"sw_version": getattr(coordinator.data, "firmware", None),
|
||||
}
|
||||
|
||||
for sensor in SENSOR_TYPES:
|
||||
for sensor, metadata in SENSOR_TYPES.items():
|
||||
if sensor in coordinator.data:
|
||||
sensors.append(BrotherPrinterSensor(coordinator, sensor, device_info))
|
||||
sensors.append(
|
||||
BrotherPrinterSensor(coordinator, sensor, metadata, device_info)
|
||||
)
|
||||
async_add_entities(sensors, False)
|
||||
|
||||
|
||||
@@ -56,20 +55,20 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity):
|
||||
self,
|
||||
coordinator: BrotherDataUpdateCoordinator,
|
||||
kind: str,
|
||||
metadata: BrotherSensorMetadata,
|
||||
device_info: DeviceInfo,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
description = SENSOR_TYPES[kind]
|
||||
self._attrs: dict[str, Any] = {}
|
||||
self._attr_device_class = description.get(ATTR_DEVICE_CLASS)
|
||||
self._attr_device_class = metadata.device_class
|
||||
self._attr_device_info = device_info
|
||||
self._attr_entity_registry_enabled_default = description[ATTR_ENABLED]
|
||||
self._attr_icon = description[ATTR_ICON]
|
||||
self._attr_name = f"{coordinator.data.model} {description[ATTR_LABEL]}"
|
||||
self._attr_state_class = description[ATTR_STATE_CLASS]
|
||||
self._attr_entity_registry_enabled_default = metadata.enabled
|
||||
self._attr_icon = metadata.icon
|
||||
self._attr_name = f"{coordinator.data.model} {metadata.label}"
|
||||
self._attr_state_class = metadata.state_class
|
||||
self._attr_unique_id = f"{coordinator.data.serial.lower()}_{kind}"
|
||||
self._attr_unit_of_measurement = description[ATTR_UNIT]
|
||||
self._attr_unit_of_measurement = metadata.unit_of_measurement
|
||||
self.kind = kind
|
||||
|
||||
@property
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Dieser Drucker ist bereits konfiguriert",
|
||||
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
|
||||
"unsupported_model": "Dieses Druckermodell wird nicht unterst\u00fctzt."
|
||||
},
|
||||
"error": {
|
||||
|
@@ -27,7 +27,6 @@ from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
@@ -88,6 +87,10 @@ async def async_setup_entry(
|
||||
class BSBLanClimate(ClimateEntity):
|
||||
"""Defines a BSBLan climate device."""
|
||||
|
||||
_attr_supported_features = SUPPORT_FLAGS
|
||||
_attr_hvac_modes = HVAC_MODES
|
||||
_attr_preset_modes = PRESET_MODES
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry_id: str,
|
||||
@@ -95,89 +98,33 @@ class BSBLanClimate(ClimateEntity):
|
||||
info: Info,
|
||||
) -> None:
|
||||
"""Initialize BSBLan climate device."""
|
||||
self._current_temperature: float | None = None
|
||||
self._available = True
|
||||
self._hvac_mode: str | None = None
|
||||
self._target_temperature: float | None = None
|
||||
self._temperature_unit = None
|
||||
self._preset_mode: str | None = None
|
||||
self._attr_available = True
|
||||
self._store_hvac_mode = None
|
||||
self._info: Info = info
|
||||
self.bsblan = bsblan
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return self._info.device_identification
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID for this sensor."""
|
||||
return self._info.device_identification
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
if self._temperature_unit == "°C":
|
||||
return TEMP_CELSIUS
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_FLAGS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return the current operation mode."""
|
||||
return self._hvac_mode
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return HVAC_MODES
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def preset_modes(self):
|
||||
"""List of available preset modes."""
|
||||
return PRESET_MODES
|
||||
|
||||
@property
|
||||
def preset_mode(self):
|
||||
"""Return the preset_mode."""
|
||||
return self._preset_mode
|
||||
self._attr_name = self._attr_unique_id = info.device_identification
|
||||
self._attr_device_info = {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, info.device_identification)},
|
||||
ATTR_NAME: "BSBLan Device",
|
||||
ATTR_MANUFACTURER: "BSBLan",
|
||||
ATTR_MODEL: info.controller_variant,
|
||||
}
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode):
|
||||
"""Set preset mode."""
|
||||
_LOGGER.debug("Setting preset mode to: %s", preset_mode)
|
||||
if preset_mode == PRESET_NONE:
|
||||
# restore previous hvac mode
|
||||
self._hvac_mode = self._store_hvac_mode
|
||||
self._attr_hvac_mode = self._store_hvac_mode
|
||||
else:
|
||||
# Store hvac mode.
|
||||
self._store_hvac_mode = self._hvac_mode
|
||||
self._store_hvac_mode = self._attr_hvac_mode
|
||||
await self.async_set_data(preset_mode=preset_mode)
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
"""Set HVAC mode."""
|
||||
_LOGGER.debug("Setting HVAC mode to: %s", hvac_mode)
|
||||
# preset should be none when hvac mode is set
|
||||
self._preset_mode = PRESET_NONE
|
||||
self._attr_preset_mode = PRESET_NONE
|
||||
await self.async_set_data(hvac_mode=hvac_mode)
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
@@ -204,39 +151,33 @@ class BSBLanClimate(ClimateEntity):
|
||||
await self.bsblan.thermostat(**data)
|
||||
except BSBLanError:
|
||||
_LOGGER.error("An error occurred while updating the BSBLan device")
|
||||
self._available = False
|
||||
self._attr_available = False
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update BSBlan entity."""
|
||||
try:
|
||||
state: State = await self.bsblan.state()
|
||||
except BSBLanError:
|
||||
if self._available:
|
||||
if self.available:
|
||||
_LOGGER.error("An error occurred while updating the BSBLan device")
|
||||
self._available = False
|
||||
self._attr_available = False
|
||||
return
|
||||
|
||||
self._available = True
|
||||
self._attr_available = True
|
||||
|
||||
self._current_temperature = float(state.current_temperature.value)
|
||||
self._target_temperature = float(state.target_temperature.value)
|
||||
self._attr_current_temperature = float(state.current_temperature.value)
|
||||
self._attr_target_temperature = float(state.target_temperature.value)
|
||||
|
||||
# check if preset is active else get hvac mode
|
||||
_LOGGER.debug("state hvac/preset mode: %s", state.hvac_mode.value)
|
||||
if state.hvac_mode.value == "2":
|
||||
self._preset_mode = PRESET_ECO
|
||||
self._attr_preset_mode = PRESET_ECO
|
||||
else:
|
||||
self._hvac_mode = BSBLAN_TO_HA_STATE[state.hvac_mode.value]
|
||||
self._preset_mode = PRESET_NONE
|
||||
self._attr_hvac_mode = BSBLAN_TO_HA_STATE[state.hvac_mode.value]
|
||||
self._attr_preset_mode = PRESET_NONE
|
||||
|
||||
self._temperature_unit = state.current_temperature.unit
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information about this BSBLan device."""
|
||||
return {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, self._info.device_identification)},
|
||||
ATTR_NAME: "BSBLan Device",
|
||||
ATTR_MANUFACTURER: "BSBLan",
|
||||
ATTR_MODEL: self._info.controller_variant,
|
||||
}
|
||||
self._attr_temperature_unit = (
|
||||
TEMP_CELSIUS
|
||||
if state.current_temperature.unit == "°C"
|
||||
else TEMP_FAHRENHEIT
|
||||
)
|
||||
|
@@ -13,7 +13,7 @@
|
||||
"host": "Host",
|
||||
"passkey": "Passkey String",
|
||||
"password": "Passwort",
|
||||
"port": "Port Nummer",
|
||||
"port": "Port",
|
||||
"username": "Benutzername"
|
||||
},
|
||||
"description": "Richte dein BSB-Lan Ger\u00e4t f\u00fcr die Integration mit dem Home Assistant ein.",
|
||||
|
@@ -119,24 +119,13 @@ class WebDavCalendarEventDevice(CalendarEventDevice):
|
||||
self.data = WebDavCalendarData(calendar, days, all_day, search)
|
||||
self.entity_id = entity_id
|
||||
self._event = None
|
||||
self._name = name
|
||||
self._offset_reached = False
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
return {"offset_reached": self._offset_reached}
|
||||
self._attr_name = name
|
||||
|
||||
@property
|
||||
def event(self):
|
||||
"""Return the next upcoming event."""
|
||||
return self._event
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
async def async_get_events(self, hass, start_date, end_date):
|
||||
"""Get all events in a specific time frame."""
|
||||
return await self.data.async_get_events(hass, start_date, end_date)
|
||||
@@ -149,8 +138,8 @@ class WebDavCalendarEventDevice(CalendarEventDevice):
|
||||
self._event = event
|
||||
return
|
||||
event = calculate_offset(event, OFFSET)
|
||||
self._offset_reached = is_offset_reached(event)
|
||||
self._event = event
|
||||
self._attr_extra_state_attributes = {"offset_reached": is_offset_reached(event)}
|
||||
|
||||
|
||||
class WebDavCalendarData:
|
||||
|
@@ -52,6 +52,9 @@ class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity):
|
||||
"""Representation of a Canary alarm control panel."""
|
||||
|
||||
coordinator: CanaryDataUpdateCoordinator
|
||||
_attr_supported_features = (
|
||||
SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, coordinator: CanaryDataUpdateCoordinator, location: Location
|
||||
@@ -59,23 +62,14 @@ class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity):
|
||||
"""Initialize a Canary security camera."""
|
||||
super().__init__(coordinator)
|
||||
self._location_id: str = location.location_id
|
||||
self._location_name: str = location.name
|
||||
self._attr_name = location.name
|
||||
self._attr_unique_id = str(self._location_id)
|
||||
|
||||
@property
|
||||
def location(self) -> Location:
|
||||
"""Return information about the location."""
|
||||
return self.coordinator.data["locations"][self._location_id]
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the alarm."""
|
||||
return self._location_name
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID of the alarm."""
|
||||
return str(self._location_id)
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of the device."""
|
||||
@@ -92,11 +86,6 @@ class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity):
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
|
@@ -21,7 +21,6 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import Throttle
|
||||
@@ -30,7 +29,6 @@ from .const import (
|
||||
CONF_FFMPEG_ARGUMENTS,
|
||||
DATA_COORDINATOR,
|
||||
DEFAULT_FFMPEG_ARGUMENTS,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
)
|
||||
@@ -73,7 +71,6 @@ async def async_setup_entry(
|
||||
coordinator,
|
||||
location_id,
|
||||
device,
|
||||
DEFAULT_TIMEOUT,
|
||||
ffmpeg_arguments,
|
||||
)
|
||||
)
|
||||
@@ -92,7 +89,6 @@ class CanaryCamera(CoordinatorEntity, Camera):
|
||||
coordinator: CanaryDataUpdateCoordinator,
|
||||
location_id: str,
|
||||
device: Device,
|
||||
timeout: int,
|
||||
ffmpeg_args: str,
|
||||
) -> None:
|
||||
"""Initialize a Canary security camera."""
|
||||
@@ -102,37 +98,21 @@ class CanaryCamera(CoordinatorEntity, Camera):
|
||||
self._ffmpeg_arguments = ffmpeg_args
|
||||
self._location_id = location_id
|
||||
self._device = device
|
||||
self._device_id: str = device.device_id
|
||||
self._device_name: str = device.name
|
||||
self._device_type_name = device.device_type["name"]
|
||||
self._timeout = timeout
|
||||
self._live_stream_session: LiveStreamSession | None = None
|
||||
self._attr_name = device.name
|
||||
self._attr_unique_id = str(device.device_id)
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, str(device.device_id))},
|
||||
"name": device.name,
|
||||
"model": device.device_type["name"],
|
||||
"manufacturer": MANUFACTURER,
|
||||
}
|
||||
|
||||
@property
|
||||
def location(self) -> Location:
|
||||
"""Return information about the location."""
|
||||
return self.coordinator.data["locations"][self._location_id]
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of this device."""
|
||||
return self._device_name
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID of this camera."""
|
||||
return str(self._device_id)
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device_info of the device."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, str(self._device_id))},
|
||||
"name": self._device_name,
|
||||
"model": self._device_type_name,
|
||||
"manufacturer": MANUFACTURER,
|
||||
}
|
||||
|
||||
@property
|
||||
def is_recording(self) -> bool:
|
||||
"""Return true if the device is recording."""
|
||||
|
@@ -17,7 +17,6 @@ from homeassistant.const import (
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -97,11 +96,9 @@ class CanarySensor(CoordinatorEntity, SensorEntity):
|
||||
super().__init__(coordinator)
|
||||
self._sensor_type = sensor_type
|
||||
self._device_id = device.device_id
|
||||
self._device_name = device.name
|
||||
self._device_type_name = device.device_type["name"]
|
||||
|
||||
sensor_type_name = sensor_type[0].replace("_", " ").title()
|
||||
self._name = f"{location.name} {device.name} {sensor_type_name}"
|
||||
self._attr_name = f"{location.name} {device.name} {sensor_type_name}"
|
||||
|
||||
canary_sensor_type = None
|
||||
if self._sensor_type[0] == "air_quality":
|
||||
@@ -116,6 +113,17 @@ class CanarySensor(CoordinatorEntity, SensorEntity):
|
||||
canary_sensor_type = SensorType.BATTERY
|
||||
|
||||
self._canary_type = canary_sensor_type
|
||||
self._attr_state = self.reading
|
||||
self._attr_unique_id = f"{device.device_id}_{sensor_type[0]}"
|
||||
self._attr_device_info = {
|
||||
"identifiers": {(DOMAIN, str(device.device_id))},
|
||||
"name": device.name,
|
||||
"model": device.device_type["name"],
|
||||
"manufacturer": MANUFACTURER,
|
||||
}
|
||||
self._attr_unit_of_measurement = sensor_type[1]
|
||||
self._attr_device_class = sensor_type[3]
|
||||
self._attr_icon = sensor_type[2]
|
||||
|
||||
@property
|
||||
def reading(self) -> float | None:
|
||||
@@ -136,46 +144,6 @@ class CanarySensor(CoordinatorEntity, SensorEntity):
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the Canary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.reading
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID of this sensor."""
|
||||
return f"{self._device_id}_{self._sensor_type[0]}"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device_info of the device."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, str(self._device_id))},
|
||||
"name": self._device_name,
|
||||
"model": self._device_type_name,
|
||||
"manufacturer": MANUFACTURER,
|
||||
}
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
return self._sensor_type[1]
|
||||
|
||||
@property
|
||||
def device_class(self) -> str | None:
|
||||
"""Device class for the sensor."""
|
||||
return self._sensor_type[3]
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Icon for the sensor."""
|
||||
return self._sensor_type[2]
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str] | None:
|
||||
"""Return the state attributes."""
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich."
|
||||
"single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
|
||||
},
|
||||
"error": {
|
||||
"invalid_known_hosts": "Bekannte Hosts m\u00fcssen eine durch Kommata getrennte Liste von Hosts sein."
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Diese Kombination aus Host und Port ist bereits konfiguriert.",
|
||||
"already_configured": "Der Dienst ist bereits konfiguriert",
|
||||
"import_failed": "Import aus Konfiguration fehlgeschlagen"
|
||||
},
|
||||
"error": {
|
||||
|
@@ -158,7 +158,7 @@ class ClimaCellSensorMetadata:
|
||||
name: str
|
||||
unit_imperial: str | None = None
|
||||
unit_metric: str | None = None
|
||||
metric_conversion: Callable | float = 1.0
|
||||
metric_conversion: Callable[[float], float] | float = 1.0
|
||||
is_metric_check: bool | None = None
|
||||
device_class: str | None = None
|
||||
value_map: IntEnum | None = None
|
||||
|
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"title": "ClimaCell",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
|
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_token": "API Token"
|
||||
"api_token": "API-Token"
|
||||
},
|
||||
"description": "F\u00fcr diese Integration ist ein API-Token erforderlich, der mit Zone: Zone: Lesen und Zone: DNS: Bearbeiten f\u00fcr alle Zonen in deinem Konto erstellt wurde.",
|
||||
"title": "Mit Cloudflare verbinden"
|
||||
|
@@ -114,9 +114,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
new_entry_type = TYPE_USE_HOME
|
||||
|
||||
for entry in self._async_current_entries(include_ignore=True):
|
||||
if entry.source == config_entries.SOURCE_IGNORE:
|
||||
continue
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
|
||||
if (cur_entry_type := _get_entry_type(entry.data)) != new_entry_type:
|
||||
continue
|
||||
|
34
homeassistant/components/co2signal/translations/ca.json
Normal file
34
homeassistant/components/co2signal/translations/ca.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "El dispositiu ja est\u00e0 configurat",
|
||||
"api_ratelimit": "S'ha superat la taxa l\u00edmit d'API",
|
||||
"unknown": "Error inesperat"
|
||||
},
|
||||
"error": {
|
||||
"api_ratelimit": "S'ha superat la taxa l\u00edmit d'API",
|
||||
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
|
||||
"unknown": "Error inesperat"
|
||||
},
|
||||
"step": {
|
||||
"coordinates": {
|
||||
"data": {
|
||||
"latitude": "Latitud",
|
||||
"longitude": "Longitud"
|
||||
}
|
||||
},
|
||||
"country": {
|
||||
"data": {
|
||||
"country_code": "Codi de pa\u00eds"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Token d'acc\u00e9s",
|
||||
"location": "Obt\u00e9 dades per"
|
||||
},
|
||||
"description": "Visita https://co2signal.com/ per demanar un token."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
homeassistant/components/co2signal/translations/de.json
Normal file
34
homeassistant/components/co2signal/translations/de.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
|
||||
"api_ratelimit": "API Ratelimit \u00fcberschritten",
|
||||
"unknown": "Unerwarteter Fehler"
|
||||
},
|
||||
"error": {
|
||||
"api_ratelimit": "API Ratelimit \u00fcberschritten",
|
||||
"invalid_auth": "Ung\u00fcltige Authentifizierung",
|
||||
"unknown": "Unerwarteter Fehler"
|
||||
},
|
||||
"step": {
|
||||
"coordinates": {
|
||||
"data": {
|
||||
"latitude": "Breitengrad",
|
||||
"longitude": "L\u00e4ngengrad"
|
||||
}
|
||||
},
|
||||
"country": {
|
||||
"data": {
|
||||
"country_code": "L\u00e4ndercode"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Zugangstoken",
|
||||
"location": "Daten abrufen f\u00fcr"
|
||||
},
|
||||
"description": "Besuche https://co2signal.com/, um ein Token anzufordern."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
homeassistant/components/co2signal/translations/et.json
Normal file
34
homeassistant/components/co2signal/translations/et.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Seade on juba h\u00e4\u00e4lestatud",
|
||||
"api_ratelimit": "API p\u00e4rigute limiit on \u00fcletatud",
|
||||
"unknown": "Ootamatu t\u00f5rge"
|
||||
},
|
||||
"error": {
|
||||
"api_ratelimit": "API p\u00e4rigute limiit on \u00fcletatud",
|
||||
"invalid_auth": "Tuvastamine nurjus",
|
||||
"unknown": "Ootamatu t\u00f5rge"
|
||||
},
|
||||
"step": {
|
||||
"coordinates": {
|
||||
"data": {
|
||||
"latitude": "Laiuskraad",
|
||||
"longitude": "Pikkuskraad"
|
||||
}
|
||||
},
|
||||
"country": {
|
||||
"data": {
|
||||
"country_code": "Riigi kood"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Juurdep\u00e4\u00e4sut\u00f5end",
|
||||
"location": "Hangi andmed"
|
||||
},
|
||||
"description": "Loa taotlemiseks k\u00fclasta https://co2signal.com/."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
homeassistant/components/co2signal/translations/nl.json
Normal file
34
homeassistant/components/co2signal/translations/nl.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Apparaat is al geconfigureerd",
|
||||
"api_ratelimit": "API Ratelimit overschreden",
|
||||
"unknown": "Onverwachte fout"
|
||||
},
|
||||
"error": {
|
||||
"api_ratelimit": "API Ratelimit overschreden",
|
||||
"invalid_auth": "Ongeldige authenticatie",
|
||||
"unknown": "Onverwachte fout"
|
||||
},
|
||||
"step": {
|
||||
"coordinates": {
|
||||
"data": {
|
||||
"latitude": "Breedtegraad",
|
||||
"longitude": "Lengtegraad"
|
||||
}
|
||||
},
|
||||
"country": {
|
||||
"data": {
|
||||
"country_code": "Landcode"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Toegangstoken",
|
||||
"location": "Gegevens ophalen voor"
|
||||
},
|
||||
"description": "Ga naar https://co2signal.com/ om een token aan te vragen."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user