This commit is contained in:
Paulus Schoutsen 2022-11-20 20:12:02 -05:00 committed by GitHub
commit 5fe426a4fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 235 additions and 37 deletions

View File

@ -7,7 +7,7 @@
"quality_scale": "internal",
"requirements": [
"bleak==0.19.2",
"bleak-retry-connector==2.8.4",
"bleak-retry-connector==2.8.5",
"bluetooth-adapters==0.7.0",
"bluetooth-auto-recovery==0.3.6",
"dbus-fast==1.61.1"

View File

@ -612,7 +612,7 @@ class TimeSMAFilter(Filter, SensorEntity):
moving_sum = 0
start = new_state.timestamp - self._time_window
prev_state = self.last_leak or self.queue[0]
prev_state = self.last_leak if self.last_leak is not None else self.queue[0]
for state in self.queue:
moving_sum += (state.timestamp - start).total_seconds() * prev_state.state
start = state.timestamp

View File

@ -1,7 +1,6 @@
"""Flo device object."""
from __future__ import annotations
import asyncio
from datetime import datetime, timedelta
from typing import Any
@ -40,14 +39,10 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
async def _async_update_data(self):
"""Update data via library."""
try:
async with timeout(10):
await asyncio.gather(
*[
self.send_presence_ping(),
self._update_device(),
self._update_consumption_data(),
]
)
async with timeout(20):
await self.send_presence_ping()
await self._update_device()
await self._update_consumption_data()
except (RequestError) as error:
raise UpdateFailed(error) from error

View File

@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.28.32"],
"requirements": ["flux_led==0.28.34"],
"quality_scale": "platinum",
"codeowners": ["@icemanch", "@bdraco"],
"iot_class": "local_push",

View File

@ -3,7 +3,7 @@
"name": "KNX",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/knx",
"requirements": ["xknx==1.2.0"],
"requirements": ["xknx==1.2.1"],
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
"quality_scale": "platinum",
"iot_class": "local_push",

View File

@ -375,4 +375,3 @@ async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> No
hubs = hass.data[DOMAIN]
for name in hubs:
await hubs[name].async_close()
del hass.data[DOMAIN]

View File

@ -132,6 +132,12 @@ async def async_modbus_setup(
await async_setup_reload_service(hass, DOMAIN, [DOMAIN])
if DOMAIN in hass.data and config[DOMAIN] == []:
hubs = hass.data[DOMAIN]
for name in hubs:
if not await hubs[name].async_setup():
return False
hass.data[DOMAIN] = hub_collect = {}
for conf_hub in config[DOMAIN]:
my_hub = ModbusHub(hass, conf_hub)

View File

@ -29,7 +29,7 @@ class NetatmoBase(Entity):
self._device_name: str = ""
self._id: str = ""
self._model: str = ""
self._config_url: str = ""
self._config_url: str | None = None
self._attr_name = None
self._attr_unique_id = None
self._attr_extra_state_attributes = {}

View File

@ -19,6 +19,7 @@ from homeassistant.const import (
ENERGY_KILO_WATT_HOUR,
ENERGY_MEGA_WATT_HOUR,
ENERGY_WATT_HOUR,
POWER_KILO_WATT,
POWER_WATT,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
@ -80,6 +81,13 @@ UNIT_DESCRIPTIONS = {
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=POWER_WATT,
),
"kW": SensorEntityDescription(
key="kW",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=POWER_KILO_WATT,
),
"Wh": SensorEntityDescription(
key="Wh",
entity_category=EntityCategory.DIAGNOSTIC,

View File

@ -18,7 +18,7 @@ from tesla_powerwall import (
from homeassistant.components import persistent_notification
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -221,6 +221,17 @@ def _fetch_powerwall_data(power_wall: Powerwall) -> PowerwallData:
)
@callback
def async_last_update_was_successful(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Return True if the last update was successful."""
return bool(
(domain_data := hass.data.get(DOMAIN))
and (entry_data := domain_data.get(entry.entry_id))
and (coordinator := entry_data.get(POWERWALL_COORDINATOR))
and coordinator.last_update_success
)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -20,11 +20,18 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
from homeassistant.data_entry_flow import FlowResult
from homeassistant.util.network import is_ip_address
from . import async_last_update_was_successful
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
ENTRY_FAILURE_STATES = {
config_entries.ConfigEntryState.SETUP_ERROR,
config_entries.ConfigEntryState.SETUP_RETRY,
}
def _login_and_fetch_site_info(
power_wall: Powerwall, password: str
) -> tuple[SiteInfo, str]:
@ -34,6 +41,17 @@ def _login_and_fetch_site_info(
return power_wall.get_site_info(), power_wall.get_gateway_din()
def _powerwall_is_reachable(ip_address: str, password: str) -> bool:
"""Check if the powerwall is reachable."""
try:
Powerwall(ip_address).login(password)
except AccessDeniedError:
return True
except PowerwallUnreachableError:
return False
return True
async def validate_input(
hass: core.HomeAssistant, data: dict[str, str]
) -> dict[str, str]:
@ -69,13 +87,31 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.title: str | None = None
self.reauth_entry: config_entries.ConfigEntry | None = None
async def _async_powerwall_is_offline(
self, entry: config_entries.ConfigEntry
) -> bool:
"""Check if the power wall is offline.
We define offline by the config entry
is in a failure/retry state or the updates
are failing and the powerwall is unreachable
since device may be updating.
"""
ip_address = entry.data[CONF_IP_ADDRESS]
password = entry.data[CONF_PASSWORD]
return bool(
entry.state in ENTRY_FAILURE_STATES
or not async_last_update_was_successful(self.hass, entry)
) and not await self.hass.async_add_executor_job(
_powerwall_is_reachable, ip_address, password
)
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
"""Handle dhcp discovery."""
self.ip_address = discovery_info.ip
gateway_din = discovery_info.hostname.upper()
# The hostname is the gateway_din (unique_id)
await self.async_set_unique_id(gateway_din)
self._abort_if_unique_id_configured(updates={CONF_IP_ADDRESS: self.ip_address})
for entry in self._async_current_entries(include_ignore=False):
if entry.data[CONF_IP_ADDRESS] == discovery_info.ip:
if entry.unique_id is not None and is_ip_address(entry.unique_id):
@ -86,6 +122,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.hass.config_entries.async_reload(entry.entry_id)
)
return self.async_abort(reason="already_configured")
if entry.unique_id == gateway_din:
if await self._async_powerwall_is_offline(entry):
if self.hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_IP_ADDRESS: self.ip_address}
):
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
return self.async_abort(reason="already_configured")
# Still need to abort for ignored entries
self._abort_if_unique_id_configured()
self.context["title_placeholders"] = {
"name": gateway_din,
"ip_address": self.ip_address,

View File

@ -3,7 +3,7 @@
"name": "RainMachine",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rainmachine",
"requirements": ["regenmaschine==2022.10.0"],
"requirements": ["regenmaschine==2022.11.0"],
"codeowners": ["@bachya"],
"iot_class": "local_polling",
"homekit": {

View File

@ -3,7 +3,7 @@
"name": "SMA Solar",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sma",
"requirements": ["pysma==0.7.2"],
"requirements": ["pysma==0.7.3"],
"codeowners": ["@kellerza", "@rklomp"],
"iot_class": "local_polling",
"loggers": ["pysma"]

View File

@ -3,7 +3,7 @@
"name": "Viessmann ViCare",
"documentation": "https://www.home-assistant.io/integrations/vicare",
"codeowners": ["@oischinger"],
"requirements": ["PyViCare==2.17.0"],
"requirements": ["PyViCare==2.19.0"],
"iot_class": "cloud_polling",
"config_flow": true,
"dhcp": [

View File

@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 11
PATCH_VERSION: Final = "3"
PATCH_VERSION: Final = "4"
__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

@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1
attrs==21.2.0
awesomeversion==22.9.0
bcrypt==3.1.7
bleak-retry-connector==2.8.4
bleak-retry-connector==2.8.5
bleak==0.19.2
bluetooth-adapters==0.7.0
bluetooth-auto-recovery==0.3.6

View File

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

View File

@ -47,7 +47,7 @@ PyTransportNSW==0.1.1
PyTurboJPEG==1.6.7
# homeassistant.components.vicare
PyViCare==2.17.0
PyViCare==2.19.0
# homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.14.3
@ -413,7 +413,7 @@ bimmer_connected==0.10.4
bizkaibus==0.1.1
# homeassistant.components.bluetooth
bleak-retry-connector==2.8.4
bleak-retry-connector==2.8.5
# homeassistant.components.bluetooth
bleak==0.19.2
@ -693,7 +693,7 @@ fjaraskupan==2.2.0
flipr-api==1.4.2
# homeassistant.components.flux_led
flux_led==0.28.32
flux_led==0.28.34
# homeassistant.components.homekit
# homeassistant.components.recorder
@ -1893,7 +1893,7 @@ pysignalclirestapi==0.3.18
pyskyqhub==0.1.4
# homeassistant.components.sma
pysma==0.7.2
pysma==0.7.3
# homeassistant.components.smappee
pysmappee==0.2.29
@ -2153,7 +2153,7 @@ raincloudy==0.0.7
raspyrfm-client==1.2.8
# homeassistant.components.rainmachine
regenmaschine==2022.10.0
regenmaschine==2022.11.0
# homeassistant.components.renault
renault-api==0.1.11
@ -2563,7 +2563,7 @@ xboxapi==2.0.1
xiaomi-ble==0.10.0
# homeassistant.components.knx
xknx==1.2.0
xknx==1.2.1
# homeassistant.components.bluesound
# homeassistant.components.fritz

View File

@ -43,7 +43,7 @@ PyTransportNSW==0.1.1
PyTurboJPEG==1.6.7
# homeassistant.components.vicare
PyViCare==2.17.0
PyViCare==2.19.0
# homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.14.3
@ -337,7 +337,7 @@ bellows==0.34.2
bimmer_connected==0.10.4
# homeassistant.components.bluetooth
bleak-retry-connector==2.8.4
bleak-retry-connector==2.8.5
# homeassistant.components.bluetooth
bleak==0.19.2
@ -515,7 +515,7 @@ fjaraskupan==2.2.0
flipr-api==1.4.2
# homeassistant.components.flux_led
flux_led==0.28.32
flux_led==0.28.34
# homeassistant.components.homekit
# homeassistant.components.recorder
@ -1334,7 +1334,7 @@ pysiaalarm==3.0.2
pysignalclirestapi==0.3.18
# homeassistant.components.sma
pysma==0.7.2
pysma==0.7.3
# homeassistant.components.smappee
pysmappee==0.2.29
@ -1489,7 +1489,7 @@ radios==0.1.1
radiotherm==2.1.0
# homeassistant.components.rainmachine
regenmaschine==2022.10.0
regenmaschine==2022.11.0
# homeassistant.components.renault
renault-api==0.1.11
@ -1776,7 +1776,7 @@ xbox-webapi==2.0.11
xiaomi-ble==0.10.0
# homeassistant.components.knx
xknx==1.2.0
xknx==1.2.1
# homeassistant.components.bluesound
# homeassistant.components.fritz

View File

@ -1,6 +1,6 @@
"""Test the Powerwall config flow."""
from unittest.mock import patch
from unittest.mock import MagicMock, patch
from tesla_powerwall import (
AccessDeniedError,
@ -18,6 +18,7 @@ from .mocks import (
MOCK_GATEWAY_DIN,
_mock_powerwall_side_effect,
_mock_powerwall_site_name,
_mock_powerwall_with_fixtures,
)
from tests.common import MockConfigEntry
@ -351,7 +352,7 @@ async def test_dhcp_discovery_update_ip_address(hass):
unique_id=MOCK_GATEWAY_DIN,
)
entry.add_to_hass(hass)
mock_powerwall = await _mock_powerwall_site_name(hass, "Some site")
mock_powerwall = MagicMock(login=MagicMock(side_effect=PowerwallUnreachableError))
with patch(
"homeassistant.components.powerwall.config_flow.Powerwall",
@ -375,6 +376,70 @@ async def test_dhcp_discovery_update_ip_address(hass):
assert entry.data[CONF_IP_ADDRESS] == "1.1.1.1"
async def test_dhcp_discovery_does_not_update_ip_when_auth_fails(hass):
"""Test we do not switch to another interface when auth is failing."""
entry = MockConfigEntry(
domain=DOMAIN,
data=VALID_CONFIG,
unique_id=MOCK_GATEWAY_DIN,
)
entry.add_to_hass(hass)
mock_powerwall = MagicMock(login=MagicMock(side_effect=AccessDeniedError("any")))
with patch(
"homeassistant.components.powerwall.config_flow.Powerwall",
return_value=mock_powerwall,
), patch(
"homeassistant.components.powerwall.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=dhcp.DhcpServiceInfo(
ip="1.1.1.1",
macaddress="AA:BB:CC:DD:EE:FF",
hostname=MOCK_GATEWAY_DIN.lower(),
),
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4"
async def test_dhcp_discovery_does_not_update_ip_when_auth_successful(hass):
"""Test we do not switch to another interface when auth is successful."""
entry = MockConfigEntry(
domain=DOMAIN,
data=VALID_CONFIG,
unique_id=MOCK_GATEWAY_DIN,
)
entry.add_to_hass(hass)
mock_powerwall = MagicMock(login=MagicMock(return_value=True))
with patch(
"homeassistant.components.powerwall.config_flow.Powerwall",
return_value=mock_powerwall,
), patch(
"homeassistant.components.powerwall.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=dhcp.DhcpServiceInfo(
ip="1.1.1.1",
macaddress="AA:BB:CC:DD:EE:FF",
hostname=MOCK_GATEWAY_DIN.lower(),
),
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4"
async def test_dhcp_discovery_updates_unique_id(hass):
"""Test we can update the unique id from dhcp."""
entry = MockConfigEntry(
@ -406,3 +471,70 @@ async def test_dhcp_discovery_updates_unique_id(hass):
assert result["reason"] == "already_configured"
assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4"
assert entry.unique_id == MOCK_GATEWAY_DIN
async def test_dhcp_discovery_updates_unique_id_when_entry_is_failed(hass):
"""Test we can update the unique id from dhcp in a failed state."""
entry = MockConfigEntry(
domain=DOMAIN,
data=VALID_CONFIG,
unique_id="1.2.3.4",
)
entry.add_to_hass(hass)
entry.state = config_entries.ConfigEntryState.SETUP_ERROR
mock_powerwall = await _mock_powerwall_site_name(hass, "Some site")
with patch(
"homeassistant.components.powerwall.config_flow.Powerwall",
return_value=mock_powerwall,
), patch(
"homeassistant.components.powerwall.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=dhcp.DhcpServiceInfo(
ip="1.2.3.4",
macaddress="AA:BB:CC:DD:EE:FF",
hostname=MOCK_GATEWAY_DIN.lower(),
),
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4"
assert entry.unique_id == MOCK_GATEWAY_DIN
async def test_discovered_wifi_does_not_update_ip_if_is_still_online(hass) -> None:
"""Test a discovery does not update the ip unless the powerwall at the old ip is offline."""
entry = MockConfigEntry(
domain=DOMAIN,
data=VALID_CONFIG,
unique_id=MOCK_GATEWAY_DIN,
)
entry.add_to_hass(hass)
mock_powerwall = await _mock_powerwall_with_fixtures(hass)
with patch(
"homeassistant.components.powerwall.config_flow.Powerwall",
return_value=mock_powerwall,
), patch(
"homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_DHCP},
data=dhcp.DhcpServiceInfo(
ip="1.2.3.5",
macaddress="AA:BB:CC:DD:EE:FF",
hostname=MOCK_GATEWAY_DIN.lower(),
),
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4"