This commit is contained in:
Paulus Schoutsen 2023-01-05 23:18:21 -05:00 committed by GitHub
commit 71ce7373a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 219 additions and 89 deletions

View File

@ -69,6 +69,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity):
return ( return (
self.vehicle.vehicle_location.location[0] self.vehicle.vehicle_location.location[0]
if self.vehicle.is_vehicle_tracking_enabled if self.vehicle.is_vehicle_tracking_enabled
and self.vehicle.vehicle_location.location
else None else None
) )
@ -78,6 +79,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity):
return ( return (
self.vehicle.vehicle_location.location[1] self.vehicle.vehicle_location.location[1]
if self.vehicle.is_vehicle_tracking_enabled if self.vehicle.is_vehicle_tracking_enabled
and self.vehicle.vehicle_location.location
else None else None
) )

View File

@ -2,7 +2,7 @@
"domain": "bmw_connected_drive", "domain": "bmw_connected_drive",
"name": "BMW Connected Drive", "name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": ["bimmer_connected==0.10.4"], "requirements": ["bimmer_connected==0.12.0"],
"codeowners": ["@gerard33", "@rikroe"], "codeowners": ["@gerard33", "@rikroe"],
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling", "iot_class": "cloud_polling",

View File

@ -17,7 +17,7 @@
"service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb" "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb"
} }
], ],
"requirements": ["bthome-ble==2.4.0"], "requirements": ["bthome-ble==2.4.1"],
"dependencies": ["bluetooth"], "dependencies": ["bluetooth"],
"codeowners": ["@Ernst79"], "codeowners": ["@Ernst79"],
"iot_class": "local_push" "iot_class": "local_push"

View File

@ -28,6 +28,7 @@ from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_PORT, CONF_PORT,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
UnitOfEnergy,
UnitOfVolume, UnitOfVolume,
) )
from homeassistant.core import CoreState, Event, HomeAssistant, callback from homeassistant.core import CoreState, Event, HomeAssistant, callback
@ -591,6 +592,21 @@ class DSMREntity(SensorEntity):
"""Entity is only available if there is a telegram.""" """Entity is only available if there is a telegram."""
return self.telegram is not None return self.telegram is not None
@property
def device_class(self) -> SensorDeviceClass | None:
"""Return the device class of this entity."""
device_class = super().device_class
# Override device class for gas sensors providing energy units, like
# kWh, MWh, GJ, etc. In those cases, the class should be energy, not gas
with suppress(ValueError):
if device_class == SensorDeviceClass.GAS and UnitOfEnergy(
str(self.native_unit_of_measurement)
):
return SensorDeviceClass.ENERGY
return device_class
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return the state of sensor, if available, translate if needed.""" """Return the state of sensor, if available, translate if needed."""

View File

@ -3,7 +3,7 @@
"name": "Rheem EcoNet Products", "name": "Rheem EcoNet Products",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/econet", "documentation": "https://www.home-assistant.io/integrations/econet",
"requirements": ["pyeconet==0.1.17"], "requirements": ["pyeconet==0.1.18"],
"codeowners": ["@vangorra", "@w1ll1am23"], "codeowners": ["@vangorra", "@w1ll1am23"],
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["paho_mqtt", "pyeconet"] "loggers": ["paho_mqtt", "pyeconet"]

View File

@ -41,20 +41,20 @@ SUPPORTED_STATE_CLASSES = {
SensorStateClass.TOTAL_INCREASING, SensorStateClass.TOTAL_INCREASING,
} }
VALID_ENERGY_UNITS: set[str] = { VALID_ENERGY_UNITS: set[str] = {
UnitOfEnergy.WATT_HOUR, UnitOfEnergy.GIGA_JOULE,
UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR,
UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR,
UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.WATT_HOUR,
} }
VALID_ENERGY_UNITS_GAS = { VALID_ENERGY_UNITS_GAS = {
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_METERS,
*VALID_ENERGY_UNITS, *VALID_ENERGY_UNITS,
} }
VALID_VOLUME_UNITS_WATER: set[str] = { VALID_VOLUME_UNITS_WATER: set[str] = {
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_METERS,
UnitOfVolume.GALLONS, UnitOfVolume.GALLONS,
UnitOfVolume.LITERS, UnitOfVolume.LITERS,

View File

@ -22,10 +22,10 @@ from .const import DOMAIN
ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,) ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,)
ENERGY_USAGE_UNITS = { ENERGY_USAGE_UNITS = {
sensor.SensorDeviceClass.ENERGY: ( sensor.SensorDeviceClass.ENERGY: (
UnitOfEnergy.GIGA_JOULE,
UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR,
UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR,
UnitOfEnergy.WATT_HOUR, UnitOfEnergy.WATT_HOUR,
UnitOfEnergy.GIGA_JOULE,
) )
} }
ENERGY_PRICE_UNITS = tuple( ENERGY_PRICE_UNITS = tuple(
@ -39,12 +39,16 @@ GAS_USAGE_DEVICE_CLASSES = (
) )
GAS_USAGE_UNITS = { GAS_USAGE_UNITS = {
sensor.SensorDeviceClass.ENERGY: ( sensor.SensorDeviceClass.ENERGY: (
UnitOfEnergy.WATT_HOUR, UnitOfEnergy.GIGA_JOULE,
UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR,
UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR,
UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.WATT_HOUR,
),
sensor.SensorDeviceClass.GAS: (
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS,
), ),
sensor.SensorDeviceClass.GAS: (UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET),
} }
GAS_PRICE_UNITS = tuple( GAS_PRICE_UNITS = tuple(
f"/{unit}" for units in GAS_USAGE_UNITS.values() for unit in units f"/{unit}" for units in GAS_USAGE_UNITS.values() for unit in units
@ -54,8 +58,9 @@ GAS_PRICE_UNIT_ERROR = "entity_unexpected_unit_gas_price"
WATER_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.WATER,) WATER_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.WATER,)
WATER_USAGE_UNITS = { WATER_USAGE_UNITS = {
sensor.SensorDeviceClass.WATER: ( sensor.SensorDeviceClass.WATER: (
UnitOfVolume.CUBIC_METERS, UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS,
UnitOfVolume.GALLONS, UnitOfVolume.GALLONS,
UnitOfVolume.LITERS, UnitOfVolume.LITERS,
), ),

View File

@ -228,7 +228,6 @@ AQHI_SENSOR = ECSensorEntityDescription(
key="aqhi", key="aqhi",
name="AQHI", name="AQHI",
device_class=SensorDeviceClass.AQI, device_class=SensorDeviceClass.AQI,
native_unit_of_measurement="AQI",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=_get_aqhi_value, value_fn=_get_aqhi_value,
) )

View File

@ -47,10 +47,19 @@ async def async_setup_services(hass: HomeAssistant) -> None:
for target in call.data[ATTR_DEVICE_ID]: for target in call.data[ATTR_DEVICE_ID]:
device = registry.async_get(target) device = registry.async_get(target)
if device: if device:
coordinator = hass.data[DOMAIN][list(device.config_entries)[0]] for key in device.config_entries:
# fully_method(coordinator.fully, *args, **kwargs) would make entry = hass.config_entries.async_get_entry(key)
# test_services.py fail. if not entry:
await getattr(coordinator.fully, fully_method.__name__)(*args, **kwargs) continue
if entry.domain != DOMAIN:
continue
coordinator = hass.data[DOMAIN][key]
# fully_method(coordinator.fully, *args, **kwargs) would make
# test_services.py fail.
await getattr(coordinator.fully, fully_method.__name__)(
*args, **kwargs
)
break
async def async_load_url(call: ServiceCall) -> None: async def async_load_url(call: ServiceCall) -> None:
"""Load a URL on the Fully Kiosk Browser.""" """Load a URL on the Fully Kiosk Browser."""

View File

@ -1,7 +1,8 @@
"""DataUpdateCoordinator for LaCrosse View.""" """DataUpdateCoordinator for LaCrosse View."""
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import timedelta
from time import time
from lacrosse_view import HTTPError, LaCrosse, Location, LoginError, Sensor from lacrosse_view import HTTPError, LaCrosse, Location, LoginError, Sensor
@ -30,7 +31,7 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]):
) -> None: ) -> None:
"""Initialize DataUpdateCoordinator for LaCrosse View.""" """Initialize DataUpdateCoordinator for LaCrosse View."""
self.api = api self.api = api
self.last_update = datetime.utcnow() self.last_update = time()
self.username = entry.data["username"] self.username = entry.data["username"]
self.password = entry.data["password"] self.password = entry.data["password"]
self.hass = hass self.hass = hass
@ -45,26 +46,22 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]):
async def _async_update_data(self) -> list[Sensor]: async def _async_update_data(self) -> list[Sensor]:
"""Get the data for LaCrosse View.""" """Get the data for LaCrosse View."""
now = datetime.utcnow() now = int(time())
if self.last_update < now - timedelta(minutes=59): # Get new token if self.last_update < now - 59 * 60: # Get new token once in a hour
self.last_update = now self.last_update = now
try: try:
await self.api.login(self.username, self.password) await self.api.login(self.username, self.password)
except LoginError as error: except LoginError as error:
raise ConfigEntryAuthFailed from error raise ConfigEntryAuthFailed from error
# Get the timestamp for yesterday at 6 PM (this is what is used in the app, i noticed it when proxying the request)
yesterday = now - timedelta(days=1)
yesterday = yesterday.replace(hour=18, minute=0, second=0, microsecond=0)
yesterday_timestamp = datetime.timestamp(yesterday)
try: try:
# Fetch last hour of data
sensors = await self.api.get_sensors( sensors = await self.api.get_sensors(
location=Location(id=self.id, name=self.name), location=Location(id=self.id, name=self.name),
tz=self.hass.config.time_zone, tz=self.hass.config.time_zone,
start=str(int(yesterday_timestamp)), start=str(now - 3600),
end=str(int(datetime.timestamp(now))), end=str(now),
) )
except HTTPError as error: except HTTPError as error:
raise ConfigEntryNotReady from error raise ConfigEntryNotReady from error

View File

@ -3,7 +3,7 @@
"name": "Reolink IP NVR/camera", "name": "Reolink IP NVR/camera",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/reolink", "documentation": "https://www.home-assistant.io/integrations/reolink",
"requirements": ["reolink-aio==0.1.1"], "requirements": ["reolink-aio==0.1.2"],
"codeowners": ["@starkillerOG"], "codeowners": ["@starkillerOG"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["reolink-aio"] "loggers": ["reolink-aio"]

View File

@ -53,6 +53,8 @@ BLE_SCANNER_OPTIONS = [
selector.SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"), selector.SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"),
] ]
INTERNAL_WIFI_AP_IP = "192.168.33.1"
async def validate_input( async def validate_input(
hass: HomeAssistant, hass: HomeAssistant,
@ -217,7 +219,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
current_entry := await self.async_set_unique_id(mac) current_entry := await self.async_set_unique_id(mac)
) and current_entry.data[CONF_HOST] == host: ) and current_entry.data[CONF_HOST] == host:
await async_reconnect_soon(self.hass, current_entry) await async_reconnect_soon(self.hass, current_entry)
self._abort_if_unique_id_configured({CONF_HOST: host}) if host == INTERNAL_WIFI_AP_IP:
# If the device is broadcasting the internal wifi ap ip
# we can't connect to it, so we should not update the
# entry with the new host as it will be unreachable
#
# This is a workaround for a bug in the firmware 0.12 (and older?)
# which should be removed once the firmware is fixed
# and the old version is no longer in use
self._abort_if_unique_id_configured()
else:
self._abort_if_unique_id_configured({CONF_HOST: host})
async def async_step_zeroconf( async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo self, discovery_info: zeroconf.ZeroconfServiceInfo

View File

@ -166,6 +166,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the SwitchBot API auth step.""" """Handle the SwitchBot API auth step."""
errors = {} errors = {}
assert self._discovered_adv is not None assert self._discovered_adv is not None
description_placeholders = {}
if user_input is not None: if user_input is not None:
try: try:
key_details = await self.hass.async_add_executor_job( key_details = await self.hass.async_add_executor_job(
@ -176,8 +177,10 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
) )
except SwitchbotAccountConnectionError as ex: except SwitchbotAccountConnectionError as ex:
raise AbortFlow("cannot_connect") from ex raise AbortFlow("cannot_connect") from ex
except SwitchbotAuthenticationError: except SwitchbotAuthenticationError as ex:
_LOGGER.debug("Authentication failed: %s", ex, exc_info=True)
errors = {"base": "auth_failed"} errors = {"base": "auth_failed"}
description_placeholders = {"error_detail": str(ex)}
else: else:
return await self.async_step_lock_key(key_details) return await self.async_step_lock_key(key_details)
@ -195,6 +198,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
), ),
description_placeholders={ description_placeholders={
"name": name_from_discovery(self._discovered_adv), "name": name_from_discovery(self._discovered_adv),
**description_placeholders,
}, },
) )

View File

@ -2,7 +2,7 @@
"domain": "switchbot", "domain": "switchbot",
"name": "SwitchBot", "name": "SwitchBot",
"documentation": "https://www.home-assistant.io/integrations/switchbot", "documentation": "https://www.home-assistant.io/integrations/switchbot",
"requirements": ["PySwitchbot==0.36.1"], "requirements": ["PySwitchbot==0.36.2"],
"config_flow": true, "config_flow": true,
"dependencies": ["bluetooth"], "dependencies": ["bluetooth"],
"codeowners": [ "codeowners": [

View File

@ -40,7 +40,7 @@
}, },
"error": { "error": {
"encryption_key_invalid": "Key ID or Encryption key is invalid", "encryption_key_invalid": "Key ID or Encryption key is invalid",
"auth_failed": "Authentication failed" "auth_failed": "Authentication failed: {error_detail}"
}, },
"abort": { "abort": {
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",

View File

@ -8,7 +8,7 @@
"unknown": "Unexpected error" "unknown": "Unexpected error"
}, },
"error": { "error": {
"auth_failed": "Authentication failed", "auth_failed": "Authentication failed: {error_detail}",
"encryption_key_invalid": "Key ID or Encryption key is invalid" "encryption_key_invalid": "Key ID or Encryption key is invalid"
}, },
"flow_title": "{name} ({address})", "flow_title": "{name} ({address})",
@ -47,18 +47,7 @@
"data": { "data": {
"address": "Device address" "address": "Device address"
} }
},
"lock_key": {
"description": "The {name} device requires encryption key, details on how to obtain it can be found in the documentation.",
"data": {
"key_id": "Key ID",
"encryption_key": "Encryption key"
} }
}
},
"error": {
"key_id_invalid": "Key ID or Encryption key is invalid",
"encryption_key_invalid": "Key ID or Encryption key is invalid"
} }
}, },
"options": { "options": {

View File

@ -3,7 +3,7 @@
"name": "Tasmota", "name": "Tasmota",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tasmota", "documentation": "https://www.home-assistant.io/integrations/tasmota",
"requirements": ["hatasmota==0.6.1"], "requirements": ["hatasmota==0.6.2"],
"dependencies": ["mqtt"], "dependencies": ["mqtt"],
"mqtt": ["tasmota/discovery/#"], "mqtt": ["tasmota/discovery/#"],
"codeowners": ["@emontnemery"], "codeowners": ["@emontnemery"],

View File

@ -21,6 +21,7 @@ from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX, LIGHT_LUX,
PERCENTAGE, PERCENTAGE,
POWER_VOLT_AMPERE_REACTIVE,
SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfApparentPower, UnitOfApparentPower,
@ -217,8 +218,10 @@ SENSOR_UNIT_MAP = {
hc.LIGHT_LUX: LIGHT_LUX, hc.LIGHT_LUX: LIGHT_LUX,
hc.MASS_KILOGRAMS: UnitOfMass.KILOGRAMS, hc.MASS_KILOGRAMS: UnitOfMass.KILOGRAMS,
hc.PERCENTAGE: PERCENTAGE, hc.PERCENTAGE: PERCENTAGE,
hc.POWER_FACTOR: None,
hc.POWER_WATT: UnitOfPower.WATT, hc.POWER_WATT: UnitOfPower.WATT,
hc.PRESSURE_HPA: UnitOfPressure.HPA, hc.PRESSURE_HPA: UnitOfPressure.HPA,
hc.REACTIVE_POWER: POWER_VOLT_AMPERE_REACTIVE,
hc.SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS, hc.SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS,
hc.SIGNAL_STRENGTH_DECIBELS_MILLIWATT: SIGNAL_STRENGTH_DECIBELS_MILLIWATT, hc.SIGNAL_STRENGTH_DECIBELS_MILLIWATT: SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
hc.SPEED_KILOMETERS_PER_HOUR: UnitOfSpeed.KILOMETERS_PER_HOUR, hc.SPEED_KILOMETERS_PER_HOUR: UnitOfSpeed.KILOMETERS_PER_HOUR,

View File

@ -65,6 +65,7 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]):
self.entity_description = description self.entity_description = description
self._removed = False self._removed = False
self._write_state = False
self._attr_available = description.available_fn(controller, obj_id) self._attr_available = description.available_fn(controller, obj_id)
self._attr_device_info = description.device_info_fn(controller.api, obj_id) self._attr_device_info = description.device_info_fn(controller.api, obj_id)
@ -117,9 +118,14 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]):
self.hass.async_create_task(self.remove_item({self._obj_id})) self.hass.async_create_task(self.remove_item({self._obj_id}))
return return
self._attr_available = description.available_fn(self.controller, self._obj_id) if (
available := description.available_fn(self.controller, self._obj_id)
) != self.available:
self._attr_available = available
self._write_state = True
self.async_update_state(event, obj_id) self.async_update_state(event, obj_id)
self.async_write_ha_state() if self._write_state:
self.async_write_ha_state()
@callback @callback
def async_signal_reachable_callback(self) -> None: def async_signal_reachable_callback(self) -> None:

View File

@ -217,6 +217,7 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]):
self.async_on_remove( self.async_on_remove(
handler.subscribe( handler.subscribe(
self.async_signalling_callback, self.async_signalling_callback,
id_filter=self._obj_id,
) )
) )
self.async_on_remove( self.async_on_remove(
@ -253,11 +254,19 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]):
self.hass.async_create_task(self.remove_item({self._obj_id})) self.hass.async_create_task(self.remove_item({self._obj_id}))
return return
update_state = False
obj = description.object_fn(self.controller.api, self._obj_id) obj = description.object_fn(self.controller.api, self._obj_id)
if (value := description.value_fn(self.controller, obj)) != self.native_value: if (value := description.value_fn(self.controller, obj)) != self.native_value:
self._attr_native_value = value self._attr_native_value = value
self._attr_available = description.available_fn(self.controller, self._obj_id) update_state = True
self.async_write_ha_state() if (
available := description.available_fn(self.controller, self._obj_id)
) != self.available:
self._attr_available = available
update_state = True
if update_state:
self.async_write_ha_state()
@callback @callback
def async_signal_reachable_callback(self) -> None: def async_signal_reachable_callback(self) -> None:

View File

@ -361,6 +361,7 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]):
self.async_on_remove( self.async_on_remove(
handler.subscribe( handler.subscribe(
self.async_signalling_callback, self.async_signalling_callback,
id_filter=self._obj_id,
) )
) )
self.async_on_remove( self.async_on_remove(
@ -410,11 +411,20 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]):
self.hass.async_create_task(self.remove_item({self._obj_id})) self.hass.async_create_task(self.remove_item({self._obj_id}))
return return
update_state = False
if not description.only_event_for_state_change: if not description.only_event_for_state_change:
obj = description.object_fn(self.controller.api, self._obj_id) obj = description.object_fn(self.controller.api, self._obj_id)
self._attr_is_on = description.is_on_fn(self.controller.api, obj) if (is_on := description.is_on_fn(self.controller.api, obj)) != self.is_on:
self._attr_available = description.available_fn(self.controller, self._obj_id) self._attr_is_on = is_on
self.async_write_ha_state() update_state = True
if (
available := description.available_fn(self.controller, self._obj_id)
) != self.available:
self._attr_available = available
update_state = True
if update_state:
self.async_write_ha_state()
@callback @callback
def async_signal_reachable_callback(self) -> None: def async_signal_reachable_callback(self) -> None:

View File

@ -163,6 +163,12 @@ class UnifiDeviceUpdateEntity(UnifiEntity[HandlerT, DataT], UpdateEntity):
description = self.entity_description description = self.entity_description
obj = description.object_fn(self.controller.api, self._obj_id) obj = description.object_fn(self.controller.api, self._obj_id)
self._attr_in_progress = description.state_fn(self.controller.api, obj) if (
in_progress := description.state_fn(self.controller.api, obj)
) != self.in_progress:
self._attr_in_progress = in_progress
self._write_state = True
self._attr_installed_version = obj.version self._attr_installed_version = obj.version
self._attr_latest_version = obj.upgrade_to_firmware or obj.version self._attr_latest_version = obj.upgrade_to_firmware or obj.version
if self.installed_version != self.latest_version:
self._write_state = True

View File

@ -755,7 +755,6 @@ class RSSISensor(Sensor, id_suffix="rssi"):
"""RSSI sensor for a device.""" """RSSI sensor for a device."""
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH
_attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_entity_registry_enabled_default = False _attr_entity_registry_enabled_default = False
_attr_should_poll = True # BaseZhaEntity defaults to False _attr_should_poll = True # BaseZhaEntity defaults to False

View File

@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023 MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 1 MINOR_VERSION: Final = 1
PATCH_VERSION: Final = "0" PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2023.1.0" version = "2023.1.1"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"

View File

@ -40,7 +40,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1 PySocks==1.7.1
# homeassistant.components.switchbot # homeassistant.components.switchbot
PySwitchbot==0.36.1 PySwitchbot==0.36.2
# homeassistant.components.transport_nsw # homeassistant.components.transport_nsw
PyTransportNSW==0.1.1 PyTransportNSW==0.1.1
@ -422,7 +422,7 @@ beautifulsoup4==4.11.1
bellows==0.34.5 bellows==0.34.5
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer_connected==0.10.4 bimmer_connected==0.12.0
# homeassistant.components.bizkaibus # homeassistant.components.bizkaibus
bizkaibus==0.1.1 bizkaibus==0.1.1
@ -488,7 +488,7 @@ brunt==1.2.0
bt_proximity==0.2.1 bt_proximity==0.2.1
# homeassistant.components.bthome # homeassistant.components.bthome
bthome-ble==2.4.0 bthome-ble==2.4.1
# homeassistant.components.bt_home_hub_5 # homeassistant.components.bt_home_hub_5
bthomehub5-devicelist==0.1.1 bthomehub5-devicelist==0.1.1
@ -858,7 +858,7 @@ hass-nabucasa==0.61.0
hass_splunk==0.1.1 hass_splunk==0.1.1
# homeassistant.components.tasmota # homeassistant.components.tasmota
hatasmota==0.6.1 hatasmota==0.6.2
# homeassistant.components.jewish_calendar # homeassistant.components.jewish_calendar
hdate==0.10.4 hdate==0.10.4
@ -1560,7 +1560,7 @@ pydroid-ipcam==2.0.0
pyebox==1.1.4 pyebox==1.1.4
# homeassistant.components.econet # homeassistant.components.econet
pyeconet==0.1.17 pyeconet==0.1.18
# homeassistant.components.edimax # homeassistant.components.edimax
pyedimax==0.2.1 pyedimax==0.2.1
@ -2190,7 +2190,7 @@ regenmaschine==2022.11.0
renault-api==0.1.11 renault-api==0.1.11
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.1.1 reolink-aio==0.1.2
# homeassistant.components.python_script # homeassistant.components.python_script
restrictedpython==5.2 restrictedpython==5.2

View File

@ -36,7 +36,7 @@ PyRMVtransport==0.3.3
PySocks==1.7.1 PySocks==1.7.1
# homeassistant.components.switchbot # homeassistant.components.switchbot
PySwitchbot==0.36.1 PySwitchbot==0.36.2
# homeassistant.components.transport_nsw # homeassistant.components.transport_nsw
PyTransportNSW==0.1.1 PyTransportNSW==0.1.1
@ -349,7 +349,7 @@ beautifulsoup4==4.11.1
bellows==0.34.5 bellows==0.34.5
# homeassistant.components.bmw_connected_drive # homeassistant.components.bmw_connected_drive
bimmer_connected==0.10.4 bimmer_connected==0.12.0
# homeassistant.components.bluetooth # homeassistant.components.bluetooth
bleak-retry-connector==2.13.0 bleak-retry-connector==2.13.0
@ -392,7 +392,7 @@ brother==2.1.1
brunt==1.2.0 brunt==1.2.0
# homeassistant.components.bthome # homeassistant.components.bthome
bthome-ble==2.4.0 bthome-ble==2.4.1
# homeassistant.components.buienradar # homeassistant.components.buienradar
buienradar==1.0.5 buienradar==1.0.5
@ -647,7 +647,7 @@ habitipy==0.2.0
hass-nabucasa==0.61.0 hass-nabucasa==0.61.0
# homeassistant.components.tasmota # homeassistant.components.tasmota
hatasmota==0.6.1 hatasmota==0.6.2
# homeassistant.components.jewish_calendar # homeassistant.components.jewish_calendar
hdate==0.10.4 hdate==0.10.4
@ -1106,7 +1106,7 @@ pydexcom==0.2.3
pydroid-ipcam==2.0.0 pydroid-ipcam==2.0.0
# homeassistant.components.econet # homeassistant.components.econet
pyeconet==0.1.17 pyeconet==0.1.18
# homeassistant.components.efergy # homeassistant.components.efergy
pyefergy==22.1.1 pyefergy==22.1.1
@ -1529,7 +1529,7 @@ regenmaschine==2022.11.0
renault-api==0.1.11 renault-api==0.1.11
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.1.1 reolink-aio==0.1.2
# homeassistant.components.python_script # homeassistant.components.python_script
restrictedpython==5.2 restrictedpython==5.2

View File

@ -4,7 +4,6 @@ import json
from pathlib import Path from pathlib import Path
from bimmer_connected.account import MyBMWAccount from bimmer_connected.account import MyBMWAccount
from bimmer_connected.api.utils import log_to_to_file
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.bmw_connected_drive.const import ( from homeassistant.components.bmw_connected_drive.const import (
@ -64,15 +63,6 @@ async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None:
} }
fetched_at = utcnow() fetched_at = utcnow()
# simulate storing fingerprints
if account.config.log_response_path:
for brand in ["bmw", "mini"]:
log_to_to_file(
json.dumps(vehicles[brand]),
account.config.log_response_path,
f"vehicles_v2_{brand}",
)
# Create a vehicle with base + specific state as provided by state/VIN API # Create a vehicle with base + specific state as provided by state/VIN API
for vehicle_base in [vehicle for brand in vehicles.values() for vehicle in brand]: for vehicle_base in [vehicle for brand in vehicles.values() for vehicle in brand]:
vehicle_state_path = ( vehicle_state_path = (
@ -93,14 +83,6 @@ async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None:
fetched_at, fetched_at,
) )
# simulate storing fingerprints
if account.config.log_response_path:
log_to_to_file(
json.dumps(vehicle_state),
account.config.log_response_path,
f"state_{vehicle_base['vin']}",
)
async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry: async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry:
"""Mock a fully setup config entry and all components based on fixtures.""" """Mock a fully setup config entry and all components based on fixtures."""

View File

@ -26,6 +26,7 @@ from homeassistant.const import (
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
VOLUME_CUBIC_METERS, VOLUME_CUBIC_METERS,
UnitOfEnergy,
UnitOfPower, UnitOfPower,
) )
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -804,3 +805,57 @@ async def test_reconnect(hass, dsmr_connection_fixture):
await hass.config_entries.async_unload(mock_entry.entry_id) await hass.config_entries.async_unload(mock_entry.entry_id)
assert mock_entry.state == config_entries.ConfigEntryState.NOT_LOADED assert mock_entry.state == config_entries.ConfigEntryState.NOT_LOADED
async def test_gas_meter_providing_energy_reading(hass, dsmr_connection_fixture):
"""Test that gas providing energy readings use the correct device class."""
(connection_factory, transport, protocol) = dsmr_connection_fixture
from dsmr_parser.obis_references import GAS_METER_READING
from dsmr_parser.objects import MBusObject
entry_data = {
"port": "/dev/ttyUSB0",
"dsmr_version": "2.2",
"precision": 4,
"reconnect_interval": 30,
"serial_id": "1234",
"serial_id_gas": "5678",
}
entry_options = {
"time_between_update": 0,
}
telegram = {
GAS_METER_READING: MBusObject(
[
{"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(123.456), "unit": UnitOfEnergy.GIGA_JOULE},
]
),
}
mock_entry = MockConfigEntry(
domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options
)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
telegram_callback = connection_factory.call_args_list[0][0][2]
telegram_callback(telegram)
await asyncio.sleep(0)
gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption")
assert gas_consumption.state == "123.456"
assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY
assert (
gas_consumption.attributes.get(ATTR_STATE_CLASS)
== SensorStateClass.TOTAL_INCREASING
)
assert (
gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfEnergy.GIGA_JOULE
)

View File

@ -1,6 +1,7 @@
"""Test the Shelly config flow.""" """Test the Shelly config flow."""
from __future__ import annotations from __future__ import annotations
from dataclasses import replace
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from aioshelly.exceptions import ( from aioshelly.exceptions import (
@ -12,6 +13,7 @@ import pytest
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.components.shelly import config_flow
from homeassistant.components.shelly.const import ( from homeassistant.components.shelly.const import (
CONF_BLE_SCANNER_MODE, CONF_BLE_SCANNER_MODE,
DOMAIN, DOMAIN,
@ -704,6 +706,30 @@ async def test_zeroconf_already_configured(hass):
assert entry.data["host"] == "1.1.1.1" assert entry.data["host"] == "1.1.1.1"
async def test_zeroconf_with_wifi_ap_ip(hass):
"""Test we ignore the Wi-FI AP IP."""
entry = MockConfigEntry(
domain="shelly", unique_id="test-mac", data={"host": "2.2.2.2"}
)
entry.add_to_hass(hass)
with patch(
"aioshelly.common.get_info",
return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False},
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
data=replace(DISCOVERY_INFO, host=config_flow.INTERNAL_WIFI_AP_IP),
context={"source": config_entries.SOURCE_ZEROCONF},
)
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "already_configured"
# Test config entry was not updated with the wifi ap ip
assert entry.data["host"] == "2.2.2.2"
async def test_zeroconf_firmware_unsupported(hass): async def test_zeroconf_firmware_unsupported(hass):
"""Test we abort if device firmware is unsupported.""" """Test we abort if device firmware is unsupported."""
with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported): with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported):

View File

@ -481,7 +481,7 @@ async def test_user_setup_wolock_auth(hass):
with patch( with patch(
"homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key",
side_effect=SwitchbotAuthenticationError, side_effect=SwitchbotAuthenticationError("error from api"),
): ):
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@ -494,6 +494,7 @@ async def test_user_setup_wolock_auth(hass):
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "lock_auth" assert result["step_id"] == "lock_auth"
assert result["errors"] == {"base": "auth_failed"} assert result["errors"] == {"base": "auth_failed"}
assert "error from api" in result["description_placeholders"]["error_detail"]
with patch_async_setup_entry() as mock_setup_entry, patch( with patch_async_setup_entry() as mock_setup_entry, patch(
"homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",