Merge branch 'dev' into tesla-mfa

This commit is contained in:
BreakingBread0
2021-07-22 23:18:53 +02:00
committed by GitHub
534 changed files with 12110 additions and 4309 deletions

View File

@@ -20,6 +20,8 @@ omit =
homeassistant/components/acmeda/helpers.py homeassistant/components/acmeda/helpers.py
homeassistant/components/acmeda/hub.py homeassistant/components/acmeda/hub.py
homeassistant/components/acmeda/sensor.py homeassistant/components/acmeda/sensor.py
homeassistant/components/adax/__init__.py
homeassistant/components/adax/climate.py
homeassistant/components/adguard/__init__.py homeassistant/components/adguard/__init__.py
homeassistant/components/adguard/const.py homeassistant/components/adguard/const.py
homeassistant/components/adguard/sensor.py homeassistant/components/adguard/sensor.py
@@ -73,6 +75,12 @@ omit =
homeassistant/components/asuswrt/router.py homeassistant/components/asuswrt/router.py
homeassistant/components/aten_pe/* homeassistant/components/aten_pe/*
homeassistant/components/atome/* 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/__init__.py
homeassistant/components/aurora/binary_sensor.py homeassistant/components/aurora/binary_sensor.py
homeassistant/components/aurora/const.py homeassistant/components/aurora/const.py
@@ -130,9 +138,7 @@ omit =
homeassistant/components/brottsplatskartan/sensor.py homeassistant/components/brottsplatskartan/sensor.py
homeassistant/components/browser/* homeassistant/components/browser/*
homeassistant/components/brunt/cover.py homeassistant/components/brunt/cover.py
homeassistant/components/bsblan/__init__.py
homeassistant/components/bsblan/climate.py homeassistant/components/bsblan/climate.py
homeassistant/components/bsblan/const.py
homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_home_hub_5/device_tracker.py
homeassistant/components/bt_smarthub/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py
homeassistant/components/buienradar/sensor.py homeassistant/components/buienradar/sensor.py
@@ -622,6 +628,7 @@ omit =
homeassistant/components/mill/__init__.py homeassistant/components/mill/__init__.py
homeassistant/components/mill/climate.py homeassistant/components/mill/climate.py
homeassistant/components/mill/const.py homeassistant/components/mill/const.py
homeassistant/components/mill/sensor.py
homeassistant/components/minecraft_server/__init__.py homeassistant/components/minecraft_server/__init__.py
homeassistant/components/minecraft_server/binary_sensor.py homeassistant/components/minecraft_server/binary_sensor.py
homeassistant/components/minecraft_server/const.py homeassistant/components/minecraft_server/const.py
@@ -690,6 +697,7 @@ omit =
homeassistant/components/neurio_energy/sensor.py homeassistant/components/neurio_energy/sensor.py
homeassistant/components/nexia/climate.py homeassistant/components/nexia/climate.py
homeassistant/components/nextcloud/* homeassistant/components/nextcloud/*
homeassistant/components/nfandroidtv/__init__.py
homeassistant/components/nfandroidtv/notify.py homeassistant/components/nfandroidtv/notify.py
homeassistant/components/niko_home_control/light.py homeassistant/components/niko_home_control/light.py
homeassistant/components/nilu/air_quality.py homeassistant/components/nilu/air_quality.py
@@ -967,7 +975,6 @@ omit =
homeassistant/components/sonos/* homeassistant/components/sonos/*
homeassistant/components/sony_projector/switch.py homeassistant/components/sony_projector/switch.py
homeassistant/components/spc/* homeassistant/components/spc/*
homeassistant/components/speedtestdotnet/*
homeassistant/components/spider/* homeassistant/components/spider/*
homeassistant/components/splunk/* homeassistant/components/splunk/*
homeassistant/components/spotify/__init__.py homeassistant/components/spotify/__init__.py

View File

@@ -9,10 +9,12 @@ homeassistant.components.actiontec.*
homeassistant.components.aftership.* homeassistant.components.aftership.*
homeassistant.components.air_quality.* homeassistant.components.air_quality.*
homeassistant.components.airly.* homeassistant.components.airly.*
homeassistant.components.airvisual.*
homeassistant.components.aladdin_connect.* homeassistant.components.aladdin_connect.*
homeassistant.components.alarm_control_panel.* homeassistant.components.alarm_control_panel.*
homeassistant.components.amazon_polly.* homeassistant.components.amazon_polly.*
homeassistant.components.ambee.* homeassistant.components.ambee.*
homeassistant.components.ambient_station.*
homeassistant.components.ampio.* homeassistant.components.ampio.*
homeassistant.components.automation.* homeassistant.components.automation.*
homeassistant.components.binary_sensor.* homeassistant.components.binary_sensor.*
@@ -41,6 +43,7 @@ homeassistant.components.fritz.*
homeassistant.components.geo_location.* homeassistant.components.geo_location.*
homeassistant.components.gios.* homeassistant.components.gios.*
homeassistant.components.group.* homeassistant.components.group.*
homeassistant.components.guardian.*
homeassistant.components.history.* homeassistant.components.history.*
homeassistant.components.homeassistant.triggers.event homeassistant.components.homeassistant.triggers.event
homeassistant.components.http.* homeassistant.components.http.*
@@ -58,6 +61,7 @@ homeassistant.components.mailbox.*
homeassistant.components.media_player.* homeassistant.components.media_player.*
homeassistant.components.mysensors.* homeassistant.components.mysensors.*
homeassistant.components.nam.* homeassistant.components.nam.*
homeassistant.components.netatmo.*
homeassistant.components.network.* homeassistant.components.network.*
homeassistant.components.no_ip.* homeassistant.components.no_ip.*
homeassistant.components.notify.* homeassistant.components.notify.*
@@ -73,6 +77,7 @@ homeassistant.components.remote.*
homeassistant.components.scene.* homeassistant.components.scene.*
homeassistant.components.select.* homeassistant.components.select.*
homeassistant.components.sensor.* homeassistant.components.sensor.*
homeassistant.components.shelly.*
homeassistant.components.slack.* homeassistant.components.slack.*
homeassistant.components.sonos.media_player homeassistant.components.sonos.media_player
homeassistant.components.ssdp.* homeassistant.components.ssdp.*

13
.vscode/tasks.json vendored
View File

@@ -5,10 +5,7 @@
"label": "Run Home Assistant Core", "label": "Run Home Assistant Core",
"type": "shell", "type": "shell",
"command": "hass -c ./config", "command": "hass -c ./config",
"group": { "group": "test",
"kind": "test",
"isDefault": true
},
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new" "panel": "new"
@@ -19,7 +16,9 @@
"label": "Pytest", "label": "Pytest",
"type": "shell", "type": "shell",
"command": "pytest --timeout=10 tests", "command": "pytest --timeout=10 tests",
"dependsOn": ["Install all Test Requirements"], "dependsOn": [
"Install all Test Requirements"
],
"group": { "group": {
"kind": "test", "kind": "test",
"isDefault": true "isDefault": true
@@ -48,7 +47,9 @@
"label": "Pylint", "label": "Pylint",
"type": "shell", "type": "shell",
"command": "pylint homeassistant", "command": "pylint homeassistant",
"dependsOn": ["Install all Requirements"], "dependsOn": [
"Install all Requirements"
],
"group": { "group": {
"kind": "test", "kind": "test",
"isDefault": true "isDefault": true

View File

@@ -22,6 +22,7 @@ homeassistant/scripts/check_config.py @kellerza
homeassistant/components/abode/* @shred86 homeassistant/components/abode/* @shred86
homeassistant/components/accuweather/* @bieniu homeassistant/components/accuweather/* @bieniu
homeassistant/components/acmeda/* @atmurray homeassistant/components/acmeda/* @atmurray
homeassistant/components/adax/* @danielhiversen
homeassistant/components/adguard/* @frenck homeassistant/components/adguard/* @frenck
homeassistant/components/advantage_air/* @Bre77 homeassistant/components/advantage_air/* @Bre77
homeassistant/components/aemet/* @noltari homeassistant/components/aemet/* @noltari
@@ -55,6 +56,7 @@ homeassistant/components/august/* @bdraco
homeassistant/components/aurora/* @djtimca homeassistant/components/aurora/* @djtimca
homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/aurora_abb_powerone/* @davet2001
homeassistant/components/auth/* @home-assistant/core homeassistant/components/auth/* @home-assistant/core
homeassistant/components/automate/* @sillyfrog
homeassistant/components/automation/* @home-assistant/core homeassistant/components/automation/* @home-assistant/core
homeassistant/components/avea/* @pattyland homeassistant/components/avea/* @pattyland
homeassistant/components/awair/* @ahayworth @danielsjf homeassistant/components/awair/* @ahayworth @danielsjf
@@ -160,6 +162,7 @@ homeassistant/components/fireservicerota/* @cyberjunky
homeassistant/components/firmata/* @DaAwesomeP homeassistant/components/firmata/* @DaAwesomeP
homeassistant/components/fixer/* @fabaff homeassistant/components/fixer/* @fabaff
homeassistant/components/flick_electric/* @ZephireNZ homeassistant/components/flick_electric/* @ZephireNZ
homeassistant/components/flipr/* @cnico
homeassistant/components/flo/* @dmulcahey homeassistant/components/flo/* @dmulcahey
homeassistant/components/flock/* @fabaff homeassistant/components/flock/* @fabaff
homeassistant/components/flume/* @ChrisMandich @bdraco homeassistant/components/flume/* @ChrisMandich @bdraco
@@ -331,6 +334,7 @@ homeassistant/components/netdata/* @fabaff
homeassistant/components/nexia/* @bdraco homeassistant/components/nexia/* @bdraco
homeassistant/components/nextbus/* @vividboarder homeassistant/components/nextbus/* @vividboarder
homeassistant/components/nextcloud/* @meichthys homeassistant/components/nextcloud/* @meichthys
homeassistant/components/nfandroidtv/* @tkdrob
homeassistant/components/nightscout/* @marciogranzotto homeassistant/components/nightscout/* @marciogranzotto
homeassistant/components/nilu/* @hfurubotten homeassistant/components/nilu/* @hfurubotten
homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nissan_leaf/* @filcole
@@ -562,6 +566,7 @@ homeassistant/components/websocket_api/* @home-assistant/core
homeassistant/components/wemo/* @esev homeassistant/components/wemo/* @esev
homeassistant/components/wiffi/* @mampfes homeassistant/components/wiffi/* @mampfes
homeassistant/components/wilight/* @leofig-rj homeassistant/components/wilight/* @leofig-rj
homeassistant/components/wirelesstag/* @sergeymaysak
homeassistant/components/withings/* @vangorra homeassistant/components/withings/* @vangorra
homeassistant/components/wled/* @frenck homeassistant/components/wled/* @frenck
homeassistant/components/wolflink/* @adamkrol93 homeassistant/components/wolflink/* @adamkrol93

View File

@@ -26,7 +26,7 @@
"user": { "user": {
"data": { "data": {
"password": "Passwort", "password": "Passwort",
"username": "E-Mail-Adresse" "username": "E-Mail"
}, },
"title": "Gib deine Abode-Anmeldeinformationen ein" "title": "Gib deine Abode-Anmeldeinformationen ein"
} }

View 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)

View 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

View 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."""

View File

@@ -0,0 +1,5 @@
"""Constants for the Adax integration."""
from typing import Final
ACCOUNT_ID: Final = "account_id"
DOMAIN: Final = "adax"

View 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"
}

View 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%]"
}
}
}

View 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"
}
}
}
}
}

View 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"
}
}
}
}
}

View 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"
}
}
}
}
}

View 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"
}
}
}
}
}

View 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"
}
}
}
}
}

View File

@@ -17,7 +17,7 @@
"host": "Host", "host": "Host",
"password": "Passwort", "password": "Passwort",
"port": "Port", "port": "Port",
"ssl": "AdGuard Home verwendet ein SSL-Zertifikat", "ssl": "Verwendet ein SSL-Zertifikat",
"username": "Benutzername", "username": "Benutzername",
"verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
}, },

View File

@@ -2,7 +2,7 @@
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity 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 homeassistant.helpers import config_validation as cv, entity_platform
from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN 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, "On"))
entities.append(AdvantageAirTimeTo(instance, ac_key, "Off")) entities.append(AdvantageAirTimeTo(instance, ac_key, "Off"))
for zone_key, zone in ac_device["zones"].items(): 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: if zone["type"] != 0:
entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) 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 # Only show wireless signal strength sensors when using wireless sensors
if zone["rssi"] > 0: if zone["rssi"] > 0:
entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key)) entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key))
@@ -144,3 +145,23 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity):
if self._zone["rssi"] >= 20: if self._zone["rssi"] >= 20:
return "mdi:wifi-strength-1" return "mdi:wifi-strength-1"
return "mdi:wifi-strength-outline" 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"]

View File

@@ -9,7 +9,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"ip_address": "IP Adresse", "ip_address": "IP-Adresse",
"port": "Port" "port": "Port"
}, },
"description": "Anschluss an die API deines Advantage Air Wandtabletts.", "description": "Anschluss an die API deines Advantage Air Wandtabletts.",

View File

@@ -1,5 +1,4 @@
{ {
"title": "AirNow",
"config": { "config": {
"step": { "step": {
"user": { "user": {

View File

@@ -1,6 +1,10 @@
"""The airvisual component.""" """The airvisual component."""
from __future__ import annotations
from collections.abc import Mapping, MutableMapping
from datetime import timedelta from datetime import timedelta
from math import ceil from math import ceil
from typing import Any, Dict, cast
from pyairvisual import CloudAPI, NodeSamba from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import ( from pyairvisual.errors import (
@@ -10,6 +14,7 @@ from pyairvisual.errors import (
NodeProError, NodeProError,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
CONF_API_KEY, CONF_API_KEY,
@@ -20,7 +25,7 @@ from homeassistant.const import (
CONF_SHOW_ON_MAP, CONF_SHOW_ON_MAP,
CONF_STATE, CONF_STATE,
) )
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import ( from homeassistant.helpers import (
aiohttp_client, aiohttp_client,
@@ -57,11 +62,8 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN)
@callback @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.""" """Generate a unique ID from a geography dict."""
if not geography_dict:
return
if CONF_CITY in geography_dict: if CONF_CITY in geography_dict:
return ", ".join( return ", ".join(
( (
@@ -76,7 +78,9 @@ def async_get_geography_id(geography_dict):
@callback @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. """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 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 @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.""" """Get all DataUpdateCoordinator objects related to a particular API key."""
coordinators = [] coordinators = []
for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items(): for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items():
config_entry = hass.config_entries.async_get_entry(entry_id) 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) coordinators.append(coordinator)
return coordinators return coordinators
@callback @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).""" """Sync the update interval for geography-based data coordinators (by API key)."""
coordinators = async_get_cloud_coordinators_by_api_key(hass, 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 @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.""" """Ensure that geography config entries have appropriate properties."""
entry_updates = {} entry_updates = {}
@@ -162,9 +172,11 @@ def _standardize_geography_config_entry(hass, config_entry):
@callback @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.""" """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 CONF_INTEGRATION_TYPE not in config_entry.data:
# If the config entry data doesn't contain the integration type, add it: # 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) 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.""" """Set up AirVisual as config entry."""
hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_LISTENER: {}}) 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) websession = aiohttp_client.async_get_clientsession(hass)
cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession) 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.""" """Get new data from the API."""
if CONF_CITY in config_entry.data: if CONF_CITY in config_entry.data:
api_coro = cloud_api.air_quality.city( api_coro = cloud_api.air_quality.city(
@@ -204,7 +216,8 @@ async def async_setup_entry(hass, config_entry):
) )
try: try:
return await api_coro data = await api_coro
return cast(Dict[str, Any], data)
except (InvalidKeyError, KeyExpiredError) as ex: except (InvalidKeyError, KeyExpiredError) as ex:
raise ConfigEntryAuthFailed from ex raise ConfigEntryAuthFailed from ex
except AirVisualError as err: except AirVisualError as err:
@@ -242,13 +255,14 @@ async def async_setup_entry(hass, config_entry):
_standardize_node_pro_config_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.""" """Get new data from the API."""
try: try:
async with NodeSamba( async with NodeSamba(
config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD] config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD]
) as node: ) 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: except NodeProError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from 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 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.""" """Migrate an old config entry."""
version = config_entry.version version = config_entry.version
@@ -317,7 +331,7 @@ async def async_migrate_entry(hass, config_entry):
return True 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 an AirVisual config entry."""
unload_ok = await hass.config_entries.async_unload_platforms( unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS config_entry, PLATFORMS
@@ -338,7 +352,7 @@ async def async_unload_entry(hass, config_entry):
return unload_ok 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.""" """Handle an options update."""
await hass.config_entries.async_reload(config_entry.entry_id) 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): class AirVisualEntity(CoordinatorEntity):
"""Define a generic AirVisual entity.""" """Define a generic AirVisual entity."""
def __init__(self, coordinator): def __init__(self, coordinator: DataUpdateCoordinator) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) 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.""" """Register callbacks."""
@callback @callback
def update(): def update() -> None:
"""Update the state.""" """Update the state."""
self.update_from_latest_data() self.update_from_latest_data()
self.async_write_ha_state() self.async_write_ha_state()
@@ -365,6 +382,6 @@ class AirVisualEntity(CoordinatorEntity):
self.update_from_latest_data() self.update_from_latest_data()
@callback @callback
def update_from_latest_data(self): def update_from_latest_data(self) -> None:
"""Update the entity from the latest data.""" """Update the entity from the latest data."""
raise NotImplementedError raise NotImplementedError

View File

@@ -1,4 +1,6 @@
"""Define a config flow manager for AirVisual.""" """Define a config flow manager for AirVisual."""
from __future__ import annotations
import asyncio import asyncio
from pyairvisual import CloudAPI, NodeSamba from pyairvisual import CloudAPI, NodeSamba
@@ -11,6 +13,7 @@ from pyairvisual.errors import (
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry, OptionsFlow
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_IP_ADDRESS, CONF_IP_ADDRESS,
@@ -21,6 +24,7 @@ from homeassistant.const import (
CONF_STATE, CONF_STATE,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
from . import async_get_geography_id from . import async_get_geography_id
@@ -64,13 +68,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 2 VERSION = 2
def __init__(self): def __init__(self) -> None:
"""Initialize the config flow.""" """Initialize the config flow."""
self._entry_data_for_reauth = None self._entry_data_for_reauth: dict[str, str] = {}
self._geo_id = None self._geo_id: str | None = None
@property @property
def geography_coords_schema(self): def geography_coords_schema(self) -> vol.Schema:
"""Return the data schema for the cloud API.""" """Return the data schema for the cloud API."""
return API_KEY_DATA_SCHEMA.extend( 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.""" """Validate a Cloud API key."""
websession = aiohttp_client.async_get_clientsession(self.hass) websession = aiohttp_client.async_get_clientsession(self.hass)
cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession) 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}, 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.""" """Handle the initialization of the integration via the cloud API."""
self._geo_id = async_get_geography_id(user_input) self._geo_id = async_get_geography_id(user_input)
await self._async_set_unique_id(self._geo_id) await self._async_set_unique_id(self._geo_id)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return await self._async_finish_geography(user_input, integration_type) 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.""" """Set the unique ID of the config flow and abort if it already exists."""
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Define the config flow to handle options.""" """Define the config flow to handle options."""
return AirVisualOptionsFlowHandler(config_entry) 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.""" """Handle the initialization of the cloud API based on latitude/longitude."""
if not user_input: if not user_input:
return self.async_show_form( return self.async_show_form(
@@ -171,7 +181,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
user_input, INTEGRATION_TYPE_GEOGRAPHY_COORDS 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.""" """Handle the initialization of the cloud API based on city/state/country."""
if not user_input: if not user_input:
return self.async_show_form( return self.async_show_form(
@@ -182,7 +194,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
user_input, INTEGRATION_TYPE_GEOGRAPHY_NAME 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.""" """Handle the initialization of the integration with a Node/Pro."""
if not user_input: if not user_input:
return self.async_show_form(step_id="node_pro", data_schema=NODE_PRO_SCHEMA) 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}, 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.""" """Handle configuration by re-auth."""
self._entry_data_for_reauth = data self._entry_data_for_reauth = data
self._geo_id = async_get_geography_id(data) self._geo_id = async_get_geography_id(data)
return await self.async_step_reauth_confirm() 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.""" """Handle re-auth completion."""
if not user_input: if not user_input:
return self.async_show_form( 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] 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.""" """Handle the start of the config flow."""
if not user_input: if not user_input:
return self.async_show_form( return self.async_show_form(
@@ -244,11 +262,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class AirVisualOptionsFlowHandler(config_entries.OptionsFlow): class AirVisualOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle an AirVisual options flow.""" """Handle an AirVisual options flow."""
def __init__(self, config_entry): def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize.""" """Initialize."""
self.config_entry = config_entry 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.""" """Manage the options."""
if user_input is not None: if user_input is not None:
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(title="", data=user_input)

View File

@@ -1,5 +1,8 @@
"""Support for AirVisual air quality sensors.""" """Support for AirVisual air quality sensors."""
from __future__ import annotations
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_LATITUDE, ATTR_LATITUDE,
ATTR_LONGITUDE, ATTR_LONGITUDE,
@@ -18,7 +21,10 @@ from homeassistant.const import (
PERCENTAGE, PERCENTAGE,
TEMP_CELSIUS, 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 . import AirVisualEntity
from .const import ( 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.""" """Set up AirVisual sensors based on a config entry."""
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
sensors: list[AirVisualGeographySensor | AirVisualNodeProSensor]
if config_entry.data[CONF_INTEGRATION_TYPE] in [ if config_entry.data[CONF_INTEGRATION_TYPE] in [
INTEGRATION_TYPE_GEOGRAPHY_COORDS, INTEGRATION_TYPE_GEOGRAPHY_COORDS,
INTEGRATION_TYPE_GEOGRAPHY_NAME, INTEGRATION_TYPE_GEOGRAPHY_NAME,
@@ -174,7 +185,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AirVisualGeographySensor(AirVisualEntity, SensorEntity): class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
"""Define an AirVisual sensor related to geography data via the Cloud API.""" """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.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
@@ -203,7 +223,7 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
return super().available and self.coordinator.data["current"]["pollution"] return super().available and self.coordinator.data["current"]["pollution"]
@callback @callback
def update_from_latest_data(self): def update_from_latest_data(self) -> None:
"""Update the entity from the latest data.""" """Update the entity from the latest data."""
try: try:
data = self.coordinator.data["current"]["pollution"] data = self.coordinator.data["current"]["pollution"]
@@ -260,18 +280,29 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): class AirVisualNodeProSensor(AirVisualEntity, SensorEntity):
"""Define an AirVisual sensor related to a Node/Pro unit.""" """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.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self._attr_device_class = device_class self._attr_device_class = device_class
self._attr_icon = icon 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._attr_unit_of_measurement = unit
self._kind = kind self._kind = kind
self._name = name
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity.""" """Return device registry information for this entity."""
return { return {
"identifiers": {(DOMAIN, self.coordinator.data["serial_number"])}, "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 @callback
def update_from_latest_data(self): def update_from_latest_data(self) -> None:
"""Update the entity from the latest data.""" """Update the entity from the latest data."""
if self._kind == SENSOR_KIND_AQI: if self._kind == SENSOR_KIND_AQI:
if self.coordinator.data["settings"]["is_aqi_usa"]: if self.coordinator.data["settings"]["is_aqi_usa"]:

View File

@@ -1,7 +1,7 @@
{ {
"config": { "config": {
"abort": { "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" "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
}, },
"error": { "error": {
@@ -40,7 +40,7 @@
}, },
"reauth_confirm": { "reauth_confirm": {
"data": { "data": {
"api_key": "API-Key" "api_key": "API-Schl\u00fcssel"
}, },
"title": "AirVisual erneut authentifizieren" "title": "AirVisual erneut authentifizieren"
}, },

View File

@@ -17,7 +17,7 @@
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
"longitude": "\u0414\u043e\u043b\u0433\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" "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": { "geography_by_name": {
@@ -25,9 +25,9 @@
"api_key": "\u041a\u043b\u044e\u0447 API", "api_key": "\u041a\u043b\u044e\u0447 API",
"city": "\u0413\u043e\u0440\u043e\u0434", "city": "\u0413\u043e\u0440\u043e\u0434",
"country": "\u0421\u0442\u0440\u0430\u043d\u0430", "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" "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": { "node_pro": {

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -48,7 +48,12 @@ ALERT_SCHEMA = vol.Schema(
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_STATE, default=STATE_ON): cv.string, 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_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean, vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
vol.Optional(CONF_ALERT_MESSAGE): cv.template, vol.Optional(CONF_ALERT_MESSAGE): cv.template,

View File

@@ -19,6 +19,7 @@ from homeassistant.components.alarm_control_panel.const import (
SUPPORT_ALARM_ARM_NIGHT, SUPPORT_ALARM_ARM_NIGHT,
) )
import homeassistant.components.climate.const as climate 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 import homeassistant.components.media_player.const as media_player
from homeassistant.const import ( from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_SUPPORTED_FEATURES,
@@ -446,9 +447,11 @@ class AlexaLockController(AlexaCapability):
if name != "lockState": if name != "lockState":
raise UnsupportedProperty(name) 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" 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 "UNLOCKED"
return "JAMMED" return "JAMMED"

View File

@@ -1,6 +1,7 @@
"""Support for Ambiclimate ac.""" """Support for Ambiclimate ac."""
import asyncio import asyncio
import logging import logging
from typing import Any
import ambiclimate import ambiclimate
import voluptuous as vol import voluptuous as vol
@@ -146,24 +147,24 @@ class AmbiclimateEntity(ClimateEntity):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self._heater = heater self._heater = heater
self._store = store self._store = store
self._attr_unique_id = self._heater.device_id self._attr_unique_id = heater.device_id
self._attr_name = self._heater.name self._attr_name = heater.name
self._attr_device_info = { self._attr_device_info = {
"identifiers": {(DOMAIN, self.unique_id)}, "identifiers": {(DOMAIN, self.unique_id)},
"name": self.name, "name": self.name,
"manufacturer": "Ambiclimate", "manufacturer": "Ambiclimate",
} }
self._attr_min_temp = self._heater.get_min_temp() self._attr_min_temp = heater.get_min_temp()
self._attr_max_temp = self._heater.get_max_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.""" """Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None: if temperature is None:
return return
await self._heater.set_target_temperature(temperature) 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.""" """Set new target hvac mode."""
if hvac_mode == HVAC_MODE_HEAT: if hvac_mode == HVAC_MODE_HEAT:
await self._heater.turn_on() await self._heater.turn_on()

View File

@@ -1,6 +1,7 @@
"""Config flow for Ambiclimate.""" """Config flow for Ambiclimate."""
import logging import logging
from aiohttp import web
import ambiclimate import ambiclimate
from homeassistant import config_entries from homeassistant import config_entries
@@ -139,7 +140,7 @@ class AmbiclimateAuthCallbackView(HomeAssistantView):
url = AUTH_CALLBACK_PATH url = AUTH_CALLBACK_PATH
name = AUTH_CALLBACK_NAME name = AUTH_CALLBACK_NAME
async def get(self, request) -> str: async def get(self, request: web.Request) -> str:
"""Receive authorization token.""" """Receive authorization token."""
code = request.query.get("code") code = request.query.get("code")
if code is None: if code is None:

View File

@@ -6,7 +6,7 @@
"missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen." "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen."
}, },
"create_entry": { "create_entry": {
"default": "Erfolgreiche Authentifizierung mit Ambiclimate" "default": "Erfolgreich authentifiziert"
}, },
"error": { "error": {
"follow_link": "Bitte folge dem Link und authentifizieren dich, bevor du auf Senden klickst", "follow_link": "Bitte folge dem Link und authentifizieren dich, bevor du auf Senden klickst",

View File

@@ -28,11 +28,13 @@ from homeassistant.const import (
IRRADIATION_WATTS_PER_SQUARE_METER, IRRADIATION_WATTS_PER_SQUARE_METER,
LIGHT_LUX, LIGHT_LUX,
PERCENTAGE, PERCENTAGE,
PRECIPITATION_INCHES,
PRECIPITATION_INCHES_PER_HOUR,
PRESSURE_INHG, PRESSURE_INHG,
SPEED_MILES_PER_HOUR, SPEED_MILES_PER_HOUR,
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
@@ -156,7 +158,7 @@ TYPE_WINDSPDMPH_AVG2M = "windspdmph_avg2m"
TYPE_WINDSPEEDMPH = "windspeedmph" TYPE_WINDSPEEDMPH = "windspeedmph"
TYPE_YEARLYRAININ = "yearlyrainin" TYPE_YEARLYRAININ = "yearlyrainin"
SENSOR_TYPES = { 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_BAROMABSIN: ("Abs Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE),
TYPE_BAROMRELIN: ("Rel 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), 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_BATTOUT: ("Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
TYPE_BATT_CO2: ("CO2 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_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_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_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_HUMIDITY10: ("Humidity 10", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY), TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
TYPE_HUMIDITY2: ("Humidity 2", 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_HUMIDITYIN: ("Humidity In", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
TYPE_LASTRAIN: ("Last Rain", None, SENSOR, DEVICE_CLASS_TIMESTAMP), TYPE_LASTRAIN: ("Last Rain", None, SENSOR, DEVICE_CLASS_TIMESTAMP),
TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, SENSOR, None), 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: ( TYPE_PM25_24H: (
"PM25 24h Avg", "PM25 24h Avg",
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@@ -277,9 +284,9 @@ SENSOR_TYPES = {
TYPE_TEMP9F: ("Temp 9", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE), TYPE_TEMP9F: ("Temp 9", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
TYPE_TEMPF: ("Temp", 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_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_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: ("Wind Dir", DEGREE, SENSOR, None),
TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", 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), 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_AVG10M: ("Wind Avg 10m", SPEED_MILES_PER_HOUR, SENSOR, None),
TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", 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_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) 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) LOGGER.error("Config entry failed: %s", err)
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
async def _async_disconnect_websocket(*_): async def _async_disconnect_websocket(_: Event) -> None:
await ambient.client.websocket.disconnect() await ambient.client.websocket.disconnect()
config_entry.async_on_unload( config_entry.async_on_unload(
@@ -378,7 +385,7 @@ class AmbientStation:
async def _attempt_connect(self) -> None: async def _attempt_connect(self) -> None:
"""Attempt to connect to the socket (retrying later on fail).""" """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.""" """Connect."""
await self.client.websocket.connect() await self.client.websocket.connect()

View File

@@ -10,7 +10,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"api_key": "API Schl\u00fcssel", "api_key": "API-Schl\u00fcssel",
"app_key": "Anwendungsschl\u00fcssel" "app_key": "Anwendungsschl\u00fcssel"
}, },
"title": "Gib deine Informationen ein" "title": "Gib deine Informationen ein"

View File

@@ -8,15 +8,15 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import ( from homeassistant.const import (
CONF_RESOURCES, CONF_RESOURCES,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
ELECTRICAL_CURRENT_AMPERE, ELECTRIC_CURRENT_AMPERE,
ELECTRICAL_VOLT_AMPERE, ELECTRIC_POTENTIAL_VOLT,
FREQUENCY_HERTZ, FREQUENCY_HERTZ,
PERCENTAGE, PERCENTAGE,
POWER_VOLT_AMPERE,
POWER_WATT, POWER_WATT,
TEMP_CELSIUS, TEMP_CELSIUS,
TIME_MINUTES, TIME_MINUTES,
TIME_SECONDS, TIME_SECONDS,
VOLT,
) )
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@@ -33,7 +33,7 @@ SENSOR_TYPES = {
"badbatts": ["Bad Batteries", "", "mdi:information-outline", None], "badbatts": ["Bad Batteries", "", "mdi:information-outline", None],
"battdate": ["Battery Replaced", "", "mdi:calendar-clock", None], "battdate": ["Battery Replaced", "", "mdi:calendar-clock", None],
"battstat": ["Battery Status", "", "mdi:information-outline", 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], "bcharge": ["Battery", PERCENTAGE, "mdi:battery", None],
"cable": ["Cable Type", "", "mdi:ethernet-cable", None], "cable": ["Cable Type", "", "mdi:ethernet-cable", None],
"cumonbatt": ["Total Time on Battery", "", "mdi:timer-outline", None], "cumonbatt": ["Total Time on Battery", "", "mdi:timer-outline", None],
@@ -46,33 +46,33 @@ SENSOR_TYPES = {
"endapc": ["Date and Time", "", "mdi:calendar-clock", None], "endapc": ["Date and Time", "", "mdi:calendar-clock", None],
"extbatts": ["External Batteries", "", "mdi:information-outline", None], "extbatts": ["External Batteries", "", "mdi:information-outline", None],
"firmware": ["Firmware Version", "", "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], "hostname": ["Hostname", "", "mdi:information-outline", None],
"humidity": ["Ambient Humidity", PERCENTAGE, "mdi:water-percent", None], "humidity": ["Ambient Humidity", PERCENTAGE, "mdi:water-percent", None],
"itemp": ["Internal Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], "itemp": ["Internal Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE],
"lastxfer": ["Last Transfer", "", "mdi:transfer", None], "lastxfer": ["Last Transfer", "", "mdi:transfer", None],
"linefail": ["Input Voltage Status", "", "mdi:information-outline", None], "linefail": ["Input Voltage Status", "", "mdi:information-outline", None],
"linefreq": ["Line Frequency", FREQUENCY_HERTZ, "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], "loadpct": ["Load", PERCENTAGE, "mdi:gauge", None],
"loadapnt": ["Load Apparent Power", 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], "mandate": ["Manufacture Date", "", "mdi:calendar", None],
"masterupd": ["Master Update", "", "mdi:information-outline", 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], "maxtime": ["Battery Timeout", "", "mdi:timer-off-outline", None],
"mbattchg": ["Battery Shutdown", PERCENTAGE, "mdi:battery-alert", 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], "mintimel": ["Shutdown Time", "", "mdi:timer-outline", None],
"model": ["Model", "", "mdi:information-outline", None], "model": ["Model", "", "mdi:information-outline", None],
"nombattv": ["Battery Nominal Voltage", VOLT, "mdi:flash", None], "nombattv": ["Battery Nominal Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
"nominv": ["Nominal Input Voltage", VOLT, "mdi:flash", None], "nominv": ["Nominal Input Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
"nomoutv": ["Nominal Output Voltage", VOLT, "mdi:flash", None], "nomoutv": ["Nominal Output Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
"nompower": ["Nominal Output Power", POWER_WATT, "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], "numxfers": ["Transfer Count", "", "mdi:counter", None],
"outcurnt": ["Output Current", ELECTRICAL_CURRENT_AMPERE, "mdi:flash", None], "outcurnt": ["Output Current", ELECTRIC_CURRENT_AMPERE, "mdi:flash", None],
"outputv": ["Output Voltage", VOLT, "mdi:flash", None], "outputv": ["Output Voltage", ELECTRIC_POTENTIAL_VOLT, "mdi:flash", None],
"reg1": ["Register 1 Fault", "", "mdi:information-outline", None], "reg1": ["Register 1 Fault", "", "mdi:information-outline", None],
"reg2": ["Register 2 Fault", "", "mdi:information-outline", None], "reg2": ["Register 2 Fault", "", "mdi:information-outline", None],
"reg3": ["Register 3 Fault", "", "mdi:information-outline", None], "reg3": ["Register 3 Fault", "", "mdi:information-outline", None],
@@ -99,9 +99,9 @@ INFERRED_UNITS = {
" Minutes": TIME_MINUTES, " Minutes": TIME_MINUTES,
" Seconds": TIME_SECONDS, " Seconds": TIME_SECONDS,
" Percent": PERCENTAGE, " Percent": PERCENTAGE,
" Volts": VOLT, " Volts": ELECTRIC_POTENTIAL_VOLT,
" Ampere": ELECTRICAL_CURRENT_AMPERE, " Ampere": ELECTRIC_CURRENT_AMPERE,
" Volt-Ampere": ELECTRICAL_VOLT_AMPERE, " Volt-Ampere": POWER_VOLT_AMPERE,
" Watts": POWER_WATT, " Watts": POWER_WATT,
" Hz": FREQUENCY_HERTZ, " Hz": FREQUENCY_HERTZ,
" C": TEMP_CELSIUS, " C": TEMP_CELSIUS,

View File

@@ -1,5 +1,4 @@
{ {
"title": "Apple TV",
"config": { "config": {
"flow_title": "{name}", "flow_title": "{name}",
"step": { "step": {

View File

@@ -35,24 +35,11 @@ class ArduinoSensor(SensorEntity):
def __init__(self, name, pin, pin_type, board): def __init__(self, name, pin, pin_type, board):
"""Initialize the sensor.""" """Initialize the sensor."""
self._pin = pin self._pin = pin
self._name = name self._attr_name = name
self.pin_type = pin_type
self.direction = "in"
self._value = None
board.set_mode(self._pin, self.direction, self.pin_type) board.set_mode(self._pin, "in", pin_type)
self._board = board 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): def update(self):
"""Get the latest value from the pin.""" """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]

View File

@@ -43,11 +43,9 @@ class ArduinoSwitch(SwitchEntity):
def __init__(self, pin, options, board): def __init__(self, pin, options, board):
"""Initialize the Pin.""" """Initialize the Pin."""
self._pin = pin self._pin = pin
self._name = options[CONF_NAME] self._attr_name = options[CONF_NAME]
self.pin_type = CONF_TYPE
self.direction = "out"
self._state = options[CONF_INITIAL] self._attr_is_on = options[CONF_INITIAL]
if options[CONF_NEGATE]: if options[CONF_NEGATE]:
self.turn_on_handler = board.set_digital_out_low 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_on_handler = board.set_digital_out_high
self.turn_off_handler = board.set_digital_out_low self.turn_off_handler = board.set_digital_out_low
board.set_mode(self._pin, self.direction, self.pin_type) board.set_mode(pin, "out", CONF_TYPE)
(self.turn_on_handler if self._state else self.turn_off_handler)(pin) (self.turn_on_handler if self.is_on 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
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the pin to high/on.""" """Turn the pin to high/on."""
self._state = True self._attr_is_on = True
self.turn_on_handler(self._pin) self.turn_on_handler(self._pin)
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the pin to low/off.""" """Turn the pin to low/off."""
self._state = False self._attr_is_on = False
self.turn_off_handler(self._pin) self.turn_off_handler(self._pin)

View File

@@ -73,34 +73,18 @@ class ArestBinarySensor(BinarySensorEntity):
def __init__(self, arest, resource, name, device_class, pin): def __init__(self, arest, resource, name, device_class, pin):
"""Initialize the aREST device.""" """Initialize the aREST device."""
self.arest = arest self.arest = arest
self._resource = resource self._attr_name = name
self._name = name self._attr_device_class = device_class
self._device_class = device_class
self._pin = pin
if self._pin is not None: if pin is not None:
request = requests.get(f"{self._resource}/mode/{self._pin}/i", timeout=10) request = requests.get(f"{resource}/mode/{pin}/i", timeout=10)
if request.status_code != HTTP_OK: if request.status_code != HTTP_OK:
_LOGGER.error("Can't set mode of %s", self._resource) _LOGGER.error("Can't set mode of %s", 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
def update(self): def update(self):
"""Get the latest data from aREST API.""" """Get the latest data from aREST API."""
self.arest.update() self.arest.update()
self._attr_is_on = bool(self.arest.data.get("state"))
class ArestData: class ArestData:

View File

@@ -139,48 +139,27 @@ class ArestSensor(SensorEntity):
): ):
"""Initialize the sensor.""" """Initialize the sensor."""
self.arest = arest self.arest = arest
self._resource = resource self._attr_name = f"{location.title()} {name.title()}"
self._name = f"{location.title()} {name.title()}"
self._variable = variable self._variable = variable
self._pin = pin self._attr_unit_of_measurement = unit_of_measurement
self._state = None
self._unit_of_measurement = unit_of_measurement
self._renderer = renderer self._renderer = renderer
if self._pin is not None: if pin is not None:
request = requests.get(f"{self._resource}/mode/{self._pin}/i", timeout=10) request = requests.get(f"{resource}/mode/{pin}/i", timeout=10)
if request.status_code != HTTP_OK: if request.status_code != HTTP_OK:
_LOGGER.error("Can't set mode of %s", self._resource) _LOGGER.error("Can't set mode of %s", 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
def update(self): def update(self):
"""Get the latest data from aREST API.""" """Get the latest data from aREST API."""
self.arest.update() self.arest.update()
self._attr_available = self.arest.available
@property values = self.arest.data
def available(self): if "error" in values:
"""Could the device be accessed during the last update call.""" self._attr_state = values["error"]
return self.arest.available else:
self._attr_state = self._renderer(
values.get("value", values.get(self._variable, None))
)
class ArestData: class ArestData:
@@ -191,7 +170,7 @@ class ArestData:
self._resource = resource self._resource = resource
self._pin = pin self._pin = pin
self.data = {} self.data = {}
self.available = True self._attr_available = True
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
@@ -212,7 +191,7 @@ class ArestData:
f"{self._resource}/digital/{self._pin}", timeout=10 f"{self._resource}/digital/{self._pin}", timeout=10
) )
self.data = {"value": response.json()["return_value"]} self.data = {"value": response.json()["return_value"]}
self.available = True self._attr_available = True
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error("No route to device %s", self._resource) _LOGGER.error("No route to device %s", self._resource)
self.available = False self._attr_available = False

View File

@@ -86,24 +86,8 @@ class ArestSwitchBase(SwitchEntity):
def __init__(self, resource, location, name): def __init__(self, resource, location, name):
"""Initialize the switch.""" """Initialize the switch."""
self._resource = resource self._resource = resource
self._name = f"{location.title()} {name.title()}" self._attr_name = f"{location.title()} {name.title()}"
self._state = None self._attr_available = True
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
class ArestSwitchFunction(ArestSwitchBase): class ArestSwitchFunction(ArestSwitchBase):
@@ -134,7 +118,7 @@ class ArestSwitchFunction(ArestSwitchBase):
) )
if request.status_code == HTTP_OK: if request.status_code == HTTP_OK:
self._state = True self._attr_is_on = True
else: else:
_LOGGER.error("Can't turn on function %s at %s", self._func, self._resource) _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: if request.status_code == HTTP_OK:
self._state = False self._attr_is_on = False
else: else:
_LOGGER.error( _LOGGER.error(
"Can't turn off function %s at %s", self._func, self._resource "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.""" """Get the latest data from aREST API and update the state."""
try: try:
request = requests.get(f"{self._resource}/{self._func}", timeout=10) request = requests.get(f"{self._resource}/{self._func}", timeout=10)
self._state = request.json()["return_value"] != 0 self._attr_is_on = request.json()["return_value"] != 0
self._available = True self._attr_available = True
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.warning("No route to device %s", self._resource) _LOGGER.warning("No route to device %s", self._resource)
self._available = False self._attr_available = False
class ArestSwitchPin(ArestSwitchBase): class ArestSwitchPin(ArestSwitchBase):
@@ -171,10 +155,10 @@ class ArestSwitchPin(ArestSwitchBase):
self._pin = pin self._pin = pin
self.invert = invert 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: if request.status_code != HTTP_OK:
_LOGGER.error("Can't set mode") _LOGGER.error("Can't set mode")
self._available = False self._attr_available = False
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the device on.""" """Turn the device on."""
@@ -183,7 +167,7 @@ class ArestSwitchPin(ArestSwitchBase):
f"{self._resource}/digital/{self._pin}/{turn_on_payload}", timeout=10 f"{self._resource}/digital/{self._pin}/{turn_on_payload}", timeout=10
) )
if request.status_code == HTTP_OK: if request.status_code == HTTP_OK:
self._state = True self._attr_is_on = True
else: else:
_LOGGER.error("Can't turn on pin %s at %s", self._pin, self._resource) _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 f"{self._resource}/digital/{self._pin}/{turn_off_payload}", timeout=10
) )
if request.status_code == HTTP_OK: if request.status_code == HTTP_OK:
self._state = False self._attr_is_on = False
else: else:
_LOGGER.error("Can't turn off pin %s at %s", self._pin, self._resource) _LOGGER.error("Can't turn off pin %s at %s", self._pin, self._resource)
@@ -203,8 +187,8 @@ class ArestSwitchPin(ArestSwitchBase):
try: try:
request = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10) request = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10)
status_value = int(self.invert) status_value = int(self.invert)
self._state = request.json()["return_value"] != status_value self._attr_is_on = request.json()["return_value"] != status_value
self._available = True self._attr_available = True
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.warning("No route to device %s", self._resource) _LOGGER.warning("No route to device %s", self._resource)
self._available = False self._attr_available = False

View File

@@ -7,6 +7,7 @@ from homeassistant.components.sensor import SensorEntity
from homeassistant.const import ( from homeassistant.const import (
DEGREE, DEGREE,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
PRECIPITATION_INCHES,
TEMP_CELSIUS, TEMP_CELSIUS,
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
) )
@@ -44,7 +45,11 @@ def discover_sensors(topic, payload):
if domain == "rain": if domain == "rain":
if len(parts) >= 3 and parts[2] == "today": if len(parts) >= 3 and parts[2] == "today":
return ArwnSensor( return ArwnSensor(
topic, "Rain Since Midnight", "since_midnight", "in", "mdi:water" topic,
"Rain Since Midnight",
"since_midnight",
PRECIPITATION_INCHES,
"mdi:water",
) )
return ( return (
ArwnSensor(topic + "/total", "Total Rainfall", "total", unit, "mdi:water"), ArwnSensor(topic + "/total", "Total Rainfall", "total", unit, "mdi:water"),

View File

@@ -75,27 +75,16 @@ class AtagEntity(CoordinatorEntity):
super().__init__(coordinator) super().__init__(coordinator)
self._id = atag_id self._id = atag_id
self._name = DOMAIN.title() self._attr_name = DOMAIN.title()
self._attr_unique_id = f"{coordinator.data.id}-{atag_id}"
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return info for device registry.""" """Return info for device registry."""
device = self.coordinator.data.id
version = self.coordinator.data.apiversion
return { return {
"identifiers": {(DOMAIN, device)}, "identifiers": {(DOMAIN, self.coordinator.data.id)},
"name": "Atag Thermostat", "name": "Atag Thermostat",
"model": "Atag One", "model": "Atag One",
"sw_version": version, "sw_version": self.coordinator.data.apiversion,
"manufacturer": "Atag", "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}"

View File

@@ -37,10 +37,14 @@ async def async_setup_entry(hass, entry, async_add_entities):
class AtagThermostat(AtagEntity, ClimateEntity): class AtagThermostat(AtagEntity, ClimateEntity):
"""Atag climate device.""" """Atag climate device."""
@property _attr_hvac_modes = HVAC_MODES
def supported_features(self): _attr_preset_modes = list(PRESET_MAP.keys())
"""Return the list of supported features.""" _attr_supported_features = SUPPORT_FLAGS
return 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 @property
def hvac_mode(self) -> str | None: def hvac_mode(self) -> str | None:
@@ -49,22 +53,12 @@ class AtagThermostat(AtagEntity, ClimateEntity):
return self.coordinator.data.climate.hvac_mode return self.coordinator.data.climate.hvac_mode
return None return None
@property
def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return HVAC_MODES
@property @property
def hvac_action(self) -> str | None: def hvac_action(self) -> str | None:
"""Return the current running hvac operation.""" """Return the current running hvac operation."""
is_active = self.coordinator.data.climate.status is_active = self.coordinator.data.climate.status
return CURRENT_HVAC_HEAT if is_active else CURRENT_HVAC_IDLE 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 @property
def current_temperature(self) -> float | None: def current_temperature(self) -> float | None:
"""Return the current temperature.""" """Return the current temperature."""
@@ -81,11 +75,6 @@ class AtagThermostat(AtagEntity, ClimateEntity):
preset = self.coordinator.data.climate.preset_mode preset = self.coordinator.data.climate.preset_mode
return PRESET_INVERTED.get(preset) 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: async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature.""" """Set new target temperature."""
await self.coordinator.data.climate.set_temp(kwargs.get(ATTR_TEMPERATURE)) await self.coordinator.data.climate.set_temp(kwargs.get(ATTR_TEMPERATURE))

View File

@@ -36,7 +36,20 @@ class AtagSensor(AtagEntity, SensorEntity):
def __init__(self, coordinator, sensor): def __init__(self, coordinator, sensor):
"""Initialize Atag sensor.""" """Initialize Atag sensor."""
super().__init__(coordinator, SENSORS[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 @property
def state(self): def state(self):
@@ -47,26 +60,3 @@ class AtagSensor(AtagEntity, SensorEntity):
def icon(self): def icon(self):
"""Return icon.""" """Return icon."""
return self.coordinator.data.report[self._id].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

View File

@@ -1,7 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert" "already_configured": "Ger\u00e4t ist bereits konfiguriert"
}, },
"error": { "error": {
"cannot_connect": "Verbindung fehlgeschlagen", "cannot_connect": "Verbindung fehlgeschlagen",

View File

@@ -22,15 +22,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AtagWaterHeater(AtagEntity, WaterHeaterEntity): class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
"""Representation of an ATAG water heater.""" """Representation of an ATAG water heater."""
@property _attr_operation_list = OPERATION_LIST
def supported_features(self): _attr_supported_features = SUPPORT_FLAGS_HEATER
"""Return the list of supported features.""" _attr_temperature_unit = TEMP_CELSIUS
return SUPPORT_FLAGS_HEATER
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property @property
def current_temperature(self): def current_temperature(self):
@@ -43,11 +37,6 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
operation = self.coordinator.data.dhw.current_operation operation = self.coordinator.data.dhw.current_operation
return operation if operation in self.operation_list else STATE_OFF 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): async def async_set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
if await self.coordinator.data.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)): if await self.coordinator.data.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)):

View File

@@ -1,6 +1,7 @@
"""Support for August lock.""" """Support for August lock."""
import logging import logging
from aiohttp import ClientResponseError
from yalexs.activity import SOURCE_PUBNUB, ActivityType from yalexs.activity import SOURCE_PUBNUB, ActivityType
from yalexs.lock import LockStatus from yalexs.lock import LockStatus
from yalexs.util import update_lock_detail_from_activity 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.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.util.dt as dt_util
from .const import DATA_AUGUST, DOMAIN from .const import DATA_AUGUST, DOMAIN
from .entity import AugustEntityMixin from .entity import AugustEntityMixin
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
LOCK_JAMMED_ERR = 531
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up August locks.""" """Set up August locks."""
@@ -44,9 +48,17 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
await self._call_lock_operation(self._data.async_unlock) await self._call_lock_operation(self._data.async_unlock)
async def _call_lock_operation(self, lock_operation): async def _call_lock_operation(self, lock_operation):
activities = await lock_operation(self._device_id) try:
for lock_activity in activities: activities = await lock_operation(self._device_id)
update_lock_detail_from_activity(self._detail, lock_activity) 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(): if self._update_lock_status_from_detail():
_LOGGER.debug( _LOGGER.debug(
@@ -91,6 +103,10 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
else: else:
self._attr_is_locked = self._lock_status is LockStatus.LOCKED 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 = { self._attr_extra_state_attributes = {
ATTR_BATTERY_LEVEL: self._detail.battery_level ATTR_BATTERY_LEVEL: self._detail.battery_level
} }

View File

@@ -2,7 +2,7 @@
"domain": "august", "domain": "august",
"name": "August", "name": "August",
"documentation": "https://www.home-assistant.io/integrations/august", "documentation": "https://www.home-assistant.io/integrations/august",
"requirements": ["yalexs==1.1.11"], "requirements": ["yalexs==1.1.12"],
"codeowners": ["@bdraco"], "codeowners": ["@bdraco"],
"dhcp": [ "dhcp": [
{ {

View 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

View 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

View 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)

View 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_{}"

View 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])

View 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,
)

View 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)
)

View 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"
]
}

View 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%]"
}
}
}

View 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"
}
}
}
}
}

View File

@@ -139,8 +139,8 @@
"on": "Nass" "on": "Nass"
}, },
"motion": { "motion": {
"off": "Ruhig", "off": "Normal",
"on": "Bewegung erkannt" "on": "Erkannt"
}, },
"moving": { "moving": {
"off": "Bewegt sich nicht", "off": "Bewegt sich nicht",
@@ -171,16 +171,16 @@
"on": "Unsicher" "on": "Unsicher"
}, },
"smoke": { "smoke": {
"off": "OK", "off": "Normal",
"on": "Rauch erkannt" "on": "Erkannt"
}, },
"sound": { "sound": {
"off": "Stille", "off": "Normal",
"on": "Ger\u00e4usch erkannt" "on": "Erkannt"
}, },
"vibration": { "vibration": {
"off": "Normal", "off": "Normal",
"on": "Vibration" "on": "Erkannt"
}, },
"window": { "window": {
"off": "Geschlossen", "off": "Geschlossen",

View File

@@ -13,7 +13,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"host": "IP Adresse", "host": "IP-Adresse",
"port": "Port" "port": "Port"
}, },
"description": "Richte deine BleBox f\u00fcr die Integration mit dem Home Assistant ein.", "description": "Richte deine BleBox f\u00fcr die Integration mit dem Home Assistant ein.",

View File

@@ -2,7 +2,7 @@
"domain": "blinksticklight", "domain": "blinksticklight",
"name": "BlinkStick", "name": "BlinkStick",
"documentation": "https://www.home-assistant.io/integrations/blinksticklight", "documentation": "https://www.home-assistant.io/integrations/blinksticklight",
"requirements": ["blinkstick==1.1.8"], "requirements": ["blinkstick==1.2.0"],
"codeowners": [], "codeowners": [],
"iot_class": "local_polling" "iot_class": "local_polling"
} }

View File

@@ -6,6 +6,7 @@ from homeassistant.const import (
AREA_SQUARE_METERS, AREA_SQUARE_METERS,
CONF_MONITORED_CONDITIONS, CONF_MONITORED_CONDITIONS,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
ELECTRIC_POTENTIAL_MILLIVOLT,
PERCENTAGE, PERCENTAGE,
PRESSURE_INHG, PRESSURE_INHG,
PRESSURE_MBAR, PRESSURE_MBAR,
@@ -32,7 +33,7 @@ SENSOR_UNITS_IMPERIAL = {
"Humidity": PERCENTAGE, "Humidity": PERCENTAGE,
"Pressure": PRESSURE_INHG, "Pressure": PRESSURE_INHG,
"Luminance": f"cd/{AREA_SQUARE_METERS}", "Luminance": f"cd/{AREA_SQUARE_METERS}",
"Voltage": "mV", "Voltage": ELECTRIC_POTENTIAL_MILLIVOLT,
} }
# Metric units # Metric units
@@ -41,7 +42,7 @@ SENSOR_UNITS_METRIC = {
"Humidity": PERCENTAGE, "Humidity": PERCENTAGE,
"Pressure": PRESSURE_MBAR, "Pressure": PRESSURE_MBAR,
"Luminance": f"cd/{AREA_SQUARE_METERS}", "Luminance": f"cd/{AREA_SQUARE_METERS}",
"Voltage": "mV", "Voltage": ELECTRIC_POTENTIAL_MILLIVOLT,
} }
# Device class # Device class

View File

@@ -203,33 +203,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class BluesoundPlayer(MediaPlayerEntity): class BluesoundPlayer(MediaPlayerEntity):
"""Representation of a Bluesound Player.""" """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.""" """Initialize the media player."""
self.host = host self.host = host
self._hass = hass self._hass = hass
self.port = port self.port = port
self._polling_session = async_get_clientsession(hass) self._polling_session = async_get_clientsession(hass)
self._polling_task = None # The actual polling task. self._polling_task = None # The actual polling task.
self._name = name self._attr_name = name
self._icon = None
self._capture_items = [] self._capture_items = []
self._services_items = [] self._services_items = []
self._preset_items = [] self._preset_items = []
self._sync_status = {} self._sync_status = {}
self._status = None self._status = None
self._last_status_update = None self._is_online = None
self._is_online = False
self._retry_remove = None self._retry_remove = None
self._muted = False
self._master = None self._master = None
self._is_master = False
self._group_name = None self._group_name = None
self._group_list = []
self._bluesound_device_name = None self._bluesound_device_name = None
self._is_master = False
self._group_list = []
self._init_callback = init_callback self._init_callback = init_callback
if self.port is None:
self.port = DEFAULT_PORT
class _TimeoutException(Exception): class _TimeoutException(Exception):
pass pass
@@ -252,12 +248,12 @@ class BluesoundPlayer(MediaPlayerEntity):
return None return None
self._sync_status = resp["SyncStatus"].copy() self._sync_status = resp["SyncStatus"].copy()
if not self._name: if not self.name:
self._name = self._sync_status.get("@name", self.host) self._attr_name = self._sync_status.get("@name", self.host)
if not self._bluesound_device_name: if not self._bluesound_device_name:
self._bluesound_device_name = self._sync_status.get("@name", self.host) self._bluesound_device_name = self._sync_status.get("@name", self.host)
if not self._icon: if not self.icon:
self._icon = self._sync_status.get("@icon", self.host) self._attr_icon = self._sync_status.get("@icon", self.host)
master = self._sync_status.get("master") master = self._sync_status.get("master")
if master is not None: if master is not None:
@@ -291,14 +287,14 @@ class BluesoundPlayer(MediaPlayerEntity):
await self.async_update_status() await self.async_update_status()
except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException): 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) await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
self.start_polling() self.start_polling()
except CancelledError: except CancelledError:
_LOGGER.debug("Stopping the polling of node %s", self._name) _LOGGER.debug("Stopping the polling of node %s", self.name)
except Exception: except Exception:
_LOGGER.exception("Unexpected error in %s", self._name) _LOGGER.exception("Unexpected error in %s", self.name)
raise raise
def start_polling(self): def start_polling(self):
@@ -402,7 +398,7 @@ class BluesoundPlayer(MediaPlayerEntity):
if response.status == HTTP_OK: if response.status == HTTP_OK:
result = await response.text() result = await response.text()
self._is_online = True 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() self._status = xmltodict.parse(result)["status"].copy()
group_name = self._status.get("groupName") group_name = self._status.get("groupName")
@@ -438,11 +434,58 @@ class BluesoundPlayer(MediaPlayerEntity):
except (asyncio.TimeoutError, ClientError): except (asyncio.TimeoutError, ClientError):
self._is_online = False self._is_online = False
self._last_status_update = None self._attr_media_position_updated_at = None
self._status = None self._status = None
self.async_write_ha_state() 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 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): async def async_trigger_sync_on_all(self):
"""Trigger sync status update on all devices.""" """Trigger sync status update on all devices."""
@@ -542,27 +585,6 @@ class BluesoundPlayer(MediaPlayerEntity):
return self._services_items 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 @property
def media_title(self): def media_title(self):
"""Title of current playing media.""" """Title of current playing media."""
@@ -617,7 +639,7 @@ class BluesoundPlayer(MediaPlayerEntity):
return None return None
mediastate = self.state 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 return None
position = self._status.get("secs") position = self._status.get("secs")
@@ -626,7 +648,9 @@ class BluesoundPlayer(MediaPlayerEntity):
position = float(position) position = float(position)
if mediastate == STATE_PLAYING: 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 return position
@@ -641,11 +665,6 @@ class BluesoundPlayer(MediaPlayerEntity):
return None return None
return float(duration) return float(duration)
@property
def media_position_updated_at(self):
"""Last time status was updated."""
return self._last_status_update
@property @property
def volume_level(self): def volume_level(self):
"""Volume level of the media player (0..1).""" """Volume level of the media player (0..1)."""
@@ -668,21 +687,11 @@ class BluesoundPlayer(MediaPlayerEntity):
mute = bool(int(mute)) mute = bool(int(mute))
return mute return mute
@property
def name(self):
"""Return the name of the device."""
return self._name
@property @property
def bluesound_device_name(self): def bluesound_device_name(self):
"""Return the device name as returned by the device.""" """Return the device name as returned by the device."""
return self._bluesound_device_name return self._bluesound_device_name
@property
def icon(self):
"""Return the icon of the device."""
return self._icon
@property @property
def source_list(self): def source_list(self):
"""List of available input sources.""" """List of available input sources."""
@@ -778,58 +787,15 @@ class BluesoundPlayer(MediaPlayerEntity):
return None return None
@property @property
def supported_features(self): def is_master(self) -> bool:
"""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):
"""Return true if player is a coordinator.""" """Return true if player is a coordinator."""
return self._is_master return self._is_master
@property @property
def is_grouped(self): def is_grouped(self) -> bool:
"""Return true if player is a coordinator.""" """Return true if player is a coordinator."""
return self._master is not None or self._is_master 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): async def async_join(self, master):
"""Join the player to a group.""" """Join the player to a group."""
master_device = [ master_device = [
@@ -849,17 +815,6 @@ class BluesoundPlayer(MediaPlayerEntity):
else: else:
_LOGGER.error("Master not found %s", master_device) _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): def rebuild_bluesound_group(self):
"""Rebuild the list of entities in speaker group.""" """Rebuild the list of entities in speaker group."""
if self._group_name is None: if self._group_name is None:

View File

@@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry, discovery from homeassistant.helpers import device_registry, discovery
import homeassistant.helpers.config_validation as cv 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.helpers.event import track_utc_time_change
from homeassistant.util import slugify from homeassistant.util import slugify
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@@ -317,6 +317,8 @@ class BMWConnectedDriveAccount:
class BMWConnectedDriveBaseEntity(Entity): class BMWConnectedDriveBaseEntity(Entity):
"""Common base for BMW entities.""" """Common base for BMW entities."""
_attr_should_poll = False
def __init__(self, account, vehicle): def __init__(self, account, vehicle):
"""Initialize sensor.""" """Initialize sensor."""
self._account = account self._account = account
@@ -326,15 +328,11 @@ class BMWConnectedDriveBaseEntity(Entity):
"vin": self._vehicle.vin, "vin": self._vehicle.vin,
ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_ATTRIBUTION: ATTRIBUTION,
} }
self._attr_device_info = {
@property "identifiers": {(DOMAIN, vehicle.vin)},
def device_info(self) -> DeviceInfo: "name": f'{vehicle.attributes.get("brand")} {vehicle.name}',
"""Return info for device registry.""" "model": vehicle.name,
return { "manufacturer": vehicle.attributes.get("brand"),
"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"),
} }
@property @property
@@ -342,14 +340,6 @@ class BMWConnectedDriveBaseEntity(Entity):
"""Return the state attributes of the sensor.""" """Return the state attributes of the sensor."""
return self._attrs return self._attrs
@property
def should_poll(self):
"""Do not poll this class.
Updates are triggered from BMWConnectedDriveAccount.
"""
return False
def update_callback(self): def update_callback(self):
"""Schedule a state update.""" """Schedule a state update."""
self.schedule_update_ha_state(True) self.schedule_update_ha_state(True)

View File

@@ -76,41 +76,45 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity):
super().__init__(account, vehicle) super().__init__(account, vehicle)
self._attribute = attribute self._attribute = attribute
self._name = f"{self._vehicle.name} {self._attribute}" self._attr_name = f"{vehicle.name} {attribute}"
self._unique_id = f"{self._vehicle.vin}-{self._attribute}" self._attr_unique_id = f"{vehicle.vin}-{attribute}"
self._sensor_name = sensor_name self._sensor_name = sensor_name
self._device_class = device_class self._attr_device_class = device_class
self._icon = icon self._attr_icon = icon
self._state = None
@property def update(self):
def unique_id(self): """Read new state data from the library."""
"""Return the unique ID of the binary sensor.""" vehicle_state = self._vehicle.state
return self._unique_id
@property # device class opening: On means open, Off means closed
def name(self): if self._attribute == "lids":
"""Return the name of the binary sensor.""" _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
return self._name 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 vehicle_state = self._vehicle.state
result = self._attrs.copy() result = self._attrs.copy()
@@ -144,40 +148,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity):
elif self._attribute == "connection_status": elif self._attribute == "connection_status":
result["connection_status"] = vehicle_state.connection_status result["connection_status"] = vehicle_state.connection_status
return sorted(result.items()) self._attr_extra_state_attributes = 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"
def _format_cbs_report(self, report): def _format_cbs_report(self, report):
result = {} result = {}

View File

@@ -29,15 +29,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity):
"""BMW Connected Drive device tracker.""" """BMW Connected Drive device tracker."""
_attr_force_update = False
_attr_icon = "mdi:car"
def __init__(self, account, vehicle): def __init__(self, account, vehicle):
"""Initialize the Tracker.""" """Initialize the Tracker."""
super().__init__(account, vehicle) super().__init__(account, vehicle)
self._unique_id = vehicle.vin self._attr_unique_id = vehicle.vin
self._location = ( self._location = (
vehicle.state.gps_position if vehicle.state.gps_position else (None, None) vehicle.state.gps_position if vehicle.state.gps_position else (None, None)
) )
self._name = vehicle.name self._attr_name = vehicle.name
@property @property
def latitude(self): def latitude(self):
@@ -49,31 +52,11 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity):
"""Return longitude value of the device.""" """Return longitude value of the device."""
return self._location[1] if self._location else None 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 @property
def source_type(self): def source_type(self):
"""Return the source type, eg gps or router, of the device.""" """Return the source type, eg gps or router, of the device."""
return SOURCE_TYPE_GPS 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): def update(self):
"""Update state of the decvice tracker.""" """Update state of the decvice tracker."""
self._location = ( self._location = (

View File

@@ -4,7 +4,6 @@ import logging
from bimmer_connected.state import LockState from bimmer_connected.state import LockState
from homeassistant.components.lock import LockEntity from homeassistant.components.lock import LockEntity
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity
from .const import CONF_ACCOUNT, DATA_ENTRIES from .const import CONF_ACCOUNT, DATA_ENTRIES
@@ -33,50 +32,17 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity):
super().__init__(account, vehicle) super().__init__(account, vehicle)
self._attribute = attribute self._attribute = attribute
self._name = f"{self._vehicle.name} {self._attribute}" self._attr_name = f"{vehicle.name} {attribute}"
self._unique_id = f"{self._vehicle.vin}-{self._attribute}" self._attr_unique_id = f"{vehicle.vin}-{attribute}"
self._sensor_name = sensor_name self._sensor_name = sensor_name
self._state = None self.door_lock_state_available = DOOR_LOCK_STATE in vehicle.available_attributes
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
def lock(self, **kwargs): def lock(self, **kwargs):
"""Lock the car.""" """Lock the car."""
_LOGGER.debug("%s: locking doors", self._vehicle.name) _LOGGER.debug("%s: locking doors", self._vehicle.name)
# Optimistic state set here because it takes some time before the # Optimistic state set here because it takes some time before the
# update callback response # update callback response
self._state = STATE_LOCKED self._attr_is_locked = True
self.schedule_update_ha_state() self.schedule_update_ha_state()
self._vehicle.remote_services.trigger_remote_door_lock() self._vehicle.remote_services.trigger_remote_door_lock()
@@ -85,18 +51,23 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity):
_LOGGER.debug("%s: unlocking doors", self._vehicle.name) _LOGGER.debug("%s: unlocking doors", self._vehicle.name)
# Optimistic state set here because it takes some time before the # Optimistic state set here because it takes some time before the
# update callback response # update callback response
self._state = STATE_UNLOCKED self._attr_is_locked = False
self.schedule_update_ha_state() self.schedule_update_ha_state()
self._vehicle.remote_services.trigger_remote_door_unlock() self._vehicle.remote_services.trigger_remote_door_unlock()
def update(self): def update(self):
"""Update state of the lock.""" """Update state of the lock."""
_LOGGER.debug("%s: updating data for %s", self._vehicle.name, self._attribute) _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 vehicle_state = self._vehicle.state
self._state = ( result = self._attrs.copy()
STATE_LOCKED if self.door_lock_state_available:
if vehicle_state.door_lock_state in [LockState.LOCKED, LockState.SECURED] result["door_lock_state"] = vehicle_state.door_lock_state.value
else STATE_UNLOCKED result["last_update_reason"] = vehicle_state.last_update_reason
) self._attr_extra_state_attributes = result

View File

@@ -503,94 +503,46 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity):
self._attribute = attribute self._attribute = attribute
self._service = service self._service = service
self._state = None if service:
if self._service: self._attr_name = f"{vehicle.name} {service.lower()}_{attribute}"
self._name = ( self._attr_unique_id = f"{vehicle.vin}-{service.lower()}-{attribute}"
f"{self._vehicle.name} {self._service.lower()}_{self._attribute}"
)
self._unique_id = (
f"{self._vehicle.vin}-{self._service.lower()}-{self._attribute}"
)
else: else:
self._name = f"{self._vehicle.name} {self._attribute}" self._attr_name = f"{vehicle.name} {attribute}"
self._unique_id = f"{self._vehicle.vin}-{self._attribute}" self._attr_unique_id = f"{vehicle.vin}-{attribute}"
self._attribute_info = attribute_info self._attribute_info = attribute_info
self._attr_entity_registry_enabled_default = attribute_info.get(
@property attribute, [None, None, None, True]
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]
)[3] )[3]
return enabled_default self._attr_device_class = attribute_info.get(
attribute, [None, None, None, None]
@property )[1]
def state(self): self._attr_unit_of_measurement = attribute_info.get(
"""Return the state of the sensor. attribute, [None, None, None, None]
)[2]
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
def update(self) -> None: def update(self) -> None:
"""Read new state data from the library.""" """Read new state data from the library."""
_LOGGER.debug("Updating %s", self._vehicle.name) _LOGGER.debug("Updating %s", self._vehicle.name)
vehicle_state = self._vehicle.state vehicle_state = self._vehicle.state
if self._attribute == "charging_status": 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: elif self.unit_of_measurement == VOLUME_GALLONS:
value = getattr(vehicle_state, self._attribute) value = getattr(vehicle_state, self._attribute)
value_converted = self.hass.config.units.volume(value, VOLUME_LITERS) 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: elif self.unit_of_measurement == LENGTH_MILES:
value = getattr(vehicle_state, self._attribute) value = getattr(vehicle_state, self._attribute)
value_converted = self.hass.config.units.length(value, LENGTH_KILOMETERS) 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: 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: elif self._service == SERVICE_LAST_TRIP:
vehicle_last_trip = self._vehicle.state.last_trip vehicle_last_trip = self._vehicle.state.last_trip
if self._attribute == "date_utc": if self._attribute == "date_utc":
date_str = getattr(vehicle_last_trip, "date") 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: else:
self._state = getattr(vehicle_last_trip, self._attribute) self._attr_state = getattr(vehicle_last_trip, self._attribute)
elif self._service == SERVICE_ALL_TRIPS: elif self._service == SERVICE_ALL_TRIPS:
vehicle_all_trips = self._vehicle.state.all_trips vehicle_all_trips = self._vehicle.state.all_trips
for attribute in ( for attribute in (
@@ -603,10 +555,21 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity):
if self._attribute.startswith(f"{attribute}_"): if self._attribute.startswith(f"{attribute}_"):
attr = getattr(vehicle_all_trips, attribute) attr = getattr(vehicle_all_trips, attribute)
sub_attr = self._attribute.replace(f"{attribute}_", "") sub_attr = self._attribute.replace(f"{attribute}_", "")
self._state = getattr(attr, sub_attr) self._attr_state = getattr(attr, sub_attr)
return return
if self._attribute == "reset_date_utc": if self._attribute == "reset_date_utc":
date_str = getattr(vehicle_all_trips, "reset_date") 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: 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]

View File

@@ -19,7 +19,7 @@
}, },
"user": { "user": {
"data": { "data": {
"access_token": "Zugriffstoken", "access_token": "Zugangstoken",
"host": "Host" "host": "Host"
} }
} }

View File

@@ -1,5 +1,4 @@
{ {
"title": "Bosch SHC",
"config": { "config": {
"step": { "step": {
"user": { "user": {
@@ -35,4 +34,4 @@
}, },
"flow_title": "Bosch SHC: {name}" "flow_title": "Bosch SHC: {name}"
} }
} }

View File

@@ -1,11 +1,12 @@
"""Broadlink entities.""" """Broadlink entities."""
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import Entity
from .const import DOMAIN from .const import DOMAIN
class BroadlinkEntity: class BroadlinkEntity(Entity):
"""Representation of a Broadlink entity.""" """Representation of a Broadlink entity."""
_attr_should_poll = False _attr_should_poll = False

View File

@@ -127,10 +127,10 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity):
self._flags = defaultdict(int) self._flags = defaultdict(int)
self._lock = asyncio.Lock() 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_is_on = True
self._attr_supported_features = SUPPORT_LEARN_COMMAND | SUPPORT_DELETE_COMMAND 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): def _extract_codes(self, commands, device=None):
"""Extract a list of codes. """Extract a list of codes.

View File

@@ -77,14 +77,12 @@ class BroadlinkSensor(BroadlinkEntity, SensorEntity):
self._coordinator = device.update_manager.coordinator self._coordinator = device.update_manager.coordinator
self._monitored_condition = monitored_condition self._monitored_condition = monitored_condition
self._attr_device_class = SENSOR_TYPES[self._monitored_condition][2] self._attr_device_class = SENSOR_TYPES[monitored_condition][2]
self._attr_name = ( self._attr_name = f"{device.name} {SENSOR_TYPES[monitored_condition][0]}"
f"{self._device.name} {SENSOR_TYPES[self._monitored_condition][0]}" self._attr_state_class = SENSOR_TYPES[monitored_condition][3]
)
self._attr_state_class = SENSOR_TYPES[self._monitored_condition][3]
self._attr_state = self._coordinator.data[monitored_condition] self._attr_state = self._coordinator.data[monitored_condition]
self._attr_unique_id = f"{self._device.unique_id}-{self._monitored_condition}" self._attr_unique_id = f"{device.unique_id}-{monitored_condition}"
self._attr_unit_of_measurement = SENSOR_TYPES[self._monitored_condition][1] self._attr_unit_of_measurement = SENSOR_TYPES[monitored_condition][1]
@callback @callback
def update_data(self): def update_data(self):

View File

@@ -135,22 +135,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC): class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC):
"""Representation of a Broadlink switch.""" """Representation of a Broadlink switch."""
_attr_assumed_state = True
_attr_device_class = DEVICE_CLASS_SWITCH
def __init__(self, device, command_on, command_off): def __init__(self, device, command_on, command_off):
"""Initialize the switch.""" """Initialize the switch."""
super().__init__(device) super().__init__(device)
self._command_on = command_on self._command_on = command_on
self._command_off = command_off self._command_off = command_off
self._coordinator = device.update_manager.coordinator self._coordinator = device.update_manager.coordinator
self._state = None
self._attr_assumed_state = True self._attr_assumed_state = True
self._attr_device_class = DEVICE_CLASS_SWITCH self._attr_device_class = DEVICE_CLASS_SWITCH
self._attr_name = f"{self._device.name} Switch" self._attr_name = f"{device.name} Switch"
self._attr_unique_id = device.unique_id
@property
def is_on(self):
"""Return True if the switch is on."""
return self._state
@callback @callback
def update_data(self): def update_data(self):
@@ -159,9 +157,8 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Call when the switch is added to hass.""" """Call when the switch is added to hass."""
if self._state is None: state = await self.async_get_last_state()
state = await self.async_get_last_state() self._attr_is_on = state is not None and state.state == STATE_ON
self._state = state is not None and state.state == STATE_ON
self.async_on_remove(self._coordinator.async_add_listener(self.update_data)) self.async_on_remove(self._coordinator.async_add_listener(self.update_data))
async def async_update(self): async def async_update(self):
@@ -171,13 +168,13 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC):
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn on the switch.""" """Turn on the switch."""
if await self._async_send_packet(self._command_on): if await self._async_send_packet(self._command_on):
self._state = True self._attr_is_on = True
self.async_write_ha_state() self.async_write_ha_state()
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn off the switch.""" """Turn off the switch."""
if await self._async_send_packet(self._command_off): if await self._async_send_packet(self._command_off):
self._state = False self._attr_is_on = False
self.async_write_ha_state() self.async_write_ha_state()
@abstractmethod @abstractmethod
@@ -229,46 +226,41 @@ class BroadlinkSP1Switch(BroadlinkSwitch):
class BroadlinkSP2Switch(BroadlinkSP1Switch): class BroadlinkSP2Switch(BroadlinkSP1Switch):
"""Representation of a Broadlink SP2 switch.""" """Representation of a Broadlink SP2 switch."""
_attr_assumed_state = False
def __init__(self, device, *args, **kwargs): def __init__(self, device, *args, **kwargs):
"""Initialize the switch.""" """Initialize the switch."""
super().__init__(device, *args, **kwargs) super().__init__(device, *args, **kwargs)
self._state = self._coordinator.data["pwr"] self._attr_is_on = self._coordinator.data["pwr"]
self._load_power = self._coordinator.data.get("power") self._attr_current_power_w = 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
@callback @callback
def update_data(self): def update_data(self):
"""Update data.""" """Update data."""
if self._coordinator.last_update_success: if self._coordinator.last_update_success:
self._state = self._coordinator.data["pwr"] self._attr_is_on = self._coordinator.data["pwr"]
self._load_power = self._coordinator.data.get("power") self._attr_current_power_w = self._coordinator.data.get("power")
self.async_write_ha_state() self.async_write_ha_state()
class BroadlinkMP1Slot(BroadlinkSwitch): class BroadlinkMP1Slot(BroadlinkSwitch):
"""Representation of a Broadlink MP1 slot.""" """Representation of a Broadlink MP1 slot."""
_attr_assumed_state = False
def __init__(self, device, slot): def __init__(self, device, slot):
"""Initialize the switch.""" """Initialize the switch."""
super().__init__(device, 1, 0) super().__init__(device, 1, 0)
self._slot = slot self._slot = slot
self._state = self._coordinator.data[f"s{slot}"] self._attr_is_on = self._coordinator.data[f"s{slot}"]
self._attr_name = f"{device.name} S{slot}"
self._attr_name = f"{self._device.name} S{self._slot}" self._attr_unique_id = f"{device.unique_id}-s{slot}"
self._attr_unique_id = f"{self._device.unique_id}-s{self._slot}"
self._attr_assumed_state = False
@callback @callback
def update_data(self): def update_data(self):
"""Update data.""" """Update data."""
if self._coordinator.last_update_success: 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() self.async_write_ha_state()
async def _async_send_packet(self, packet): async def _async_send_packet(self, packet):
@@ -286,22 +278,23 @@ class BroadlinkMP1Slot(BroadlinkSwitch):
class BroadlinkBG1Slot(BroadlinkSwitch): class BroadlinkBG1Slot(BroadlinkSwitch):
"""Representation of a Broadlink BG1 slot.""" """Representation of a Broadlink BG1 slot."""
_attr_assumed_state = False
def __init__(self, device, slot): def __init__(self, device, slot):
"""Initialize the switch.""" """Initialize the switch."""
super().__init__(device, 1, 0) super().__init__(device, 1, 0)
self._slot = slot 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_device_class = DEVICE_CLASS_OUTLET
self._attr_unique_id = f"{self._device.unique_id}-s{self._slot}" self._attr_unique_id = f"{device.unique_id}-s{slot}"
self._attr_assumed_state = False
@callback @callback
def update_data(self): def update_data(self):
"""Update data.""" """Update data."""
if self._coordinator.last_update_success: 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() self.async_write_ha_state()
async def _async_send_packet(self, packet): async def _async_send_packet(self, packet):

View File

@@ -4,13 +4,13 @@
"already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_configured": "Ger\u00e4t ist bereits konfiguriert",
"already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt",
"cannot_connect": "Verbindung fehlgeschlagen", "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", "not_supported": "Ger\u00e4t nicht unterst\u00fctzt",
"unknown": "Unerwarteter Fehler" "unknown": "Unerwarteter Fehler"
}, },
"error": { "error": {
"cannot_connect": "Verbindung fehlgeschlagen", "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse",
"unknown": "Unerwarteter Fehler" "unknown": "Unerwarteter Fehler"
}, },
"flow_title": "{name} ({model} unter {host})", "flow_title": "{name} ({model} unter {host})",

View File

@@ -3,15 +3,10 @@ from __future__ import annotations
from typing import Final from typing import Final
from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
from homeassistant.const import ( from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE
ATTR_DEVICE_CLASS,
ATTR_ICON,
DEVICE_CLASS_TIMESTAMP,
PERCENTAGE,
)
from .model import SensorDescription from .model import BrotherSensorMetadata
ATTR_BELT_UNIT_REMAINING_LIFE: Final = "belt_unit_remaining_life" ATTR_BELT_UNIT_REMAINING_LIFE: Final = "belt_unit_remaining_life"
ATTR_BLACK_DRUM_COUNTER: Final = "black_drum_counter" 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_LIFE: Final = "drum_remaining_life"
ATTR_DRUM_REMAINING_PAGES: Final = "drum_remaining_pages" ATTR_DRUM_REMAINING_PAGES: Final = "drum_remaining_pages"
ATTR_DUPLEX_COUNTER: Final = "duplex_unit_pages_counter" ATTR_DUPLEX_COUNTER: Final = "duplex_unit_pages_counter"
ATTR_ENABLED: Final = "enabled"
ATTR_FUSER_REMAINING_LIFE: Final = "fuser_remaining_life" ATTR_FUSER_REMAINING_LIFE: Final = "fuser_remaining_life"
ATTR_LABEL: Final = "label"
ATTR_LASER_REMAINING_LIFE: Final = "laser_remaining_life" ATTR_LASER_REMAINING_LIFE: Final = "laser_remaining_life"
ATTR_MAGENTA_DRUM_COUNTER: Final = "magenta_drum_counter" ATTR_MAGENTA_DRUM_COUNTER: Final = "magenta_drum_counter"
ATTR_MAGENTA_DRUM_REMAINING_LIFE: Final = "magenta_drum_remaining_life" 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_PF_KIT_MP_REMAINING_LIFE: Final = "pf_kit_mp_remaining_life"
ATTR_REMAINING_PAGES: Final = "remaining_pages" ATTR_REMAINING_PAGES: Final = "remaining_pages"
ATTR_STATUS: Final = "status" ATTR_STATUS: Final = "status"
ATTR_UNIT: Final = "unit"
ATTR_UPTIME: Final = "uptime" ATTR_UPTIME: Final = "uptime"
ATTR_YELLOW_DRUM_COUNTER: Final = "yellow_drum_counter" ATTR_YELLOW_DRUM_COUNTER: Final = "yellow_drum_counter"
ATTR_YELLOW_DRUM_REMAINING_LIFE: Final = "yellow_drum_remaining_life" 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]] = { SENSOR_TYPES: Final[dict[str, BrotherSensorMetadata]] = {
ATTR_STATUS: { ATTR_STATUS: BrotherSensorMetadata(
ATTR_ICON: "mdi:printer", icon="mdi:printer",
ATTR_LABEL: ATTR_STATUS.title(), label=ATTR_STATUS.title(),
ATTR_UNIT: None, unit_of_measurement=None,
ATTR_ENABLED: True, enabled=True,
ATTR_STATE_CLASS: None, ),
}, ATTR_PAGE_COUNTER: BrotherSensorMetadata(
ATTR_PAGE_COUNTER: { icon="mdi:file-document-outline",
ATTR_ICON: "mdi:file-document-outline", label=ATTR_PAGE_COUNTER.replace("_", " ").title(),
ATTR_LABEL: ATTR_PAGE_COUNTER.replace("_", " ").title(), unit_of_measurement=UNIT_PAGES,
ATTR_UNIT: UNIT_PAGES, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_BW_COUNTER: BrotherSensorMetadata(
ATTR_BW_COUNTER: { icon="mdi:file-document-outline",
ATTR_ICON: "mdi:file-document-outline", label=ATTR_BW_COUNTER.replace("_", " ").title(),
ATTR_LABEL: ATTR_BW_COUNTER.replace("_", " ").title(), unit_of_measurement=UNIT_PAGES,
ATTR_UNIT: UNIT_PAGES, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_COLOR_COUNTER: BrotherSensorMetadata(
ATTR_COLOR_COUNTER: { icon="mdi:file-document-outline",
ATTR_ICON: "mdi:file-document-outline", label=ATTR_COLOR_COUNTER.replace("_", " ").title(),
ATTR_LABEL: ATTR_COLOR_COUNTER.replace("_", " ").title(), unit_of_measurement=UNIT_PAGES,
ATTR_UNIT: UNIT_PAGES, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_DUPLEX_COUNTER: BrotherSensorMetadata(
ATTR_DUPLEX_COUNTER: { icon="mdi:file-document-outline",
ATTR_ICON: "mdi:file-document-outline", label=ATTR_DUPLEX_COUNTER.replace("_", " ").title(),
ATTR_LABEL: ATTR_DUPLEX_COUNTER.replace("_", " ").title(), unit_of_measurement=UNIT_PAGES,
ATTR_UNIT: UNIT_PAGES, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_DRUM_REMAINING_LIFE: BrotherSensorMetadata(
ATTR_DRUM_REMAINING_LIFE: { icon="mdi:chart-donut",
ATTR_ICON: "mdi:chart-donut", label=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_LABEL: ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_BLACK_DRUM_REMAINING_LIFE: BrotherSensorMetadata(
ATTR_BLACK_DRUM_REMAINING_LIFE: { icon="mdi:chart-donut",
ATTR_ICON: "mdi:chart-donut", label=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_LABEL: ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_CYAN_DRUM_REMAINING_LIFE: BrotherSensorMetadata(
ATTR_CYAN_DRUM_REMAINING_LIFE: { icon="mdi:chart-donut",
ATTR_ICON: "mdi:chart-donut", label=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_LABEL: ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_MAGENTA_DRUM_REMAINING_LIFE: BrotherSensorMetadata(
ATTR_MAGENTA_DRUM_REMAINING_LIFE: { icon="mdi:chart-donut",
ATTR_ICON: "mdi:chart-donut", label=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_LABEL: ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_YELLOW_DRUM_REMAINING_LIFE: BrotherSensorMetadata(
ATTR_YELLOW_DRUM_REMAINING_LIFE: { icon="mdi:chart-donut",
ATTR_ICON: "mdi:chart-donut", label=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_LABEL: ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_BELT_UNIT_REMAINING_LIFE: BrotherSensorMetadata(
ATTR_BELT_UNIT_REMAINING_LIFE: { icon="mdi:current-ac",
ATTR_ICON: "mdi:current-ac", label=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(),
ATTR_LABEL: ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_FUSER_REMAINING_LIFE: BrotherSensorMetadata(
ATTR_FUSER_REMAINING_LIFE: { icon="mdi:water-outline",
ATTR_ICON: "mdi:water-outline", label=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(),
ATTR_LABEL: ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_LASER_REMAINING_LIFE: BrotherSensorMetadata(
ATTR_LASER_REMAINING_LIFE: { icon="mdi:spotlight-beam",
ATTR_ICON: "mdi:spotlight-beam", label=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(),
ATTR_LABEL: ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_PF_KIT_1_REMAINING_LIFE: BrotherSensorMetadata(
ATTR_PF_KIT_1_REMAINING_LIFE: { icon="mdi:printer-3d",
ATTR_ICON: "mdi:printer-3d", label=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(),
ATTR_LABEL: ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_PF_KIT_MP_REMAINING_LIFE: BrotherSensorMetadata(
ATTR_PF_KIT_MP_REMAINING_LIFE: { icon="mdi:printer-3d",
ATTR_ICON: "mdi:printer-3d", label=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(),
ATTR_LABEL: ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_BLACK_TONER_REMAINING: BrotherSensorMetadata(
ATTR_BLACK_TONER_REMAINING: { icon="mdi:printer-3d-nozzle",
ATTR_ICON: "mdi:printer-3d-nozzle", label=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(),
ATTR_LABEL: ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_CYAN_TONER_REMAINING: BrotherSensorMetadata(
ATTR_CYAN_TONER_REMAINING: { icon="mdi:printer-3d-nozzle",
ATTR_ICON: "mdi:printer-3d-nozzle", label=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(),
ATTR_LABEL: ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_MAGENTA_TONER_REMAINING: BrotherSensorMetadata(
ATTR_MAGENTA_TONER_REMAINING: { icon="mdi:printer-3d-nozzle",
ATTR_ICON: "mdi:printer-3d-nozzle", label=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(),
ATTR_LABEL: ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_YELLOW_TONER_REMAINING: BrotherSensorMetadata(
ATTR_YELLOW_TONER_REMAINING: { icon="mdi:printer-3d-nozzle",
ATTR_ICON: "mdi:printer-3d-nozzle", label=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(),
ATTR_LABEL: ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_BLACK_INK_REMAINING: BrotherSensorMetadata(
ATTR_BLACK_INK_REMAINING: { icon="mdi:printer-3d-nozzle",
ATTR_ICON: "mdi:printer-3d-nozzle", label=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(),
ATTR_LABEL: ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_CYAN_INK_REMAINING: BrotherSensorMetadata(
ATTR_CYAN_INK_REMAINING: { icon="mdi:printer-3d-nozzle",
ATTR_ICON: "mdi:printer-3d-nozzle", label=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(),
ATTR_LABEL: ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_MAGENTA_INK_REMAINING: BrotherSensorMetadata(
ATTR_MAGENTA_INK_REMAINING: { icon="mdi:printer-3d-nozzle",
ATTR_ICON: "mdi:printer-3d-nozzle", label=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(),
ATTR_LABEL: ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_YELLOW_INK_REMAINING: BrotherSensorMetadata(
ATTR_YELLOW_INK_REMAINING: { icon="mdi:printer-3d-nozzle",
ATTR_ICON: "mdi:printer-3d-nozzle", label=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(),
ATTR_LABEL: ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), unit_of_measurement=PERCENTAGE,
ATTR_UNIT: PERCENTAGE, enabled=True,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, ATTR_UPTIME: BrotherSensorMetadata(
ATTR_UPTIME: { icon=None,
ATTR_ICON: None, label=ATTR_UPTIME.title(),
ATTR_LABEL: ATTR_UPTIME.title(), unit_of_measurement=None,
ATTR_UNIT: None, enabled=False,
ATTR_ENABLED: False, device_class=DEVICE_CLASS_TIMESTAMP,
ATTR_STATE_CLASS: None, ),
ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
},
} }

View File

@@ -1,15 +1,15 @@
"""Type definitions for Brother integration.""" """Type definitions for Brother integration."""
from __future__ import annotations from __future__ import annotations
from typing import TypedDict from typing import NamedTuple
class SensorDescription(TypedDict, total=False): class BrotherSensorMetadata(NamedTuple):
"""Sensor description class.""" """Metadata for an individual Brother sensor."""
icon: str | None icon: str | None
label: str label: str
unit: str | None unit_of_measurement: str | None
enabled: bool enabled: bool
state_class: str | None state_class: str | None = None
device_class: str | None device_class: str | None = None

View File

@@ -3,9 +3,8 @@ from __future__ import annotations
from typing import Any 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.config_entries import ConfigEntry
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -14,17 +13,15 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import BrotherDataUpdateCoordinator from . import BrotherDataUpdateCoordinator
from .const import ( from .const import (
ATTR_COUNTER, ATTR_COUNTER,
ATTR_ENABLED,
ATTR_LABEL,
ATTR_MANUFACTURER, ATTR_MANUFACTURER,
ATTR_REMAINING_PAGES, ATTR_REMAINING_PAGES,
ATTR_UNIT,
ATTR_UPTIME, ATTR_UPTIME,
ATTRS_MAP, ATTRS_MAP,
DATA_CONFIG_ENTRY, DATA_CONFIG_ENTRY,
DOMAIN, DOMAIN,
SENSOR_TYPES, SENSOR_TYPES,
) )
from .model import BrotherSensorMetadata
async def async_setup_entry( async def async_setup_entry(
@@ -43,9 +40,11 @@ async def async_setup_entry(
"sw_version": getattr(coordinator.data, "firmware", None), "sw_version": getattr(coordinator.data, "firmware", None),
} }
for sensor in SENSOR_TYPES: for sensor, metadata in SENSOR_TYPES.items():
if sensor in coordinator.data: 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) async_add_entities(sensors, False)
@@ -56,20 +55,20 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity):
self, self,
coordinator: BrotherDataUpdateCoordinator, coordinator: BrotherDataUpdateCoordinator,
kind: str, kind: str,
metadata: BrotherSensorMetadata,
device_info: DeviceInfo, device_info: DeviceInfo,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
description = SENSOR_TYPES[kind]
self._attrs: dict[str, Any] = {} 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_device_info = device_info
self._attr_entity_registry_enabled_default = description[ATTR_ENABLED] self._attr_entity_registry_enabled_default = metadata.enabled
self._attr_icon = description[ATTR_ICON] self._attr_icon = metadata.icon
self._attr_name = f"{coordinator.data.model} {description[ATTR_LABEL]}" self._attr_name = f"{coordinator.data.model} {metadata.label}"
self._attr_state_class = description[ATTR_STATE_CLASS] self._attr_state_class = metadata.state_class
self._attr_unique_id = f"{coordinator.data.serial.lower()}_{kind}" 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 self.kind = kind
@property @property

View File

@@ -1,7 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "Dieser Drucker ist bereits konfiguriert", "already_configured": "Ger\u00e4t ist bereits konfiguriert",
"unsupported_model": "Dieses Druckermodell wird nicht unterst\u00fctzt." "unsupported_model": "Dieses Druckermodell wird nicht unterst\u00fctzt."
}, },
"error": { "error": {

View File

@@ -27,7 +27,6 @@ from homeassistant.const import (
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
@@ -88,6 +87,10 @@ async def async_setup_entry(
class BSBLanClimate(ClimateEntity): class BSBLanClimate(ClimateEntity):
"""Defines a BSBLan climate device.""" """Defines a BSBLan climate device."""
_attr_supported_features = SUPPORT_FLAGS
_attr_hvac_modes = HVAC_MODES
_attr_preset_modes = PRESET_MODES
def __init__( def __init__(
self, self,
entry_id: str, entry_id: str,
@@ -95,89 +98,33 @@ class BSBLanClimate(ClimateEntity):
info: Info, info: Info,
) -> None: ) -> None:
"""Initialize BSBLan climate device.""" """Initialize BSBLan climate device."""
self._current_temperature: float | None = None self._attr_available = True
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._store_hvac_mode = None self._store_hvac_mode = None
self._info: Info = info
self.bsblan = bsblan self.bsblan = bsblan
self._attr_name = self._attr_unique_id = info.device_identification
@property self._attr_device_info = {
def name(self) -> str: ATTR_IDENTIFIERS: {(DOMAIN, info.device_identification)},
"""Return the name of the entity.""" ATTR_NAME: "BSBLan Device",
return self._info.device_identification ATTR_MANUFACTURER: "BSBLan",
ATTR_MODEL: info.controller_variant,
@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
async def async_set_preset_mode(self, preset_mode): async def async_set_preset_mode(self, preset_mode):
"""Set preset mode.""" """Set preset mode."""
_LOGGER.debug("Setting preset mode to: %s", preset_mode) _LOGGER.debug("Setting preset mode to: %s", preset_mode)
if preset_mode == PRESET_NONE: if preset_mode == PRESET_NONE:
# restore previous hvac mode # restore previous hvac mode
self._hvac_mode = self._store_hvac_mode self._attr_hvac_mode = self._store_hvac_mode
else: else:
# Store hvac mode. # 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) await self.async_set_data(preset_mode=preset_mode)
async def async_set_hvac_mode(self, hvac_mode): async def async_set_hvac_mode(self, hvac_mode):
"""Set HVAC mode.""" """Set HVAC mode."""
_LOGGER.debug("Setting HVAC mode to: %s", hvac_mode) _LOGGER.debug("Setting HVAC mode to: %s", hvac_mode)
# preset should be none when hvac mode is set # 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) await self.async_set_data(hvac_mode=hvac_mode)
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
@@ -204,39 +151,33 @@ class BSBLanClimate(ClimateEntity):
await self.bsblan.thermostat(**data) await self.bsblan.thermostat(**data)
except BSBLanError: except BSBLanError:
_LOGGER.error("An error occurred while updating the BSBLan device") _LOGGER.error("An error occurred while updating the BSBLan device")
self._available = False self._attr_available = False
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update BSBlan entity.""" """Update BSBlan entity."""
try: try:
state: State = await self.bsblan.state() state: State = await self.bsblan.state()
except BSBLanError: except BSBLanError:
if self._available: if self.available:
_LOGGER.error("An error occurred while updating the BSBLan device") _LOGGER.error("An error occurred while updating the BSBLan device")
self._available = False self._attr_available = False
return return
self._available = True self._attr_available = True
self._current_temperature = float(state.current_temperature.value) self._attr_current_temperature = float(state.current_temperature.value)
self._target_temperature = float(state.target_temperature.value) self._attr_target_temperature = float(state.target_temperature.value)
# check if preset is active else get hvac mode # check if preset is active else get hvac mode
_LOGGER.debug("state hvac/preset mode: %s", state.hvac_mode.value) _LOGGER.debug("state hvac/preset mode: %s", state.hvac_mode.value)
if state.hvac_mode.value == "2": if state.hvac_mode.value == "2":
self._preset_mode = PRESET_ECO self._attr_preset_mode = PRESET_ECO
else: else:
self._hvac_mode = BSBLAN_TO_HA_STATE[state.hvac_mode.value] self._attr_hvac_mode = BSBLAN_TO_HA_STATE[state.hvac_mode.value]
self._preset_mode = PRESET_NONE self._attr_preset_mode = PRESET_NONE
self._temperature_unit = state.current_temperature.unit self._attr_temperature_unit = (
TEMP_CELSIUS
@property if state.current_temperature.unit == "°C"
def device_info(self) -> DeviceInfo: else TEMP_FAHRENHEIT
"""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,
}

View File

@@ -13,7 +13,7 @@
"host": "Host", "host": "Host",
"passkey": "Passkey String", "passkey": "Passkey String",
"password": "Passwort", "password": "Passwort",
"port": "Port Nummer", "port": "Port",
"username": "Benutzername" "username": "Benutzername"
}, },
"description": "Richte dein BSB-Lan Ger\u00e4t f\u00fcr die Integration mit dem Home Assistant ein.", "description": "Richte dein BSB-Lan Ger\u00e4t f\u00fcr die Integration mit dem Home Assistant ein.",

View File

@@ -119,24 +119,13 @@ class WebDavCalendarEventDevice(CalendarEventDevice):
self.data = WebDavCalendarData(calendar, days, all_day, search) self.data = WebDavCalendarData(calendar, days, all_day, search)
self.entity_id = entity_id self.entity_id = entity_id
self._event = None self._event = None
self._name = name self._attr_name = name
self._offset_reached = False
@property
def extra_state_attributes(self):
"""Return the device state attributes."""
return {"offset_reached": self._offset_reached}
@property @property
def event(self): def event(self):
"""Return the next upcoming event.""" """Return the next upcoming event."""
return self._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): async def async_get_events(self, hass, start_date, end_date):
"""Get all events in a specific time frame.""" """Get all events in a specific time frame."""
return await self.data.async_get_events(hass, start_date, end_date) return await self.data.async_get_events(hass, start_date, end_date)
@@ -149,8 +138,8 @@ class WebDavCalendarEventDevice(CalendarEventDevice):
self._event = event self._event = event
return return
event = calculate_offset(event, OFFSET) event = calculate_offset(event, OFFSET)
self._offset_reached = is_offset_reached(event)
self._event = event self._event = event
self._attr_extra_state_attributes = {"offset_reached": is_offset_reached(event)}
class WebDavCalendarData: class WebDavCalendarData:

View File

@@ -52,6 +52,9 @@ class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity):
"""Representation of a Canary alarm control panel.""" """Representation of a Canary alarm control panel."""
coordinator: CanaryDataUpdateCoordinator coordinator: CanaryDataUpdateCoordinator
_attr_supported_features = (
SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT
)
def __init__( def __init__(
self, coordinator: CanaryDataUpdateCoordinator, location: Location self, coordinator: CanaryDataUpdateCoordinator, location: Location
@@ -59,23 +62,14 @@ class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity):
"""Initialize a Canary security camera.""" """Initialize a Canary security camera."""
super().__init__(coordinator) super().__init__(coordinator)
self._location_id: str = location.location_id 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 @property
def location(self) -> Location: def location(self) -> Location:
"""Return information about the location.""" """Return information about the location."""
return self.coordinator.data["locations"][self._location_id] 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 @property
def state(self) -> str | None: def state(self) -> str | None:
"""Return the state of the device.""" """Return the state of the device."""
@@ -92,11 +86,6 @@ class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity):
return None 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 @property
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes.""" """Return the state attributes."""

View File

@@ -21,7 +21,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import Throttle from homeassistant.util import Throttle
@@ -30,7 +29,6 @@ from .const import (
CONF_FFMPEG_ARGUMENTS, CONF_FFMPEG_ARGUMENTS,
DATA_COORDINATOR, DATA_COORDINATOR,
DEFAULT_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS,
DEFAULT_TIMEOUT,
DOMAIN, DOMAIN,
MANUFACTURER, MANUFACTURER,
) )
@@ -73,7 +71,6 @@ async def async_setup_entry(
coordinator, coordinator,
location_id, location_id,
device, device,
DEFAULT_TIMEOUT,
ffmpeg_arguments, ffmpeg_arguments,
) )
) )
@@ -92,7 +89,6 @@ class CanaryCamera(CoordinatorEntity, Camera):
coordinator: CanaryDataUpdateCoordinator, coordinator: CanaryDataUpdateCoordinator,
location_id: str, location_id: str,
device: Device, device: Device,
timeout: int,
ffmpeg_args: str, ffmpeg_args: str,
) -> None: ) -> None:
"""Initialize a Canary security camera.""" """Initialize a Canary security camera."""
@@ -102,37 +98,21 @@ class CanaryCamera(CoordinatorEntity, Camera):
self._ffmpeg_arguments = ffmpeg_args self._ffmpeg_arguments = ffmpeg_args
self._location_id = location_id self._location_id = location_id
self._device = device 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._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 @property
def location(self) -> Location: def location(self) -> Location:
"""Return information about the location.""" """Return information about the location."""
return self.coordinator.data["locations"][self._location_id] 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 @property
def is_recording(self) -> bool: def is_recording(self) -> bool:
"""Return true if the device is recording.""" """Return true if the device is recording."""

View File

@@ -17,7 +17,6 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -97,11 +96,9 @@ class CanarySensor(CoordinatorEntity, SensorEntity):
super().__init__(coordinator) super().__init__(coordinator)
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._device_id = device.device_id 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() 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 canary_sensor_type = None
if self._sensor_type[0] == "air_quality": if self._sensor_type[0] == "air_quality":
@@ -116,6 +113,17 @@ class CanarySensor(CoordinatorEntity, SensorEntity):
canary_sensor_type = SensorType.BATTERY canary_sensor_type = SensorType.BATTERY
self._canary_type = canary_sensor_type 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 @property
def reading(self) -> float | None: def reading(self) -> float | None:
@@ -136,46 +144,6 @@ class CanarySensor(CoordinatorEntity, SensorEntity):
return None 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 @property
def extra_state_attributes(self) -> dict[str, str] | None: def extra_state_attributes(self) -> dict[str, str] | None:
"""Return the state attributes.""" """Return the state attributes."""

View File

@@ -1,7 +1,7 @@
{ {
"config": { "config": {
"abort": { "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": { "error": {
"invalid_known_hosts": "Bekannte Hosts m\u00fcssen eine durch Kommata getrennte Liste von Hosts sein." "invalid_known_hosts": "Bekannte Hosts m\u00fcssen eine durch Kommata getrennte Liste von Hosts sein."

View File

@@ -1,7 +1,7 @@
{ {
"config": { "config": {
"abort": { "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" "import_failed": "Import aus Konfiguration fehlgeschlagen"
}, },
"error": { "error": {

View File

@@ -158,7 +158,7 @@ class ClimaCellSensorMetadata:
name: str name: str
unit_imperial: str | None = None unit_imperial: str | None = None
unit_metric: 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 is_metric_check: bool | None = None
device_class: str | None = None device_class: str | None = None
value_map: IntEnum | None = None value_map: IntEnum | None = None

View File

@@ -1,5 +1,4 @@
{ {
"title": "ClimaCell",
"config": { "config": {
"step": { "step": {
"user": { "user": {

View File

@@ -26,7 +26,7 @@
}, },
"user": { "user": {
"data": { "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.", "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" "title": "Mit Cloudflare verbinden"

View File

@@ -114,9 +114,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
else: else:
new_entry_type = TYPE_USE_HOME new_entry_type = TYPE_USE_HOME
for entry in self._async_current_entries(include_ignore=True): for entry in self._async_current_entries(include_ignore=False):
if entry.source == config_entries.SOURCE_IGNORE:
continue
if (cur_entry_type := _get_entry_type(entry.data)) != new_entry_type: if (cur_entry_type := _get_entry_type(entry.data)) != new_entry_type:
continue continue

View 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."
}
}
}
}

View 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."
}
}
}
}

View 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/."
}
}
}
}

View 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