Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/linting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
workflow_dispatch:

env:
DEFAULT_PYTHON: "3.11"
DEFAULT_PYTHON: "3.12"

permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
- published

env:
DEFAULT_PYTHON: "3.11"
DEFAULT_PYTHON: "3.12"

permissions:
contents: read
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
workflow_dispatch:

env:
DEFAULT_PYTHON: "3.11"
DEFAULT_PYTHON: "3.12"

permissions:
contents: read
Expand All @@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.11", "3.12", "3.13", "3.14"]
python: ["3.12", "3.13", "3.14"]
steps:
- name: ⤵️ Check out code from GitHub
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/typing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
workflow_dispatch:

env:
DEFAULT_PYTHON: "3.11"
DEFAULT_PYTHON: "3.12"

permissions:
contents: read
Expand Down
45 changes: 17 additions & 28 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description = "Asynchronous Python client for the Fumis WiRCU API."
authors = [{ name = "Franck Nijhof", email = "opensource@frenck.dev" }]
maintainers = [{ name = "Franck Nijhof", email = "opensource@frenck.dev" }]
license = "MIT"
requires-python = ">=3.11"
requires-python = ">=3.12"
readme = "README.md"
keywords = [
"api",
Expand All @@ -29,7 +29,6 @@ classifiers = [
"Framework :: AsyncIO",
"Intended Audience :: Developers",
"Natural Language :: English",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
Expand All @@ -38,7 +37,7 @@ classifiers = [
]
dynamic = ["dependencies"]
packages = [{ include = "fumis", from = "src" }]
dependencies = ['aiohttp (>=3.0.0)', 'awesomeversion (>=22.0.0)', 'mashumaro (>=3.13,<4.0.0)', 'orjson (>=3.9.8)', 'yarl (>=1.6.0)']
dependencies = ['aiohttp (>=3.0.0)', 'awesomeversion (>=22.0.0)', 'orjson (>=3.9.8)', 'probatio (>=0.9.0)', 'yarl (>=1.6.0)']

[project.optional-dependencies]
cli = ["rich>=14.0.0", "textual>=8.0.0,<9.0.0; python_version<'4.0'", "textual-plotext>=1.0.0,<2.0.0; python_version<'4.0'", "typer>=0.15.1"]
Expand Down Expand Up @@ -89,7 +88,7 @@ omit = [
]

[tool.ty.environment]
python-version = "3.11"
python-version = "3.12"

[tool.pylint.MASTER]
ignore = ["tests"]
Expand Down
34 changes: 17 additions & 17 deletions src/fumis/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

import json
import sys
from dataclasses import replace
from typing import TYPE_CHECKING, Annotated

import orjson
import typer
from rich.console import Console
from rich.panel import Panel
Expand Down Expand Up @@ -153,11 +155,6 @@ async def _fetch_info(mac: str, password: str) -> FumisInfo:
return await fumis.update_info()


def _emit_json(payload: object) -> None:
"""Emit a payload as indented JSON on stdout."""
typer.echo(json.dumps(payload, indent=2, default=str))


STATUS_ICONS: dict[StoveStatus, str] = {
StoveStatus.OFF: "[dim]\u2b58[/dim]",
StoveStatus.PRE_HEATING: "[yellow]\u25b2[/yellow]",
Expand Down Expand Up @@ -215,13 +212,13 @@ def _render_info( # noqa: PLR0912, PLR0915 # pylint: disable=too-many-branches

status_table.add_row("\U0001f3e0 Status", _status_display(c))

if error := c.stove_error:
if (error := c.stove_error) != StoveError.NO_ERROR:
error_label = f"E{c.error:03d}" if error == StoveError.UNKNOWN else str(error)
status_table.add_row(
"\u274c Error",
f"[red bold]{error_label}[/red bold] [dim]{error.description}[/dim]",
)
if alert := c.stove_alert:
if (alert := c.stove_alert) != StoveAlert.NO_ALERT:
alert_label = f"A{c.alert:03d}" if alert == StoveAlert.UNKNOWN else str(alert)
status_table.add_row(
"\u26a0\ufe0f Alert",
Expand Down Expand Up @@ -349,9 +346,10 @@ async def info_command(
info = await _fetch_info(mac, password)

if output_json:
data = info.to_dict()
data["unit"]["id"] = "**REDACTED**"
_emit_json(data)
redacted = replace(info, unit=replace(info.unit, id="**REDACTED**"))
# pylint: disable-next=no-member
payload = orjson.dumps(redacted, default=str, option=orjson.OPT_INDENT_2)
typer.echo(payload.decode())
return

_render_info(info)
Expand Down Expand Up @@ -532,21 +530,23 @@ async def errors_command(
c = info.controller

# Current error
if error := c.stove_error:
error = c.stove_error
if error == StoveError.NO_ERROR:
console.print("\u2705 [green bold]No active error[/green bold]")
else:
label = f"E{c.error:03d}" if error == StoveError.UNKNOWN else str(error)
console.print(f"\u274c [red bold]Error {label}:[/red bold] {error.description}")
else:
console.print("\u2705 [green bold]No active error[/green bold]")

# Current alert
if alert := c.stove_alert:
alert = c.stove_alert
if alert == StoveAlert.NO_ALERT:
console.print("\u2705 [green bold]No active alert[/green bold]")
else:
label = f"A{c.alert:03d}" if alert == StoveAlert.UNKNOWN else str(alert)
console.print(
f"\u26a0\ufe0f [yellow bold]Alert {label}:[/yellow bold]"
f" {alert.description}"
)
else:
console.print("\u2705 [green bold]No active alert[/green bold]")

# Error history from diagnostic variables
# var[36..95] in groups of 4: [sequence, error_code, date(YYYYMMDD), time]
Expand All @@ -573,7 +573,7 @@ async def errors_command(
table.add_column("Date")
for i, (code, date_val, time_val) in enumerate(history, 1):
err = StoveError.from_code(code)
if err is None or err == StoveError.UNKNOWN:
if err == StoveError.UNKNOWN:
label = f"E{code:03d}"
desc = f"Unknown ({code})"
else:
Expand Down
4 changes: 2 additions & 2 deletions src/fumis/cli/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ def update_info(self, info: FumisInfo) -> None:
eco_str = "\U0001f331 On" if eco.enabled else "Off"

error_str = ""
if error := c.stove_error:
if (error := c.stove_error) != StoveError.NO_ERROR:
label = f"E{c.error:03d}" if error == StoveError.UNKNOWN else str(error)
error_str = f"\u274c {label}: {error.description}"
if alert := c.stove_alert:
if (alert := c.stove_alert) != StoveAlert.NO_ALERT:
label = f"A{c.alert:03d}" if alert == StoveAlert.UNKNOWN else str(alert)
error_str += f" \u26a0\ufe0f {label}: {alert.description}"

Expand Down
26 changes: 12 additions & 14 deletions src/fumis/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ class StoveError(StrEnum):
Use `StoveError.from_code()` to convert the raw integer from the API.
"""

NO_ERROR = "E000"
"""No active error."""

IGNITION_FAILED = "E101"
"""Ignition failed, water overtemperature, or backfire protection."""

Expand Down Expand Up @@ -197,13 +200,8 @@ class StoveError(StrEnum):
"""Unknown or unmapped error code from the API."""

@classmethod
def from_code(cls, code: int) -> StoveError | None:
"""Convert a raw error code integer to a StoveError enum.

Returns None when there is no active error (code 0).
"""
if code == 0:
return None
def from_code(cls, code: int) -> StoveError:
"""Convert a raw error code integer to a StoveError enum."""
formatted = f"E{code:03d}"
try:
return cls(formatted)
Expand All @@ -225,6 +223,9 @@ class StoveAlert(StrEnum):
Use `StoveAlert.from_code()` to convert the raw integer from the API.
"""

NO_ALERT = "A000"
"""No active alert."""

LOW_FUEL = "A001"
"""Low fuel level - refuel the tank."""

Expand All @@ -250,13 +251,8 @@ class StoveAlert(StrEnum):
"""Unknown or unmapped alert code from the API."""

@classmethod
def from_code(cls, code: int) -> StoveAlert | None:
"""Convert a raw alert code integer to a StoveAlert enum.

Returns None when there is no active alert (code 0).
"""
if code == 0:
return None
def from_code(cls, code: int) -> StoveAlert:
"""Convert a raw alert code integer to a StoveAlert enum."""
formatted = f"A{code:03d}"
try:
return cls(formatted)
Expand All @@ -272,6 +268,7 @@ def description(self) -> str:


_ERROR_DESCRIPTIONS: dict[StoveError, str] = {
StoveError.NO_ERROR: "No error",
StoveError.IGNITION_FAILED: (
"Ignition failed / water overtemperature / backfire protection"
),
Expand Down Expand Up @@ -301,6 +298,7 @@ def description(self) -> str:
}

_ALERT_DESCRIPTIONS: dict[StoveAlert, str] = {
StoveAlert.NO_ALERT: "No alert",
StoveAlert.LOW_FUEL: "Low fuel level",
StoveAlert.SERVICE_DUE: "Service due",
StoveAlert.FLUE_GAS_WARNING: "Flue gas temperature warning",
Expand Down
Loading