This commit is contained in:
Franck Nijhof 2023-12-14 21:44:14 +01:00 committed by GitHub
commit d56b79a993
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 121 additions and 170 deletions

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aemet",
"iot_class": "cloud_polling",
"loggers": ["aemet_opendata"],
"requirements": ["AEMET-OpenData==0.4.6"]
"requirements": ["AEMET-OpenData==0.4.7"]
}

View File

@ -91,7 +91,6 @@ from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.core import HomeAssistant
from homeassistant.helpers.network import is_cloud_connection
from homeassistant.util.network import is_local
from . import indieauth
@ -165,8 +164,6 @@ class AuthProvidersView(HomeAssistantView):
providers = []
for provider in hass.auth.auth_providers:
additional_data = {}
if provider.type == "trusted_networks":
if cloud_connection:
# Skip quickly as trusted networks are not available on cloud
@ -179,30 +176,12 @@ class AuthProvidersView(HomeAssistantView):
except InvalidAuthError:
# Not a trusted network, so we don't expose that trusted_network authenticator is setup
continue
elif (
provider.type == "homeassistant"
and not cloud_connection
and is_local(remote_address)
and "person" in hass.config.components
):
# We are local, return user id and username
users = await provider.store.async_get_users()
additional_data["users"] = {
user.id: credentials.data["username"]
for user in users
for credentials in user.credentials
if (
credentials.auth_provider_type == provider.type
and credentials.auth_provider_id == provider.id
)
}
providers.append(
{
"name": provider.name,
"id": provider.id,
"type": provider.type,
**additional_data,
}
)

View File

@ -70,20 +70,22 @@ async def async_setup_entry(
coordinator = get_coordinator(hass, entry.entry_id)
@callback
def _add_entities() -> None:
def _add_entities(devices: set[str] | None = None) -> None:
"""Add devices."""
if not coordinator.new_devices:
if devices is None:
devices = coordinator.new_devices
if not devices:
return
async_add_entities(
FritzboxBinarySensor(coordinator, ain, description)
for ain in coordinator.new_devices
for ain in devices
for description in BINARY_SENSOR_TYPES
if description.suitable(coordinator.data.devices[ain])
)
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
_add_entities()
_add_entities(set(coordinator.data.devices.keys()))
class FritzboxBinarySensor(FritzBoxDeviceEntity, BinarySensorEntity):

View File

@ -19,17 +19,17 @@ async def async_setup_entry(
coordinator = get_coordinator(hass, entry.entry_id)
@callback
def _add_entities() -> None:
def _add_entities(templates: set[str] | None = None) -> None:
"""Add templates."""
if not coordinator.new_templates:
if templates is None:
templates = coordinator.new_templates
if not templates:
return
async_add_entities(
FritzBoxTemplate(coordinator, ain) for ain in coordinator.new_templates
)
async_add_entities(FritzBoxTemplate(coordinator, ain) for ain in templates)
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
_add_entities()
_add_entities(set(coordinator.data.templates.keys()))
class FritzBoxTemplate(FritzBoxEntity, ButtonEntity):

View File

@ -52,19 +52,21 @@ async def async_setup_entry(
coordinator = get_coordinator(hass, entry.entry_id)
@callback
def _add_entities() -> None:
def _add_entities(devices: set[str] | None = None) -> None:
"""Add devices."""
if not coordinator.new_devices:
if devices is None:
devices = coordinator.new_devices
if not devices:
return
async_add_entities(
FritzboxThermostat(coordinator, ain)
for ain in coordinator.new_devices
for ain in devices
if coordinator.data.devices[ain].has_thermostat
)
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
_add_entities()
_add_entities(set(coordinator.data.devices.keys()))
class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):

View File

@ -24,19 +24,21 @@ async def async_setup_entry(
coordinator = get_coordinator(hass, entry.entry_id)
@callback
def _add_entities() -> None:
def _add_entities(devices: set[str] | None = None) -> None:
"""Add devices."""
if not coordinator.new_devices:
if devices is None:
devices = coordinator.new_devices
if not devices:
return
async_add_entities(
FritzboxCover(coordinator, ain)
for ain in coordinator.new_devices
for ain in devices
if coordinator.data.devices[ain].has_blind
)
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
_add_entities()
_add_entities(set(coordinator.data.devices.keys()))
class FritzboxCover(FritzBoxDeviceEntity, CoverEntity):

View File

@ -30,22 +30,21 @@ async def async_setup_entry(
coordinator = get_coordinator(hass, entry.entry_id)
@callback
def _add_entities() -> None:
def _add_entities(devices: set[str] | None = None) -> None:
"""Add devices."""
if not coordinator.new_devices:
if devices is None:
devices = coordinator.new_devices
if not devices:
return
async_add_entities(
FritzboxLight(
coordinator,
ain,
)
for ain in coordinator.new_devices
if (coordinator.data.devices[ain]).has_lightbulb
FritzboxLight(coordinator, ain)
for ain in devices
if coordinator.data.devices[ain].has_lightbulb
)
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
_add_entities()
_add_entities(set(coordinator.data.devices.keys()))
class FritzboxLight(FritzBoxDeviceEntity, LightEntity):

View File

@ -215,20 +215,22 @@ async def async_setup_entry(
coordinator = get_coordinator(hass, entry.entry_id)
@callback
def _add_entities() -> None:
def _add_entities(devices: set[str] | None = None) -> None:
"""Add devices."""
if not coordinator.new_devices:
if devices is None:
devices = coordinator.new_devices
if not devices:
return
async_add_entities(
FritzBoxSensor(coordinator, ain, description)
for ain in coordinator.new_devices
for ain in devices
for description in SENSOR_TYPES
if description.suitable(coordinator.data.devices[ain])
)
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
_add_entities()
_add_entities(set(coordinator.data.devices.keys()))
class FritzBoxSensor(FritzBoxDeviceEntity, SensorEntity):

View File

@ -19,19 +19,21 @@ async def async_setup_entry(
coordinator = get_coordinator(hass, entry.entry_id)
@callback
def _add_entities() -> None:
def _add_entities(devices: set[str] | None = None) -> None:
"""Add devices."""
if not coordinator.new_devices:
if devices is None:
devices = coordinator.new_devices
if not devices:
return
async_add_entities(
FritzboxSwitch(coordinator, ain)
for ain in coordinator.new_devices
for ain in devices
if coordinator.data.devices[ain].has_switch
)
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
_add_entities()
_add_entities(set(coordinator.data.devices.keys()))
class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity):

View File

@ -74,7 +74,8 @@ class FullyKioskEntity(CoordinatorEntity[FullyKioskDataUpdateCoordinator], Entit
@callback
def message_callback(message: mqtt.ReceiveMessage) -> None:
payload = json.loads(message.payload)
event_callback(**payload)
if "event" in payload and payload["event"] == event:
event_callback(**payload)
topic_template = data["settings"]["mqttEventTopic"]
topic = (
@ -82,4 +83,5 @@ class FullyKioskEntity(CoordinatorEntity[FullyKioskDataUpdateCoordinator], Entit
.replace("$event", event)
.replace("$deviceId", data["deviceID"])
)
return await mqtt.async_subscribe(self.hass, topic, message_callback)

View File

@ -2,7 +2,6 @@
from __future__ import annotations
from http import HTTPStatus
from ipaddress import ip_address
import logging
from typing import Any
@ -51,12 +50,10 @@ from homeassistant.helpers import (
)
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.network import is_cloud_connection
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.storage import Store
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.network import is_local
_LOGGER = logging.getLogger(__name__)
@ -588,33 +585,8 @@ class ListPersonsView(HomeAssistantView):
async def get(self, request: web.Request) -> web.Response:
"""Return a list of persons if request comes from a local IP."""
try:
remote_address = ip_address(request.remote) # type: ignore[arg-type]
except ValueError:
return self.json_message(
message="Invalid remote IP",
status_code=HTTPStatus.BAD_REQUEST,
message_code="invalid_remote_ip",
)
hass: HomeAssistant = request.app["hass"]
if is_cloud_connection(hass) or not is_local(remote_address):
return self.json_message(
message="Not local",
status_code=HTTPStatus.BAD_REQUEST,
message_code="not_local",
)
yaml, storage, _ = hass.data[DOMAIN]
persons = [*yaml.async_items(), *storage.async_items()]
return self.json(
{
person[ATTR_USER_ID]: {
ATTR_NAME: person[ATTR_NAME],
CONF_PICTURE: person.get(CONF_PICTURE),
}
for person in persons
if person.get(ATTR_USER_ID)
}
return self.json_message(
message="Not local",
status_code=HTTPStatus.BAD_REQUEST,
message_code="not_local",
)

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "platinum",
"requirements": ["renault-api==0.2.0"]
"requirements": ["renault-api==0.2.1"]
}

View File

@ -43,13 +43,15 @@ SERVICE_CHARGE_SET_SCHEDULE_SCHEMA = vol.Schema(
{
vol.Required("id"): cv.positive_int,
vol.Optional("activated"): cv.boolean,
vol.Optional("monday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("tuesday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("wednesday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("thursday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("friday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("saturday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("sunday"): vol.Schema(SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("monday"): vol.Any(None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("tuesday"): vol.Any(None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("wednesday"): vol.Any(
None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA
),
vol.Optional("thursday"): vol.Any(None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("friday"): vol.Any(None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("saturday"): vol.Any(None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
vol.Optional("sunday"): vol.Any(None, SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA),
}
)
SERVICE_CHARGE_SET_SCHEDULES_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend(

View File

@ -0,0 +1 @@
reload:

View File

@ -0,0 +1,8 @@
{
"services": {
"reload": {
"name": "[%key:common::action::reload%]",
"description": "Reloads RESTful commands from the YAML-configuration."
}
}
}

View File

@ -260,8 +260,8 @@ class UniFiController:
for entry in async_entries_for_config_entry(
entity_registry, self.config_entry.entry_id
):
if entry.domain == Platform.DEVICE_TRACKER:
macs.append(entry.unique_id.split("-", 1)[0])
if entry.domain == Platform.DEVICE_TRACKER and "-" in entry.unique_id:
macs.append(entry.unique_id.split("-", 1)[1])
for mac in self.option_supported_clients + self.option_block_clients + macs:
if mac not in self.api.clients and mac in self.api.clients_all:

View File

@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["zeroconf"],
"quality_scale": "internal",
"requirements": ["zeroconf==0.128.4"]
"requirements": ["zeroconf==0.128.5"]
}

View File

@ -7,7 +7,7 @@ from typing import Final
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 12
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)

View File

@ -57,7 +57,7 @@ voluptuous-serialize==2.6.0
voluptuous==0.13.1
webrtc-noise-gain==1.2.3
yarl==1.9.2
zeroconf==0.128.4
zeroconf==0.128.5
# Constrain pycryptodome to avoid vulnerability
# see https://github.com/home-assistant/core/pull/16238

View File

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

View File

@ -4,7 +4,7 @@
-r requirements.txt
# homeassistant.components.aemet
AEMET-OpenData==0.4.6
AEMET-OpenData==0.4.7
# homeassistant.components.aladdin_connect
AIOAladdinConnect==0.1.58
@ -2332,7 +2332,7 @@ raspyrfm-client==1.2.8
regenmaschine==2023.06.0
# homeassistant.components.renault
renault-api==0.2.0
renault-api==0.2.1
# homeassistant.components.renson
renson-endura-delta==1.6.0
@ -2810,7 +2810,7 @@ zamg==0.3.3
zengge==0.2
# homeassistant.components.zeroconf
zeroconf==0.128.4
zeroconf==0.128.5
# homeassistant.components.zeversolar
zeversolar==0.3.1

View File

@ -4,7 +4,7 @@
-r requirements_test.txt
# homeassistant.components.aemet
AEMET-OpenData==0.4.6
AEMET-OpenData==0.4.7
# homeassistant.components.aladdin_connect
AIOAladdinConnect==0.1.58
@ -1744,7 +1744,7 @@ rapt-ble==0.1.2
regenmaschine==2023.06.0
# homeassistant.components.renault
renault-api==0.2.0
renault-api==0.2.1
# homeassistant.components.renson
renson-endura-delta==1.6.0
@ -2105,7 +2105,7 @@ yt-dlp==2023.11.16
zamg==0.3.3
# homeassistant.components.zeroconf
zeroconf==0.128.4
zeroconf==0.128.5
# homeassistant.components.zeversolar
zeversolar==0.3.1

View File

@ -1,12 +1,10 @@
"""Tests for the login flow."""
from collections.abc import Callable
from http import HTTPStatus
from typing import Any
from unittest.mock import patch
import pytest
from homeassistant.auth.models import User
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@ -67,22 +65,16 @@ async def _test_fetch_auth_providers_home_assistant(
hass: HomeAssistant,
aiohttp_client: ClientSessionGenerator,
ip: str,
additional_expected_fn: Callable[[User], dict[str, Any]],
) -> None:
"""Test fetching auth providers for homeassistant auth provider."""
client = await async_setup_auth(
hass, aiohttp_client, [{"type": "homeassistant"}], custom_ip=ip
)
provider = hass.auth.auth_providers[0]
credentials = await provider.async_get_or_create_credentials({"username": "hello"})
user = await hass.auth.async_get_or_create_user(credentials)
expected = {
"name": "Home Assistant Local",
"type": "homeassistant",
"id": None,
**additional_expected_fn(user),
}
resp = await client.get("/auth/providers")
@ -105,9 +97,7 @@ async def test_fetch_auth_providers_home_assistant_person_not_loaded(
ip: str,
) -> None:
"""Test fetching auth providers for homeassistant auth provider, where person integration is not loaded."""
await _test_fetch_auth_providers_home_assistant(
hass, aiohttp_client, ip, lambda _: {}
)
await _test_fetch_auth_providers_home_assistant(hass, aiohttp_client, ip)
@pytest.mark.parametrize(
@ -134,7 +124,6 @@ async def test_fetch_auth_providers_home_assistant_person_loaded(
hass,
aiohttp_client,
ip,
lambda user: {"users": {user.id: user.name}} if is_local else {},
)

View File

@ -107,19 +107,35 @@ async def test_switches_mqtt_update(
assert entity
assert entity.state == "on"
async_fire_mqtt_message(hass, "fully/event/onScreensaverStart/abcdef-123456", "{}")
async_fire_mqtt_message(
hass,
"fully/event/onScreensaverStart/abcdef-123456",
'{"deviceId": "abcdef-123456","event": "onScreensaverStart"}',
)
entity = hass.states.get("switch.amazon_fire_screensaver")
assert entity.state == "on"
async_fire_mqtt_message(hass, "fully/event/onScreensaverStop/abcdef-123456", "{}")
async_fire_mqtt_message(
hass,
"fully/event/onScreensaverStop/abcdef-123456",
'{"deviceId": "abcdef-123456","event": "onScreensaverStop"}',
)
entity = hass.states.get("switch.amazon_fire_screensaver")
assert entity.state == "off"
async_fire_mqtt_message(hass, "fully/event/screenOff/abcdef-123456", "{}")
async_fire_mqtt_message(
hass,
"fully/event/screenOff/abcdef-123456",
'{"deviceId": "abcdef-123456","event": "screenOff"}',
)
entity = hass.states.get("switch.amazon_fire_screen")
assert entity.state == "off"
async_fire_mqtt_message(hass, "fully/event/screenOn/abcdef-123456", "{}")
async_fire_mqtt_message(
hass,
"fully/event/screenOn/abcdef-123456",
'{"deviceId": "abcdef-123456","event": "screenOn"}',
)
entity = hass.states.get("switch.amazon_fire_screen")
assert entity.state == "on"

View File

@ -1,5 +1,4 @@
"""The tests for the person component."""
from collections.abc import Callable
from http import HTTPStatus
from typing import Any
from unittest.mock import patch
@ -31,7 +30,6 @@ from homeassistant.setup import async_setup_component
from .conftest import DEVICE_TRACKER, DEVICE_TRACKER_2
from tests.common import MockUser, mock_component, mock_restore_cache
from tests.test_util import mock_real_ip
from tests.typing import ClientSessionGenerator, WebSocketGenerator
@ -852,42 +850,10 @@ async def test_entities_in_person(hass: HomeAssistant) -> None:
]
@pytest.mark.parametrize(
("ip", "status_code", "expected_fn"),
[
(
"192.168.0.10",
HTTPStatus.OK,
lambda user: {
user["user_id"]: {"name": user["name"], "picture": user["picture"]}
},
),
(
"::ffff:192.168.0.10",
HTTPStatus.OK,
lambda user: {
user["user_id"]: {"name": user["name"], "picture": user["picture"]}
},
),
(
"1.2.3.4",
HTTPStatus.BAD_REQUEST,
lambda _: {"code": "not_local", "message": "Not local"},
),
(
"2001:db8::1",
HTTPStatus.BAD_REQUEST,
lambda _: {"code": "not_local", "message": "Not local"},
),
],
)
async def test_list_persons(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
hass_admin_user: MockUser,
ip: str,
status_code: HTTPStatus,
expected_fn: Callable[[dict[str, Any]], dict[str, Any]],
) -> None:
"""Test listing persons from a not local ip address."""
@ -902,11 +868,10 @@ async def test_list_persons(
assert await async_setup_component(hass, DOMAIN, config)
await async_setup_component(hass, "api", {})
mock_real_ip(hass.http.app)(ip)
client = await hass_client_no_auth()
resp = await client.get("/api/person/list")
assert resp.status == status_code
assert resp.status == HTTPStatus.BAD_REQUEST
result = await resp.json()
assert result == expected_fn(admin)
assert result == {"code": "not_local", "message": "Not local"}

View File

@ -203,13 +203,12 @@ async def test_service_set_charge_schedule_multi(
{
"id": 2,
"activated": True,
"monday": {"startTime": "T12:00Z", "duration": 15},
"tuesday": {"startTime": "T12:00Z", "duration": 15},
"wednesday": {"startTime": "T12:00Z", "duration": 15},
"thursday": {"startTime": "T12:00Z", "duration": 15},
"friday": {"startTime": "T12:00Z", "duration": 15},
"saturday": {"startTime": "T12:00Z", "duration": 15},
"sunday": {"startTime": "T12:00Z", "duration": 15},
"monday": {"startTime": "T12:00Z", "duration": 30},
"tuesday": {"startTime": "T12:00Z", "duration": 30},
"wednesday": None,
"friday": {"startTime": "T12:00Z", "duration": 30},
"saturday": {"startTime": "T12:00Z", "duration": 30},
"sunday": {"startTime": "T12:00Z", "duration": 30},
},
{"id": 3},
]
@ -238,6 +237,15 @@ async def test_service_set_charge_schedule_multi(
mock_call_data: list[ChargeSchedule] = mock_action.mock_calls[0][1][0]
assert mock_action.mock_calls[0][1] == (mock_call_data,)
# Monday updated with new values
assert mock_call_data[1].monday.startTime == "T12:00Z"
assert mock_call_data[1].monday.duration == 30
# Wednesday has original values cleared
assert mock_call_data[1].wednesday is None
# Thursday keeps original values
assert mock_call_data[1].thursday.startTime == "T23:30Z"
assert mock_call_data[1].thursday.duration == 15
async def test_service_invalid_device_id(
hass: HomeAssistant, config_entry: ConfigEntry