Skip to content

Commit

Permalink
dev8
Browse files Browse the repository at this point in the history
  • Loading branch information
spacemanspiff2007 committed Apr 15, 2024
1 parent d8faf40 commit aa83fc1
Show file tree
Hide file tree
Showing 12 changed files with 84 additions and 26 deletions.
2 changes: 1 addition & 1 deletion docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Example:
.. autopydantic_model:: sml2mqtt.config.inputs.HttpSourceSettings
:exclude-members: get_device_name
:exclude-members: get_device_name, get_request_timeout

Example:

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ pytest-asyncio == 0.23.6
aioresponses == 0.7.6

# Linter
ruff == 0.3.4
ruff == 0.3.7
4 changes: 2 additions & 2 deletions requirements_setup.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
aiomqtt == 2.0.1
pyserial-asyncio == 0.6
easyconfig == 0.3.2
pydantic == 2.6.4
pydantic == 2.7.0
smllib == 1.4
aiohttp == 3.9.3
aiohttp == 3.9.4
3 changes: 2 additions & 1 deletion src/sml2mqtt/__log__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def setup_log():
# This is the longest logger name str
chars = 0
for device in sml2mqtt.CONFIG.inputs:
chars = max(len(f'sml.device.{device.url}'), chars)
# Name of the longest logger, should be the device status
chars = max(len(get_logger(device.get_device_name()).getChild('status').name), chars)
log_format = logging.Formatter("[{asctime:s}] [{name:" + str(chars) + "s}] {levelname:8s} | {message:s}", style='{')

# File Handler
Expand Down
2 changes: 1 addition & 1 deletion src/sml2mqtt/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '3.0.DEV-7'
__version__ = '3.0.DEV-8'
4 changes: 2 additions & 2 deletions src/sml2mqtt/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ class Settings(AppBaseModel):
def default_config() -> Settings:
# File defaults
return Settings(
inputs=[SerialSourceSettings(type='serial', url='COM1', timeout=3),
SerialSourceSettings(type='serial', url='/dev/ttyS0', timeout=3), ],
inputs=[SerialSourceSettings(type='serial', url='COM1', timeout=6),
SerialSourceSettings(type='serial', url='/dev/ttyS0', timeout=6), ],
devices={
'device_id_hex': SmlDeviceConfig(
mqtt=OptionalMqttPublishConfig(topic='DEVICE_BASE_TOPIC'),
Expand Down
22 changes: 16 additions & 6 deletions src/sml2mqtt/config/inputs.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
from typing import Literal

import serial
from aiohttp import ClientTimeout
from easyconfig import BaseModel
from pydantic import (
AnyHttpUrl,
Field,
StrictFloat,
StrictInt,
confloat,
constr,
field_validator,
model_validator,
)
from typing_extensions import override

from sml2mqtt.config.types import log


class SmlSourceSettingsBase(BaseModel):
def get_device_name(self) -> str:
Expand All @@ -25,7 +27,7 @@ class SerialSourceSettings(SmlSourceSettingsBase):

url: constr(strip_whitespace=True, min_length=1, strict=True) = Field(..., description='Device path')
timeout: StrictInt | StrictFloat = Field(
default=3, description='Seconds after which a timeout will be detected (default=3)')
default=6, description='Seconds after which a timeout will be detected (default=6)')

baudrate: int = Field(9600, in_file=False)
parity: str = Field('None', in_file=False)
Expand Down Expand Up @@ -76,9 +78,9 @@ class HttpSourceSettings(SmlSourceSettingsBase):

url: AnyHttpUrl = Field(..., description='Url')
timeout: StrictInt | StrictFloat = Field(
default=3, description='Seconds after which a timeout will be detected (default=3)')
default=6, description='Seconds after which a timeout will be detected (default=6)')

interval: StrictInt | StrictFloat = Field(default=1, description='Delay between requests', ge=0.1)
interval: StrictInt | StrictFloat = Field(default=2, description='Delay between requests', ge=0.1)
user: str = Field(default='', description='User (if needed)')
password: str = Field(default='', description='Password (if needed)')

Expand All @@ -88,8 +90,16 @@ def get_device_name(self) -> str:

@model_validator(mode='after')
def check_timeout_gt_interval(self):
if self.timeout <= self.interval:
msg = 'Timeout must be greater than interval'
if self.interval * 2 > self.timeout:
msg = 'Timeout must be greater equal than 2 * interval'
raise ValueError(msg)

# Timeout is interval, and we automatically retry 3 times before we fail
if self.interval * 3 > self.timeout:
log.warning('The recommendation for timeout should is least 3 * interval '
f'({self.interval * 3:.0f})! Is {self.timeout}')

return self

def get_request_timeout(self) -> ClientTimeout:
return ClientTimeout(self.interval)
5 changes: 5 additions & 0 deletions src/sml2mqtt/config/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

from pydantic import Strict, StrictFloat, StrictInt, StringConstraints

from sml2mqtt.__log__ import get_logger


log = get_logger('config')


ObisHex = Annotated[
str,
Expand Down
10 changes: 5 additions & 5 deletions src/sml2mqtt/sml_source/http.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from asyncio import sleep
from asyncio import TimeoutError, sleep
from typing import TYPE_CHECKING, Final

from aiohttp import BasicAuth, ClientError, ClientSession, ClientTimeout
Expand Down Expand Up @@ -53,7 +53,7 @@ async def create(cls, device: DeviceProto, settings: HttpSourceSettings):
if settings.user or settings.password:
auth = BasicAuth(settings.user, settings.password)

return cls(device, str(settings.url), settings.interval, auth, timeout=ClientTimeout(settings.timeout / 2))
return cls(device, str(settings.url), settings.interval, auth, timeout=settings.get_request_timeout())

def __init__(self, device: DeviceProto,
url: str, interval: float,
Expand Down Expand Up @@ -98,11 +98,11 @@ async def _http_task(self) -> None:
payload = await resp.read()
com_errors = 0
except Exception as e:
if isinstance(e, (ClientError, HttpStatusError)):
if isinstance(e, (ClientError, HttpStatusError, TimeoutError)):
com_errors += 1
max_ignore: int = 3
max_ignore: int = 7
if com_errors <= max_ignore:
interval = com_errors * self.interval / max_ignore
interval = (((com_errors - 0.5) ** 2) / 4 + 0.5) * self.interval
log.debug(f'Ignored {com_errors:d}/{max_ignore:d} {e}')
continue

Expand Down
4 changes: 2 additions & 2 deletions tests/config/test_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ def test_default():
inputs:
- type: serial
url: COM1 # Device path
timeout: 3 # Seconds after which a timeout will be detected (default=3)
timeout: 6 # Seconds after which a timeout will be detected (default=6)
- type: serial
url: /dev/ttyS0 # Device path
timeout: 3 # Seconds after which a timeout will be detected (default=3)
timeout: 6 # Seconds after which a timeout will be detected (default=6)
devices: # Device configuration by ID or url
device_id_hex:
mqtt: # Optional MQTT configuration for this meter.
Expand Down
4 changes: 2 additions & 2 deletions tests/test_source/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


async def test_create_http_no_auth(device_mock):
cfg = HttpSourceSettings(type='http', url='http://localhost/a', interval=3, timeout=6)
cfg = HttpSourceSettings(type='http', url='http://localhost/a', interval=3, timeout=9)
obj = await create_source(device_mock, cfg)

assert isinstance(obj, HttpSource)
Expand All @@ -20,7 +20,7 @@ async def test_create_http_no_auth(device_mock):


async def test_create_http_auth(device_mock):
cfg = HttpSourceSettings(type='http', url='http://localhost/a', interval=3, timeout=6, user='u', password='p')
cfg = HttpSourceSettings(type='http', url='http://localhost/a', interval=3, timeout=9, user='u', password='p')
obj = await create_source(device_mock, cfg)

assert isinstance(obj, HttpSource)
Expand Down
48 changes: 45 additions & 3 deletions tests/test_source/test_http.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
from asyncio import TimeoutError

import pytest
from aiohttp import ClientTimeout
Expand Down Expand Up @@ -34,13 +35,32 @@ async def test_200(sml_data_1, device_mock, source):


@pytest.mark.skipif(sys.platform.lower() != "win32", reason="It's a mystery why this fails in CI")
async def test_400(device_mock, source):
async def test_400_then_200(sml_data_1, device_mock, source):

with aioresponses() as m:
m.get(source.url, status=404)
m.get(source.url, status=404)
m.get(source.url, status=404)
m.get(source.url, status=404)
m.get(source.url, body=sml_data_1)

source.start()
try:
await wait_for_call(device_mock.on_source_data, 1)
finally:
await source.cancel_and_wait()

device_mock.on_source_data.assert_called_once_with(sml_data_1)
device_mock.on_source_failed.assert_not_called()
device_mock.on_error.assert_not_called()

await close_session()


@pytest.mark.skipif(sys.platform.lower() != "win32", reason="It's a mystery why this fails in CI")
async def test_400(device_mock, source):

with aioresponses() as m:
for _ in range(10):
m.get(source.url, status=404)

source.start()
try:
Expand All @@ -57,3 +77,25 @@ async def test_400(device_mock, source):

def test_error_repr():
assert str(HttpStatusError(404)) == 'HttpStatusError: 404'


@pytest.mark.skipif(sys.platform.lower() != "win32", reason="It's a mystery why this fails in CI")
async def test_timeout(device_mock, source):

e = TimeoutError()

with aioresponses() as m:
for _ in range(10):
m.get(source.url, exception=e)

source.start()
try:
await wait_for_call(device_mock.on_error, 1)
finally:
await source.cancel_and_wait()

device_mock.on_source_data.assert_not_called()
device_mock.on_source_failed.assert_not_called()
device_mock.on_error.assert_called_once_with(e, show_traceback=False)

await close_session()

0 comments on commit aa83fc1

Please sign in to comment.