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

View File

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

13
.vscode/tasks.json vendored
View File

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

View File

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

View File

@@ -26,7 +26,7 @@
"user": {
"data": {
"password": "Passwort",
"username": "E-Mail-Adresse"
"username": "E-Mail"
},
"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",
"password": "Passwort",
"port": "Port",
"ssl": "AdGuard Home verwendet ein SSL-Zertifikat",
"ssl": "Verwendet ein SSL-Zertifikat",
"username": "Benutzername",
"verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
},

View File

@@ -2,7 +2,7 @@
import voluptuous as vol
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
from homeassistant.const import PERCENTAGE
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
from homeassistant.helpers import config_validation as cv, entity_platform
from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN
@@ -25,9 +25,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(AdvantageAirTimeTo(instance, ac_key, "On"))
entities.append(AdvantageAirTimeTo(instance, ac_key, "Off"))
for zone_key, zone in ac_device["zones"].items():
# Only show damper sensors when zone is in temperature control
# Only show damper and temp sensors when zone is in temperature control
if zone["type"] != 0:
entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key))
entities.append(AdvantageAirZoneTemp(instance, ac_key, zone_key))
# Only show wireless signal strength sensors when using wireless sensors
if zone["rssi"] > 0:
entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key))
@@ -144,3 +145,23 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity):
if self._zone["rssi"] >= 20:
return "mdi:wifi-strength-1"
return "mdi:wifi-strength-outline"
class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity):
"""Representation of Advantage Air Zone wireless signal sensor."""
_attr_unit_of_measurement = TEMP_CELSIUS
_attr_state_class = STATE_CLASS_MEASUREMENT
_attr_icon = "mdi:thermometer"
_attr_entity_registry_enabled_default = False
def __init__(self, instance, ac_key, zone_key):
"""Initialize an Advantage Air Zone Temp Sensor."""
super().__init__(instance, ac_key, zone_key)
self._attr_name = f'{self._zone["name"]} Temperature'
self._attr_unique_id = f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-temp'
@property
def state(self):
"""Return the current value of the measured temperature."""
return self._zone["measuredTemp"]

View File

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

View File

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

View File

@@ -1,6 +1,10 @@
"""The airvisual component."""
from __future__ import annotations
from collections.abc import Mapping, MutableMapping
from datetime import timedelta
from math import ceil
from typing import Any, Dict, cast
from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import (
@@ -10,6 +14,7 @@ from pyairvisual.errors import (
NodeProError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_API_KEY,
@@ -20,7 +25,7 @@ from homeassistant.const import (
CONF_SHOW_ON_MAP,
CONF_STATE,
)
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import (
aiohttp_client,
@@ -57,11 +62,8 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN)
@callback
def async_get_geography_id(geography_dict):
def async_get_geography_id(geography_dict: Mapping[str, Any]) -> str:
"""Generate a unique ID from a geography dict."""
if not geography_dict:
return
if CONF_CITY in geography_dict:
return ", ".join(
(
@@ -76,7 +78,9 @@ def async_get_geography_id(geography_dict):
@callback
def async_get_cloud_api_update_interval(hass, api_key, num_consumers):
def async_get_cloud_api_update_interval(
hass: HomeAssistant, api_key: str, num_consumers: int
) -> timedelta:
"""Get a leveled scan interval for a particular cloud API key.
This will shift based on the number of active consumers, thus keeping the user
@@ -97,18 +101,22 @@ def async_get_cloud_api_update_interval(hass, api_key, num_consumers):
@callback
def async_get_cloud_coordinators_by_api_key(hass, api_key):
def async_get_cloud_coordinators_by_api_key(
hass: HomeAssistant, api_key: str
) -> list[DataUpdateCoordinator]:
"""Get all DataUpdateCoordinator objects related to a particular API key."""
coordinators = []
for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items():
config_entry = hass.config_entries.async_get_entry(entry_id)
if config_entry.data.get(CONF_API_KEY) == api_key:
if config_entry and config_entry.data.get(CONF_API_KEY) == api_key:
coordinators.append(coordinator)
return coordinators
@callback
def async_sync_geo_coordinator_update_intervals(hass, api_key):
def async_sync_geo_coordinator_update_intervals(
hass: HomeAssistant, api_key: str
) -> None:
"""Sync the update interval for geography-based data coordinators (by API key)."""
coordinators = async_get_cloud_coordinators_by_api_key(hass, api_key)
@@ -129,7 +137,9 @@ def async_sync_geo_coordinator_update_intervals(hass, api_key):
@callback
def _standardize_geography_config_entry(hass, config_entry):
def _standardize_geography_config_entry(
hass: HomeAssistant, config_entry: ConfigEntry
) -> None:
"""Ensure that geography config entries have appropriate properties."""
entry_updates = {}
@@ -162,9 +172,11 @@ def _standardize_geography_config_entry(hass, config_entry):
@callback
def _standardize_node_pro_config_entry(hass, config_entry):
def _standardize_node_pro_config_entry(
hass: HomeAssistant, config_entry: ConfigEntry
) -> None:
"""Ensure that Node/Pro config entries have appropriate properties."""
entry_updates = {}
entry_updates: dict[str, Any] = {}
if CONF_INTEGRATION_TYPE not in config_entry.data:
# If the config entry data doesn't contain the integration type, add it:
@@ -179,7 +191,7 @@ def _standardize_node_pro_config_entry(hass, config_entry):
hass.config_entries.async_update_entry(config_entry, **entry_updates)
async def async_setup_entry(hass, config_entry):
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up AirVisual as config entry."""
hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_LISTENER: {}})
@@ -189,7 +201,7 @@ async def async_setup_entry(hass, config_entry):
websession = aiohttp_client.async_get_clientsession(hass)
cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession)
async def async_update_data():
async def async_update_data() -> dict[str, Any]:
"""Get new data from the API."""
if CONF_CITY in config_entry.data:
api_coro = cloud_api.air_quality.city(
@@ -204,7 +216,8 @@ async def async_setup_entry(hass, config_entry):
)
try:
return await api_coro
data = await api_coro
return cast(Dict[str, Any], data)
except (InvalidKeyError, KeyExpiredError) as ex:
raise ConfigEntryAuthFailed from ex
except AirVisualError as err:
@@ -242,13 +255,14 @@ async def async_setup_entry(hass, config_entry):
_standardize_node_pro_config_entry(hass, config_entry)
async def async_update_data():
async def async_update_data() -> dict[str, Any]:
"""Get new data from the API."""
try:
async with NodeSamba(
config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD]
) as node:
return await node.async_get_latest_measurements()
data = await node.async_get_latest_measurements()
return cast(Dict[str, Any], data)
except NodeProError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err
@@ -275,7 +289,7 @@ async def async_setup_entry(hass, config_entry):
return True
async def async_migrate_entry(hass, config_entry):
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate an old config entry."""
version = config_entry.version
@@ -317,7 +331,7 @@ async def async_migrate_entry(hass, config_entry):
return True
async def async_unload_entry(hass, config_entry):
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload an AirVisual config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
@@ -338,7 +352,7 @@ async def async_unload_entry(hass, config_entry):
return unload_ok
async def async_reload_entry(hass, config_entry):
async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Handle an options update."""
await hass.config_entries.async_reload(config_entry.entry_id)
@@ -346,16 +360,19 @@ async def async_reload_entry(hass, config_entry):
class AirVisualEntity(CoordinatorEntity):
"""Define a generic AirVisual entity."""
def __init__(self, coordinator):
def __init__(self, coordinator: DataUpdateCoordinator) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
async def async_added_to_hass(self):
self._attr_extra_state_attributes: MutableMapping[str, Any] = {
ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION
}
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@callback
def update():
def update() -> None:
"""Update the state."""
self.update_from_latest_data()
self.async_write_ha_state()
@@ -365,6 +382,6 @@ class AirVisualEntity(CoordinatorEntity):
self.update_from_latest_data()
@callback
def update_from_latest_data(self):
def update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""
raise NotImplementedError

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{
"config": {
"abort": {
"already_configured": "Diese Koordinaten oder Node/Pro ID sind bereits registriert.",
"already_configured": "Diese Node/Pro ID oder Standort ist bereits konfiguriert.",
"reauth_successful": "Die erneute Authentifizierung war erfolgreich"
},
"error": {
@@ -40,7 +40,7 @@
},
"reauth_confirm": {
"data": {
"api_key": "API-Key"
"api_key": "API-Schl\u00fcssel"
},
"title": "AirVisual erneut authentifizieren"
},

View File

@@ -17,7 +17,7 @@
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430"
},
"description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0448\u0438\u0440\u043e\u0442\u044b/\u0434\u043e\u043b\u0433\u043e\u0442\u044b.",
"description": "\u0414\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u043f\u043e \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual.",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
},
"geography_by_name": {
@@ -25,9 +25,9 @@
"api_key": "\u041a\u043b\u044e\u0447 API",
"city": "\u0413\u043e\u0440\u043e\u0434",
"country": "\u0421\u0442\u0440\u0430\u043d\u0430",
"state": "\u0448\u0442\u0430\u0442"
"state": "\u0428\u0442\u0430\u0442"
},
"description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b.",
"description": "\u0414\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual.",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
},
"node_pro": {

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_ENTITY_ID): cv.entity_id,
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
vol.Required(CONF_REPEAT): vol.All(
cv.ensure_list,
[vol.Coerce(float)],
# Minimum delay is 1 second = 0.016 minutes
[vol.Range(min=0.016)],
),
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
vol.Optional(CONF_ALERT_MESSAGE): cv.template,

View File

@@ -19,6 +19,7 @@ from homeassistant.components.alarm_control_panel.const import (
SUPPORT_ALARM_ARM_NIGHT,
)
import homeassistant.components.climate.const as climate
from homeassistant.components.lock import STATE_LOCKING, STATE_UNLOCKING
import homeassistant.components.media_player.const as media_player
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES,
@@ -446,9 +447,11 @@ class AlexaLockController(AlexaCapability):
if name != "lockState":
raise UnsupportedProperty(name)
if self.entity.state == STATE_LOCKED:
# If its unlocking its still locked and not unlocked yet
if self.entity.state in (STATE_UNLOCKING, STATE_LOCKED):
return "LOCKED"
if self.entity.state == STATE_UNLOCKED:
# If its locking its still unlocked and not locked yet
if self.entity.state in (STATE_LOCKING, STATE_UNLOCKED):
return "UNLOCKED"
return "JAMMED"

View File

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

View File

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

View File

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

View File

@@ -28,11 +28,13 @@ from homeassistant.const import (
IRRADIATION_WATTS_PER_SQUARE_METER,
LIGHT_LUX,
PERCENTAGE,
PRECIPITATION_INCHES,
PRECIPITATION_INCHES_PER_HOUR,
PRESSURE_INHG,
SPEED_MILES_PER_HOUR,
TEMP_FAHRENHEIT,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.dispatcher import (
@@ -156,7 +158,7 @@ TYPE_WINDSPDMPH_AVG2M = "windspdmph_avg2m"
TYPE_WINDSPEEDMPH = "windspeedmph"
TYPE_YEARLYRAININ = "yearlyrainin"
SENSOR_TYPES = {
TYPE_24HOURRAININ: ("24 Hr Rain", "in", SENSOR, None),
TYPE_24HOURRAININ: ("24 Hr Rain", PRECIPITATION_INCHES, SENSOR, None),
TYPE_BAROMABSIN: ("Abs Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE),
TYPE_BAROMRELIN: ("Rel Pressure", PRESSURE_INHG, SENSOR, DEVICE_CLASS_PRESSURE),
TYPE_BATT10: ("Battery 10", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
@@ -172,11 +174,16 @@ SENSOR_TYPES = {
TYPE_BATTOUT: ("Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
TYPE_BATT_CO2: ("CO2 Battery", None, BINARY_SENSOR, DEVICE_CLASS_BATTERY),
TYPE_CO2: ("co2", CONCENTRATION_PARTS_PER_MILLION, SENSOR, DEVICE_CLASS_CO2),
TYPE_DAILYRAININ: ("Daily Rain", "in", SENSOR, None),
TYPE_DAILYRAININ: ("Daily Rain", PRECIPITATION_INCHES, SENSOR, None),
TYPE_DEWPOINT: ("Dew Point", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
TYPE_EVENTRAININ: ("Event Rain", "in", SENSOR, None),
TYPE_EVENTRAININ: ("Event Rain", PRECIPITATION_INCHES, SENSOR, None),
TYPE_FEELSLIKE: ("Feels Like", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
TYPE_HOURLYRAININ: ("Hourly Rain Rate", "in/hr", SENSOR, None),
TYPE_HOURLYRAININ: (
"Hourly Rain Rate",
PRECIPITATION_INCHES_PER_HOUR,
SENSOR,
None,
),
TYPE_HUMIDITY10: ("Humidity 10", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
TYPE_HUMIDITY2: ("Humidity 2", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
@@ -191,7 +198,7 @@ SENSOR_TYPES = {
TYPE_HUMIDITYIN: ("Humidity In", PERCENTAGE, SENSOR, DEVICE_CLASS_HUMIDITY),
TYPE_LASTRAIN: ("Last Rain", None, SENSOR, DEVICE_CLASS_TIMESTAMP),
TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, SENSOR, None),
TYPE_MONTHLYRAININ: ("Monthly Rain", "in", SENSOR, None),
TYPE_MONTHLYRAININ: ("Monthly Rain", PRECIPITATION_INCHES, SENSOR, None),
TYPE_PM25_24H: (
"PM25 24h Avg",
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@@ -277,9 +284,9 @@ SENSOR_TYPES = {
TYPE_TEMP9F: ("Temp 9", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
TYPE_TEMPF: ("Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
TYPE_TEMPINF: ("Inside Temp", TEMP_FAHRENHEIT, SENSOR, DEVICE_CLASS_TEMPERATURE),
TYPE_TOTALRAININ: ("Lifetime Rain", "in", SENSOR, None),
TYPE_TOTALRAININ: ("Lifetime Rain", PRECIPITATION_INCHES, SENSOR, None),
TYPE_UV: ("uv", "Index", SENSOR, None),
TYPE_WEEKLYRAININ: ("Weekly Rain", "in", SENSOR, None),
TYPE_WEEKLYRAININ: ("Weekly Rain", PRECIPITATION_INCHES, SENSOR, None),
TYPE_WINDDIR: ("Wind Dir", DEGREE, SENSOR, None),
TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", DEGREE, SENSOR, None),
TYPE_WINDDIR_AVG2M: ("Wind Dir Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None),
@@ -288,7 +295,7 @@ SENSOR_TYPES = {
TYPE_WINDSPDMPH_AVG10M: ("Wind Avg 10m", SPEED_MILES_PER_HOUR, SENSOR, None),
TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", SPEED_MILES_PER_HOUR, SENSOR, None),
TYPE_WINDSPEEDMPH: ("Wind Speed", SPEED_MILES_PER_HOUR, SENSOR, None),
TYPE_YEARLYRAININ: ("Yearly Rain", "in", SENSOR, None),
TYPE_YEARLYRAININ: ("Yearly Rain", PRECIPITATION_INCHES, SENSOR, None),
}
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
@@ -320,7 +327,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
LOGGER.error("Config entry failed: %s", err)
raise ConfigEntryNotReady from err
async def _async_disconnect_websocket(*_):
async def _async_disconnect_websocket(_: Event) -> None:
await ambient.client.websocket.disconnect()
config_entry.async_on_unload(
@@ -378,7 +385,7 @@ class AmbientStation:
async def _attempt_connect(self) -> None:
"""Attempt to connect to the socket (retrying later on fail)."""
async def connect(timestamp: int | None = None):
async def connect(timestamp: int | None = None) -> None:
"""Connect."""
await self.client.websocket.connect()

View File

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

View File

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

View File

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

View File

@@ -35,24 +35,11 @@ class ArduinoSensor(SensorEntity):
def __init__(self, name, pin, pin_type, board):
"""Initialize the sensor."""
self._pin = pin
self._name = name
self.pin_type = pin_type
self.direction = "in"
self._value = None
self._attr_name = name
board.set_mode(self._pin, self.direction, self.pin_type)
board.set_mode(self._pin, "in", pin_type)
self._board = board
@property
def state(self):
"""Return the state of the sensor."""
return self._value
@property
def name(self):
"""Get the name of the sensor."""
return self._name
def update(self):
"""Get the latest value from the pin."""
self._value = self._board.get_analog_inputs()[self._pin][1]
self._attr_state = self._board.get_analog_inputs()[self._pin][1]

View File

@@ -43,11 +43,9 @@ class ArduinoSwitch(SwitchEntity):
def __init__(self, pin, options, board):
"""Initialize the Pin."""
self._pin = pin
self._name = options[CONF_NAME]
self.pin_type = CONF_TYPE
self.direction = "out"
self._attr_name = options[CONF_NAME]
self._state = options[CONF_INITIAL]
self._attr_is_on = options[CONF_INITIAL]
if options[CONF_NEGATE]:
self.turn_on_handler = board.set_digital_out_low
@@ -56,25 +54,15 @@ class ArduinoSwitch(SwitchEntity):
self.turn_on_handler = board.set_digital_out_high
self.turn_off_handler = board.set_digital_out_low
board.set_mode(self._pin, self.direction, self.pin_type)
(self.turn_on_handler if self._state else self.turn_off_handler)(pin)
@property
def name(self):
"""Get the name of the pin."""
return self._name
@property
def is_on(self):
"""Return true if pin is high/on."""
return self._state
board.set_mode(pin, "out", CONF_TYPE)
(self.turn_on_handler if self.is_on else self.turn_off_handler)(pin)
def turn_on(self, **kwargs):
"""Turn the pin to high/on."""
self._state = True
self._attr_is_on = True
self.turn_on_handler(self._pin)
def turn_off(self, **kwargs):
"""Turn the pin to low/off."""
self._state = False
self._attr_is_on = False
self.turn_off_handler(self._pin)

View File

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

View File

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

View File

@@ -86,24 +86,8 @@ class ArestSwitchBase(SwitchEntity):
def __init__(self, resource, location, name):
"""Initialize the switch."""
self._resource = resource
self._name = f"{location.title()} {name.title()}"
self._state = None
self._available = True
@property
def name(self):
"""Return the name of the switch."""
return self._name
@property
def is_on(self):
"""Return true if device is on."""
return self._state
@property
def available(self):
"""Could the device be accessed during the last update call."""
return self._available
self._attr_name = f"{location.title()} {name.title()}"
self._attr_available = True
class ArestSwitchFunction(ArestSwitchBase):
@@ -134,7 +118,7 @@ class ArestSwitchFunction(ArestSwitchBase):
)
if request.status_code == HTTP_OK:
self._state = True
self._attr_is_on = True
else:
_LOGGER.error("Can't turn on function %s at %s", self._func, self._resource)
@@ -145,7 +129,7 @@ class ArestSwitchFunction(ArestSwitchBase):
)
if request.status_code == HTTP_OK:
self._state = False
self._attr_is_on = False
else:
_LOGGER.error(
"Can't turn off function %s at %s", self._func, self._resource
@@ -155,11 +139,11 @@ class ArestSwitchFunction(ArestSwitchBase):
"""Get the latest data from aREST API and update the state."""
try:
request = requests.get(f"{self._resource}/{self._func}", timeout=10)
self._state = request.json()["return_value"] != 0
self._available = True
self._attr_is_on = request.json()["return_value"] != 0
self._attr_available = True
except requests.exceptions.ConnectionError:
_LOGGER.warning("No route to device %s", self._resource)
self._available = False
self._attr_available = False
class ArestSwitchPin(ArestSwitchBase):
@@ -171,10 +155,10 @@ class ArestSwitchPin(ArestSwitchBase):
self._pin = pin
self.invert = invert
request = requests.get(f"{self._resource}/mode/{self._pin}/o", timeout=10)
request = requests.get(f"{resource}/mode/{pin}/o", timeout=10)
if request.status_code != HTTP_OK:
_LOGGER.error("Can't set mode")
self._available = False
self._attr_available = False
def turn_on(self, **kwargs):
"""Turn the device on."""
@@ -183,7 +167,7 @@ class ArestSwitchPin(ArestSwitchBase):
f"{self._resource}/digital/{self._pin}/{turn_on_payload}", timeout=10
)
if request.status_code == HTTP_OK:
self._state = True
self._attr_is_on = True
else:
_LOGGER.error("Can't turn on pin %s at %s", self._pin, self._resource)
@@ -194,7 +178,7 @@ class ArestSwitchPin(ArestSwitchBase):
f"{self._resource}/digital/{self._pin}/{turn_off_payload}", timeout=10
)
if request.status_code == HTTP_OK:
self._state = False
self._attr_is_on = False
else:
_LOGGER.error("Can't turn off pin %s at %s", self._pin, self._resource)
@@ -203,8 +187,8 @@ class ArestSwitchPin(ArestSwitchBase):
try:
request = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10)
status_value = int(self.invert)
self._state = request.json()["return_value"] != status_value
self._available = True
self._attr_is_on = request.json()["return_value"] != status_value
self._attr_available = True
except requests.exceptions.ConnectionError:
_LOGGER.warning("No route to device %s", self._resource)
self._available = False
self._attr_available = False

View File

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

View File

@@ -75,27 +75,16 @@ class AtagEntity(CoordinatorEntity):
super().__init__(coordinator)
self._id = atag_id
self._name = DOMAIN.title()
self._attr_name = DOMAIN.title()
self._attr_unique_id = f"{coordinator.data.id}-{atag_id}"
@property
def device_info(self) -> DeviceInfo:
"""Return info for device registry."""
device = self.coordinator.data.id
version = self.coordinator.data.apiversion
return {
"identifiers": {(DOMAIN, device)},
"identifiers": {(DOMAIN, self.coordinator.data.id)},
"name": "Atag Thermostat",
"model": "Atag One",
"sw_version": version,
"sw_version": self.coordinator.data.apiversion,
"manufacturer": "Atag",
}
@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name
@property
def unique_id(self):
"""Return a unique ID to use for this entity."""
return f"{self.coordinator.data.id}-{self._id}"

View File

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

View File

@@ -36,7 +36,20 @@ class AtagSensor(AtagEntity, SensorEntity):
def __init__(self, coordinator, sensor):
"""Initialize Atag sensor."""
super().__init__(coordinator, SENSORS[sensor])
self._name = sensor
self._attr_name = sensor
if coordinator.data.report[self._id].sensorclass in [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
]:
self._attr_device_class = coordinator.data.report[self._id].sensorclass
if coordinator.data.report[self._id].measure in [
PRESSURE_BAR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
PERCENTAGE,
TIME_HOURS,
]:
self._attr_unit_of_measurement = coordinator.data.report[self._id].measure
@property
def state(self):
@@ -47,26 +60,3 @@ class AtagSensor(AtagEntity, SensorEntity):
def icon(self):
"""Return icon."""
return self.coordinator.data.report[self._id].icon
@property
def device_class(self):
"""Return deviceclass."""
if self.coordinator.data.report[self._id].sensorclass in [
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
]:
return self.coordinator.data.report[self._id].sensorclass
return None
@property
def unit_of_measurement(self):
"""Return measure."""
if self.coordinator.data.report[self._id].measure in [
PRESSURE_BAR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
PERCENTAGE,
TIME_HOURS,
]:
return self.coordinator.data.report[self._id].measure
return None

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
"""Support for August lock."""
import logging
from aiohttp import ClientResponseError
from yalexs.activity import SOURCE_PUBNUB, ActivityType
from yalexs.lock import LockStatus
from yalexs.util import update_lock_detail_from_activity
@@ -9,12 +10,15 @@ from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.util.dt as dt_util
from .const import DATA_AUGUST, DOMAIN
from .entity import AugustEntityMixin
_LOGGER = logging.getLogger(__name__)
LOCK_JAMMED_ERR = 531
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up August locks."""
@@ -44,7 +48,15 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
await self._call_lock_operation(self._data.async_unlock)
async def _call_lock_operation(self, lock_operation):
try:
activities = await lock_operation(self._device_id)
except ClientResponseError as err:
if err.status == LOCK_JAMMED_ERR:
self._detail.lock_status = LockStatus.JAMMED
self._detail.lock_status_datetime = dt_util.utcnow()
else:
raise
else:
for lock_activity in activities:
update_lock_detail_from_activity(self._detail, lock_activity)
@@ -91,6 +103,10 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
else:
self._attr_is_locked = self._lock_status is LockStatus.LOCKED
self._attr_is_jammed = self._lock_status is LockStatus.JAMMED
self._attr_is_locking = self._lock_status is LockStatus.LOCKING
self._attr_is_unlocking = self._lock_status is LockStatus.UNLOCKING
self._attr_extra_state_attributes = {
ATTR_BATTERY_LEVEL: self._detail.battery_level
}

View File

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

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"
},
"motion": {
"off": "Ruhig",
"on": "Bewegung erkannt"
"off": "Normal",
"on": "Erkannt"
},
"moving": {
"off": "Bewegt sich nicht",
@@ -171,16 +171,16 @@
"on": "Unsicher"
},
"smoke": {
"off": "OK",
"on": "Rauch erkannt"
"off": "Normal",
"on": "Erkannt"
},
"sound": {
"off": "Stille",
"on": "Ger\u00e4usch erkannt"
"off": "Normal",
"on": "Erkannt"
},
"vibration": {
"off": "Normal",
"on": "Vibration"
"on": "Erkannt"
},
"window": {
"off": "Geschlossen",

View File

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

View File

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

View File

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

View File

@@ -203,33 +203,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class BluesoundPlayer(MediaPlayerEntity):
"""Representation of a Bluesound Player."""
def __init__(self, hass, host, port=None, name=None, init_callback=None):
_attr_media_content_type = MEDIA_TYPE_MUSIC
def __init__(self, hass, host, port=DEFAULT_PORT, name=None, init_callback=None):
"""Initialize the media player."""
self.host = host
self._hass = hass
self.port = port
self._polling_session = async_get_clientsession(hass)
self._polling_task = None # The actual polling task.
self._name = name
self._icon = None
self._attr_name = name
self._capture_items = []
self._services_items = []
self._preset_items = []
self._sync_status = {}
self._status = None
self._last_status_update = None
self._is_online = False
self._is_online = None
self._retry_remove = None
self._muted = False
self._master = None
self._is_master = False
self._group_name = None
self._group_list = []
self._bluesound_device_name = None
self._is_master = False
self._group_list = []
self._init_callback = init_callback
if self.port is None:
self.port = DEFAULT_PORT
class _TimeoutException(Exception):
pass
@@ -252,12 +248,12 @@ class BluesoundPlayer(MediaPlayerEntity):
return None
self._sync_status = resp["SyncStatus"].copy()
if not self._name:
self._name = self._sync_status.get("@name", self.host)
if not self.name:
self._attr_name = self._sync_status.get("@name", self.host)
if not self._bluesound_device_name:
self._bluesound_device_name = self._sync_status.get("@name", self.host)
if not self._icon:
self._icon = self._sync_status.get("@icon", self.host)
if not self.icon:
self._attr_icon = self._sync_status.get("@icon", self.host)
master = self._sync_status.get("master")
if master is not None:
@@ -291,14 +287,14 @@ class BluesoundPlayer(MediaPlayerEntity):
await self.async_update_status()
except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException):
_LOGGER.info("Node %s is offline, retrying later", self._name)
_LOGGER.info("Node %s is offline, retrying later", self.name)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
self.start_polling()
except CancelledError:
_LOGGER.debug("Stopping the polling of node %s", self._name)
_LOGGER.debug("Stopping the polling of node %s", self.name)
except Exception:
_LOGGER.exception("Unexpected error in %s", self._name)
_LOGGER.exception("Unexpected error in %s", self.name)
raise
def start_polling(self):
@@ -402,7 +398,7 @@ class BluesoundPlayer(MediaPlayerEntity):
if response.status == HTTP_OK:
result = await response.text()
self._is_online = True
self._last_status_update = dt_util.utcnow()
self._attr_media_position_updated_at = dt_util.utcnow()
self._status = xmltodict.parse(result)["status"].copy()
group_name = self._status.get("groupName")
@@ -438,11 +434,58 @@ class BluesoundPlayer(MediaPlayerEntity):
except (asyncio.TimeoutError, ClientError):
self._is_online = False
self._last_status_update = None
self._attr_media_position_updated_at = None
self._status = None
self.async_write_ha_state()
_LOGGER.info("Client connection error, marking %s as offline", self._name)
_LOGGER.info("Client connection error, marking %s as offline", self.name)
raise
self.update_state_attr()
def update_state_attr(self):
"""Update state attributes."""
if self._status is None:
self._attr_state = STATE_OFF
self._attr_supported_features = 0
elif self.is_grouped and not self.is_master:
self._attr_state = STATE_GROUPED
self._attr_supported_features = (
SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
)
else:
status = self._status.get("state")
self._attr_state = STATE_IDLE
if status in ("pause", "stop"):
self._attr_state = STATE_PAUSED
elif status in ("stream", "play"):
self._attr_state = STATE_PLAYING
supported = SUPPORT_CLEAR_PLAYLIST
if self._status.get("indexing", "0") == "0":
supported = (
supported
| SUPPORT_PAUSE
| SUPPORT_PREVIOUS_TRACK
| SUPPORT_NEXT_TRACK
| SUPPORT_PLAY_MEDIA
| SUPPORT_STOP
| SUPPORT_PLAY
| SUPPORT_SELECT_SOURCE
| SUPPORT_SHUFFLE_SET
)
if self.volume_level is not None and self.volume_level >= 0:
supported = (
supported
| SUPPORT_VOLUME_STEP
| SUPPORT_VOLUME_SET
| SUPPORT_VOLUME_MUTE
)
if self._status.get("canSeek", "") == "1":
supported = supported | SUPPORT_SEEK
self._attr_supported_features = supported
self._attr_extra_state_attributes = {}
if self._group_list:
self._attr_extra_state_attributes = {ATTR_BLUESOUND_GROUP: self._group_list}
self._attr_extra_state_attributes[ATTR_MASTER] = self._is_master
self._attr_shuffle = self._status.get("shuffle", "0") == "1"
async def async_trigger_sync_on_all(self):
"""Trigger sync status update on all devices."""
@@ -542,27 +585,6 @@ class BluesoundPlayer(MediaPlayerEntity):
return self._services_items
@property
def media_content_type(self):
"""Content type of current playing media."""
return MEDIA_TYPE_MUSIC
@property
def state(self):
"""Return the state of the device."""
if self._status is None:
return STATE_OFF
if self.is_grouped and not self.is_master:
return STATE_GROUPED
status = self._status.get("state")
if status in ("pause", "stop"):
return STATE_PAUSED
if status in ("stream", "play"):
return STATE_PLAYING
return STATE_IDLE
@property
def media_title(self):
"""Title of current playing media."""
@@ -617,7 +639,7 @@ class BluesoundPlayer(MediaPlayerEntity):
return None
mediastate = self.state
if self._last_status_update is None or mediastate == STATE_IDLE:
if self.media_position_updated_at is None or mediastate == STATE_IDLE:
return None
position = self._status.get("secs")
@@ -626,7 +648,9 @@ class BluesoundPlayer(MediaPlayerEntity):
position = float(position)
if mediastate == STATE_PLAYING:
position += (dt_util.utcnow() - self._last_status_update).total_seconds()
position += (
dt_util.utcnow() - self.media_position_updated_at
).total_seconds()
return position
@@ -641,11 +665,6 @@ class BluesoundPlayer(MediaPlayerEntity):
return None
return float(duration)
@property
def media_position_updated_at(self):
"""Last time status was updated."""
return self._last_status_update
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
@@ -668,21 +687,11 @@ class BluesoundPlayer(MediaPlayerEntity):
mute = bool(int(mute))
return mute
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def bluesound_device_name(self):
"""Return the device name as returned by the device."""
return self._bluesound_device_name
@property
def icon(self):
"""Return the icon of the device."""
return self._icon
@property
def source_list(self):
"""List of available input sources."""
@@ -778,58 +787,15 @@ class BluesoundPlayer(MediaPlayerEntity):
return None
@property
def supported_features(self):
"""Flag of media commands that are supported."""
if self._status is None:
return 0
if self.is_grouped and not self.is_master:
return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
supported = SUPPORT_CLEAR_PLAYLIST
if self._status.get("indexing", "0") == "0":
supported = (
supported
| SUPPORT_PAUSE
| SUPPORT_PREVIOUS_TRACK
| SUPPORT_NEXT_TRACK
| SUPPORT_PLAY_MEDIA
| SUPPORT_STOP
| SUPPORT_PLAY
| SUPPORT_SELECT_SOURCE
| SUPPORT_SHUFFLE_SET
)
current_vol = self.volume_level
if current_vol is not None and current_vol >= 0:
supported = (
supported
| SUPPORT_VOLUME_STEP
| SUPPORT_VOLUME_SET
| SUPPORT_VOLUME_MUTE
)
if self._status.get("canSeek", "") == "1":
supported = supported | SUPPORT_SEEK
return supported
@property
def is_master(self):
def is_master(self) -> bool:
"""Return true if player is a coordinator."""
return self._is_master
@property
def is_grouped(self):
def is_grouped(self) -> bool:
"""Return true if player is a coordinator."""
return self._master is not None or self._is_master
@property
def shuffle(self):
"""Return true if shuffle is active."""
return self._status.get("shuffle", "0") == "1"
async def async_join(self, master):
"""Join the player to a group."""
master_device = [
@@ -849,17 +815,6 @@ class BluesoundPlayer(MediaPlayerEntity):
else:
_LOGGER.error("Master not found %s", master_device)
@property
def extra_state_attributes(self):
"""List members in group."""
attributes = {}
if self._group_list:
attributes = {ATTR_BLUESOUND_GROUP: self._group_list}
attributes[ATTR_MASTER] = self._is_master
return attributes
def rebuild_bluesound_group(self):
"""Rebuild the list of entities in speaker group."""
if self._group_name is None:

View File

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

View File

@@ -76,41 +76,45 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity):
super().__init__(account, vehicle)
self._attribute = attribute
self._name = f"{self._vehicle.name} {self._attribute}"
self._unique_id = f"{self._vehicle.vin}-{self._attribute}"
self._attr_name = f"{vehicle.name} {attribute}"
self._attr_unique_id = f"{vehicle.vin}-{attribute}"
self._sensor_name = sensor_name
self._device_class = device_class
self._icon = icon
self._state = None
self._attr_device_class = device_class
self._attr_icon = icon
@property
def unique_id(self):
"""Return the unique ID of the binary sensor."""
return self._unique_id
def update(self):
"""Read new state data from the library."""
vehicle_state = self._vehicle.state
@property
def name(self):
"""Return the name of the binary sensor."""
return self._name
# device class opening: On means open, Off means closed
if self._attribute == "lids":
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
self._attr_state = not vehicle_state.all_lids_closed
if self._attribute == "windows":
self._attr_state = not vehicle_state.all_windows_closed
# device class lock: On means unlocked, Off means locked
if self._attribute == "door_lock_state":
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
self._attr_state = vehicle_state.door_lock_state not in [
LockState.LOCKED,
LockState.SECURED,
]
# device class light: On means light detected, Off means no light
if self._attribute == "lights_parking":
self._attr_state = vehicle_state.are_parking_lights_on
# device class problem: On means problem detected, Off means no problem
if self._attribute == "condition_based_services":
self._attr_state = not vehicle_state.are_all_cbs_ok
if self._attribute == "check_control_messages":
self._attr_state = vehicle_state.has_check_control_messages
# device class power: On means power detected, Off means no power
if self._attribute == "charging_status":
self._attr_state = vehicle_state.charging_status in [ChargingState.CHARGING]
# device class plug: On means device is plugged in,
# Off means device is unplugged
if self._attribute == "connection_status":
self._attr_state = vehicle_state.connection_status == "CONNECTED"
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return self._icon
@property
def device_class(self):
"""Return the class of the binary sensor."""
return self._device_class
@property
def is_on(self):
"""Return the state of the binary sensor."""
return self._state
@property
def extra_state_attributes(self):
"""Return the state attributes of the binary sensor."""
vehicle_state = self._vehicle.state
result = self._attrs.copy()
@@ -144,40 +148,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity):
elif self._attribute == "connection_status":
result["connection_status"] = vehicle_state.connection_status
return sorted(result.items())
def update(self):
"""Read new state data from the library."""
vehicle_state = self._vehicle.state
# device class opening: On means open, Off means closed
if self._attribute == "lids":
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
self._state = not vehicle_state.all_lids_closed
if self._attribute == "windows":
self._state = not vehicle_state.all_windows_closed
# device class lock: On means unlocked, Off means locked
if self._attribute == "door_lock_state":
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
self._state = vehicle_state.door_lock_state not in [
LockState.LOCKED,
LockState.SECURED,
]
# device class light: On means light detected, Off means no light
if self._attribute == "lights_parking":
self._state = vehicle_state.are_parking_lights_on
# device class problem: On means problem detected, Off means no problem
if self._attribute == "condition_based_services":
self._state = not vehicle_state.are_all_cbs_ok
if self._attribute == "check_control_messages":
self._state = vehicle_state.has_check_control_messages
# device class power: On means power detected, Off means no power
if self._attribute == "charging_status":
self._state = vehicle_state.charging_status in [ChargingState.CHARGING]
# device class plug: On means device is plugged in,
# Off means device is unplugged
if self._attribute == "connection_status":
self._state = vehicle_state.connection_status == "CONNECTED"
self._attr_extra_state_attributes = sorted(result.items())
def _format_cbs_report(self, report):
result = {}

View File

@@ -29,15 +29,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity):
"""BMW Connected Drive device tracker."""
_attr_force_update = False
_attr_icon = "mdi:car"
def __init__(self, account, vehicle):
"""Initialize the Tracker."""
super().__init__(account, vehicle)
self._unique_id = vehicle.vin
self._attr_unique_id = vehicle.vin
self._location = (
vehicle.state.gps_position if vehicle.state.gps_position else (None, None)
)
self._name = vehicle.name
self._attr_name = vehicle.name
@property
def latitude(self):
@@ -49,31 +52,11 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity):
"""Return longitude value of the device."""
return self._location[1] if self._location else None
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def unique_id(self):
"""Return the unique ID."""
return self._unique_id
@property
def source_type(self):
"""Return the source type, eg gps or router, of the device."""
return SOURCE_TYPE_GPS
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return "mdi:car"
@property
def force_update(self):
"""All updates do not need to be written to the state machine."""
return False
def update(self):
"""Update state of the decvice tracker."""
self._location = (

View File

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

View File

@@ -503,94 +503,46 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity):
self._attribute = attribute
self._service = service
self._state = None
if self._service:
self._name = (
f"{self._vehicle.name} {self._service.lower()}_{self._attribute}"
)
self._unique_id = (
f"{self._vehicle.vin}-{self._service.lower()}-{self._attribute}"
)
if service:
self._attr_name = f"{vehicle.name} {service.lower()}_{attribute}"
self._attr_unique_id = f"{vehicle.vin}-{service.lower()}-{attribute}"
else:
self._name = f"{self._vehicle.name} {self._attribute}"
self._unique_id = f"{self._vehicle.vin}-{self._attribute}"
self._attr_name = f"{vehicle.name} {attribute}"
self._attr_unique_id = f"{vehicle.vin}-{attribute}"
self._attribute_info = attribute_info
@property
def unique_id(self):
"""Return the unique ID of the sensor."""
return self._unique_id
@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
def icon(self):
"""Icon to use in the frontend, if any."""
vehicle_state = self._vehicle.state
charging_state = vehicle_state.charging_status in [ChargingState.CHARGING]
if self._attribute == "charging_level_hv":
return icon_for_battery_level(
battery_level=vehicle_state.charging_level_hv, charging=charging_state
)
icon = self._attribute_info.get(self._attribute, [None, None, None, None])[0]
return icon
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
enabled_default = self._attribute_info.get(
self._attribute, [None, None, None, True]
self._attr_entity_registry_enabled_default = attribute_info.get(
attribute, [None, None, None, True]
)[3]
return enabled_default
@property
def state(self):
"""Return the state of the sensor.
The return type of this call depends on the attribute that
is configured.
"""
return self._state
@property
def device_class(self) -> str:
"""Get the device class."""
clss = self._attribute_info.get(self._attribute, [None, None, None, None])[1]
return clss
@property
def unit_of_measurement(self) -> str:
"""Get the unit of measurement."""
unit = self._attribute_info.get(self._attribute, [None, None, None, None])[2]
return unit
self._attr_device_class = attribute_info.get(
attribute, [None, None, None, None]
)[1]
self._attr_unit_of_measurement = attribute_info.get(
attribute, [None, None, None, None]
)[2]
def update(self) -> None:
"""Read new state data from the library."""
_LOGGER.debug("Updating %s", self._vehicle.name)
vehicle_state = self._vehicle.state
if self._attribute == "charging_status":
self._state = getattr(vehicle_state, self._attribute).value
self._attr_state = getattr(vehicle_state, self._attribute).value
elif self.unit_of_measurement == VOLUME_GALLONS:
value = getattr(vehicle_state, self._attribute)
value_converted = self.hass.config.units.volume(value, VOLUME_LITERS)
self._state = round(value_converted)
self._attr_state = round(value_converted)
elif self.unit_of_measurement == LENGTH_MILES:
value = getattr(vehicle_state, self._attribute)
value_converted = self.hass.config.units.length(value, LENGTH_KILOMETERS)
self._state = round(value_converted)
self._attr_state = round(value_converted)
elif self._service is None:
self._state = getattr(vehicle_state, self._attribute)
self._attr_state = getattr(vehicle_state, self._attribute)
elif self._service == SERVICE_LAST_TRIP:
vehicle_last_trip = self._vehicle.state.last_trip
if self._attribute == "date_utc":
date_str = getattr(vehicle_last_trip, "date")
self._state = dt_util.parse_datetime(date_str).isoformat()
self._attr_state = dt_util.parse_datetime(date_str).isoformat()
else:
self._state = getattr(vehicle_last_trip, self._attribute)
self._attr_state = getattr(vehicle_last_trip, self._attribute)
elif self._service == SERVICE_ALL_TRIPS:
vehicle_all_trips = self._vehicle.state.all_trips
for attribute in (
@@ -603,10 +555,21 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity):
if self._attribute.startswith(f"{attribute}_"):
attr = getattr(vehicle_all_trips, attribute)
sub_attr = self._attribute.replace(f"{attribute}_", "")
self._state = getattr(attr, sub_attr)
self._attr_state = getattr(attr, sub_attr)
return
if self._attribute == "reset_date_utc":
date_str = getattr(vehicle_all_trips, "reset_date")
self._state = dt_util.parse_datetime(date_str).isoformat()
self._attr_state = dt_util.parse_datetime(date_str).isoformat()
else:
self._state = getattr(vehicle_all_trips, self._attribute)
self._attr_state = getattr(vehicle_all_trips, self._attribute)
vehicle_state = self._vehicle.state
charging_state = vehicle_state.charging_status in [ChargingState.CHARGING]
if self._attribute == "charging_level_hv":
self._attr_icon = icon_for_battery_level(
battery_level=vehicle_state.charging_level_hv, charging=charging_state
)
self._attr_icon = self._attribute_info.get(
self._attribute, [None, None, None, None]
)[0]

View File

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

View File

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

View File

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

View File

@@ -127,10 +127,10 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity):
self._flags = defaultdict(int)
self._lock = asyncio.Lock()
self._attr_name = f"{self._device.name} Remote"
self._attr_name = f"{device.name} Remote"
self._attr_is_on = True
self._attr_supported_features = SUPPORT_LEARN_COMMAND | SUPPORT_DELETE_COMMAND
self._attr_unique_id = self._device.unique_id
self._attr_unique_id = device.unique_id
def _extract_codes(self, commands, device=None):
"""Extract a list of codes.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import Throttle
@@ -30,7 +29,6 @@ from .const import (
CONF_FFMPEG_ARGUMENTS,
DATA_COORDINATOR,
DEFAULT_FFMPEG_ARGUMENTS,
DEFAULT_TIMEOUT,
DOMAIN,
MANUFACTURER,
)
@@ -73,7 +71,6 @@ async def async_setup_entry(
coordinator,
location_id,
device,
DEFAULT_TIMEOUT,
ffmpeg_arguments,
)
)
@@ -92,7 +89,6 @@ class CanaryCamera(CoordinatorEntity, Camera):
coordinator: CanaryDataUpdateCoordinator,
location_id: str,
device: Device,
timeout: int,
ffmpeg_args: str,
) -> None:
"""Initialize a Canary security camera."""
@@ -102,37 +98,21 @@ class CanaryCamera(CoordinatorEntity, Camera):
self._ffmpeg_arguments = ffmpeg_args
self._location_id = location_id
self._device = device
self._device_id: str = device.device_id
self._device_name: str = device.name
self._device_type_name = device.device_type["name"]
self._timeout = timeout
self._live_stream_session: LiveStreamSession | None = None
self._attr_name = device.name
self._attr_unique_id = str(device.device_id)
self._attr_device_info = {
"identifiers": {(DOMAIN, str(device.device_id))},
"name": device.name,
"model": device.device_type["name"],
"manufacturer": MANUFACTURER,
}
@property
def location(self) -> Location:
"""Return information about the location."""
return self.coordinator.data["locations"][self._location_id]
@property
def name(self) -> str:
"""Return the name of this device."""
return self._device_name
@property
def unique_id(self) -> str:
"""Return the unique ID of this camera."""
return str(self._device_id)
@property
def device_info(self) -> DeviceInfo:
"""Return the device_info of the device."""
return {
"identifiers": {(DOMAIN, str(self._device_id))},
"name": self._device_name,
"model": self._device_type_name,
"manufacturer": MANUFACTURER,
}
@property
def is_recording(self) -> bool:
"""Return true if the device is recording."""

View File

@@ -17,7 +17,6 @@ from homeassistant.const import (
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -97,11 +96,9 @@ class CanarySensor(CoordinatorEntity, SensorEntity):
super().__init__(coordinator)
self._sensor_type = sensor_type
self._device_id = device.device_id
self._device_name = device.name
self._device_type_name = device.device_type["name"]
sensor_type_name = sensor_type[0].replace("_", " ").title()
self._name = f"{location.name} {device.name} {sensor_type_name}"
self._attr_name = f"{location.name} {device.name} {sensor_type_name}"
canary_sensor_type = None
if self._sensor_type[0] == "air_quality":
@@ -116,6 +113,17 @@ class CanarySensor(CoordinatorEntity, SensorEntity):
canary_sensor_type = SensorType.BATTERY
self._canary_type = canary_sensor_type
self._attr_state = self.reading
self._attr_unique_id = f"{device.device_id}_{sensor_type[0]}"
self._attr_device_info = {
"identifiers": {(DOMAIN, str(device.device_id))},
"name": device.name,
"model": device.device_type["name"],
"manufacturer": MANUFACTURER,
}
self._attr_unit_of_measurement = sensor_type[1]
self._attr_device_class = sensor_type[3]
self._attr_icon = sensor_type[2]
@property
def reading(self) -> float | None:
@@ -136,46 +144,6 @@ class CanarySensor(CoordinatorEntity, SensorEntity):
return None
@property
def name(self) -> str:
"""Return the name of the Canary sensor."""
return self._name
@property
def state(self) -> float | None:
"""Return the state of the sensor."""
return self.reading
@property
def unique_id(self) -> str:
"""Return the unique ID of this sensor."""
return f"{self._device_id}_{self._sensor_type[0]}"
@property
def device_info(self) -> DeviceInfo:
"""Return the device_info of the device."""
return {
"identifiers": {(DOMAIN, str(self._device_id))},
"name": self._device_name,
"model": self._device_type_name,
"manufacturer": MANUFACTURER,
}
@property
def unit_of_measurement(self) -> str | None:
"""Return the unit of measurement."""
return self._sensor_type[1]
@property
def device_class(self) -> str | None:
"""Device class for the sensor."""
return self._sensor_type[3]
@property
def icon(self) -> str | None:
"""Icon for the sensor."""
return self._sensor_type[2]
@property
def extra_state_attributes(self) -> dict[str, str] | None:
"""Return the state attributes."""

View File

@@ -1,7 +1,7 @@
{
"config": {
"abort": {
"single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich."
"single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich."
},
"error": {
"invalid_known_hosts": "Bekannte Hosts m\u00fcssen eine durch Kommata getrennte Liste von Hosts sein."

View File

@@ -1,7 +1,7 @@
{
"config": {
"abort": {
"already_configured": "Diese Kombination aus Host und Port ist bereits konfiguriert.",
"already_configured": "Der Dienst ist bereits konfiguriert",
"import_failed": "Import aus Konfiguration fehlgeschlagen"
},
"error": {

View File

@@ -158,7 +158,7 @@ class ClimaCellSensorMetadata:
name: str
unit_imperial: str | None = None
unit_metric: str | None = None
metric_conversion: Callable | float = 1.0
metric_conversion: Callable[[float], float] | float = 1.0
is_metric_check: bool | None = None
device_class: str | None = None
value_map: IntEnum | None = None

View File

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

View File

@@ -26,7 +26,7 @@
},
"user": {
"data": {
"api_token": "API Token"
"api_token": "API-Token"
},
"description": "F\u00fcr diese Integration ist ein API-Token erforderlich, der mit Zone: Zone: Lesen und Zone: DNS: Bearbeiten f\u00fcr alle Zonen in deinem Konto erstellt wurde.",
"title": "Mit Cloudflare verbinden"

View File

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

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