Skip to content

API Enhancements and Additions#24

Closed
ruaan-deysel wants to merge 2 commits intokclif9:mainfrom
ruaan-deysel:api-enhancements
Closed

API Enhancements and Additions#24
ruaan-deysel wants to merge 2 commits intokclif9:mainfrom
ruaan-deysel:api-enhancements

Conversation

@ruaan-deysel
Copy link

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-api package. It migrates the project to hatchling for 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:

  • Migrated from setuptools/requirements.txt to hatchling and hatch-vcs, with all configuration now in pyproject.toml. Removed legacy files: setup.py, requirements.txt, and mypy.ini. Added a py.typed marker 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:

  • Adopted strict typing throughout the project, enabled mypy strict mode, and switched to modern Pydantic v2 best practices using ConfigDict and model_config. Added the pydantic.mypy plugin for better type checking. [1] [2] [3] [4] [5] [6] [7]

API & Model Enhancements:

  • Added new models and fields: ActronAirIndoorUnit, new fields for ActronAirOutdoorUnit, ActronAirLiveAircon, ActronAirACSystem, ActronAirZone, ActronAirPeripheral, and ActronAirStatus. These additions improve the detail and usability of the API. [1] [2] [3] [4] [5]
  • The ActronAirAPI and ActronAirOAuth2DeviceCodeAuth classes now accept an optional externally managed aiohttp.ClientSession, allowing callers to control session lifecycle—important for integrations like Home Assistant. [1] [2] [3] [4] [5] [6]

Backward Compatibility & Bug Fixes:

  • Changed the quiet_mode_enabled alias from QuietModeEnabled to QuietMode in ActronAirUserAirconSettings and added a model validator to accept both keys from the API, ensuring backward compatibility with different device firmware versions. [1] [2]

Other Improvements:

  • Improved test and linting tool configuration, including pytest-asyncio auto mode and expanded coverage settings. [1] [2]
  • Minor bug fix: ensure temperature limits in ActronAirStatus are 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]

…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.
Copilot AI review requested due to automatic review settings February 20, 2026 12:54
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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), adds CHANGELOG.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())
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +257 to 260
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
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 139 to +143
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")
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@@ -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")
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enabled_zones is defined with Field([]), which creates a mutable default shared across instances. Use Field(default_factory=list, alias="EnabledZones") instead.

Suggested change
enabled_zones: list[bool] = Field([], alias="EnabledZones")
enabled_zones: list[bool] = Field(default_factory=list, alias="EnabledZones")

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,3 @@
"""Single-source package version for actron-neo-api."""

__version__ = "0.5.0"
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
__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"

Copilot uses AI. Check for mistakes.
Comment on lines 28 to +30
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")
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Owner

@kclif9 kclif9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. API Enhancements
  2. Change to build system should be its own PR
  3. 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")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible typo on Supply (Missing letter l)

"Capacity_kW": 10.0,
"CompSpeed": 70.0,
"CompPower": 3000,
"SuppyCurrentRMS_A": 15.0,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible typo on Supply (Missing letter l)

"CompSpeed": 70.0,
"CompPower": 3000,
"SuppyCurrentRMS_A": 15.0,
"SuppyPowerRMS_W": 3500.0,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible typo on Supply (Missing letter l)

data = {
"Capacity_kW": 7.1,
"SupplyVoltage_Vac": 240.5,
"SuppyCurrentRMS_A": 12.3, # API typo
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Comment on lines -2 to +3
requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard coded version should be in sync with pyproject

@codecov
Copy link

codecov bot commented Feb 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.65%. Comparing base (d39aff3) to head (49e3794).
✅ All tests successful. No failed tests found.

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.
📢 Have feedback on the report? Share it here.

@ruaan-deysel
Copy link
Author

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.

  1. API Enhancements
  2. Change to build system should be its own PR
  3. 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.

Cool. Will split it into smaller PRs. 👍🏻

@ruaan-deysel
Copy link
Author

I'm closing this PR and working on few smaller PRs to submit for review.

@ruaan-deysel ruaan-deysel deleted the api-enhancements branch February 21, 2026 04:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants