Conversation
…and OAuth handler - Introduced optional `session` parameter in `ActronAirAPI` and `ActronAirOAuth2DeviceCodeAuth` to allow external session management. - Implemented context manager for session handling in OAuth2. - Updated tests to verify session handling behavior. feat: Introduce ActronAirIndoorUnit model with fan mode capabilities - Added `ActronAirIndoorUnit` model with fields for auto fan capability and supported fan modes. - Implemented `supported_fan_mode_list` property to decode bitmap fan modes into human-readable strings. feat: Enhance ActronAirOutdoorUnit and ActronAirLiveAircon models with new fields - Added multiple new fields to `ActronAirOutdoorUnit` including capacity, supply voltage, and error codes. - Extended `ActronAirLiveAircon` with additional fields for fan status and error codes. feat: Expand ActronAirZone and ActronAirPeripheral models with new properties - Introduced new fields in `ActronAirZone` for temperature control and airflow settings. - Added new properties in `ActronAirPeripheral` for RSSI, connection state, and control capabilities. feat: Update ActronAirStatus model with new fields for status tracking - Added `last_status_update` and `time_since_last_contact` fields to `ActronAirStatus`. chore: Implement single-source versioning and add changelog - Created `_version.py` for centralized version management. - Added `CHANGELOG.md` to document project changes. chore: Modernize project structure and dependencies - Migrated to `hatchling` for build management, removing legacy files. - Updated Pydantic models to use `model_config` for better practices. - Enabled strict type checking with mypy and updated pytest configuration.
There was a problem hiding this comment.
Pull request overview
This pull request modernizes packaging/build configuration and expands the Actron Neo API’s typed Pydantic models and client capabilities (notably session injection for HA-style lifecycle management), with corresponding test additions.
Changes:
- Migrates build/version/config to
pyproject.toml(Hatchling + hatch-vcs), addsCHANGELOG.md,py.typed, and_version.py. - Extends multiple models (
Status,System,Zone,Peripheral,Settings) with new fields and updated Pydantic v2 configuration patterns. - Adds/updates tests to cover new model fields, session injection, and version/type marker behavior.
Reviewed changes
Copilot reviewed 23 out of 26 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
pyproject.toml |
Moves build + tool configuration into PEP 621/pyproject with Hatch tooling and strict mypy config. |
.pre-commit-config.yaml |
Updates mypy hook config to rely on pyproject configuration. |
.gitignore |
Ignores local discovery/credential artifacts. |
CHANGELOG.md |
Documents additions/changes included in this PR. |
setup.py |
Removes legacy setuptools build entrypoint. |
requirements.txt |
Removes legacy dependency list in favor of pyproject dependencies. |
mypy.ini |
Removes legacy mypy config in favor of pyproject mypy config. |
src/actron_neo_api/__init__.py |
Exposes __version__ from the new version module. |
src/actron_neo_api/_version.py |
Introduces an internal version constant. |
src/actron_neo_api/py.typed |
Adds PEP 561 marker for downstream typing support. |
src/actron_neo_api/actron.py |
Adds optional external aiohttp.ClientSession injection and updates request/close handling. |
src/actron_neo_api/oauth.py |
Adds optional external session support via an async contextmanager session provider. |
src/actron_neo_api/models/__init__.py |
Exports the newly added ActronAirIndoorUnit. |
src/actron_neo_api/models/schemas.py |
Re-exports ActronAirIndoorUnit via schemas. |
src/actron_neo_api/models/auth.py |
Migrates Pydantic config to ConfigDict (model_config). |
src/actron_neo_api/models/system.py |
Adds ActronAirIndoorUnit and new fields for outdoor/live/system models. |
src/actron_neo_api/models/status.py |
Adds new top-level status fields and ensures temp limits are returned as floats. |
src/actron_neo_api/models/settings.py |
Adds QuietMode alias normalization and Pydantic model_config. |
src/actron_neo_api/models/zone.py |
Adds new zone/peripheral fields and has_temp_control; ensures min/max temp returns floats. |
tests/test_actron.py |
Adds tests covering injected session behavior in ActronAirAPI. |
tests/test_oauth.py |
Adds tests covering injected session behavior in OAuth handler. |
tests/test_models.py |
Adds tests for __version__, py.typed, and config modernization checks. |
tests/test_settings.py |
Adds tests for QuietMode alias fallback behavior. |
tests/test_status.py |
Adds tests for new status fields and end-to-end parsing with new nested fields. |
tests/test_system.py |
Adds tests for new system/indoor/outdoor/live model fields and helpers. |
tests/test_zone.py |
Adds tests for new zone/peripheral fields and has_temp_control. |
| ) | ||
|
|
||
| return await response.json() | ||
| return dict(await response.json()) |
There was a problem hiding this comment.
return dict(await response.json()) assumes the response JSON is a mapping. If the API returns a list (or another JSON type), dict(...) will either throw or produce unintended results. Prefer returning the decoded JSON as-is (adjust the return type accordingly), or validate isinstance(data, dict) and raise a clear ActronAirAPIError when it isn’t.
| return dict(await response.json()) | |
| data = await response.json() | |
| if not isinstance(data, dict): | |
| raise ActronAirAPIError( | |
| f"Invalid API response type '{type(data).__name__}'; expected JSON object." | |
| ) | |
| return data |
| if self._session and not self._session.closed and not self._external_session: | ||
| await self._session.close() | ||
| if not self._external_session: | ||
| self._session = None |
There was a problem hiding this comment.
With injected sessions, _external_session stays True forever. If the injected session is later closed and _get_session() creates a replacement session, close() will still skip closing it, leaking resources. Consider either raising if an injected session is closed, or switching _external_session to False when creating a new session so the client owns and closes it.
| sensors: dict[str, ActronAirZoneSensor] = Field({}, alias="Sensors") | ||
| nv_vav: bool = Field(False, alias="NV_VAV") | ||
| nv_itc: bool = Field(False, alias="NV_ITC") | ||
| temperature_setpoint_c: float | None = Field(None, alias="TemperatureSetpoint_oC") | ||
| airflow_setpoint: int | None = Field(None, alias="AirflowSetpoint") |
There was a problem hiding this comment.
sensors uses Field({}), which is a mutable default shared across instances. Switch to Field(default_factory=dict, alias="Sensors") to prevent cross-instance state leakage.
| @@ -25,12 +25,34 @@ class ActronAirUserAirconSettings(BaseModel): | |||
| temperature_setpoint_cool_c: float = Field(0.0, alias="TemperatureSetpoint_Cool_oC") | |||
| temperature_setpoint_heat_c: float = Field(0.0, alias="TemperatureSetpoint_Heat_oC") | |||
| enabled_zones: list[bool] = Field([], alias="EnabledZones") | |||
There was a problem hiding this comment.
enabled_zones is defined with Field([]), which creates a mutable default shared across instances. Use Field(default_factory=list, alias="EnabledZones") instead.
| enabled_zones: list[bool] = Field([], alias="EnabledZones") | |
| enabled_zones: list[bool] = Field(default_factory=list, alias="EnabledZones") |
| @@ -0,0 +1,3 @@ | |||
| """Single-source package version for actron-neo-api.""" | |||
|
|
|||
| __version__ = "0.5.0" | |||
There was a problem hiding this comment.
This hard-coded __version__ conflicts with the pyproject.toml configuration that sets project.version as dynamic from VCS (tool.hatch.version source = "vcs"). As written, actron_neo_api.__version__ can easily diverge from the built distribution version. Consider configuring Hatch to write the VCS-derived version into this file at build time (e.g., via tool.hatch.version.path/version file), or drop hatch-vcs and make the version file the single source of truth.
| __version__ = "0.5.0" | |
| from importlib.metadata import PackageNotFoundError, version | |
| try: | |
| __version__: str = version("actron-neo-api") | |
| except PackageNotFoundError: | |
| # Fallback for environments where the package metadata is unavailable. | |
| # This keeps imports working while still avoiding a hard-coded version | |
| # that can diverge from the distribution version. | |
| __version__ = "0.0.0" |
| last_known_state: dict[str, Any] = Field({}, alias="lastKnownState") | ||
| last_status_update: str | None = Field(None, alias="lastStatusUpdate") | ||
| time_since_last_contact: str | None = Field(None, alias="timeSinceLastContact") |
There was a problem hiding this comment.
Avoid mutable defaults on Pydantic models: Field({}) will share the same dict across instances. Use Field(default_factory=dict, alias="lastKnownState") for last_known_state instead. Also consider updating other mutable defaults in this model (e.g., Field([]) / []) to use default_factory=list for the same reason.
kclif9
left a comment
There was a problem hiding this comment.
Thanks @ruaan-deysel for contributing! Really appreciate having some extra hands on this!
Overall I agree with the approach taken, but possibly should be three PR's.
- API Enhancements
- Change to build system should be its own PR
- mypy strict should be split out from feature changes to enable isolation of the changes
Thanks for your help! Possibly shift the build system & mypy into their own PRs and then I'm happy for the API enhancements to jump in. I'll approve workflows so tests can be run so any issues found can be identified too.
| family: str | None = Field(None, alias="Family") | ||
| capacity_kw: float | None = Field(None, alias="Capacity_kW") | ||
| supply_voltage_vac: float | None = Field(None, alias="SupplyVoltage_Vac") | ||
| supply_current_rms_a: float | None = Field(None, alias="SuppyCurrentRMS_A") |
There was a problem hiding this comment.
Possible typo on Supply (Missing letter l)
| capacity_kw: float | None = Field(None, alias="Capacity_kW") | ||
| supply_voltage_vac: float | None = Field(None, alias="SupplyVoltage_Vac") | ||
| supply_current_rms_a: float | None = Field(None, alias="SuppyCurrentRMS_A") | ||
| supply_power_rms_w: float | None = Field(None, alias="SuppyPowerRMS_W") |
There was a problem hiding this comment.
Possible typo on Supply (Missing letter l)
| "Capacity_kW": 10.0, | ||
| "CompSpeed": 70.0, | ||
| "CompPower": 3000, | ||
| "SuppyCurrentRMS_A": 15.0, |
There was a problem hiding this comment.
Possible typo on Supply (Missing letter l)
| "CompSpeed": 70.0, | ||
| "CompPower": 3000, | ||
| "SuppyCurrentRMS_A": 15.0, | ||
| "SuppyPowerRMS_W": 3500.0, |
There was a problem hiding this comment.
Possible typo on Supply (Missing letter l)
| data = { | ||
| "Capacity_kW": 7.1, | ||
| "SupplyVoltage_Vac": 240.5, | ||
| "SuppyCurrentRMS_A": 12.3, # API typo |
There was a problem hiding this comment.
Possible typo on Supply (Missing letter l) - It's correct in the API when I look at the json output
"SupplyVoltage_Vac": 716,
"SupplyCurrentRMS_A": 0,
"SupplyPowerRMS_W": 0,
| "Capacity_kW": 7.1, | ||
| "SupplyVoltage_Vac": 240.5, | ||
| "SuppyCurrentRMS_A": 12.3, # API typo | ||
| "SuppyPowerRMS_W": 2950.0, # API typo |
There was a problem hiding this comment.
Possible typo on Supply (Missing letter l) - It's correct in the API when I look at the json output
"SupplyVoltage_Vac": 716,
"SupplyCurrentRMS_A": 0,
"SupplyPowerRMS_W": 0,
| requires = ["setuptools>=64", "setuptools-scm>=8"] | ||
| build-backend = "setuptools.build_meta" | ||
| requires = ["hatchling", "hatch-vcs"] | ||
| build-backend = "hatchling.build" |
There was a problem hiding this comment.
The change to the build system is a considerably large change to be in the same as the API enhancements. Consider splitting this into a separate PR?
| @@ -0,0 +1,3 @@ | |||
| """Single-source package version for actron-neo-api.""" | |||
|
|
|||
| __version__ = "0.5.0" | |||
There was a problem hiding this comment.
Hard coded version should be in sync with pyproject
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #24 +/- ##
==========================================
+ Coverage 96.42% 96.65% +0.22%
==========================================
Files 13 14 +1
Lines 1007 1076 +69
==========================================
+ Hits 971 1040 +69
Misses 36 36 ☔ View full report in Codecov by Sentry. |
Cool. Will split it into smaller PRs. 👍🏻 |
|
I'm closing this PR and working on few smaller PRs to submit for review. |
Excellent work on the ActronAir python library. Thought I give a hand and add some additional enhancement and additions in that I noticed is missing. Hope this also helps out to add more features to the Actron Air HA integration.
Below is what Copilot generated with all changes and additions in the code.
This pull request modernizes the build system, enhances type safety, and introduces several new features and models to the
actron-neo-apipackage. It migrates the project tohatchlingfor builds, improves Pydantic usage, adds a changelog, and expands the API models with new fields and capabilities. Backward compatibility is maintained for some API changes, and type information is now exposed for downstream consumers.Build System Modernization & Packaging Improvements:
setuptools/requirements.txttohatchlingandhatch-vcs, with all configuration now inpyproject.toml. Removed legacy files:setup.py,requirements.txt, andmypy.ini. Added apy.typedmarker for type hinting support and a single-source version module (_version.py). [1] [2] [3] [4] [5] [6] [7] [8] [9]Type Safety & Pydantic Best Practices:
ConfigDictandmodel_config. Added thepydantic.mypyplugin for better type checking. [1] [2] [3] [4] [5] [6] [7]API & Model Enhancements:
ActronAirIndoorUnit, new fields forActronAirOutdoorUnit,ActronAirLiveAircon,ActronAirACSystem,ActronAirZone,ActronAirPeripheral, andActronAirStatus. These additions improve the detail and usability of the API. [1] [2] [3] [4] [5]ActronAirAPIandActronAirOAuth2DeviceCodeAuthclasses now accept an optional externally managedaiohttp.ClientSession, allowing callers to control session lifecycle—important for integrations like Home Assistant. [1] [2] [3] [4] [5] [6]Backward Compatibility & Bug Fixes:
quiet_mode_enabledalias fromQuietModeEnabledtoQuietModeinActronAirUserAirconSettingsand added a model validator to accept both keys from the API, ensuring backward compatibility with different device firmware versions. [1] [2]Other Improvements:
pytest-asyncioauto mode and expanded coverage settings. [1] [2]ActronAirStatusare always returned as floats.References: [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17] [18] [19] [20] [21] [22] [23] [24] [25] [26] [27] [28]