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

View File

@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "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"],
"config_flow": true,
"iot_class": "cloud_polling",

View File

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

View File

@ -28,6 +28,7 @@ from homeassistant.const import (
CONF_HOST,
CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
UnitOfEnergy,
UnitOfVolume,
)
from homeassistant.core import CoreState, Event, HomeAssistant, callback
@ -591,6 +592,21 @@ class DSMREntity(SensorEntity):
"""Entity is only available if there is a telegram."""
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
def native_value(self) -> StateType:
"""Return the state of sensor, if available, translate if needed."""

View File

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

View File

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

View File

@ -22,10 +22,10 @@ from .const import DOMAIN
ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,)
ENERGY_USAGE_UNITS = {
sensor.SensorDeviceClass.ENERGY: (
UnitOfEnergy.GIGA_JOULE,
UnitOfEnergy.KILO_WATT_HOUR,
UnitOfEnergy.MEGA_WATT_HOUR,
UnitOfEnergy.WATT_HOUR,
UnitOfEnergy.GIGA_JOULE,
)
}
ENERGY_PRICE_UNITS = tuple(
@ -39,12 +39,16 @@ GAS_USAGE_DEVICE_CLASSES = (
)
GAS_USAGE_UNITS = {
sensor.SensorDeviceClass.ENERGY: (
UnitOfEnergy.WATT_HOUR,
UnitOfEnergy.GIGA_JOULE,
UnitOfEnergy.KILO_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(
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_UNITS = {
sensor.SensorDeviceClass.WATER: (
UnitOfVolume.CUBIC_METERS,
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS,
UnitOfVolume.GALLONS,
UnitOfVolume.LITERS,
),

View File

@ -228,7 +228,6 @@ AQHI_SENSOR = ECSensorEntityDescription(
key="aqhi",
name="AQHI",
device_class=SensorDeviceClass.AQI,
native_unit_of_measurement="AQI",
state_class=SensorStateClass.MEASUREMENT,
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]:
device = registry.async_get(target)
if device:
coordinator = hass.data[DOMAIN][list(device.config_entries)[0]]
# fully_method(coordinator.fully, *args, **kwargs) would make
# test_services.py fail.
await getattr(coordinator.fully, fully_method.__name__)(*args, **kwargs)
for key in device.config_entries:
entry = hass.config_entries.async_get_entry(key)
if not entry:
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:
"""Load a URL on the Fully Kiosk Browser."""

View File

@ -1,7 +1,8 @@
"""DataUpdateCoordinator for LaCrosse View."""
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
@ -30,7 +31,7 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]):
) -> None:
"""Initialize DataUpdateCoordinator for LaCrosse View."""
self.api = api
self.last_update = datetime.utcnow()
self.last_update = time()
self.username = entry.data["username"]
self.password = entry.data["password"]
self.hass = hass
@ -45,26 +46,22 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]):
async def _async_update_data(self) -> list[Sensor]:
"""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
try:
await self.api.login(self.username, self.password)
except LoginError as 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:
# Fetch last hour of data
sensors = await self.api.get_sensors(
location=Location(id=self.id, name=self.name),
tz=self.hass.config.time_zone,
start=str(int(yesterday_timestamp)),
end=str(int(datetime.timestamp(now))),
start=str(now - 3600),
end=str(now),
)
except HTTPError as error:
raise ConfigEntryNotReady from error

View File

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

View File

@ -53,6 +53,8 @@ BLE_SCANNER_OPTIONS = [
selector.SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"),
]
INTERNAL_WIFI_AP_IP = "192.168.33.1"
async def validate_input(
hass: HomeAssistant,
@ -217,7 +219,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
current_entry := await self.async_set_unique_id(mac)
) and current_entry.data[CONF_HOST] == host:
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(
self, discovery_info: zeroconf.ZeroconfServiceInfo

View File

@ -166,6 +166,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the SwitchBot API auth step."""
errors = {}
assert self._discovered_adv is not None
description_placeholders = {}
if user_input is not None:
try:
key_details = await self.hass.async_add_executor_job(
@ -176,8 +177,10 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
)
except SwitchbotAccountConnectionError as 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"}
description_placeholders = {"error_detail": str(ex)}
else:
return await self.async_step_lock_key(key_details)
@ -195,6 +198,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
),
description_placeholders={
"name": name_from_discovery(self._discovered_adv),
**description_placeholders,
},
)

View File

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

View File

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

View File

@ -8,7 +8,7 @@
"unknown": "Unexpected error"
},
"error": {
"auth_failed": "Authentication failed",
"auth_failed": "Authentication failed: {error_detail}",
"encryption_key_invalid": "Key ID or Encryption key is invalid"
},
"flow_title": "{name} ({address})",
@ -47,18 +47,7 @@
"data": {
"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": {
@ -70,4 +59,4 @@
}
}
}
}
}

View File

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

View File

@ -21,6 +21,7 @@ from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
POWER_VOLT_AMPERE_REACTIVE,
SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfApparentPower,
@ -217,8 +218,10 @@ SENSOR_UNIT_MAP = {
hc.LIGHT_LUX: LIGHT_LUX,
hc.MASS_KILOGRAMS: UnitOfMass.KILOGRAMS,
hc.PERCENTAGE: PERCENTAGE,
hc.POWER_FACTOR: None,
hc.POWER_WATT: UnitOfPower.WATT,
hc.PRESSURE_HPA: UnitOfPressure.HPA,
hc.REACTIVE_POWER: POWER_VOLT_AMPERE_REACTIVE,
hc.SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS,
hc.SIGNAL_STRENGTH_DECIBELS_MILLIWATT: SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
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._removed = False
self._write_state = False
self._attr_available = description.available_fn(controller, 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}))
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_write_ha_state()
if self._write_state:
self.async_write_ha_state()
@callback
def async_signal_reachable_callback(self) -> None:

View File

@ -217,6 +217,7 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]):
self.async_on_remove(
handler.subscribe(
self.async_signalling_callback,
id_filter=self._obj_id,
)
)
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}))
return
update_state = False
obj = description.object_fn(self.controller.api, self._obj_id)
if (value := description.value_fn(self.controller, obj)) != self.native_value:
self._attr_native_value = value
self._attr_available = description.available_fn(self.controller, self._obj_id)
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
def async_signal_reachable_callback(self) -> None:

View File

@ -361,6 +361,7 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]):
self.async_on_remove(
handler.subscribe(
self.async_signalling_callback,
id_filter=self._obj_id,
)
)
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}))
return
update_state = False
if not description.only_event_for_state_change:
obj = description.object_fn(self.controller.api, self._obj_id)
self._attr_is_on = description.is_on_fn(self.controller.api, obj)
self._attr_available = description.available_fn(self.controller, self._obj_id)
self.async_write_ha_state()
if (is_on := description.is_on_fn(self.controller.api, obj)) != self.is_on:
self._attr_is_on = is_on
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
def async_signal_reachable_callback(self) -> None:

View File

@ -163,6 +163,12 @@ class UnifiDeviceUpdateEntity(UnifiEntity[HandlerT, DataT], UpdateEntity):
description = self.entity_description
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_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."""
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_entity_registry_enabled_default = False
_attr_should_poll = True # BaseZhaEntity defaults to False

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import json
from pathlib import Path
from bimmer_connected.account import MyBMWAccount
from bimmer_connected.api.utils import log_to_to_file
from homeassistant import config_entries
from homeassistant.components.bmw_connected_drive.const import (
@ -64,15 +63,6 @@ async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None:
}
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
for vehicle_base in [vehicle for brand in vehicles.values() for vehicle in brand]:
vehicle_state_path = (
@ -93,14 +83,6 @@ async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None:
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:
"""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_UNKNOWN,
VOLUME_CUBIC_METERS,
UnitOfEnergy,
UnitOfPower,
)
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)
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."""
from __future__ import annotations
from dataclasses import replace
from unittest.mock import AsyncMock, Mock, patch
from aioshelly.exceptions import (
@ -12,6 +13,7 @@ import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components import zeroconf
from homeassistant.components.shelly import config_flow
from homeassistant.components.shelly.const import (
CONF_BLE_SCANNER_MODE,
DOMAIN,
@ -704,6 +706,30 @@ async def test_zeroconf_already_configured(hass):
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):
"""Test we abort if device firmware is unsupported."""
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(
"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["flow_id"],
@ -494,6 +494,7 @@ async def test_user_setup_wolock_auth(hass):
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "lock_auth"
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(
"homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",