Compare commits

..

6 Commits

Author SHA1 Message Date
Abílio Costa
512ff4b5dc Merge branch 'dev' into enable_duplicated_log_file 2025-12-16 18:03:32 +00:00
abmantis
fadc252661 Move both check and rename to executor 2025-12-16 13:01:52 +00:00
abmantis
b5142754fc Remove log; move io out of the event loop 2025-12-15 17:10:46 +00:00
Abílio Costa
f88129ed11 Apply suggestion from @abmantis 2025-12-11 11:47:12 +00:00
Abílio Costa
5b000d02db Merge branch 'dev' into enable_duplicated_log_file 2025-12-11 11:46:32 +00:00
abmantis
1578bb1dfc Enable log file on supervised when HA_DUPLICATE_LOG_FILE env var is set 2025-12-10 18:38:26 +00:00
11 changed files with 25 additions and 243 deletions

View File

@@ -624,13 +624,16 @@ async def async_enable_logging(
if log_file is None:
default_log_path = hass.config.path(ERROR_LOG_FILENAME)
if "SUPERVISOR" in os.environ:
_LOGGER.info("Running in Supervisor, not logging to file")
if "SUPERVISOR" in os.environ and "HA_DUPLICATE_LOG_FILE" not in os.environ:
# Rename the default log file if it exists, since previous versions created
# it even on Supervisor
if os.path.isfile(default_log_path):
with contextlib.suppress(OSError):
os.rename(default_log_path, f"{default_log_path}.old")
def rename_old_file() -> None:
"""Rename old log file in executor."""
if os.path.isfile(default_log_path):
with contextlib.suppress(OSError):
os.rename(default_log_path, f"{default_log_path}.old")
await hass.async_add_executor_job(rename_old_file)
err_log_path = None
else:
err_log_path = default_log_path

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/mill",
"iot_class": "local_polling",
"loggers": ["mill", "mill_local"],
"requirements": ["millheater==0.14.1", "mill-local==0.5.0"]
"requirements": ["millheater==0.14.1", "mill-local==0.3.0"]
}

View File

@@ -24,7 +24,6 @@ _PLATFORMS: list[Platform] = [
Platform.TIME,
Platform.SWITCH,
Platform.NUMBER,
Platform.SELECT,
]
PLATFORM_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)

View File

@@ -1,95 +0,0 @@
"""Nintendo Switch Parental Controls select entity definitions."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from enum import StrEnum
from typing import Any
from pynintendoparental.enum import DeviceTimerMode
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import NintendoParentalControlsConfigEntry, NintendoUpdateCoordinator
from .entity import Device, NintendoDevice
PARALLEL_UPDATES = 1
class NintendoParentalSelect(StrEnum):
"""Store keys for Nintendo Parental Controls select entities."""
TIMER_MODE = "timer_mode"
@dataclass(kw_only=True, frozen=True)
class NintendoParentalControlsSelectEntityDescription(SelectEntityDescription):
"""Description for Nintendo Parental Controls select entities."""
get_option: Callable[[Device], DeviceTimerMode | None]
set_option_fn: Callable[[Device, DeviceTimerMode], Coroutine[Any, Any, None]]
options_enum: type[DeviceTimerMode]
SELECT_DESCRIPTIONS: tuple[NintendoParentalControlsSelectEntityDescription, ...] = (
NintendoParentalControlsSelectEntityDescription(
key=NintendoParentalSelect.TIMER_MODE,
translation_key=NintendoParentalSelect.TIMER_MODE,
get_option=lambda device: device.timer_mode,
set_option_fn=lambda device, option: device.set_timer_mode(option),
options_enum=DeviceTimerMode,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: NintendoParentalControlsConfigEntry,
async_add_devices: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the select platform."""
async_add_devices(
NintendoParentalSelectEntity(
coordinator=entry.runtime_data,
device=device,
description=description,
)
for device in entry.runtime_data.api.devices.values()
for description in SELECT_DESCRIPTIONS
)
class NintendoParentalSelectEntity(NintendoDevice, SelectEntity):
"""Nintendo Parental Controls select entity."""
entity_description: NintendoParentalControlsSelectEntityDescription
def __init__(
self,
coordinator: NintendoUpdateCoordinator,
device: Device,
description: NintendoParentalControlsSelectEntityDescription,
) -> None:
"""Initialize the select entity."""
super().__init__(coordinator=coordinator, device=device, key=description.key)
self.entity_description = description
@property
def current_option(self) -> str | None:
"""Return the current selected option."""
option = self.entity_description.get_option(self._device)
return option.name.lower() if option else None
@property
def options(self) -> list[str]:
"""Return a list of available options."""
return [option.name.lower() for option in self.entity_description.options_enum]
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
enum_option = self.entity_description.options_enum[option.upper()]
await self.entity_description.set_option_fn(self._device, enum_option)
await self.coordinator.async_request_refresh()

View File

@@ -37,15 +37,6 @@
"name": "Max screentime today"
}
},
"select": {
"timer_mode": {
"name": "Restriction mode",
"state": {
"daily": "Same for all days",
"each_day_of_the_week": "Different for each day"
}
}
},
"sensor": {
"playing_time": {
"name": "Used screen time"

2
requirements_all.txt generated
View File

@@ -1482,7 +1482,7 @@ micloud==0.5
microBeesPy==0.3.5
# homeassistant.components.mill
mill-local==0.5.0
mill-local==0.3.0
# homeassistant.components.mill
millheater==0.14.1

View File

@@ -1289,7 +1289,7 @@ micloud==0.5
microBeesPy==0.3.5
# homeassistant.components.mill
mill-local==0.5.0
mill-local==0.3.0
# homeassistant.components.mill
millheater==0.14.1

View File

@@ -6,7 +6,6 @@ from unittest.mock import AsyncMock, MagicMock, patch
from pynintendoparental import NintendoParental
from pynintendoparental.device import Device
from pynintendoparental.enum import DeviceTimerMode
import pytest
from homeassistant.components.nintendo_parental_controls.const import DOMAIN
@@ -40,11 +39,9 @@ def mock_nintendo_device() -> Device:
mock.today_playing_time = 110
mock.today_time_remaining = 10
mock.bedtime_alarm = time(hour=19)
mock.timer_mode = DeviceTimerMode.DAILY
mock.add_extra_time.return_value = None
mock.set_bedtime_alarm.return_value = None
mock.update_max_daily_playtime.return_value = None
mock.set_timer_mode.return_value = None
mock.forced_termination_mode = True
mock.model = "Test Model"
mock.generation = "P00"

View File

@@ -1,58 +0,0 @@
# serializer version: 1
# name: test_select[select.home_assistant_test_restriction_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'daily',
'each_day_of_the_week',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': None,
'entity_id': 'select.home_assistant_test_restriction_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Restriction mode',
'platform': 'nintendo_parental_controls',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': <NintendoParentalSelect.TIMER_MODE: 'timer_mode'>,
'unique_id': 'testdevid_timer_mode',
'unit_of_measurement': None,
})
# ---
# name: test_select[select.home_assistant_test_restriction_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Home Assistant Test Restriction mode',
'options': list([
'daily',
'each_day_of_the_week',
]),
}),
'context': <ANY>,
'entity_id': 'select.home_assistant_test_restriction_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'daily',
})
# ---

View File

@@ -1,64 +0,0 @@
"""Tests for Nintendo Switch Parental Controls select platform."""
from unittest.mock import AsyncMock, patch
from pynintendoparental.enum import DeviceTimerMode
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.select import (
ATTR_OPTION,
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_select(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_nintendo_client: AsyncMock,
mock_nintendo_device: AsyncMock,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test select platform."""
with patch(
"homeassistant.components.nintendo_parental_controls._PLATFORMS",
[Platform.SELECT],
):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_select_option(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_nintendo_client: AsyncMock,
mock_nintendo_device: AsyncMock,
) -> None:
"""Test select option service."""
with patch(
"homeassistant.components.nintendo_parental_controls._PLATFORMS",
[Platform.SELECT],
):
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.home_assistant_test_restriction_mode",
ATTR_OPTION: DeviceTimerMode.EACH_DAY_OF_THE_WEEK.name.lower(),
},
blocking=True,
)
mock_nintendo_device.set_timer_mode.assert_awaited_once_with(
DeviceTimerMode.EACH_DAY_OF_THE_WEEK
)

View File

@@ -130,8 +130,16 @@ async def test_async_enable_logging(
cleanup_log_files()
@pytest.mark.parametrize(
("extra_env", "log_file_count", "old_log_file_count"),
[({}, 0, 1), ({"HA_DUPLICATE_LOG_FILE": "1"}, 1, 0)],
)
async def test_async_enable_logging_supervisor(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
extra_env: dict[str, str],
log_file_count: int,
old_log_file_count: int,
) -> None:
"""Test to ensure the default log file is not created on Supervisor installations."""
@@ -141,14 +149,14 @@ async def test_async_enable_logging_supervisor(
assert len(glob.glob(ARG_LOG_FILE)) == 0
with (
patch.dict(os.environ, {"SUPERVISOR": "1"}),
patch.dict(os.environ, {"SUPERVISOR": "1", **extra_env}),
patch(
"homeassistant.bootstrap.async_activate_log_queue_handler"
) as mock_async_activate_log_queue_handler,
patch("logging.getLogger"),
):
await bootstrap.async_enable_logging(hass)
assert len(glob.glob(CONFIG_LOG_FILE)) == 0
assert len(glob.glob(CONFIG_LOG_FILE)) == log_file_count
mock_async_activate_log_queue_handler.assert_called_once()
mock_async_activate_log_queue_handler.reset_mock()
@@ -162,9 +170,10 @@ async def test_async_enable_logging_supervisor(
await hass.async_add_executor_job(write_log_file)
assert len(glob.glob(CONFIG_LOG_FILE)) == 1
assert len(glob.glob(f"{CONFIG_LOG_FILE}.old")) == 0
await bootstrap.async_enable_logging(hass)
assert len(glob.glob(CONFIG_LOG_FILE)) == 0
assert len(glob.glob(f"{CONFIG_LOG_FILE}.old")) == 1
assert len(glob.glob(CONFIG_LOG_FILE)) == log_file_count
assert len(glob.glob(f"{CONFIG_LOG_FILE}.old")) == old_log_file_count
mock_async_activate_log_queue_handler.assert_called_once()
mock_async_activate_log_queue_handler.reset_mock()