Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tests, improve architecture #105

Merged
merged 4 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,35 @@ jobs:
- name: Run mypy
run: |
poetry run mypy eq3btsmart custom_components/eq3btsmart

tests:
needs: cache
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Python
uses: actions/setup-python@v4
with:
python-version: "3.11"

- name: Load cached Poetry installation
uses: actions/cache@v3
with:
path: ~/.local
key: poetry-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}

- name: Load cached Poetry venv
uses: actions/cache@v3
with:
path: ~/.venv
key: poetry-venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}

- name: Install package
run: |
poetry install --no-interaction

- name: Run tests
run: |
poetry run pytest --cov=eq3btsmart --cov-report=xml --cov-report=term tests
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ repos:
- id: trailing-whitespace
- id: check-added-large-files
- id: check-case-conflict
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: check-toml
Expand Down
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,8 @@
"**/.ruff_cache": true,
"**/dist": true,
},
"extensions.ignoreRecommendations": false
"extensions.ignoreRecommendations": false,
"python.testing.pytestArgs": [
"tests",
]
}
13 changes: 11 additions & 2 deletions custom_components/eq3btsmart/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
"""Platform for eQ-3 binary sensor entities."""


from custom_components.eq3btsmart.eq3_entity import Eq3Entity
from custom_components.eq3btsmart.models import Eq3Config, Eq3ConfigEntry
from eq3btsmart import Thermostat
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
Expand All @@ -23,6 +21,8 @@
ENTITY_NAME_MONITORING,
ENTITY_NAME_WINDOW_OPEN,
)
from .eq3_entity import Eq3Entity
from .models import Eq3Config, Eq3ConfigEntry


async def async_setup_entry(
Expand Down Expand Up @@ -156,6 +156,9 @@ def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat):

@property
def is_on(self) -> bool | None:
if self._thermostat.status is None:
return None

return self._thermostat.status.is_low_battery


Expand All @@ -171,6 +174,9 @@ def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat):

@property
def is_on(self) -> bool | None:
if self._thermostat.status is None:
return None

return self._thermostat.status.is_window_open


Expand All @@ -186,4 +192,7 @@ def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat):

@property
def is_on(self) -> bool | None:
if self._thermostat.status is None:
return None

return self._thermostat.status.is_dst
14 changes: 7 additions & 7 deletions custom_components/eq3btsmart/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@

import logging

from custom_components.eq3btsmart.eq3_entity import Eq3Entity
from custom_components.eq3btsmart.models import Eq3Config, Eq3ConfigEntry
from eq3btsmart import Thermostat
from eq3btsmart.adapter.eq3_schedule_time import Eq3ScheduleTime
from eq3btsmart.adapter.eq3_temperature import Eq3Temperature
from eq3btsmart.const import WeekDay
from eq3btsmart.eq3_schedule_time import Eq3ScheduleTime
from eq3btsmart.eq3_temperature import Eq3Temperature
from eq3btsmart.models import Schedule, ScheduleDay, ScheduleHour
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry, UndefinedType
Expand All @@ -24,6 +22,8 @@
ENTITY_NAME_FETCH_SCHEDULE,
SERVICE_SET_SCHEDULE,
)
from .eq3_entity import Eq3Entity
from .models import Eq3Config, Eq3ConfigEntry
from .schemas import SCHEMA_SCHEDULE_SET

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -138,8 +138,8 @@ def extra_state_attributes(self):
for day in self._thermostat.schedule.schedule_days:
schedule[str(day.week_day)] = [
{
"target_temperature": schedule_hour.target_temperature.friendly_value,
"next_change_at": schedule_hour.next_change_at.friendly_value.isoformat(),
"target_temperature": schedule_hour.target_temperature.value,
"next_change_at": schedule_hour.next_change_at.value.isoformat(),
}
for schedule_hour in day.schedule_hours
]
Expand All @@ -157,4 +157,4 @@ def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat):
self._attr_entity_category = EntityCategory.DIAGNOSTIC

async def async_press(self) -> None:
await self._thermostat.async_get_info()
await self._thermostat.async_get_status()
78 changes: 53 additions & 25 deletions custom_components/eq3btsmart/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from datetime import timedelta
from typing import Callable

from custom_components.eq3btsmart.eq3_entity import Eq3Entity
from custom_components.eq3btsmart.models import Eq3Config, Eq3ConfigEntry
from eq3btsmart import Thermostat
from eq3btsmart.const import EQ3BT_MAX_TEMP, EQ3BT_OFF_TEMP, Eq3Preset, OperationMode
from homeassistant.components.climate import ClimateEntity, HVACMode
Expand Down Expand Up @@ -38,6 +36,8 @@
Preset,
TargetTemperatureSelector,
)
from .eq3_entity import Eq3Entity
from .models import Eq3Config, Eq3ConfigEntry

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -110,10 +110,12 @@ async def _async_scan_loop(self, now=None) -> None:
def _on_updated(self):
self._is_available = True

if self._thermostat.status is None:
return

if (
self._thermostat.status.target_temperature is not None
and self._target_temperature_to_set
== self._thermostat.status.target_temperature.friendly_value
self._target_temperature_to_set
== self._thermostat.status.target_temperature.value
):
self._is_setting_temperature = False

Expand All @@ -123,7 +125,7 @@ def _on_updated(self):
):
# temperature may have been updated from the thermostat
self._target_temperature_to_set = (
self._thermostat.status.target_temperature.friendly_value
self._thermostat.status.target_temperature.value
)

if self.entity_id is None:
Expand All @@ -140,6 +142,9 @@ def available(self) -> bool:

@property
def hvac_action(self) -> HVACAction | None:
if self._thermostat.status is None:
return None

if self._thermostat.status.operation_mode == OperationMode.OFF:
return HVACAction.OFF

Expand All @@ -154,23 +159,24 @@ def current_temperature(self) -> float | None:
case CurrentTemperatureSelector.NOTHING:
return None
case CurrentTemperatureSelector.VALVE:
if (
self._thermostat.status.valve is None
or self._thermostat.status.target_temperature is None
):
if self._thermostat.status is None:
return None

return (
(1 - self._thermostat.status.valve / 100) * 2
+ self._thermostat.status.target_temperature.friendly_value
+ self._thermostat.status.target_temperature.value
- 2
)
case CurrentTemperatureSelector.UI:
return self._target_temperature_to_set
case CurrentTemperatureSelector.DEVICE:
if self._thermostat.status is None:
return None

if self._thermostat.status.target_temperature is None:
return None

return self._thermostat.status.target_temperature.friendly_value
return self._thermostat.status.target_temperature.value
case CurrentTemperatureSelector.ENTITY:
state = self.hass.states.get(self._eq3_config.external_temp_sensor)
if state is not None:
Expand All @@ -187,10 +193,13 @@ def target_temperature(self) -> float | None:
case TargetTemperatureSelector.TARGET:
return self._target_temperature_to_set
case TargetTemperatureSelector.LAST_REPORTED:
if self._thermostat.status is None:
return None

if self._thermostat.status.target_temperature is None:
return None

return self._thermostat.status.target_temperature.friendly_value
return self._thermostat.status.target_temperature.value

return None

Expand Down Expand Up @@ -240,7 +249,7 @@ async def async_set_temperature_now(self) -> None:

@property
def hvac_mode(self) -> HVACMode | None:
if self._thermostat.status.operation_mode is None:
if self._thermostat.status is None:
return None

return EQ_TO_HA_HVAC[self._thermostat.status.operation_mode]
Expand All @@ -251,9 +260,9 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
self._target_temperature_to_set = EQ3BT_OFF_TEMP
self._is_setting_temperature = True
case _:
if self._thermostat.status.target_temperature is not None:
if self._thermostat.status is not None:
self._target_temperature_to_set = (
self._thermostat.status.target_temperature.friendly_value
self._thermostat.status.target_temperature.value
)
self._is_setting_temperature = False

Expand All @@ -262,6 +271,9 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:

@property
def preset_mode(self) -> str | None:
if self._thermostat.status is None:
return None

if self._thermostat.status.is_window_open:
return Preset.WINDOW_OPEN
if self._thermostat.status.is_boost:
Expand All @@ -270,18 +282,22 @@ def preset_mode(self) -> str | None:
return Preset.LOW_BATTERY
if self._thermostat.status.is_away:
return Preset.AWAY
if self._thermostat.status.operation_mode == OperationMode.ON:
return Preset.OPEN

if self._thermostat.status.presets is None:
return None

if (
self._thermostat.status.target_temperature
== self._thermostat.status.eco_temperature
== self._thermostat.status.presets.eco_temperature
):
return Preset.ECO
if (
self._thermostat.status.target_temperature
== self._thermostat.status.comfort_temperature
== self._thermostat.status.presets.comfort_temperature
):
return Preset.COMFORT
if self._thermostat.status.operation_mode == OperationMode.ON:
return Preset.OPEN
return PRESET_NONE

async def async_set_preset_mode(self, preset_mode: str) -> None:
Expand All @@ -291,20 +307,26 @@ async def async_set_preset_mode(self, preset_mode: str) -> None:
case Preset.AWAY:
await self._thermostat.async_set_away(True)
case Preset.ECO:
if self._thermostat.status is None:
return
if self._thermostat.status.is_boost:
await self._thermostat.async_set_boost(False)
if self._thermostat.status.is_away:
await self._thermostat.async_set_away(False)

await self._thermostat.async_set_preset(Eq3Preset.ECO)
case Preset.COMFORT:
if self._thermostat.status is None:
return
if self._thermostat.status.is_boost:
await self._thermostat.async_set_boost(False)
if self._thermostat.status.is_away:
await self._thermostat.async_set_away(False)

await self._thermostat.async_set_preset(Eq3Preset.COMFORT)
case Preset.OPEN:
if self._thermostat.status is None:
return
if self._thermostat.status.is_boost:
await self._thermostat.async_set_boost(False)
if self._thermostat.status.is_away:
Expand All @@ -313,8 +335,11 @@ async def async_set_preset_mode(self, preset_mode: str) -> None:
await self._thermostat.async_set_mode(OperationMode.ON)

# by now, the target temperature should have been (maybe set) and fetched
self._target_temperature_to_set = self._thermostat.status.target_temperature
self._is_setting_temperature = False
if self._thermostat.status is not None:
self._target_temperature_to_set = (
self._thermostat.status.target_temperature.value
)
self._is_setting_temperature = False

@property
def device_info(self) -> DeviceInfo | None:
Expand All @@ -323,19 +348,22 @@ def device_info(self) -> DeviceInfo | None:
manufacturer=MANUFACTURER,
model=DEVICE_MODEL,
identifiers={(DOMAIN, self._eq3_config.mac_address)},
sw_version=str(self._thermostat.device_data.firmware_version or "unknown"),
sw_version=str(
self._thermostat.device_data.firmware_version
if self._thermostat.device_data is not None
else "unknown"
),
connections={(CONNECTION_BLUETOOTH, self._eq3_config.mac_address)},
)

async def async_scan(self) -> None:
"""Update the data from the thermostat."""

try:
await self._thermostat.async_get_info()
await self._thermostat.async_get_status()
if self._is_setting_temperature:
await self.async_set_temperature_now()
except Exception as ex:
# self._is_available = False
self.schedule_update_ha_state()
_LOGGER.error(
f"[{self._eq3_config.name}] Error updating: {ex}",
Expand Down
10 changes: 5 additions & 5 deletions custom_components/eq3btsmart/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@
import logging
from typing import Any

from custom_components.eq3btsmart.schemas import (
SCHEMA_NAME,
SCHEMA_NAME_MAC,
SCHEMA_OPTIONS,
)
from homeassistant import config_entries
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from homeassistant.config_entries import ConfigEntry, OptionsFlow
Expand All @@ -32,6 +27,11 @@
DEFAULT_TARGET_TEMP_SELECTOR,
DOMAIN,
)
from .schemas import (
SCHEMA_NAME,
SCHEMA_NAME_MAC,
SCHEMA_OPTIONS,
)

_LOGGER = logging.getLogger(__name__)

Expand Down
3 changes: 2 additions & 1 deletion custom_components/eq3btsmart/eq3_entity.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from custom_components.eq3btsmart.models import Eq3Config
from eq3btsmart.thermostat import Thermostat
from homeassistant.helpers.entity import Entity

from .models import Eq3Config


class Eq3Entity(Entity):
"""Base class for all eQ-3 entities."""
Expand Down
Loading
Loading