Skip to content

Commit 85b794d

Browse files
drc38lbbrhzn
andauthored
switch to new websockets asyncio (lbbrhzn#1439)
* switch to new websockets asyncio * add errors raised * fix error raise * extra detail on nosub test * add test for autoconfig * remove old tests * update deprecated enums in v16 tests * extra test for reboot required * update pre-commit * fix key --------- Co-authored-by: lbbrhzn <[email protected]>
1 parent 9bddf55 commit 85b794d

10 files changed

+124
-145
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ repos:
1212
# Run the formatter.
1313
- id: ruff-format
1414
- repo: https://github.com/pre-commit/pre-commit-hooks
15-
rev: v4.5.0
15+
rev: v5.0.0
1616
hooks:
1717
- id: check-executables-have-shebangs
1818
stages: [manual]

custom_components/ocpp/api.py

+33-23
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
from homeassistant.config_entries import ConfigEntry
99
from homeassistant.const import STATE_OK
1010
from homeassistant.core import HomeAssistant
11-
from websockets import Subprotocol
12-
import websockets.protocol
11+
from websockets import Subprotocol, NegotiationError
1312
import websockets.server
13+
from websockets.asyncio.server import ServerConnection
1414

1515
from .chargepoint import CentralSystemSettings
1616
from .ocppv16 import ChargePoint as ChargePointv16
@@ -21,7 +21,6 @@
2121
CONF_CSID,
2222
CONF_HOST,
2323
CONF_PORT,
24-
CONF_SKIP_SCHEMA_VALIDATION,
2524
CONF_SSL,
2625
CONF_SSL_CERTFILE_PATH,
2726
CONF_SSL_KEYFILE_PATH,
@@ -34,7 +33,6 @@
3433
DEFAULT_CSID,
3534
DEFAULT_HOST,
3635
DEFAULT_PORT,
37-
DEFAULT_SKIP_SCHEMA_VALIDATION,
3836
DEFAULT_SSL,
3937
DEFAULT_SSL_CERTFILE_PATH,
4038
DEFAULT_SSL_KEYFILE_PATH,
@@ -112,10 +110,11 @@ async def create(hass: HomeAssistant, entry: ConfigEntry):
112110
"""Create instance and start listening for OCPP connections on given port."""
113111
self = CentralSystem(hass, entry)
114112

115-
server = await websockets.server.serve(
113+
server = await websockets.serve(
116114
self.on_connect,
117115
self.host,
118116
self.port,
117+
select_subprotocol=self.select_subprotocol,
119118
subprotocols=self.subprotocols,
120119
ping_interval=None, # ping interval is not used here, because we send pings mamually in ChargePoint.monitor_connection()
121120
ping_timeout=None,
@@ -125,27 +124,38 @@ async def create(hass: HomeAssistant, entry: ConfigEntry):
125124
self._server = server
126125
return self
127126

128-
async def on_connect(self, websocket: websockets.server.WebSocketServerProtocol):
127+
def select_subprotocol(
128+
self, connection: ServerConnection, subprotocols
129+
) -> Subprotocol | None:
130+
"""Override default subprotocol selection."""
131+
132+
# Server offers at least one subprotocol but client doesn't offer any.
133+
# Default to None
134+
if not subprotocols:
135+
return None
136+
137+
# Server and client both offer subprotocols. Look for a shared one.
138+
proposed_subprotocols = set(subprotocols)
139+
for subprotocol in proposed_subprotocols:
140+
if subprotocol in self.subprotocols:
141+
return subprotocol
142+
143+
# No common subprotocol was found.
144+
raise NegotiationError(
145+
"invalid subprotocol; expected one of " + ", ".join(self.subprotocols)
146+
)
147+
148+
async def on_connect(self, websocket: ServerConnection):
129149
"""Request handler executed for every new OCPP connection."""
130-
if self.config.get(CONF_SKIP_SCHEMA_VALIDATION, DEFAULT_SKIP_SCHEMA_VALIDATION):
131-
_LOGGER.warning("Skipping websocket subprotocol validation")
150+
if websocket.subprotocol is not None:
151+
_LOGGER.info("Websocket Subprotocol matched: %s", websocket.subprotocol)
132152
else:
133-
if websocket.subprotocol is not None:
134-
_LOGGER.info("Websocket Subprotocol matched: %s", websocket.subprotocol)
135-
else:
136-
# In the websockets lib if no subprotocols are supported by the
137-
# client and the server, it proceeds without a subprotocol,
138-
# so we have to manually close the connection.
139-
_LOGGER.warning(
140-
"Protocols mismatched | expected Subprotocols: %s,"
141-
" but client supports %s | Closing connection",
142-
websocket.available_subprotocols,
143-
websocket.request_headers.get("Sec-WebSocket-Protocol", ""),
144-
)
145-
return await websocket.close()
153+
_LOGGER.info(
154+
"Websocket Subprotocol not provided by charger: default to ocpp1.6"
155+
)
146156

147-
_LOGGER.info(f"Charger websocket path={websocket.path}")
148-
cp_id = websocket.path.strip("/")
157+
_LOGGER.info(f"Charger websocket path={websocket.request.path}")
158+
cp_id = websocket.request.path.strip("/")
149159
cp_id = cp_id[cp_id.rfind("/") + 1 :]
150160
if self.settings.cpid not in self.charge_points:
151161
_LOGGER.info(f"Charger {cp_id} connected to {self.host}:{self.port}.")

custom_components/ocpp/chargepoint.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
from homeassistant.helpers import device_registry, entity_component, entity_registry
2222
import homeassistant.helpers.config_validation as cv
2323
import voluptuous as vol
24-
import websockets.server
24+
from websockets.asyncio.server import ServerConnection
25+
from websockets.exceptions import WebSocketException
26+
from websockets.protocol import State
2527

2628
from ocpp.charge_point import ChargePoint as cp
2729
from ocpp.v16 import call as callv16
@@ -471,7 +473,7 @@ async def monitor_connection(self):
471473
self._metrics[cstat.latency_pong.value].unit = "ms"
472474
connection = self._connection
473475
timeout_counter = 0
474-
while connection.open:
476+
while connection.state is State.OPEN:
475477
try:
476478
await asyncio.sleep(self.central.websocket_ping_interval)
477479
time0 = time.perf_counter()
@@ -529,7 +531,7 @@ async def run(self, tasks):
529531
await asyncio.gather(*self.tasks)
530532
except TimeoutError:
531533
pass
532-
except websockets.exceptions.WebSocketException as websocket_exception:
534+
except WebSocketException as websocket_exception:
533535
_LOGGER.debug(f"Connection closed to '{self.id}': {websocket_exception}")
534536
except Exception as other_exception:
535537
_LOGGER.error(
@@ -542,13 +544,13 @@ async def run(self, tasks):
542544
async def stop(self):
543545
"""Close connection and cancel ongoing tasks."""
544546
self.status = STATE_UNAVAILABLE
545-
if self._connection.open:
547+
if self._connection.state is State.OPEN:
546548
_LOGGER.debug(f"Closing websocket to '{self.id}'")
547549
await self._connection.close()
548550
for task in self.tasks:
549551
task.cancel()
550552

551-
async def reconnect(self, connection: websockets.server.WebSocketServerProtocol):
553+
async def reconnect(self, connection: ServerConnection):
552554
"""Reconnect charge point."""
553555
_LOGGER.debug(f"Reconnect websocket to {self.id}")
554556

custom_components/ocpp/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"issue_tracker": "https://github.com/lbbrhzn/ocpp/issues",
1515
"requirements": [
1616
"ocpp>=1.0.0",
17-
"websockets>=12.0"
17+
"websockets>=13.1"
1818
],
1919
"version": "0.6.1"
2020
}

custom_components/ocpp/ocppv16.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from homeassistant.config_entries import ConfigEntry
1010
from homeassistant.core import HomeAssistant
1111
import voluptuous as vol
12-
import websockets.server
12+
from websockets.asyncio.server import ServerConnection
1313

1414
from ocpp.routing import on
1515
from ocpp.v16 import call, call_result
@@ -74,7 +74,7 @@ class ChargePoint(cp):
7474
def __init__(
7575
self,
7676
id: str,
77-
connection: websockets.server.WebSocketServerProtocol,
77+
connection: ServerConnection,
7878
hass: HomeAssistant,
7979
entry: ConfigEntry,
8080
central: CentralSystemSettings,

custom_components/ocpp/ocppv201.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from homeassistant.const import UnitOfTime
1212
from homeassistant.core import HomeAssistant, SupportsResponse, ServiceResponse
1313
from homeassistant.exceptions import ServiceValidationError, HomeAssistantError
14-
import websockets.server
14+
from websockets.asyncio.server import ServerConnection
1515

1616
from ocpp.routing import on
1717
from ocpp.v201 import call, call_result
@@ -85,7 +85,7 @@ class ChargePoint(cp):
8585
def __init__(
8686
self,
8787
id: str,
88-
connection: websockets.server.WebSocketServerProtocol,
88+
connection: ServerConnection,
8989
hass: HomeAssistant,
9090
entry: ConfigEntry,
9191
central: CentralSystemSettings,

tests/charge_point_test.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
from pytest_homeassistant_custom_component.common import MockConfigEntry
2424
from typing import Any
2525
from collections.abc import Callable, Awaitable
26-
import websockets
26+
from websockets import connect
27+
from websockets.asyncio.client import ClientConnection
2728

2829

2930
async def set_switch(hass: HomeAssistant, cs: CentralSystem, key: str, on: bool):
@@ -108,12 +109,12 @@ async def run_charge_point_test(
108109
config_entry: MockConfigEntry,
109110
identity: str,
110111
subprotocols: list[str] | None,
111-
charge_point: Callable[[websockets.WebSocketClientProtocol], ChargePoint],
112+
charge_point: Callable[[ClientConnection], ChargePoint],
112113
parallel_tests: list[Callable[[ChargePoint], Awaitable]],
113114
) -> Any:
114115
"""Connect web socket client to the CSMS and run a number of tests in parallel."""
115116
completed: list[list[bool]] = [[] for _ in parallel_tests]
116-
async with websockets.connect(
117+
async with connect(
117118
f"ws://127.0.0.1:{config_entry.data[CONF_PORT]}/{identity}",
118119
subprotocols=[Subprotocol(s) for s in subprotocols]
119120
if subprotocols is not None

tests/conftest.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ def skip_notifications_fixture():
3535
def bypass_get_data_fixture():
3636
"""Skip calls to get data from API."""
3737
future = asyncio.Future()
38-
future.set_result(websockets.WebSocketServer)
38+
future.set_result(websockets.asyncio.server.Server)
3939
with (
40-
patch("websockets.server.serve", return_value=future),
41-
patch("websockets.server.WebSocketServer.close"),
42-
patch("websockets.server.WebSocketServer.wait_closed"),
40+
patch("websockets.asyncio.server.serve", return_value=future),
41+
patch("websockets.asyncio.server.Server.close"),
42+
patch("websockets.asyncio.server.Server.wait_closed"),
4343
):
4444
yield
4545

tests/const.py

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
CONF_PORT: 9002,
7777
CONF_CPID: "test_cpid_2",
7878
CONF_SKIP_SCHEMA_VALIDATION: True,
79+
CONF_MONITORED_VARIABLES_AUTOCONFIG: False,
7980
}
8081

8182
# separate entry for switch so tests can run concurrently

0 commit comments

Comments
 (0)