diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b98f767d..b217366a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,15 +15,15 @@ repos: language: system types: [python] require_serial: true - - id: mypy - name: mypy - entry: poetry run mypy + - id: ruff-check + name: ruff check + entry: poetry run ruff check --fix language: system types: [python] require_serial: true - - id: flake8 - name: flake8 - entry: poetry run flake8 + - id: mypy + name: mypy + entry: poetry run mypy language: system types: [python] require_serial: true diff --git a/custom_components/google_home/__init__.py b/custom_components/google_home/__init__.py index c3e7a7b9..215fb1bf 100644 --- a/custom_components/google_home/__init__.py +++ b/custom_components/google_home/__init__.py @@ -1,5 +1,4 @@ -""" -Custom integration to integrate Google Home with Home Assistant. +"""Custom integration to integrate Google Home with Home Assistant. For more details about this integration, please refer to https://github.com/leikoilja/ha-google-home @@ -7,7 +6,7 @@ from datetime import timedelta import logging -from typing import cast +from typing import TYPE_CHECKING, cast from homeassistant.components import zeroconf from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -28,9 +27,11 @@ STARTUP_MESSAGE, UPDATE_INTERVAL, ) -from .models import GoogleHomeDevice from .types import GoogleHomeConfigEntry +if TYPE_CHECKING: + from .models import GoogleHomeDevice + _LOGGER: logging.Logger = logging.getLogger(__package__) diff --git a/custom_components/google_home/api.py b/custom_components/google_home/api.py index 78fe3826..a5818498 100644 --- a/custom_components/google_home/api.py +++ b/custom_components/google_home/api.py @@ -6,15 +6,12 @@ from http import HTTPStatus import ipaddress import logging -from typing import Literal, cast +from typing import TYPE_CHECKING, Literal, cast from aiohttp import ClientError, ClientSession, ClientTimeout from aiohttp.client_exceptions import ClientConnectorError, ContentTypeError from glocaltokens.client import Device, GLocalAuthenticationTokens from glocaltokens.utils.token import is_aas_et -from zeroconf import Zeroconf - -from homeassistant.core import HomeAssistant from .const import ( API_ENDPOINT_ALARM_DELETE, @@ -35,11 +32,16 @@ from .models import GoogleHomeDevice from .types import AlarmJsonDict, JsonDict, TimerJsonDict +if TYPE_CHECKING: + from zeroconf import Zeroconf + + from homeassistant.core import HomeAssistant + _LOGGER: logging.Logger = logging.getLogger(__package__) class GlocaltokensApiClient: - """API client""" + """API client.""" def __init__( self, @@ -69,7 +71,7 @@ def __init__( self.zeroconf_instance = zeroconf_instance async def async_get_master_token(self) -> str: - """Get master API token""" + """Get master API token.""" def _get_master_token() -> str | None: return self._client.get_master_token() @@ -80,7 +82,7 @@ def _get_master_token() -> str | None: return master_token async def async_get_access_token(self) -> str: - """Get access token using master token""" + """Get access token using master token.""" def _get_access_token() -> str | None: return self._client.get_access_token() @@ -92,7 +94,9 @@ def _get_access_token() -> str | None: async def get_google_devices(self) -> list[GoogleHomeDevice]: """Get google device authentication tokens. - Note this method will fetch necessary access tokens if missing""" + + Note this method will fetch necessary access tokens if missing. + """ if not self.google_devices: @@ -116,7 +120,7 @@ def _get_google_devices() -> list[Device]: return self.google_devices async def get_android_id(self) -> str: - """Generate random android_id""" + """Generate random android_id.""" def _get_android_id() -> str: return self._client.get_android_id() @@ -125,15 +129,16 @@ def _get_android_id() -> str: @staticmethod def create_url(ip_address: str, port: int, api_endpoint: str) -> str: - """Creates url to endpoint. - Note: port argument is unused because all request must be done to 8443""" + """Create url to endpoint. + + Note: port argument is unused because all request must be done to 8443. + """ if isinstance(ipaddress.ip_address(ip_address), ipaddress.IPv6Address): ip_address = f"[{ip_address}]" return f"https://{ip_address}:{port}/{api_endpoint}" async def update_google_devices_information(self) -> list[GoogleHomeDevice]: - """Retrieves devices from glocaltokens and - fetches alarm/timer data from each of the device""" + """Retrieve devices from glocaltokens and fetches alarm/timer data from each of the device.""" devices = await self.get_google_devices() @@ -151,14 +156,13 @@ async def update_google_devices_information(self) -> list[GoogleHomeDevice]: device.name, ) - coordinator_data = await asyncio.gather( + return await asyncio.gather( *[ self.collect_data_from_endpoints(device) for device in devices if device.ip_address and device.auth_token ] ) - return coordinator_data async def collect_data_from_endpoints( self, device: GoogleHomeDevice @@ -166,13 +170,12 @@ async def collect_data_from_endpoints( """Collect data from different endpoints.""" device = await self.update_alarms_and_timers(device) device = await self.update_alarm_volume(device) - device = await self.update_do_not_disturb(device) - return device + return await self.update_do_not_disturb(device) async def update_alarms_and_timers( self, device: GoogleHomeDevice ) -> GoogleHomeDevice: - """Fetches timers and alarms from google device""" + """Fetch timers and alarms from google device.""" response = await self.request( method="GET", endpoint=API_ENDPOINT_ALARMS, device=device, polling=True ) @@ -201,8 +204,10 @@ async def update_alarms_and_timers( async def delete_alarm_or_timer( self, device: GoogleHomeDevice, item_to_delete: str ) -> None: - """Deletes a timer or alarm. - Can also delete multiple if a list is provided (Not implemented yet).""" + """Delete a timer or alarm. + + Can also delete multiple if a list is provided (Not implemented yet). + """ data = {"ids": [item_to_delete]} @@ -247,7 +252,7 @@ async def delete_alarm_or_timer( ) async def reboot_google_device(self, device: GoogleHomeDevice) -> None: - """Reboots a Google Home device if it supports this.""" + """Reboot a Google Home device if it supports this.""" # "now" means reboot and "fdr" means factory reset (Not implemented). data = {"params": "now"} @@ -271,7 +276,7 @@ async def reboot_google_device(self, device: GoogleHomeDevice) -> None: async def update_do_not_disturb( self, device: GoogleHomeDevice, enable: bool | None = None ) -> GoogleHomeDevice: - """Gets or sets the do not disturb setting on a Google Home device.""" + """Get or set the do not disturb setting on a Google Home device.""" data = None polling = False @@ -324,7 +329,7 @@ async def update_do_not_disturb( async def update_alarm_volume( self, device: GoogleHomeDevice, volume: int | None = None ) -> GoogleHomeDevice: - """Gets or sets the alarm volume setting on a Google Home device.""" + """Get or set the alarm volume setting on a Google Home device.""" data: JsonDict | None = None polling = False @@ -394,7 +399,7 @@ async def request( data: JsonDict | None = None, polling: bool = False, ) -> JsonDict | None: - """Shared request method""" + """Shared request method.""" if device.ip_address is None: _LOGGER.warning("Device %s doesn't have an IP address!", device.name) @@ -479,15 +484,14 @@ async def request( device.name, ) device.available = False - except ClientError as ex: + except ClientError: # Make sure that we log the exception from the client if one occurred. - _LOGGER.error( - "Request from %s device error: %s", + _LOGGER.exception( + "Request from %s device error", device.name, - ex, ) device.available = False - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.debug( "%s device timed out while performing a request to it - Raw data: %s", device.name, diff --git a/custom_components/google_home/config_flow.py b/custom_components/google_home/config_flow.py index daa9bde4..75e5a065 100644 --- a/custom_components/google_home/config_flow.py +++ b/custom_components/google_home/config_flow.py @@ -1,10 +1,10 @@ -"""Adds config flow for Google Home""" +"""Adds config flow for Google Home.""" from __future__ import annotations from datetime import timedelta import logging -from typing import Self +from typing import TYPE_CHECKING, Self from requests.exceptions import RequestException import voluptuous as vol @@ -27,7 +27,9 @@ UPDATE_INTERVAL, ) from .exceptions import InvalidMasterToken -from .types import ConfigFlowDict, GoogleHomeConfigEntry, OptionsFlowDict + +if TYPE_CHECKING: + from .types import ConfigFlowDict, GoogleHomeConfigEntry, OptionsFlowDict _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -81,20 +83,19 @@ async def async_step_user( title = f"{MANUFACTURER} (master_token)" else: self._errors["base"] = "master-token-invalid" + # master_token not provided, so use username/password authentication + elif len(password) < MAX_PASSWORD_LENGTH: + client = GlocaltokensApiClient( + hass=self.hass, + session=session, + username=username, + password=password, + ) + master_token = await self._get_master_token(client) + if not master_token: + self._errors["base"] = "auth" else: - # master_token not provided, so use username/password authentication - if len(password) < MAX_PASSWORD_LENGTH: - client = GlocaltokensApiClient( - hass=self.hass, - session=session, - username=username, - password=password, - ) - master_token = await self._get_master_token(client) - if not master_token: - self._errors["base"] = "auth" - else: - self._errors["base"] = "pass-len" + self._errors["base"] = "pass-len" if client and not self._errors: config_data: dict[str, str] = {} @@ -112,6 +113,7 @@ async def async_step_user( def async_get_options_flow( config_entry: GoogleHomeConfigEntry, ) -> GoogleHomeOptionsFlowHandler: + """Handle options flow.""" return GoogleHomeOptionsFlowHandler(config_entry) async def _show_config_form(self) -> ConfigFlowResult: @@ -130,22 +132,22 @@ async def _show_config_form(self) -> ConfigFlowResult: @staticmethod async def _get_master_token(client: GlocaltokensApiClient) -> str: - """Returns master token if credentials are valid.""" + """Return master token if credentials are valid.""" master_token = "" try: master_token = await client.async_get_master_token() - except (InvalidMasterToken, RequestException) as exception: - _LOGGER.error(exception) + except (InvalidMasterToken, RequestException): + _LOGGER.exception("Failed to get master token") return master_token @staticmethod async def _get_access_token(client: GlocaltokensApiClient) -> str: - """Returns access token if master token is valid.""" + """Return access token if master token is valid.""" access_token = "" try: access_token = await client.async_get_access_token() - except (InvalidMasterToken, RequestException) as exception: - _LOGGER.error(exception) + except (InvalidMasterToken, RequestException): + _LOGGER.exception("Failed to get access token") return access_token diff --git a/custom_components/google_home/entity.py b/custom_components/google_home/entity.py index 6f6e8bf9..2bdb56d9 100644 --- a/custom_components/google_home/entity.py +++ b/custom_components/google_home/entity.py @@ -1,24 +1,28 @@ -"""Defines base entities for Google Home""" +"""Defines base entities for Google Home.""" from __future__ import annotations from abc import ABC, abstractmethod +from typing import TYPE_CHECKING -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from .api import GlocaltokensApiClient from .const import DEFAULT_NAME, DOMAIN, MANUFACTURER from .models import GoogleHomeDevice +if TYPE_CHECKING: + from homeassistant.helpers.device_registry import DeviceInfo + + from .api import GlocaltokensApiClient + class GoogleHomeBaseEntity( CoordinatorEntity[DataUpdateCoordinator[list[GoogleHomeDevice]]], ABC ): - """Base entity base for Google Home sensors""" + """Base entity base for Google Home sensors.""" def __init__( self, @@ -28,6 +32,7 @@ def __init__( device_name: str, device_model: str, ): + """Create Google Home base entity.""" super().__init__(coordinator) self.client = client self.device_id = device_id @@ -51,6 +56,7 @@ def unique_id(self) -> str: # type: ignore[override] @property def device_info(self) -> DeviceInfo | None: # type: ignore[override] + """Return device info.""" return { "identifiers": {(DOMAIN, self.device_id)}, "name": f"{DEFAULT_NAME} {self.device_name}", @@ -59,8 +65,7 @@ def device_info(self) -> DeviceInfo | None: # type: ignore[override] } def get_device(self) -> GoogleHomeDevice | None: - """Return the device matched by device name - from the list of google devices in coordinator_data""" + """Return the device matched by device name from the list of google devices in coordinator_data.""" matched_devices: list[GoogleHomeDevice] = [ device for device in self.coordinator.data diff --git a/custom_components/google_home/models.py b/custom_components/google_home/models.py index 18c54541..aab60f65 100644 --- a/custom_components/google_home/models.py +++ b/custom_components/google_home/models.py @@ -1,29 +1,32 @@ -"""Models for Google Home""" +"""Models for Google Home.""" from __future__ import annotations from datetime import timedelta from enum import Enum import sys +from typing import TYPE_CHECKING from homeassistant.util.dt import as_local, utc_from_timestamp from .const import DATETIME_STR_FORMAT, GOOGLE_HOME_ALARM_DEFAULT_VALUE -from .types import ( - AlarmJsonDict, - GoogleHomeAlarmDict, - GoogleHomeTimerDict, - TimerJsonDict, -) + +if TYPE_CHECKING: + from .types import ( + AlarmJsonDict, + GoogleHomeAlarmDict, + GoogleHomeTimerDict, + TimerJsonDict, + ) def convert_from_ms_to_s(timestamp: int) -> int: - """Converts from milliseconds to seconds""" + """Convert from milliseconds to seconds.""" return round(timestamp / 1000) class GoogleHomeDevice: - """Local representation of Google Home device""" + """Local representation of Google Home device.""" def __init__( self, @@ -33,6 +36,7 @@ def __init__( ip_address: str | None = None, hardware: str | None = None, ): + """Create Google Home device object.""" self.device_id = device_id self.name = name self.auth_token = auth_token @@ -45,7 +49,7 @@ def __init__( self._alarms: list[GoogleHomeAlarm] = [] def set_alarms(self, alarms: list[AlarmJsonDict]) -> None: - """Stores alarms as GoogleHomeAlarm objects""" + """Store alarms as GoogleHomeAlarm objects.""" self._alarms = [ GoogleHomeAlarm( alarm_id=alarm["id"], @@ -58,7 +62,7 @@ def set_alarms(self, alarms: list[AlarmJsonDict]) -> None: ] def set_timers(self, timers: list[TimerJsonDict]) -> None: - """Stores timers as GoogleHomeTimer objects""" + """Store timers as GoogleHomeTimer objects.""" self._timers = [ GoogleHomeTimer( timer_id=timer["id"], @@ -71,7 +75,7 @@ def set_timers(self, timers: list[TimerJsonDict]) -> None: ] def get_sorted_alarms(self) -> list[GoogleHomeAlarm]: - """Returns alarms in a sorted order. Inactive & missed alarms are at the end.""" + """Return alarms in a sorted order. Inactive & missed alarms are at the end.""" return sorted( self._alarms, key=lambda k: ( @@ -83,19 +87,19 @@ def get_sorted_alarms(self) -> list[GoogleHomeAlarm]: ) def get_next_alarm(self) -> GoogleHomeAlarm | None: - """Returns next alarm""" + """Return next alarm.""" alarms = self.get_sorted_alarms() return alarms[0] if alarms else None def get_sorted_timers(self) -> list[GoogleHomeTimer]: - """Returns timers in a sorted order. If timer is paused, put it in the end.""" + """Return timers in a sorted order. If timer is paused, put it in the end.""" return sorted( self._timers, key=lambda k: k.fire_time if k.fire_time is not None else sys.maxsize, ) def get_next_timer(self) -> GoogleHomeTimer | None: - """Returns next alarm""" + """Return next alarm.""" timers = self.get_sorted_timers() return timers[0] if timers else None @@ -117,7 +121,7 @@ def get_alarm_volume(self) -> int: class GoogleHomeTimer: - """Local representation of Google Home timer""" + """Local representation of Google Home timer.""" def __init__( self, @@ -127,6 +131,7 @@ def __init__( status: int, label: str | None, ) -> None: + """Create Google Home Timer object.""" self.timer_id = timer_id self.duration = str(timedelta(seconds=convert_from_ms_to_s(duration))) self.status = GoogleHomeTimerStatus(status) @@ -157,7 +162,7 @@ def as_dict(self) -> GoogleHomeTimerDict: class GoogleHomeAlarm: - """Local representation of Google Home alarm""" + """Local representation of Google Home alarm.""" def __init__( self, @@ -167,6 +172,7 @@ def __init__( label: str | None, recurrence: str | None, ) -> None: + """Create Google Home Alarm object.""" self.alarm_id = alarm_id self.recurrence = recurrence self.fire_time = convert_from_ms_to_s(fire_time) @@ -192,7 +198,7 @@ def as_dict(self) -> GoogleHomeAlarmDict: class GoogleHomeAlarmStatus(Enum): - """Definition of Google Home alarm status""" + """Definition of Google Home alarm status.""" NONE = 0 SET = 1 @@ -203,7 +209,7 @@ class GoogleHomeAlarmStatus(Enum): class GoogleHomeTimerStatus(Enum): - """Definition of Google Home timer status""" + """Definition of Google Home timer status.""" NONE = 0 SET = 1 diff --git a/custom_components/google_home/number.py b/custom_components/google_home/number.py index 101344bd..a166b867 100644 --- a/custom_components/google_home/number.py +++ b/custom_components/google_home/number.py @@ -1,17 +1,14 @@ -"""Number Platform for Google Home""" +"""Number Platform for Google Home.""" from __future__ import annotations import logging +from typing import TYPE_CHECKING from homeassistant.components.number import NumberEntity from homeassistant.const import PERCENTAGE -from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .api import GlocaltokensApiClient from .const import ( DATA_CLIENT, DATA_COORDINATOR, @@ -24,8 +21,15 @@ LABEL_ALARM_VOLUME, ) from .entity import GoogleHomeBaseEntity -from .models import GoogleHomeDevice -from .types import GoogleHomeConfigEntry + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + + from .api import GlocaltokensApiClient + from .models import GoogleHomeDevice + from .types import GoogleHomeConfigEntry _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -35,20 +39,19 @@ async def async_setup_entry( entry: GoogleHomeConfigEntry, async_add_devices: AddEntitiesCallback, ) -> bool: - """Setup switch platform.""" + """Set up switch platform.""" client: GlocaltokensApiClient = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] coordinator: DataUpdateCoordinator[list[GoogleHomeDevice]] = hass.data[DOMAIN][ entry.entry_id ][DATA_COORDINATOR] - numbers: list[NumberEntity] = [] - for device in coordinator.data: - if device.auth_token and device.available: - numbers.append( - AlarmVolumeNumber( - coordinator, client, device.device_id, device.name, device.hardware - ) - ) + numbers = [ + AlarmVolumeNumber( + coordinator, client, device.device_id, device.name, device.hardware + ) + for device in coordinator.data + if device.auth_token and device.available + ] if numbers: async_add_devices(numbers) @@ -93,11 +96,10 @@ def native_value(self) -> float: # type: ignore[override] if device is None: return GOOGLE_HOME_ALARM_DEFAULT_VALUE - volume = device.get_alarm_volume() - return volume + return device.get_alarm_volume() async def async_set_native_value(self, value: float) -> None: - """Sets the alarm volume""" + """Set the alarm volume.""" device = self.get_device() if device is None: _LOGGER.error("Device %s not found.", self.device_name) diff --git a/custom_components/google_home/sensor.py b/custom_components/google_home/sensor.py index cc639e84..2e8547c1 100644 --- a/custom_components/google_home/sensor.py +++ b/custom_components/google_home/sensor.py @@ -1,17 +1,16 @@ -"""Sensor platform for Google Home""" +"""Sensor platform for Google Home.""" from __future__ import annotations import logging +from typing import TYPE_CHECKING import voluptuous as vol from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import STATE_UNAVAILABLE -from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import Entity, EntityCategory -from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ALARM_AND_TIMER_ID_LENGTH, @@ -35,14 +34,19 @@ ) from .entity import GoogleHomeBaseEntity from .models import GoogleHomeAlarmStatus, GoogleHomeDevice, GoogleHomeTimerStatus -from .types import ( - AlarmsAttributes, - DeviceAttributes, - GoogleHomeAlarmDict, - GoogleHomeConfigEntry, - GoogleHomeTimerDict, - TimersAttributes, -) + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant, ServiceCall + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + from .types import ( + AlarmsAttributes, + DeviceAttributes, + GoogleHomeAlarmDict, + GoogleHomeConfigEntry, + GoogleHomeTimerDict, + TimersAttributes, + ) _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -52,7 +56,7 @@ async def async_setup_entry( entry: GoogleHomeConfigEntry, async_add_devices: AddEntitiesCallback, ) -> bool: - """Setup sensor platform.""" + """Set up sensor platform.""" client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] sensors: list[Entity] = [] @@ -134,6 +138,7 @@ def label(self) -> str: @property def state(self) -> str | None: # type: ignore[override] + """Return device IP address if any.""" device = self.get_device() return device.ip_address if device else None @@ -152,7 +157,7 @@ def extra_state_attributes(self) -> DeviceAttributes: # type: ignore[override] @staticmethod def get_device_attributes(device: GoogleHomeDevice) -> DeviceAttributes: - """Device representation as dictionary""" + """Device representation as dictionary.""" return { "device_id": device.device_id, "device_name": device.name, @@ -189,6 +194,7 @@ def label(self) -> str: @property def state(self) -> str | None: # type: ignore[override] + """Return next alarm if available.""" device = self.get_device() if not device: return None @@ -211,7 +217,7 @@ def extra_state_attributes(self) -> AlarmsAttributes: # type: ignore[override] } def _get_next_alarm_status(self) -> str: - """Update next alarm status from coordinator""" + """Update next alarm status from coordinator.""" device = self.get_device() next_alarm = device.get_next_alarm() if device else None return ( @@ -221,13 +227,13 @@ def _get_next_alarm_status(self) -> str: ) def _get_alarm_volume(self) -> float: - """Update alarm volume status from coordinator""" + """Update alarm volume status from coordinator.""" device = self.get_device() alarm_volume = device.get_alarm_volume() if device else None return alarm_volume if alarm_volume else GOOGLE_HOME_ALARM_DEFAULT_VALUE def _get_alarms_data(self) -> list[GoogleHomeAlarmDict]: - """Update alarms data extracting it from coordinator""" + """Update alarms data extracting it from coordinator.""" device = self.get_device() return ( [alarm.as_dict() for alarm in device.get_sorted_alarms()] if device else [] @@ -235,13 +241,13 @@ def _get_alarms_data(self) -> list[GoogleHomeAlarmDict]: @staticmethod def is_valid_alarm_id(alarm_id: str) -> bool: - """Checks if the alarm id provided is valid.""" + """Check if the alarm id provided is valid.""" return ( alarm_id.startswith("alarm/") and len(alarm_id) == ALARM_AND_TIMER_ID_LENGTH ) async def async_delete_alarm(self, call: ServiceCall) -> None: - """Service call to delete alarm on device""" + """Service call to delete alarm on device.""" device = self.get_device() if device is None: @@ -274,6 +280,7 @@ def label(self) -> str: @property def state(self) -> str | None: # type: ignore[override] + """Return next timer if available.""" device = self.get_device() if not device: return None @@ -293,7 +300,7 @@ def extra_state_attributes(self) -> TimersAttributes: # type: ignore[override] } def _get_next_timer_status(self) -> str: - """Update next timer status from coordinator""" + """Update next timer status from coordinator.""" device = self.get_device() next_timer = device.get_next_timer() if device else None return ( @@ -303,7 +310,7 @@ def _get_next_timer_status(self) -> str: ) def _get_timers_data(self) -> list[GoogleHomeTimerDict]: - """Update timers data extracting it from coordinator""" + """Update timers data extracting it from coordinator.""" device = self.get_device() return ( [timer.as_dict() for timer in device.get_sorted_timers()] if device else [] @@ -311,13 +318,13 @@ def _get_timers_data(self) -> list[GoogleHomeTimerDict]: @staticmethod def is_valid_timer_id(timer_id: str) -> bool: - """Checks if the timer id provided is valid.""" + """Check if the timer id provided is valid.""" return ( timer_id.startswith("timer/") and len(timer_id) == ALARM_AND_TIMER_ID_LENGTH ) async def async_delete_timer(self, call: ServiceCall) -> None: - """Service call to delete alarm on device""" + """Service call to delete alarm on device.""" device = self.get_device() if device is None: diff --git a/custom_components/google_home/switch.py b/custom_components/google_home/switch.py index d7ab87ef..90566e8f 100644 --- a/custom_components/google_home/switch.py +++ b/custom_components/google_home/switch.py @@ -1,14 +1,12 @@ -"""Switch platform for Google Home""" +"""Switch platform for Google Home.""" from __future__ import annotations import logging -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.components.switch import SwitchEntity -from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory -from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( DATA_CLIENT, @@ -18,7 +16,12 @@ LABEL_DO_NOT_DISTURB, ) from .entity import GoogleHomeBaseEntity -from .types import GoogleHomeConfigEntry + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + from .types import GoogleHomeConfigEntry _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -28,22 +31,21 @@ async def async_setup_entry( entry: GoogleHomeConfigEntry, async_add_devices: AddEntitiesCallback, ) -> bool: - """Setup switch platform.""" + """Set up switch platform.""" client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] - switches: list[SwitchEntity] = [] - for device in coordinator.data: - if device.auth_token and device.available: - switches.append( - DoNotDisturbSwitch( - coordinator, - client, - device.device_id, - device.name, - device.hardware, - ) - ) + switches = [ + DoNotDisturbSwitch( + coordinator, + client, + device.device_id, + device.name, + device.hardware, + ) + for device in coordinator.data + if device.auth_token and device.available + ] if switches: async_add_devices(switches) @@ -70,12 +72,10 @@ def is_on(self) -> bool: # type: ignore[override] if device is None: return False - is_enabled = device.get_do_not_disturb() - - return is_enabled + return device.get_do_not_disturb() async def set_do_not_disturb(self, enable: bool) -> None: - """Sets Do Not Disturb mode.""" + """Set Do Not Disturb mode.""" device = self.get_device() if device is None: _LOGGER.error("Device %s is not found.", self.device_name) diff --git a/custom_components/google_home/types.py b/custom_components/google_home/types.py index b7d899b7..ff4c7170 100644 --- a/custom_components/google_home/types.py +++ b/custom_components/google_home/types.py @@ -3,13 +3,13 @@ from __future__ import annotations from collections.abc import Mapping -from typing import TypedDict, Union +from typing import TypedDict from homeassistant.config_entries import ConfigEntry class AlarmJsonDict(TypedDict, total=False): - """Typed dict for JSON representation of alarm returned by Google Home API""" + """Typed dict for JSON representation of alarm returned by Google Home API.""" id: str fire_time: int @@ -19,7 +19,7 @@ class AlarmJsonDict(TypedDict, total=False): class TimerJsonDict(TypedDict, total=False): - """Typed dict for JSON representation of timer returned by Google Home API""" + """Typed dict for JSON representation of timer returned by Google Home API.""" id: str fire_time: int @@ -29,7 +29,7 @@ class TimerJsonDict(TypedDict, total=False): class GoogleHomeAlarmDict(TypedDict): - """Typed dict representation of Google Home alarm""" + """Typed dict representation of Google Home alarm.""" alarm_id: str fire_time: int @@ -41,7 +41,7 @@ class GoogleHomeAlarmDict(TypedDict): class GoogleHomeTimerDict(TypedDict): - """Typed dict representation of Google Home timer""" + """Typed dict representation of Google Home timer.""" timer_id: str fire_time: int | None @@ -53,7 +53,7 @@ class GoogleHomeTimerDict(TypedDict): class DeviceAttributes(TypedDict): - """Typed dict for device attributes""" + """Typed dict for device attributes.""" device_id: str | None device_name: str @@ -63,7 +63,7 @@ class DeviceAttributes(TypedDict): class AlarmsAttributes(TypedDict): - """Typed dict for alarms attributes""" + """Typed dict for alarms attributes.""" next_alarm_status: str alarm_volume: float @@ -71,14 +71,14 @@ class AlarmsAttributes(TypedDict): class TimersAttributes(TypedDict): - """Typed dict for timers attributes""" + """Typed dict for timers attributes.""" next_timer_status: str timers: list[GoogleHomeTimerDict] class ConfigFlowDict(TypedDict): - """Typed dict for config flow handler""" + """Typed dict for config flow handler.""" username: str password: str @@ -86,14 +86,14 @@ class ConfigFlowDict(TypedDict): class OptionsFlowDict(TypedDict): - """Typed dict for options flow handler""" + """Typed dict for options flow handler.""" update_interval: int type JsonDict = Mapping[ str, - Union[bool, float, int, str, list[str], list[AlarmJsonDict], list[TimerJsonDict]], + bool | float | int | str | list[str] | list[AlarmJsonDict] | list[TimerJsonDict], ] type GoogleHomeConfigEntry = ConfigEntry[None] diff --git a/pyproject.toml b/pyproject.toml index 7031d204..d87da18e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,10 +32,134 @@ ruff = "^0.7.2" types-requests = "<2.31.0.7" voluptuous-stubs = "^0.1.1" +[tool.ruff] +required-version = ">=0.7.0" +target-version = "py312" + +[tool.ruff.lint.isort] +force-sort-within-sections = true +combine-as-imports = true +split-on-trailing-comma = false +known-first-party = [ + "homeassistant", +] + +[tool.ruff.lint] +select = [ + "A001", # Variable {name} is shadowing a Python builtin + "ASYNC", # flake8-async + "B", # flake8-bugbear + "BLE", + "C", # complexity, including flake8-comprehensions + "COM818", # Trailing comma on bare tuple prohibited + "D", # flake8-docstrings + "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() + "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) + "E", # pycodestyle + "F", # pyflakes/autoflake + "FLY", # flynt + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat + "ICN001", # import concentions; {name} should be imported as {asname} + "LOG", # flake8-logging + "N804", # First argument of a class method should be named cls + "N805", # First argument of a method should be named self + "N815", # Variable {name} in class scope should not be mixedCase + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "PYI", # flake8-pyi + "RET", # flake8-return + "RSE", # flake8-raise + "RUF005", # Consider iterable unpacking instead of concatenation + "RUF006", # Store a reference to the return value of asyncio.create_task + "RUF010", # Use explicit conversion flag + "RUF013", # PEP 484 prohibits implicit Optional + "RUF017", # Avoid quadratic list summation + "RUF018", # Avoid assignment expressions in assert statements + "RUF019", # Unnecessary key check before dictionary access + # "RUF100", # Unused `noqa` directive; temporarily every now and then to clean them up + "S102", # Use of exec detected + "S103", # bad-file-permissions + "S108", # hardcoded-temp-file + "S306", # suspicious-mktemp-usage + "S307", # suspicious-eval-usage + "S313", # suspicious-xmlc-element-tree-usage + "S314", # suspicious-xml-element-tree-usage + "S315", # suspicious-xml-expat-reader-usage + "S316", # suspicious-xml-expat-builder-usage + "S317", # suspicious-xml-sax-usage + "S318", # suspicious-xml-mini-dom-usage + "S319", # suspicious-xml-pull-dom-usage + "S320", # suspicious-xmle-tree-usage + "S601", # paramiko-call + "S602", # subprocess-popen-with-shell-equals-true + "S604", # call-with-shell-equals-true + "S608", # hardcoded-sql-expression + "S609", # unix-command-wildcard-injection + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T100", # Trace found: {name} used + "TCH", # flake8-type-checking + "TID251", # Banned imports + "TRY", # tryceratops + "UP", # pyupgrade + "W", # pycodestyle +] +ignore = [ + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "E501", # line too long + "PLR0911", # Too many return statements ({returns} > {max_returns}) + "PLR0912", # Too many branches ({branches} > {max_branches}) + "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) + "PLR0915", # Too many statements ({statements} > {max_statements}) + "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable + "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target + "TRY003", # Avoid specifying long messages outside the exception class + # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q", + "COM812", + "COM819", + "ISC001", +] + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false + +[tool.ruff.lint.mccabe] +max-complexity = 25 + +[tool.pylint.main] +py-version = "3.12" +load-plugins = [ + "pylint.extensions.code_style", + "pylint.extensions.typing", +] + [tool.pylint.messages_control] # Reasons disabled: # too-many-* - not enforced for the sake of readability # too-few-* - same as too-many-* +# --- +# Pylint CodeStyle plugin +# consider-using-assignment-expr - decision to use := better left to devs +# consider-using-assignment-expr - too opinionated disable = [ "duplicate-code", "line-too-long", @@ -45,22 +169,14 @@ disable = [ "too-many-instance-attributes", "too-many-locals", "too-many-positional-arguments", + "consider-using-assignment-expr", + "consider-using-tuple", ] [tool.pylint.format] -max-line-length = 88 - -[tool.ruff] -required-version = ">=0.7.0" -target-version = "py312" - -[tool.ruff.lint.isort] -force-sort-within-sections = true -combine-as-imports = true -split-on-trailing-comma = false -known-first-party = [ - "homeassistant", -] +expected-line-ending-format = "LF" +max-line-length = 100 +min-similarity-lines = 7 [tool.mypy] python_version = "3.12" diff --git a/script/publish_release.py b/script/publish_release.py index f25b4c30..d386196b 100644 --- a/script/publish_release.py +++ b/script/publish_release.py @@ -7,15 +7,18 @@ import json import os import sys +from typing import TYPE_CHECKING from github import Auth, Github, InputGitTreeElement from github.ContentFile import ContentFile -from github.GitRelease import GitRelease -from github.Repository import Repository + +if TYPE_CHECKING: + from github.GitRelease import GitRelease + from github.Repository import Repository def main() -> int: - """Main function""" + """Run main function.""" github_token = os.environ.get("GITHUB_TOKEN") assert github_token is not None, "GITHUB_TOKEN is not set" print("Fetching draft release...") @@ -32,7 +35,7 @@ def main() -> int: def update_manifests(repo: Repository, version: str) -> None: - """Update manifest.json and hacs.json""" + """Update manifest.json and hacs.json.""" print("Updating manifest.json...") manifest = repo.get_contents("custom_components/google_home/manifest.json") assert isinstance(manifest, ContentFile) @@ -79,7 +82,7 @@ def update_manifests(repo: Repository, version: str) -> None: def publish_release(release: GitRelease) -> None: - """Publish draft release""" + """Publish draft release.""" print("Publishing new release...") release.update_release( name=release.title.split()[-1], diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 0df8e08b..00000000 --- a/setup.cfg +++ /dev/null @@ -1,16 +0,0 @@ -[flake8] -exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build -doctests = True -# To work with Black -max-line-length = 88 -# E501: line too long -# W503: Line break occurred before a binary operator -# E203: Whitespace before ':' -# D202 No blank lines allowed after function docstring -# W504 line break after binary operator -ignore = - E501, - W503, - E203, - D202, - W504