Skip to content

Commit b96983e

Browse files
dev: Improved everything
1 parent d1e5d96 commit b96983e

File tree

10 files changed

+2250
-129
lines changed

10 files changed

+2250
-129
lines changed

.github/workflows/lint.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ jobs:
2222
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
2323
with:
2424
python-version: "3.13"
25-
cache: "pip"
25+
26+
- name: Install uv
27+
run: pip install uv
2628

2729
- name: Install requirements
28-
run: python3 -m pip install -r requirements.txt
30+
run: uv pip install --system -r pyproject.toml
2931

3032
- name: Lint
31-
run: python3 -m ruff check .
33+
run: uv run ruff check .
3234

3335
- name: Format
34-
run: python3 -m ruff format . --check
36+
run: uv run ruff format . --check

custom_components/panda_status/config_flow.py

Lines changed: 12 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,17 @@
22

33
from __future__ import annotations
44

5+
from typing import Any
6+
57
import voluptuous as vol
68

79
from custom_components.panda_status import tools
8-
from homeassistant import config_entries
9-
from homeassistant.config_entries import OptionsFlowWithReload
10+
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
1011
from homeassistant.const import CONF_NAME, CONF_URL
11-
from homeassistant.core import callback
1212
from homeassistant.helpers import selector
1313

1414
from .const import DOMAIN, LOGGER
15-
from .websocket import (
16-
PandaStatusWebSocket,
17-
PandaStatusWebsocketCommunicationError,
18-
PandaStatusWebsocketError,
19-
)
15+
from .websocket import PandaStatusWebsocketCommunicationError, PandaStatusWebsocketError
2016

2117
OPTIONS_SCHEMA = vol.Schema(
2218
{
@@ -30,27 +26,23 @@
3026
)
3127

3228

33-
async def _test_credentials(url: str) -> dict:
34-
"""Validate credentials."""
35-
client = PandaStatusWebSocket(url=url, session=None)
36-
return await client.async_get_data()
37-
38-
39-
class PandaStatusFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
29+
class PandaStatusFlowHandler(ConfigFlow, domain=DOMAIN):
4030
"""Config flow for PandaStatus."""
4131

4232
VERSION = 1
4333
MINOR_VERSION = 1
4434

4535
async def async_step_user(
46-
self, user_input: dict | None = None
47-
) -> config_entries.ConfigFlowResult:
36+
self, user_input: dict[str, Any] | None = None
37+
) -> ConfigFlowResult:
4838
"""Handle a flow initialized by the user."""
4939
errors = {}
5040
if user_input is not None:
5141
try:
5242
# Try and get initial data to validate the connection
53-
initial_data = await _test_credentials(url=user_input[CONF_URL])
43+
initial_data = await tools.test_credentials(
44+
url=user_input.get(CONF_URL)
45+
)
5446
except PandaStatusWebsocketCommunicationError as exception:
5547
LOGGER.error(exception)
5648
errors["base"] = "connection"
@@ -63,7 +55,7 @@ async def async_step_user(
6355

6456
# Use tools to extract printer and device name
6557
printer_name = tools.get_printer_name(initial_data=initial_data)
66-
device_name = user_input[CONF_NAME] or tools.get_device_name(
58+
device_name = user_input.get(CONF_NAME, None) or tools.get_device_name(
6759
initial_data=initial_data
6860
)
6961

@@ -72,7 +64,7 @@ async def async_step_user(
7264
title=device_name,
7365
description=f"Panda status for {printer_name}",
7466
data={
75-
CONF_URL: user_input[CONF_URL],
67+
CONF_URL: user_input.get(CONF_URL),
7668
CONF_NAME: device_name,
7769
},
7870
)
@@ -82,59 +74,3 @@ async def async_step_user(
8274
data_schema=OPTIONS_SCHEMA,
8375
errors=errors,
8476
)
85-
86-
@staticmethod
87-
@callback
88-
def async_get_options_flow(
89-
config_entry: config_entries.ConfigEntry,
90-
) -> config_entries.OptionsFlow:
91-
"""Return the options flow handler for PandaStatus."""
92-
return PandaStatusOptionsFlowHandler(config_entry)
93-
94-
95-
class PandaStatusOptionsFlowHandler(OptionsFlowWithReload):
96-
"""Options flow for PandaStatus."""
97-
98-
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
99-
"""Initialize options flow."""
100-
self.config_entry = config_entry
101-
102-
async def async_step_init(
103-
self, user_input: dict | None = None
104-
) -> config_entries.ConfigFlowResult:
105-
"""Manage the options."""
106-
errors = {}
107-
if user_input is not None:
108-
try:
109-
# Try and get initial data to validate the connection
110-
initial_data = await _test_credentials(url=user_input[CONF_URL])
111-
except PandaStatusWebsocketCommunicationError as exception:
112-
LOGGER.error(exception)
113-
errors["base"] = "connection"
114-
except PandaStatusWebsocketError as exception:
115-
LOGGER.exception(exception)
116-
errors["base"] = "unknown"
117-
else:
118-
# Use tools to extract printer and device name
119-
printer_name = tools.get_printer_name(initial_data=initial_data)
120-
device_name = user_input[CONF_NAME] or tools.get_device_name(
121-
initial_data=initial_data
122-
)
123-
124-
# If user provided a name, use it; otherwise, use the device name
125-
return self.async_create_entry(
126-
title=device_name,
127-
description=f"Panda status for {printer_name}",
128-
data={
129-
CONF_URL: user_input[CONF_URL],
130-
CONF_NAME: device_name,
131-
},
132-
)
133-
134-
return self.async_show_form(
135-
step_id="init",
136-
data_schema=self.add_suggested_values_to_schema(
137-
OPTIONS_SCHEMA, self.config_entry.options
138-
),
139-
errors=errors,
140-
)
Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1-
"""System Health platform for panda_status custom component."""
1+
"""System health platform for the Panda Status custom component."""
22

3-
from typing import TYPE_CHECKING, Any
3+
from __future__ import annotations
4+
5+
import logging
6+
from typing import TYPE_CHECKING, Any, cast
47

5-
from custom_components.panda_status.const import DOMAIN
6-
from homeassistant.components import system_health
78
from homeassistant.const import CONF_URL
89
from homeassistant.core import HomeAssistant, callback
910

11+
from . import tools
12+
from .const import DOMAIN
13+
from .websocket import PandaStatusWebsocketError, PandaStatusWebsocketTimeoutError
14+
1015
if TYPE_CHECKING:
16+
from homeassistant.components import system_health
17+
1118
from .data import PandaStatusConfigEntry
1219

20+
_LOGGER = logging.getLogger(__name__)
21+
1322

1423
@callback
1524
def async_register(
@@ -21,11 +30,30 @@ def async_register(
2130

2231

2332
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
24-
"""Get info for the info page."""
25-
config_entry: PandaStatusConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
26-
27-
return {
28-
"can_reach_server": system_health.async_check_can_reach_url(
29-
hass, config_entry.data[CONF_URL]
30-
),
31-
}
33+
"""Return system-health information for the Panda Status integration."""
34+
# Grab the first config entry for this integration.
35+
# If none are present we report nothing.
36+
config_entries = hass.config_entries.async_entries(DOMAIN)
37+
if not config_entries:
38+
_LOGGER.warning("No %s config entries found", DOMAIN)
39+
return {}
40+
41+
config_entry: PandaStatusConfigEntry = cast(
42+
"PandaStatusConfigEntry", config_entries[0]
43+
)
44+
45+
url = config_entry.data.get(CONF_URL)
46+
if not url:
47+
_LOGGER.warning("Config entry for %s does not contain a URL", DOMAIN)
48+
return {"websocket_reachable": "Missing URL"}
49+
50+
try:
51+
await tools.test_credentials(url)
52+
except (TimeoutError, PandaStatusWebsocketTimeoutError):
53+
data = "timeout"
54+
except (Exception, PandaStatusWebsocketError): # noqa: BLE001
55+
data = "unreachable"
56+
else:
57+
data = "ok"
58+
59+
return {"websocket_reachable": data}

custom_components/panda_status/tools.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from typing import Any
44

5+
from custom_components.panda_status import PandaStatusWebSocket
6+
57

68
def extract_value(data: dict, dotted_key: str) -> Any:
79
"""Extract nested JSON value using dotted path."""
@@ -22,3 +24,9 @@ def get_printer_name(initial_data: dict) -> str:
2224
def get_device_name(initial_data: dict) -> str:
2325
"""Get device name from initial data."""
2426
return f"{extract_value(initial_data, 'printer.name')} - Panda Status"
27+
28+
29+
async def test_credentials(url: str) -> dict:
30+
"""Validate credentials."""
31+
client = PandaStatusWebSocket(url=url, session=None)
32+
return await client.async_get_data()
Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,40 @@
11
{
2-
"config": {
3-
"step": {
4-
"user": {
5-
"description": "If you need help with the configuration have a look here: https://github.com/ping-localhost/panda_status",
6-
"data": {
7-
"url": "URL",
8-
"name": "Name for the device (optional)"
9-
}
10-
}
11-
},
12-
"error": {
13-
"connection": "Unable to connect to the server.",
14-
"unknown": "Unknown error occurred."
15-
},
16-
"abort": {
17-
"already_configured": "This entry is already configured."
2+
"config": {
3+
"step": {
4+
"user": {
5+
"description": "If you need help with the configuration have a look here: https://github.com/ping-localhost/panda_status",
6+
"data": {
7+
"url": "URL",
8+
"name": "Name for the device (optional)"
189
}
10+
}
1911
},
20-
"system_health": {
21-
"info": {
22-
"can_reach_server": "WebSocket reachable"
12+
"error": {
13+
"connection": "Unable to connect to the server.",
14+
"unknown": "Unknown error occurred."
15+
},
16+
"abort": {
17+
"already_configured": "This entry is already configured."
18+
}
19+
},
20+
"options": {
21+
"step": {
22+
"init": {
23+
"description": "If you need help with the configuration have a look here: https://github.com/ping-localhost/panda_status",
24+
"data": {
25+
"url": "URL",
26+
"name": "Name for the device (optional)"
2327
}
28+
}
29+
},
30+
"error": {
31+
"connection": "Unable to connect to the server.",
32+
"unknown": "Unknown error occurred."
33+
}
34+
},
35+
"system_health": {
36+
"info": {
37+
"websocket_reachable": "WebSocket reachable"
2438
}
39+
}
2540
}

custom_components/panda_status/websocket.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import logging
88

99
from websockets.asyncio.client import ClientConnection, connect
10-
from websockets.exceptions import ConnectionClosed
10+
from websockets.exceptions import ConnectionClosed, InvalidStatus
1111

1212
_LOGGER = logging.getLogger(__name__)
1313

@@ -16,6 +16,12 @@ class PandaStatusWebsocketError(Exception):
1616
"""Exception to indicate a general WebSocket error."""
1717

1818

19+
class PandaStatusWebsocketTimeoutError(
20+
PandaStatusWebsocketError,
21+
):
22+
"""Exception to indicate a timeout error."""
23+
24+
1925
class PandaStatusWebsocketCommunicationError(
2026
PandaStatusWebsocketError,
2127
):
@@ -55,18 +61,18 @@ async def async_get_data(self) -> dict:
5561
PandaStatusWebsocketError: If JSON is invalid or connection fails.
5662
5763
"""
58-
async with self._session as websocket:
59-
try:
64+
try:
65+
async with self._session as websocket:
6066
data = json.loads(await websocket.recv())
61-
except TimeoutError as e:
62-
msg = f"Timeout error getting data - {e}"
63-
raise PandaStatusWebsocketCommunicationError(msg) from e
64-
except (OSError, ConnectionClosed, TypeError) as e:
65-
msg = f"Communication error - {e}"
66-
raise PandaStatusWebsocketCommunicationError(msg) from e
67-
except Exception as e:
68-
msg = f"Unexpected error parsing data payload - {e}"
69-
raise PandaStatusWebsocketError(msg) from e
67+
except TimeoutError as e:
68+
msg = f"Timeout error getting data - {e}"
69+
raise PandaStatusWebsocketTimeoutError(msg) from e
70+
except (OSError, ConnectionClosed, TypeError, InvalidStatus) as e:
71+
msg = f"Communication error - {e}"
72+
raise PandaStatusWebsocketCommunicationError(msg) from e
73+
except Exception as e:
74+
msg = f"Unexpected error parsing data payload - {e}"
75+
raise PandaStatusWebsocketError(msg) from e
7076

7177
_LOGGER.debug("Latest data received: %s", json.dumps(data))
7278

pyproject.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[project]
2+
name = "panda_status"
3+
version = "1.2.0"
4+
requires-python = ">=3.13.2"
5+
dependencies = [
6+
"colorlog>=6.9.0",
7+
"hassil>=3.2.0",
8+
"home-assistant-intents==2025.8.29",
9+
"homeassistant==2025.8.1",
10+
"mutagen>=1.47.0",
11+
"numpy>=2.3.2",
12+
"pip>=25.2",
13+
"ruff>=0.12.12",
14+
"websockets>=15.0.1",
15+
]
16+
17+
[dependency-groups]
18+
dev = [
19+
"ruff>=0.12.12"
20+
]

requirements.txt

Lines changed: 0 additions & 5 deletions
This file was deleted.

scripts/setup

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ set -e
44

55
cd "$(dirname "$0")/.."
66

7-
python3 -m pip install --requirement requirements.txt
7+
uv venv && uv pip install -r pyproject.toml

0 commit comments

Comments
 (0)