diff -pruN 2025.3.1-1/.devcontainer/Dockerfile 2025.9.1-1/.devcontainer/Dockerfile
--- 2025.3.1-1/.devcontainer/Dockerfile	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/.devcontainer/Dockerfile	2025-09-06 19:46:05.000000000 +0000
@@ -1,8 +1,8 @@
 # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/python-3/.devcontainer/base.Dockerfile
 
 # [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
-ARG VARIANT="3.10-bullseye"
-FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
+ARG VARIANT="3.13"
+FROM mcr.microsoft.com/devcontainers/python:${VARIANT}
 
 # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
 ARG NODE_VERSION="none"
diff -pruN 2025.3.1-1/.devcontainer/devcontainer.json 2025.9.1-1/.devcontainer/devcontainer.json
--- 2025.3.1-1/.devcontainer/devcontainer.json	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/.devcontainer/devcontainer.json	2025-09-06 19:46:05.000000000 +0000
@@ -5,36 +5,40 @@
 	  "dockerfile": "Dockerfile",
 	  "context": ".."
 	},
-  "postCreateCommand": "pip3 install -e .",
-  "runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
-  "extensions": [
-    "ms-python.python",
-    "ms-python.vscode-pylance",
-    "visualstudioexptteam.vscodeintellicode",
-    "esbenp.prettier-vscode"
-  ],
-  "mounts": [ "type=volume,target=/var/lib/docker" ],
-  "settings": {
-    "terminal.integrated.profiles.linux": {
-      "zsh": {
-        "path": "/usr/bin/zsh"
+  "postCreateCommand": "pip install -r requirements_tests.txt && pre-commit install",
+  "postStartCommand": "pip install -r requirements_tests.txt && pip install -e .",
+  "runArgs": ["-e", "GIT_EDITOR=code --wait"],
+  "customizations": {
+    "vscode": {
+      "extensions": [
+        "charliermarsh.ruff",
+        "esbenp.prettier-vscode",
+        "ms-python.python",
+        "ms-python.vscode-pylance",
+        "visualstudioexptteam.vscodeintellicode"
+      ],
+      "settings": {
+        "terminal.integrated.profiles.linux": {
+          "zsh": {
+            "path": "/usr/bin/zsh"
+          }
+        },
+        "terminal.integrated.defaultProfile.linux": "zsh",
+        "editor.formatOnPaste": false,
+        "editor.formatOnSave": true,
+        "editor.formatOnType": true,
+        "files.trimTrailingWhitespace": true,
+        "python.pythonPath": "/usr/local/bin/python3",
+        "python.linting.pylintEnabled": true,
+        "python.linting.enabled": true,
+        "python.formatting.provider": "ruff",
+        "python.linting.mypyPath": "/usr/local/bin/mypy",
+        "python.linting.pylintPath": "/usr/local/bin/pylint",
+        "python.linting.pydocstylePath": "/usr/local/bin/pydocstyle",
+        "[python]": {
+          "editor.defaultFormatter": "charliermarsh.ruff"
+        }
       }
-    },
-    "terminal.integrated.defaultProfile.linux": "zsh",
-    "editor.formatOnPaste": false,
-    "editor.formatOnSave": true,
-    "editor.formatOnType": true,
-    "files.trimTrailingWhitespace": true,
-    "python.pythonPath": "/usr/local/bin/python3",
-    "python.linting.pylintEnabled": true,
-    "python.linting.enabled": true,
-    "python.formatting.provider": "black",
-    "python.formatting.blackArgs": ["--target-version", "py39"],
-    "python.formatting.blackPath": "/usr/local/bin/black",
-    "python.linting.banditPath": "/usr/local/bin/bandit",
-    "python.linting.flake8Path": "/usr/local/bin/flake8",
-    "python.linting.mypyPath": "/usr/local/bin/mypy",
-    "python.linting.pylintPath": "/usr/local/bin/pylint",
-    "python.linting.pydocstylePath": "/usr/local/bin/pydocstyle"
+    }
   }
 }
diff -pruN 2025.3.1-1/.github/renovate.json 2025.9.1-1/.github/renovate.json
--- 2025.3.1-1/.github/renovate.json	1970-01-01 00:00:00.000000000 +0000
+++ 2025.9.1-1/.github/renovate.json	2025-09-06 19:46:05.000000000 +0000
@@ -0,0 +1,48 @@
+{
+    "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+    "rebaseWhen": "behind-base-branch",
+    "dependencyDashboard": true,
+    "labels": [
+        "dependencies",
+        "no-stale"
+    ],
+    "commitMessagePrefix": "⬆️",
+    "pre-commit": {
+        "enabled": true
+    },
+    "packageRules": [
+        {
+            "matchManagers": [
+                "pep621"
+            ],
+            "addLabels": [
+                "python"
+            ]
+        },
+        {
+            "matchManagers": [
+                "pip_requirements"
+            ],
+            "rangeStrategy": "pin"
+        },
+        {
+            "matchManagers": [
+                "github-actions"
+            ],
+            "addLabels": [
+                "github_actions"
+            ],
+            "rangeStrategy": "pin"
+        },
+        {
+            "matchManagers": [
+                "github-actions"
+            ],
+            "matchUpdateTypes": [
+                "minor",
+                "patch"
+            ],
+            "automerge": true
+        }
+    ]
+}
diff -pruN 2025.3.1-1/.github/workflows/ci.yml 2025.9.1-1/.github/workflows/ci.yml
--- 2025.3.1-1/.github/workflows/ci.yml	1970-01-01 00:00:00.000000000 +0000
+++ 2025.9.1-1/.github/workflows/ci.yml	2025-09-06 19:46:05.000000000 +0000
@@ -0,0 +1,41 @@
+name: CI
+
+on:
+  push:
+    branches: [main]
+  pull_request:
+    branches: [main]
+
+jobs:
+  tests:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v5.0.0
+      - name: Set up Python 3.13
+        uses: actions/setup-python@v6.0.0
+        with:
+          python-version: 3.13
+      - name: Install dependencies
+        run: |
+          pip install -e .
+          pip install -r requirements_tests.txt
+      - name: Execute tests
+        run: |
+          pytest --timeout=10 tests
+
+  pre-commit:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v5.0.0
+      - name: Set up Python 3.13
+        uses: actions/setup-python@v6.0.0
+        with:
+          python-version: 3.13
+      - name: Install dependencies
+        run: |
+          pip install -e .
+          pip install -r requirements_tests.txt
+      - name: Run pre-commit checks
+        run: SKIP=no-commit-to-branch pre-commit run --all-files
diff -pruN 2025.3.1-1/.github/workflows/pythonpublish.yml 2025.9.1-1/.github/workflows/pythonpublish.yml
--- 2025.3.1-1/.github/workflows/pythonpublish.yml	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/.github/workflows/pythonpublish.yml	2025-09-06 19:46:05.000000000 +0000
@@ -9,23 +9,25 @@ on:
 
 jobs:
   deploy:
-
     runs-on: ubuntu-latest
 
     steps:
-    - uses: actions/checkout@v4.2.2
-    - name: Set up Python
-      uses: actions/setup-python@v5.4.0
-      with:
-        python-version: '3.x'
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install setuptools wheel twine
-    - name: Build and publish
-      env:
-        TWINE_USERNAME: __token__
-        TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
-      run: |
-        python setup.py sdist bdist_wheel
-        twine upload dist/*
+      - uses: actions/checkout@v5.0.0
+      - name: Set up Python
+        uses: actions/setup-python@v6.0.0
+        with:
+          python-version: "3.x"
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install build twine
+      - name: Set package version
+        run: |
+          sed -i "s/^version = \".*\"/version = \"${{ github.event.release.tag_name }}\"/" pyproject.toml
+      - name: Build and publish
+        env:
+          TWINE_USERNAME: __token__
+          TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
+        run: |
+          python -m build
+          twine upload dist/*
diff -pruN 2025.3.1-1/.github/workflows/test.yml 2025.9.1-1/.github/workflows/test.yml
--- 2025.3.1-1/.github/workflows/test.yml	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/.github/workflows/test.yml	1970-01-01 00:00:00.000000000 +0000
@@ -1,27 +0,0 @@
-# This workflow will install Python dependencies, run tests and lint with a single version of Python
-# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
-
-name: Run Tests
-
-on:
-  push:
-    branches: [main]
-  pull_request:
-    branches: [main]
-
-jobs:
-  build:
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v4.2.2
-      - name: Set up Python 3.9
-        uses: actions/setup-python@v5.4.0
-        with:
-          python-version: 3.9
-      - name: Install dependencies
-        run: |
-          pip install tox
-      - name: Run Tox
-        run: |
-          tox
diff -pruN 2025.3.1-1/.pre-commit-config.yaml 2025.9.1-1/.pre-commit-config.yaml
--- 2025.3.1-1/.pre-commit-config.yaml	1970-01-01 00:00:00.000000000 +0000
+++ 2025.9.1-1/.pre-commit-config.yaml	2025-09-06 19:46:05.000000000 +0000
@@ -0,0 +1,51 @@
+repos:
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.12.12
+    hooks:
+      - id: ruff
+        args:
+          - --fix
+          - --unsafe-fixes
+      - id: ruff-format
+  - repo: https://github.com/asottile/pyupgrade
+    rev: v3.20.0
+    hooks:
+      - id: pyupgrade
+        args:
+          - --py313-plus
+  - repo: https://github.com/codespell-project/codespell
+    rev: v2.4.1
+    hooks:
+      - id: codespell
+        args:
+          - --ignore-words-list=hass,frequence
+          - --skip="./.*,*.csv,*.json"
+          - --quiet-level=2
+        exclude_types:
+          - csv
+          - json
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v6.0.0
+    hooks:
+      - id: check-executables-have-shebangs
+      - id: check-merge-conflict
+      - id: detect-private-key
+      - id: no-commit-to-branch
+        args: [--branch, main, --branch, dev]
+      - id: requirements-txt-fixer
+
+  - repo: local
+    hooks:
+      - id: mypy
+        name: Check with mypy
+        entry: scripts/run-in-env.sh mypy
+        language: script
+        types:
+          - python
+
+      - id: pylint
+        name: Check with pylint
+        entry: scripts/run-in-env.sh pylint
+        language: script
+        types:
+          - python
diff -pruN 2025.3.1-1/aioecowitt/__init__.py 2025.9.1-1/aioecowitt/__init__.py
--- 2025.3.1-1/aioecowitt/__init__.py	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/aioecowitt/__init__.py	2025-09-06 19:46:05.000000000 +0000
@@ -1,5 +1,7 @@
 """aioEcoWitt API wrapper."""
 
-from .server import EcoWittListener
 from .sensor import EcoWittSensor, EcoWittSensorTypes
+from .server import EcoWittListener
 from .station import EcoWittStation
+
+__all__ = ["EcoWittListener", "EcoWittSensor", "EcoWittSensorTypes", "EcoWittStation"]
diff -pruN 2025.3.1-1/aioecowitt/__main__.py 2025.9.1-1/aioecowitt/__main__.py
--- 2025.3.1-1/aioecowitt/__main__.py	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/aioecowitt/__main__.py	2025-09-06 19:46:05.000000000 +0000
@@ -8,7 +8,7 @@ import sys
 from aioecowitt import EcoWittListener, EcoWittSensor
 
 
-def usage():
+def usage() -> None:
     """Print usage of the CLI."""
     print(f"Usage: {sys.argv[0]} port")
 
@@ -16,14 +16,15 @@ def usage():
 async def my_handler(sensor: EcoWittSensor) -> None:
     """Callback handler for printing data."""
     print("In my handler")
-    print(f"{str(sensor)}")
+    print(f"{sensor!s}")
 
 
 async def run_server(ecowitt_ws: EcoWittListener) -> None:
     """Run server in endless mode."""
+    event = asyncio.Event()
     await ecowitt_ws.start()
-    while True:
-        await asyncio.sleep(100000)
+    # use event to wait endless instead of sleep in a loop
+    await event.wait()
 
 
 def main() -> None:
@@ -38,7 +39,7 @@ def main() -> None:
     ecowitt_server.new_sensor_cb.append(my_handler)
     try:
         asyncio.run(run_server(ecowitt_server))
-    except Exception as err:  # pylint: disable=broad-except
+    except Exception as err:  # pylint: disable=broad-except # noqa: BLE001
         print(str(err))
     print("Exiting")
 
diff -pruN 2025.3.1-1/aioecowitt/calc.py 2025.9.1-1/aioecowitt/calc.py
--- 2025.3.1-1/aioecowitt/calc.py	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/aioecowitt/calc.py	2025-09-06 19:46:05.000000000 +0000
@@ -2,165 +2,123 @@
 
 from __future__ import annotations
 
-import datetime as dt
+from typing import TYPE_CHECKING
 
 import meteocalc
 
-from .sensor import SENSOR_MAP, EcoWittSensorTypes
+from .sensor import SENSOR_MAP
 
+if TYPE_CHECKING:
+    import datetime as dt
 
-def _ftoc(fahrenheit: float | str) -> float:
+_INCHES_2_MM = 25.4
+_WATT_METERS_SQUARED_2_LUX = 0.0079
+_MPH_2_KMH = 1.60934
+_KM_2_MI = 0.6213712
+_INHG_2_HPA = 33.86
+
+
+def convert_fahrenheit_to_celsius(
+    data: dict[str, str | int | float | dt.datetime | None],
+    key_source: str,
+    key_destination: str,
+) -> None:
     """Convert f to c."""
-    return round(meteocalc.Temp(fahrenheit, "F").c, 1)
+    if key_source in data:
+        data[key_destination] = round(meteocalc.Temp(data[key_source], "F").c, 1)
 
 
-def _timestamp_to_datetime(timestamp: int) -> dt.datetime:
-    return dt.datetime.fromtimestamp(timestamp, dt.timezone.utc)
+def _convert_inches_to_mm(
+    data: dict[str, str | int | float | dt.datetime | None],
+    key_source: str,
+    key_destination: str,
+) -> None:
+    """Convert source value from inches to mm and save it into destination."""
+    if key_source in data:
+        data[key_destination] = round(data[key_source] * _INCHES_2_MM, 1)
 
 
-def weather_datapoints(
+def weather_datapoints(  # noqa: C901, PLR0912
     data: dict[str, str],
 ) -> dict[str, str | int | float | dt.datetime | None]:
     """Calculate and convert weather data."""
-    mph_kmh = 1.60934
-    in_hpa = 33.86
-    in_mm = 25.4
-    km_mi = 0.6213712
-    wm_lux = 0.0079
-
-    # basic conversions
-    if "humidityin" in data:
-        data["humidityin"] = int(data["humidityin"])
-    if "humidity" in data:
-        data["humidity"] = int(data["humidity"])
-    if "winddir" in data:
-        data["winddir"] = int(data["winddir"])
-    if "winddir_avg10m" in data:
-        data["winddir_avg10m"] = int(data["winddir_avg10m"])
-    if "uv" in data:
-        data["uv"] = int(data["uv"])
+    for key, value in data.copy().items():
+        if key in SENSOR_MAP:
+            mapping = SENSOR_MAP[key]
+            if value:
+                data[key] = mapping.stype.convert_fn(value)
+            else:
+                data[key] = None
+
     if "solarradiation" in data:
-        data["solarradiation"] = float(data["solarradiation"])
-        data["solarradiation_lux"] = round(data["solarradiation"] / wm_lux, 1)
+        data["solarradiation_lux"] = round(
+            data["solarradiation"] / _WATT_METERS_SQUARED_2_LUX, 1
+        )
 
     # lightning
-    if "lightning_time" in data:
-        if data["lightning_time"]:
-            data["lightning_time"] = _timestamp_to_datetime(int(data["lightning_time"]))
-        else:
-            data["lightning_time"] = None
-    if "lightning_num" in data:
-        data["lightning_num"] = int(data["lightning_num"])
-    if "lightning" in data:
-        if data["lightning"]:
-            data["lightning"] = int(data["lightning"])
-            data["lightning_mi"] = int(round(data["lightning"] * km_mi))
-        else:
-            data["lightning"] = None
+    if value := data.get("lightning"):
+        data["lightning_mi"] = round(value * _KM_2_MI)
 
-    # temperatures
-    if "tempf" in data:
-        data["tempf"] = float(data["tempf"])
-        data["tempc"] = _ftoc(data["tempf"])
-    if "tempinf" in data:
-        data["tempinf"] = float(data["tempinf"])
-        data["tempinc"] = _ftoc(data["tempinf"])
-    # (WH45)
-    if "tf_co2" in data:
-        data["tf_co2"] = float(data["tf_co2"])
-        data["tf_co2c"] = _ftoc(data["tf_co2"])
-    # WN34 Soil Temperature Sensor
-    for j in range(1, 9):
-        wnf = f"tf_ch{j}"
-        wnc = f"tf_ch{j}c"
-        if wnf in data:
-            data[wnf] = float(data[wnf])
-            data[wnc] = _ftoc(data[wnf])
-
-    # numbered WH31 temp/humid
-    for j in range(1, 9):
-        tmpf = f"temp{j}f"
-        tmpc = f"temp{j}c"
-        hum = f"humidity{j}"
-        if tmpf in data:
-            data[tmpf] = float(data[tmpf])
-            data[tmpc] = _ftoc(data[tmpf])
-        if hum in data:
-            data[hum] = int(data[hum])
+    for source, dst in (
+        ("tempf", "tempc"),
+        ("tempinf", "tempinc"),
+        ("tf_co2", "tf_co2c"),
+        ("temp1f", "temp1c"),
+        ("temp2f", "temp2c"),
+        ("temp3f", "temp3c"),
+        ("temp4f", "temp4c"),
+        ("temp5f", "temp5c"),
+        ("temp6f", "temp6c"),
+        ("temp7f", "temp7c"),
+        ("temp8f", "temp8c"),
+        ("tf_ch1", "tf_ch1c"),
+        ("tf_ch2", "tf_ch2c"),
+        ("tf_ch3", "tf_ch3c"),
+        ("tf_ch4", "tf_ch4c"),
+        ("tf_ch5", "tf_ch5c"),
+        ("tf_ch6", "tf_ch6c"),
+        ("tf_ch7", "tf_ch7c"),
+        ("tf_ch8", "tf_ch8c"),
+    ):
+        convert_fahrenheit_to_celsius(data, source, dst)
 
     # speeds
-    if "windspeedmph" in data:
-        data["windspeedmph"] = float(data["windspeedmph"])
-        data["windspeedkmh"] = round(data["windspeedmph"] * mph_kmh, 1)
-    if "windgustmph" in data:
-        data["windgustmph"] = float(data["windgustmph"])
-        data["windgustkmh"] = round(data["windgustmph"] * mph_kmh, 1)
-    # I assume this is MPH?
-    if "maxdailygust" in data:
-        data["maxdailygust"] = float(data["maxdailygust"])
-        data["maxdailygustkmh"] = round(data["maxdailygust"] * mph_kmh, 1)
-    if "windspdmph_avg10m" in data:
-        data["windspdmph_avg10m"] = float(data["windspdmph_avg10m"])
-        data["windspdkmh_avg10m"] = round(float(data["windspdmph_avg10m"] * mph_kmh), 1)
-
-    # distances
-    if "rainratein" in data:
-        data["rainratein"] = float(data["rainratein"])
-        data["rainratemm"] = round(data["rainratein"] * in_mm, 1)
-    if "eventrainin" in data:
-        data["eventrainin"] = float(data["eventrainin"])
-        data["eventrainmm"] = round(data["eventrainin"] * in_mm, 1)
-    if "hourlyrainin" in data:
-        data["hourlyrainin"] = float(data["hourlyrainin"])
-        data["hourlyrainmm"] = round(data["hourlyrainin"] * in_mm, 1)
-    if "dailyrainin" in data:
-        data["dailyrainin"] = float(data["dailyrainin"])
-        data["dailyrainmm"] = round(data["dailyrainin"] * in_mm, 1)
-    if "weeklyrainin" in data:
-        data["weeklyrainin"] = float(data["weeklyrainin"])
-        data["weeklyrainmm"] = round(data["weeklyrainin"] * in_mm, 1)
-    if "monthlyrainin" in data:
-        data["monthlyrainin"] = float(data["monthlyrainin"])
-        data["monthlyrainmm"] = round(data["monthlyrainin"] * in_mm, 1)
-    if "yearlyrainin" in data:
-        data["yearlyrainin"] = float(data["yearlyrainin"])
-        data["yearlyrainmm"] = round(data["yearlyrainin"] * in_mm, 1)
-    if "totalrainin" in data:
-        data["totalrainin"] = float(data["totalrainin"])
-        data["totalrainmm"] = round(data["totalrainin"] * in_mm, 1)
-
-    # piezo rain sensor
-    if "rrain_piezo" in data:
-        data["rrain_piezo"] = float(data["rrain_piezo"])
-        data["rrain_piezomm"] = round(data["rrain_piezo"] * in_mm, 1)
-    if "erain_piezo" in data:
-        data["erain_piezo"] = float(data["erain_piezo"])
-        data["erain_piezomm"] = round(data["erain_piezo"] * in_mm, 1)
-    if "hrain_piezo" in data:
-        data["hrain_piezo"] = float(data["hrain_piezo"])
-        data["hrain_piezomm"] = round(data["hrain_piezo"] * in_mm, 1)
-    if "drain_piezo" in data:
-        data["drain_piezo"] = float(data["drain_piezo"])
-        data["drain_piezomm"] = round(data["drain_piezo"] * in_mm, 1)
-    if "wrain_piezo" in data:
-        data["wrain_piezo"] = float(data["wrain_piezo"])
-        data["wrain_piezomm"] = round(data["wrain_piezo"] * in_mm, 1)
-    if "mrain_piezo" in data:
-        data["mrain_piezo"] = float(data["mrain_piezo"])
-        data["mrain_piezomm"] = round(data["mrain_piezo"] * in_mm, 1)
-    if "yrain_piezo" in data:
-        data["yrain_piezo"] = float(data["yrain_piezo"])
-        data["yrain_piezomm"] = round(data["yrain_piezo"] * in_mm, 1)
-    if "srain_piezo" in data:
-        data["srain_piezo"] = int(data["srain_piezo"])
+    for source, dst in (
+        ("windspeedmph", "windspeedkmh"),
+        ("windgustmph", "windgustkmh"),
+        ("maxdailygust", "maxdailygustkmh"),
+        ("windspdmph_avg10m", "windspdkmh_avg10m"),
+    ):
+        if source in data:
+            data[dst] = round(data[source] * _MPH_2_KMH, 1)
+
+    for key in (
+        "eventrain",
+        "hourlyrain",
+        "dailyrain",
+        "weeklyrain",
+        "monthlyrain",
+        "yearlyrain",
+        "totalrain",
+        "rainrate",
+    ):
+        _convert_inches_to_mm(data, f"{key}in", f"{key}mm")
+
+    for key in (
+        "erain_piezo",
+        "hrain_piezo",
+        "drain_piezo",
+        "wrain_piezo",
+        "mrain_piezo",
+        "yrain_piezo",
+        "rrain_piezo",
+    ):
+        _convert_inches_to_mm(data, key, f"{key}mm")
 
     # Pressure
-    if "baromrelin" in data:
-        data["baromrelin"] = float(data["baromrelin"])
-        data["baromrelhpa"] = round(data["baromrelin"] * in_hpa, 1)
-    if "baromabsin" in data:
-        data["baromabsin"] = float(data["baromabsin"])
-        data["baromabshpa"] = round(data["baromabsin"] * in_hpa, 1)
+    for key in ("baromrel", "baromabs"):
+        if value := data.get(f"{key}in"):
+            data[f"{key}hpa"] = round(value * _INHG_2_HPA, 1)
 
     # Wind chill
     if "tempf" in data and "windspeedmph" in data:
@@ -188,96 +146,4 @@ def weather_datapoints(
         data["tempfeelsf"] = round(feels_like.f, 1)
         data["tempfeelsc"] = round(feels_like.c, 1)
 
-    # Soil moisture (WH51)
-    for j in range(1, 17):
-        name = f"soilmoisture{j}"
-        if name in data:
-            data[name] = int(data[name])
-
-    # PM 2.5 sensor (WH41)
-    for j in range(1, 5):
-        pmc = f"pm25_ch{j}"
-        pma = f"pm25_avg_24h_ch{j}"
-        if pmc in data:
-            data[pmc] = float(data[pmc])
-        if pma in data:
-            data[pma] = float(data[pma])
-
-    # Leak sensor (WH55)
-    for j in range(1, 5):
-        leak = f"leak_ch{j}"
-        if leak in data:
-            data[leak] = int(data[leak])
-
-    # Wetness sensor (WN35)
-    for j in range(1, 8):
-        leaf = f"leafwetness_ch{j}"
-        if leaf in data:
-            data[leaf] = int(data[leaf])
-
-    # CO2 indoor air quality (WH45) (note temp is in temps above)
-    pm_floats = [
-        "pm25",
-        "pm25_24h",
-        "pm10",
-        "pm10_24",
-    ]
-    for prefix in pm_floats:
-        name = f"{prefix}_co2"
-        if name in data:
-            data[name] = float(data[name])
-    if "co2" in data:
-        data["co2"] = int(data["co2"])
-    if "co2_24h" in data:
-        data["co2_24h"] = int(data["co2_24h"])
-    if "humi_co2" in data:
-        data["humi_co2"] = int(data["humi_co2"])
-
-    # Batteries
-    bat_names = [
-        "wh25",
-        "wh26",
-        "wh40",
-        "wh57",
-        "wh65",
-        "wh68",
-        "wh80",
-        "wh90",
-        "co2_",
-    ]
-    for prefix in bat_names:
-        name = f"{prefix}batt"
-        if name in data:
-            batt_type = SENSOR_MAP[name].stype
-            if batt_type == EcoWittSensorTypes.BATTERY_PERCENTAGE:
-                data[name] = int(data[name]) * 20
-            else:
-                data[name] = float(data[name])
-
-    bat_range_names = [
-        "",  # for just 'batt'
-        "soil",
-        "pm25",
-        "leak",
-        "tf_",  # WN34 voltage type
-        "leaf_",
-    ]
-    for r_prefix in bat_range_names:
-        for j in range(1, 9):
-            name = f"{r_prefix}batt{j}"
-            if name in data:
-                batt_type = SENSOR_MAP[name].stype
-                if batt_type == EcoWittSensorTypes.BATTERY_PERCENTAGE:
-                    data[name] = int(data[name]) * 20
-                else:
-                    data[name] = float(data[name])
-
-    bat_full_names = [
-        "ws90cap_volt",
-        "console_batt",
-    ]
-    for name in bat_full_names:
-        if name in data:
-            data[name] = float(data[name])
-
     return data
diff -pruN 2025.3.1-1/aioecowitt/sensor.py 2025.9.1-1/aioecowitt/sensor.py
--- 2025.3.1-1/aioecowitt/sensor.py	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/aioecowitt/sensor.py	2025-09-06 19:46:05.000000000 +0000
@@ -3,12 +3,14 @@
 from __future__ import annotations
 
 import datetime as dt
-from typing import Callable
-
-from dataclasses import dataclass, field
 import enum
+from dataclasses import dataclass, field
+from typing import TYPE_CHECKING, Self
+
+if TYPE_CHECKING:
+    from collections.abc import Callable
 
-from .station import EcoWittStation
+    from .station import EcoWittStation
 
 
 @dataclass
@@ -26,7 +28,7 @@ class EcoWittSensor:
 
     def update_value(
         self,
-        value: None | str | int | float | dt.datetime,
+        value: None | str | float | dt.datetime,
         last_update: float,
         last_update_m: float,
     ) -> None:
@@ -44,40 +46,64 @@ class EcoWittSensor:
             callback()
 
 
-class EcoWittSensorTypes(enum.Enum):
+def _convert_timestamp(value: str) -> dt.datetime:
+    return dt.datetime.fromtimestamp(int(value), dt.UTC)
+
+
+@enum.unique
+class EcoWittSensorTypes(enum.IntEnum):
     """EcoWitt sensor types."""
 
-    INTERNAL = 1
-    PRESSURE_HPA = 2
-    PRESSURE_INHG = 3
-    RAIN_COUNT_MM = 4
-    RAIN_COUNT_INCHES = 5
-    RAIN_RATE_MM = 6
-    RAIN_RATE_INCHES = 7
-    HUMIDITY = 8
-    DEGREE = 9
-    SPEED_KPH = 10
-    SPEED_MPH = 11
-    TEMPERATURE_C = 12
-    TEMPERATURE_F = 13
-    WATT_METERS_SQUARED = 14
-    UV_INDEX = 15
-    PM25 = 16
-    PM10 = 17
-    TIMESTAMP = 18
-    LIGHTNING_COUNT = 19
-    LIGHTNING_DISTANCE_KM = 20
-    LIGHTNING_DISTANCE_MILES = 21
-    LEAK = 22
-    VOLTAGE = 23
-    BATTERY_BINARY = 24
-    BATTERY_VOLTAGE = 25
-    BATTERY_PERCENTAGE = 26
-    CO2_PPM = 27
-    LUX = 28
-    PERCENTAGE = 29
-    SOIL_RAWADC = 30
-    RAIN_STATE = 31
+    convert_fn: Callable[[str], str | int | float | dt.datetime]
+
+    def __new__(
+        cls,
+        value: int,
+        convert_fn: Callable[[str], str | int | float | dt.datetime],
+    ) -> Self:
+        """Create new EcoWittSensorTypes."""
+        obj = int.__new__(cls, value)
+        obj._value_ = value
+        obj.convert_fn = convert_fn
+        return obj
+
+    INTERNAL = 1, lambda x: x
+    PRESSURE_HPA = 2, lambda x: x  # HA should convert
+    PRESSURE_INHG = 3, float
+    RAIN_COUNT_MM = 4, lambda x: x  # HA should convert
+    RAIN_COUNT_INCHES = 5, float
+    RAIN_RATE_MM = 6, lambda x: x  # HA should convert
+    RAIN_RATE_INCHES = 7, float
+    HUMIDITY = 8, int
+    DEGREE = 9, int
+    SPEED_KPH = 10, lambda x: x  # HA should convert
+    SPEED_MPH = 11, float
+    TEMPERATURE_C = 12, lambda x: x  # HA should convert
+    TEMPERATURE_F = 13, float
+    WATT_METERS_SQUARED = 14, float
+    UV_INDEX = 15, int
+    PM25 = 16, float
+    PM10 = 17, float
+    TIMESTAMP = 18, _convert_timestamp
+    LIGHTNING_COUNT = 19, int
+    LIGHTNING_DISTANCE_KM = 20, int
+    LIGHTNING_DISTANCE_MILES = 21, lambda x: x  # HA should convert
+    LEAK = 22, int
+    VOLTAGE = 23, float
+    BATTERY_BINARY = 24, float
+    BATTERY_VOLTAGE = 25, float
+    BATTERY_PERCENTAGE = 26, lambda x: int(x) * 20
+    CO2_PPM = 27, int
+    LUX = 28, lambda x: x  # HA should convert
+    PERCENTAGE = 29, int
+    SOIL_RAWADC = 30, lambda x: x  # Keep it as it comes in
+    RAIN_STATE = 31, int
+    SOIL_MOISTURE = 32, int
+    VPD_INHG = 33, float
+    PM1 = 34, float
+    PM4 = 35, float
+    DISTANCE_MM = 36, int
+    HEAT_COUNT = 37, int
 
 
 @dataclass
@@ -93,6 +119,7 @@ SENSOR_MAP: dict[str, EcoWittMapping] =
     "baromrelhpa": EcoWittMapping("Relative Pressure", EcoWittSensorTypes.PRESSURE_HPA),
     "baromabsin": EcoWittMapping("Absolute Pressure", EcoWittSensorTypes.PRESSURE_INHG),
     "baromrelin": EcoWittMapping("Relative Pressure", EcoWittSensorTypes.PRESSURE_INHG),
+    "vpd": EcoWittMapping("Vapour Pressure Deficit", EcoWittSensorTypes.VPD_INHG),
     "rainratein": EcoWittMapping("Rain Rate", EcoWittSensorTypes.RAIN_RATE_INCHES),
     "eventrainin": EcoWittMapping("Event Rain", EcoWittSensorTypes.RAIN_COUNT_INCHES),
     "hourlyrainin": EcoWittMapping("Hourly Rain", EcoWittSensorTypes.RAIN_COUNT_INCHES),
@@ -191,22 +218,54 @@ SENSOR_MAP: dict[str, EcoWittMapping] =
     ),
     "solarradiation_lux": EcoWittMapping("Solar Lux", EcoWittSensorTypes.LUX),
     "uv": EcoWittMapping("UV Index", EcoWittSensorTypes.UV_INDEX),
-    "soilmoisture1": EcoWittMapping("Soil Moisture 1", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture2": EcoWittMapping("Soil Moisture 2", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture3": EcoWittMapping("Soil Moisture 3", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture4": EcoWittMapping("Soil Moisture 4", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture5": EcoWittMapping("Soil Moisture 5", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture6": EcoWittMapping("Soil Moisture 6", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture7": EcoWittMapping("Soil Moisture 7", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture8": EcoWittMapping("Soil Moisture 8", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture9": EcoWittMapping("Soil Moisture 9", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture10": EcoWittMapping("Soil Moisture 10", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture11": EcoWittMapping("Soil Moisture 11", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture12": EcoWittMapping("Soil Moisture 12", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture13": EcoWittMapping("Soil Moisture 13", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture14": EcoWittMapping("Soil Moisture 14", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture15": EcoWittMapping("Soil Moisture 15", EcoWittSensorTypes.HUMIDITY),
-    "soilmoisture16": EcoWittMapping("Soil Moisture 16", EcoWittSensorTypes.HUMIDITY),
+    "soilmoisture1": EcoWittMapping(
+        "Soil Moisture 1", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture2": EcoWittMapping(
+        "Soil Moisture 2", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture3": EcoWittMapping(
+        "Soil Moisture 3", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture4": EcoWittMapping(
+        "Soil Moisture 4", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture5": EcoWittMapping(
+        "Soil Moisture 5", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture6": EcoWittMapping(
+        "Soil Moisture 6", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture7": EcoWittMapping(
+        "Soil Moisture 7", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture8": EcoWittMapping(
+        "Soil Moisture 8", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture9": EcoWittMapping(
+        "Soil Moisture 9", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture10": EcoWittMapping(
+        "Soil Moisture 10", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture11": EcoWittMapping(
+        "Soil Moisture 11", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture12": EcoWittMapping(
+        "Soil Moisture 12", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture13": EcoWittMapping(
+        "Soil Moisture 13", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture14": EcoWittMapping(
+        "Soil Moisture 14", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture15": EcoWittMapping(
+        "Soil Moisture 15", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
+    "soilmoisture16": EcoWittMapping(
+        "Soil Moisture 16", EcoWittSensorTypes.SOIL_MOISTURE
+    ),
     "soilad1": EcoWittMapping("Soil AD 1", EcoWittSensorTypes.SOIL_RAWADC),
     "soilad2": EcoWittMapping("Soil AD 2", EcoWittSensorTypes.SOIL_RAWADC),
     "soilad3": EcoWittMapping("Soil AD 3", EcoWittSensorTypes.SOIL_RAWADC),
@@ -247,6 +306,10 @@ SENSOR_MAP: dict[str, EcoWittMapping] =
     "tf_co2": EcoWittMapping("WH45 Temperature", EcoWittSensorTypes.TEMPERATURE_F),
     "tf_co2c": EcoWittMapping("WH45 Temperature", EcoWittSensorTypes.TEMPERATURE_C),
     "humi_co2": EcoWittMapping("WH45 Humidity", EcoWittSensorTypes.HUMIDITY),
+    "pm1_co2": EcoWittMapping("WH46 PM1 CO2", EcoWittSensorTypes.PM1),
+    "pm1_24h_co2": EcoWittMapping("WH46 PM1 CO2 24h average", EcoWittSensorTypes.PM1),
+    "pm4_co2": EcoWittMapping("WH46 PM4 CO2", EcoWittSensorTypes.PM4),
+    "pm4_24h_co2": EcoWittMapping("WH46 PM4 CO2 24h average", EcoWittSensorTypes.PM4),
     "pm25_co2": EcoWittMapping("WH45 PM2.5 CO2", EcoWittSensorTypes.PM25),
     "pm25_24h_co2": EcoWittMapping(
         "WH45 PM2.5 CO2 24h average", EcoWittSensorTypes.PM25
@@ -258,6 +321,11 @@ SENSOR_MAP: dict[str, EcoWittMapping] =
     "co2": EcoWittMapping("WH45 CO2", EcoWittSensorTypes.CO2_PPM),
     "co2_24h": EcoWittMapping("WH45 CO2 24h average", EcoWittSensorTypes.CO2_PPM),
     "co2_batt": EcoWittMapping("WH45 Battery", EcoWittSensorTypes.BATTERY_PERCENTAGE),
+    "co2in": EcoWittMapping("Console CO2", EcoWittSensorTypes.CO2_PPM),
+    "co2in_24h": EcoWittMapping("Console CO2 24h average", EcoWittSensorTypes.CO2_PPM),
+    "console_batt": EcoWittMapping(
+        "Console Battery", EcoWittSensorTypes.BATTERY_VOLTAGE
+    ),
     "leak_ch1": EcoWittMapping("Leak Detection 1", EcoWittSensorTypes.LEAK),
     "leak_ch2": EcoWittMapping("Leak Detection 2", EcoWittSensorTypes.LEAK),
     "leak_ch3": EcoWittMapping("Leak Detection 3", EcoWittSensorTypes.LEAK),
@@ -269,9 +337,8 @@ SENSOR_MAP: dict[str, EcoWittMapping] =
     "wh65batt": EcoWittMapping("WH65 Battery", EcoWittSensorTypes.BATTERY_BINARY),
     "wh68batt": EcoWittMapping("WH68 Battery", EcoWittSensorTypes.BATTERY_VOLTAGE),
     "wh80batt": EcoWittMapping("WH80 Battery", EcoWittSensorTypes.BATTERY_VOLTAGE),
-    "console_batt": EcoWittMapping(
-        "Console Battery", EcoWittSensorTypes.BATTERY_VOLTAGE
-    ),
+    "wh85batt": EcoWittMapping("WH85 Battery", EcoWittSensorTypes.BATTERY_VOLTAGE),
+    "wh90batt": EcoWittMapping("WH90 Battery", EcoWittSensorTypes.BATTERY_VOLTAGE),
     "soilbatt1": EcoWittMapping("Soil Battery 1", EcoWittSensorTypes.BATTERY_VOLTAGE),
     "soilbatt2": EcoWittMapping("Soil Battery 2", EcoWittSensorTypes.BATTERY_VOLTAGE),
     "soilbatt3": EcoWittMapping("Soil Battery 3", EcoWittSensorTypes.BATTERY_VOLTAGE),
@@ -424,9 +491,37 @@ SENSOR_MAP: dict[str, EcoWittMapping] =
     "leaf_batt8": EcoWittMapping(
         "Leaf Wetness 8 Battery", EcoWittSensorTypes.BATTERY_VOLTAGE
     ),
+    "depth_ch1": EcoWittMapping("Current Depth 1", EcoWittSensorTypes.DISTANCE_MM),
+    "depth_ch2": EcoWittMapping("Current Depth 2", EcoWittSensorTypes.DISTANCE_MM),
+    "depth_ch3": EcoWittMapping("Current Depth 3", EcoWittSensorTypes.DISTANCE_MM),
+    "depth_ch4": EcoWittMapping("Current Depth 4", EcoWittSensorTypes.DISTANCE_MM),
+    "thi_ch1": EcoWittMapping(
+        "Total Historical Depth Index 1", EcoWittSensorTypes.DISTANCE_MM
+    ),
+    "thi_ch2": EcoWittMapping(
+        "Total Historical Depth Index 2", EcoWittSensorTypes.DISTANCE_MM
+    ),
+    "thi_ch3": EcoWittMapping(
+        "Total Historical Depth Index 3", EcoWittSensorTypes.DISTANCE_MM
+    ),
+    "thi_ch4": EcoWittMapping(
+        "Total Historical Depth Index 4", EcoWittSensorTypes.DISTANCE_MM
+    ),
+    "air_ch1": EcoWittMapping("Air Gap 1", EcoWittSensorTypes.DISTANCE_MM),
+    "air_ch2": EcoWittMapping("Air Gap 2", EcoWittSensorTypes.DISTANCE_MM),
+    "air_ch3": EcoWittMapping("Air Gap 3", EcoWittSensorTypes.DISTANCE_MM),
+    "air_ch4": EcoWittMapping("Air Gap 4", EcoWittSensorTypes.DISTANCE_MM),
+    "ldsbatt1": EcoWittMapping("LDS 1 Battery", EcoWittSensorTypes.BATTERY_VOLTAGE),
+    "ldsbatt2": EcoWittMapping("LDS 2 Battery", EcoWittSensorTypes.BATTERY_VOLTAGE),
+    "ldsbatt3": EcoWittMapping("LDS 3 Battery", EcoWittSensorTypes.BATTERY_VOLTAGE),
+    "ldsbatt4": EcoWittMapping("LDS 4 Battery", EcoWittSensorTypes.BATTERY_VOLTAGE),
+    "ldsheat_ch1": EcoWittMapping("Heater-on Counter 1", EcoWittSensorTypes.HEAT_COUNT),
+    "ldsheat_ch2": EcoWittMapping("Heater-on Counter 2", EcoWittSensorTypes.HEAT_COUNT),
+    "ldsheat_ch3": EcoWittMapping("Heater-on Counter 3", EcoWittSensorTypes.HEAT_COUNT),
+    "ldsheat_ch4": EcoWittMapping("Heater-on Counter 4", EcoWittSensorTypes.HEAT_COUNT),
     "dateutc": EcoWittMapping("dateutc", EcoWittSensorTypes.INTERNAL),
     "fields": EcoWittMapping("field list", EcoWittSensorTypes.INTERNAL),
-    "wh90batt": EcoWittMapping("WH90 Battery", EcoWittSensorTypes.BATTERY_VOLTAGE),
+    "ws85cap_volt": EcoWittMapping("WH85 Capacitor", EcoWittSensorTypes.VOLTAGE),
     "ws90cap_volt": EcoWittMapping("WH90 Capacitor", EcoWittSensorTypes.VOLTAGE),
     "rrain_piezo": EcoWittMapping(
         "Rain Rate Piezo", EcoWittSensorTypes.RAIN_RATE_INCHES
diff -pruN 2025.3.1-1/aioecowitt/server.py 2025.9.1-1/aioecowitt/server.py
--- 2025.3.1-1/aioecowitt/server.py	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/aioecowitt/server.py	2025-09-06 19:46:05.000000000 +0000
@@ -4,13 +4,16 @@ from __future__ import annotations
 
 import logging
 import time
-from typing import Callable
+from typing import TYPE_CHECKING
 
 from aiohttp import web
 
-from .sensor import EcoWittSensor, SENSOR_MAP
-from .station import extract_station, EcoWittStation
 from .calc import weather_datapoints
+from .sensor import SENSOR_MAP, EcoWittSensor
+from .station import EcoWittStation, extract_station
+
+if TYPE_CHECKING:
+    from collections.abc import Callable
 
 _LOGGER = logging.getLogger(__name__)
 _ECOWITT_LISTEN_PORT = 49199
@@ -19,7 +22,9 @@ _ECOWITT_LISTEN_PORT = 49199
 class EcoWittListener:
     """EcoWitt Server API server."""
 
-    def __init__(self, port: int = _ECOWITT_LISTEN_PORT, path: str = None):
+    def __init__(
+        self, port: int = _ECOWITT_LISTEN_PORT, path: str | None = None
+    ) -> None:
         """Initialize EcoWitt Server."""
         # API Constants
         self.port: int = port
@@ -40,7 +45,7 @@ class EcoWittListener:
         self.stations: dict[str, EcoWittStation] = {}
 
     def _new_sensor_cb(self, sensor: EcoWittSensor) -> None:
-        """Internal new sensor callback
+        """Internal new sensor callback.
 
         binds to self.new_sensor_cb
         """
@@ -63,7 +68,7 @@ class EcoWittListener:
         last_update = time.time()
         last_update_m = time.monotonic()
 
-        for datapoint in weather_data.keys():
+        for datapoint in weather_data:
             sensor_id = f"{station.key}.{datapoint}"
             sensor = self.sensors.get(sensor_id)
             if sensor is None:
@@ -85,12 +90,12 @@ class EcoWittListener:
                 self.sensors[sensor_id] = sensor
                 try:
                     self._new_sensor_cb(sensor)
-                except Exception as err:  # pylint: disable=broad-except
+                except Exception as err:  # pylint: disable=broad-except # noqa: BLE001
                     _LOGGER.warning("EcoWitt new sensor callback error: %s", err)
 
             try:
                 sensor.update_value(weather_data[datapoint], last_update, last_update_m)
-            except Exception as err:  # pylint: disable=broad-except
+            except Exception as err:  # pylint: disable=broad-except # noqa: BLE001
                 _LOGGER.warning("Sensor update error: %s", err)
 
     async def handler(self, request: web.BaseRequest) -> web.Response:
@@ -100,6 +105,7 @@ class EcoWittListener:
         if self.path is not None and request.path != self.path:
             return web.Response(status=404)
         data = await request.post()
+        _LOGGER.debug("Received data: %s", data)
 
         # data is not a dict, it's a MultiDict
         self.last_values[data["PASSKEY"]] = data.copy()
@@ -110,7 +116,6 @@ class EcoWittListener:
 
     async def start(self) -> None:
         """Listen and process."""
-
         self.server = web.Server(self.handler)
         self.runner = web.ServerRunner(self.server)
         await self.runner.setup()
diff -pruN 2025.3.1-1/aioecowitt/station.py 2025.9.1-1/aioecowitt/station.py
--- 2025.3.1-1/aioecowitt/station.py	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/aioecowitt/station.py	2025-09-06 19:46:05.000000000 +0000
@@ -4,7 +4,6 @@ from __future__ import annotations
 
 from dataclasses import dataclass, field
 
-
 VERSION_FIELDS = {
     "ws90_ver",
 }
diff -pruN 2025.3.1-1/debian/changelog 2025.9.1-1/debian/changelog
--- 2025.3.1-1/debian/changelog	2025-03-07 19:07:37.000000000 +0000
+++ 2025.9.1-1/debian/changelog	2025-09-08 08:57:48.000000000 +0000
@@ -1,3 +1,13 @@
+aioecowitt (2025.9.1-1) unstable; urgency=medium
+
+  * New upstream release.
+  * Switch to autopkgtest-pkg-pybuild.
+  * Remove 'Rules-Requires-Root: no', now the default.
+  * Update Standards-Version.
+  * Add debian/salsa-ci.yml.
+
+ -- Edward Betts <edward@4angle.com>  Mon, 08 Sep 2025 09:57:48 +0100
+
 aioecowitt (2025.3.1-1) unstable; urgency=medium
 
   * New upstream release.
diff -pruN 2025.3.1-1/debian/control 2025.9.1-1/debian/control
--- 2025.3.1-1/debian/control	2025-03-07 19:07:37.000000000 +0000
+++ 2025.9.1-1/debian/control	2025-09-08 08:57:48.000000000 +0000
@@ -15,11 +15,11 @@ Build-Depends-Indep:
  python3-pytest <!nocheck>,
  python3-pytest-aiohttp <!nocheck>,
  python3-pytest-timeout <!nocheck>,
-Rules-Requires-Root: no
-Standards-Version: 4.7.1
+Standards-Version: 4.7.2
 Homepage: https://github.com/home-assistant-libs/aioecowitt
 Vcs-Browser: https://salsa.debian.org/homeassistant-team/deps/aioecowitt
 Vcs-Git: https://salsa.debian.org/homeassistant-team/deps/aioecowitt.git
+Testsuite: autopkgtest-pkg-pybuild
 
 Package: python3-aioecowitt
 Architecture: all
diff -pruN 2025.3.1-1/debian/salsa-ci.yml 2025.9.1-1/debian/salsa-ci.yml
--- 2025.3.1-1/debian/salsa-ci.yml	1970-01-01 00:00:00.000000000 +0000
+++ 2025.9.1-1/debian/salsa-ci.yml	2025-09-02 12:47:21.000000000 +0000
@@ -0,0 +1,3 @@
+---
+include:
+  - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml
diff -pruN 2025.3.1-1/debian/tests/control 2025.9.1-1/debian/tests/control
--- 2025.3.1-1/debian/tests/control	2025-02-18 21:25:10.000000000 +0000
+++ 2025.9.1-1/debian/tests/control	1970-01-01 00:00:00.000000000 +0000
@@ -1,8 +0,0 @@
-Tests:
- run-tests,
-Depends:
- python3-all,
- python3-pytest,
- python3-pytest-aiohttp,
- python3-pytest-timeout,
- @,
diff -pruN 2025.3.1-1/debian/tests/run-tests 2025.9.1-1/debian/tests/run-tests
--- 2025.3.1-1/debian/tests/run-tests	2022-07-29 08:36:56.000000000 +0000
+++ 2025.9.1-1/debian/tests/run-tests	1970-01-01 00:00:00.000000000 +0000
@@ -1,6 +0,0 @@
-#!/bin/sh
-set -e
-cp -r test* "$AUTOPKGTEST_TMP/" && cd "$AUTOPKGTEST_TMP"
-for py in $(py3versions -s); do
-    $py -Wd -m pytest -v -x 2>&1
-done
diff -pruN 2025.3.1-1/misc/__init__.py 2025.9.1-1/misc/__init__.py
--- 2025.3.1-1/misc/__init__.py	1970-01-01 00:00:00.000000000 +0000
+++ 2025.9.1-1/misc/__init__.py	2025-09-06 19:46:05.000000000 +0000
@@ -0,0 +1 @@
+"""Miscellaneous utilities."""
diff -pruN 2025.3.1-1/misc/fake_client.py 2025.9.1-1/misc/fake_client.py
--- 2025.3.1-1/misc/fake_client.py	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/misc/fake_client.py	2025-09-06 19:46:05.000000000 +0000
@@ -1,13 +1,10 @@
-"""
-A bone-simple fake client used to test the hass integration
-"""
+"""A bone-simple fake client used to test the hass integration."""
 
 import http.client
 import sys
 import urllib.parse
 
 MY_PASSKEY = "34271334ED1FADA6D8B988B14267E55D"
-# MY_PASSKEY = '35271334ED1FADA7D8B988B22222E22D'
 
 paramset_a = {
     "PASSKEY": MY_PASSKEY,
@@ -89,14 +86,15 @@ paramset_b = {
 }
 
 
-def usage():
-    print("Usage: {0} host port".format(sys.argv[0]))
+def usage() -> None:
+    """Print usage of the CLI."""
+    print(f"Usage: {sys.argv[0]} host port")
 
 
 if __name__ == "__main__":
     if len(sys.argv) < 3:
         usage()
-        exit(1)
+        sys.exit(1)
 
     host = sys.argv[1]
     port = sys.argv[2]
@@ -105,10 +103,10 @@ if __name__ == "__main__":
     if len(sys.argv) > 3 and sys.argv[3] == "add":
         paramset_b["humidity2"] = 21
 
-    print("Connecting to host {0} on port {0}".format(host, port))
+    print(f"Connecting to host {host} on port {port}")
     conn = http.client.HTTPConnection(host, port)
     headers = {"Content-type": "application/x-www-form-urlencoded"}
-    params = urllib.parse.urlencode(paramset_b)
+    params = urllib.parse.urlencode(paramset_b)  # pylint: disable=invalid-name
     print(params)
     conn.request("POST", "", params, headers)
     response = conn.getresponse()
diff -pruN 2025.3.1-1/pyproject.toml 2025.9.1-1/pyproject.toml
--- 2025.3.1-1/pyproject.toml	1970-01-01 00:00:00.000000000 +0000
+++ 2025.9.1-1/pyproject.toml	2025-09-06 19:46:05.000000000 +0000
@@ -0,0 +1,70 @@
+[build-system]
+build-backend = "setuptools.build_meta"
+requires = ["setuptools>=77.0"]
+
+[project]
+name = "aioecowitt"
+version = "0.0.0"
+license = "Apache-2.0"
+description = "Python wrapper for EcoWitt Protocol"
+readme = "README.md"
+authors = [{ name = "Home Assistant Team", email = "hello@home-assistant.io" }]
+requires-python = ">=3.13"
+classifiers = [
+  "Development Status :: 4 - Beta",
+  "Intended Audience :: Developers",
+  "Natural Language :: English",
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3.13",
+  "Topic :: Home Automation",
+]
+dependencies = [
+  "aiohttp>3",
+  "meteocalc>=1.1.0",
+]
+
+[project.urls]
+"Source code" = "https://github.com/home-assistant-libs/aioecowitt"
+
+[project.scripts]
+ecowitt-testserver = "aioecowitt.__main__:main"
+
+[tool.pytest.ini_options]
+asyncio_default_fixture_loop_scope = "function"
+asyncio_mode = "auto"
+
+[tool.setuptools.packages.find]
+include = ["aioecowitt*"]
+
+[tool.ruff.lint]
+select = ["ALL"]
+
+ignore = [
+    "ANN401", # Opinioated warning on disallowing dynamically typed expressions
+    "D203",   # Conflicts with other rules
+    "D213",   # Conflicts with other rules
+    "EM101",  # raw-string-in-exception
+
+    "D105", # Missing docstring in magic method
+    "D107", # Missing docstring in `__init__`
+
+    "FBT", # flake8-boolean-trap
+
+    "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
+
+    # Conflicts with the Ruff formatter
+    "COM812",
+
+    "TRY003", # raise-vanilla-args: Avoid specifying long messages outside the exception class
+    "T201", # print found,
+    "D401", # First line of docstring should be in imperative mood
+
+]
+
+[tool.ruff.lint.per-file-ignores]
+"tests/**" = [
+    "D100",    # Missing docstring in public module
+    "D103",    # Missing docstring in public function
+    "D104",    # Missing docstring in public package
+    "S101",    # Use of assert detected
+]
diff -pruN 2025.3.1-1/requirements_tests.txt 2025.9.1-1/requirements_tests.txt
--- 2025.3.1-1/requirements_tests.txt	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/requirements_tests.txt	2025-09-06 19:46:05.000000000 +0000
@@ -1,9 +1,8 @@
-pytest==8.3.5
-pytest-aiohttp==1.1.0
-pytest-timeout==2.3.1
-black==25.1.0
-flake8==7.1.2
-isort==6.0.1
-mypy==1.15.0
+mypy==1.17.1
+pre-commit==4.3.0
 pydocstyle==6.3.0
-pylint==3.3.4
\ No newline at end of file
+pylint==3.3.8
+pytest==8.4.2
+pytest-aiohttp==1.1.0
+pytest-asyncio==1.1.0
+pytest-timeout==2.4.0
diff -pruN 2025.3.1-1/scripts/run-in-env.sh 2025.9.1-1/scripts/run-in-env.sh
--- 2025.3.1-1/scripts/run-in-env.sh	1970-01-01 00:00:00.000000000 +0000
+++ 2025.9.1-1/scripts/run-in-env.sh	2025-09-06 19:46:05.000000000 +0000
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+set -euxo pipefail
+
+# Activate pyenv and virtualenv if present, then run the specified command
+
+# pyenv, pyenv-virtualenv
+if [ -s .python-version ]; then
+    PYENV_VERSION=$(head -n 1 .python-version)
+    export PYENV_VERSION
+fi
+
+# other common virtualenvs
+my_path=$(git rev-parse --show-toplevel)
+
+for venv in venv .venv .; do
+  if [ -f "${my_path}/${venv}/bin/activate" ]; then
+    . "${my_path}/${venv}/bin/activate"
+  fi
+done
+
+exec "$@"
diff -pruN 2025.3.1-1/setup.cfg 2025.9.1-1/setup.cfg
--- 2025.3.1-1/setup.cfg	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/setup.cfg	2025-09-06 19:46:05.000000000 +0000
@@ -1,31 +1,5 @@
-[flake8]
-exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build
-# To work with Black
-max-line-length = 100
-# E501: line too long
-# W503: Line break occurred before a binary operator
-# E203: Whitespace before ':'
-# D202 No blank lines allowed after function docstring
-# W504 line break after binary operator
-ignore =
-    E501,
-    W503,
-    E203,
-    D202,
-    W504,
-    E266
-per-file-ignores = __init__.py:F401
-
-[isort]
-profile = black
-multi_line_output = 3
-include_trailing_comma = True
-force_grid_wrap = 0
-use_parentheses = True
-line_length = 88
-
 [mypy]
-python_version = 3.9
+python_version = 3.13
 ignore_errors = true
 follow_imports = silent
 ignore_missing_imports = true
diff -pruN 2025.3.1-1/setup.py 2025.9.1-1/setup.py
--- 2025.3.1-1/setup.py	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/setup.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,40 +0,0 @@
-"""Setup module for EcoWitt."""
-
-from pathlib import Path
-
-from setuptools import find_packages, setup
-
-PROJECT_DIR = Path(__file__).parent.resolve()
-README_FILE = PROJECT_DIR / "README.md"
-VERSION = "2025.3.1"
-
-
-setup(
-    name="aioecowitt",
-    version=VERSION,
-    url="https://github.com/home-assistant-libs/aioecowitt",
-    download_url="https://github.com/home-assistant-libs/aioecowitt",
-    author="Home Assistant Team",
-    author_email="hello@home-assistant.io",
-    description="Python wrapper for EcoWitt Protocol",
-    long_description=README_FILE.read_text(encoding="utf-8"),
-    long_description_content_type="text/markdown",
-    packages=find_packages(exclude=["tests.*", "tests", "misc"]),
-    package_data={"aioecowitt": ["py.typed"]},
-    python_requires=">=3.9",
-    install_requires=["aiohttp>3", "meteocalc>=1.1.0"],
-    entry_points={"console_scripts": ["ecowitt-testserver = aioecowitt.__main__:main"]},
-    include_package_data=True,
-    zip_safe=False,
-    classifiers=[
-        "Development Status :: 4 - Beta",
-        "Intended Audience :: Developers",
-        "Natural Language :: English",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Topic :: Home Automation",
-        "License :: OSI Approved :: Apache Software License",
-    ],
-)
diff -pruN 2025.3.1-1/tests/conftest.py 2025.9.1-1/tests/conftest.py
--- 2025.3.1-1/tests/conftest.py	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/tests/conftest.py	2025-09-06 19:46:05.000000000 +0000
@@ -1,19 +1,23 @@
+"""Configuration."""
+
 import pytest
+from pytest_aiohttp import AiohttpClient, AiohttpRawServer
 
 from aioecowitt import server
 
 
-@pytest.fixture
-def ecowitt_server():
+@pytest.fixture(name="ecowitt_server")
+def ecowitt_server_fixture() -> server.EcoWittListener:
     """EcoWitt server fixture."""
-    ecowitt_server = server.EcoWittListener()
-    yield ecowitt_server
+    return server.EcoWittListener()
 
 
 @pytest.fixture
-def ecowitt_http(event_loop, aiohttp_raw_server, aiohttp_client, ecowitt_server):
+async def ecowitt_http(
+    aiohttp_raw_server: AiohttpRawServer,
+    aiohttp_client: AiohttpClient,
+    ecowitt_server: server.EcoWittListener,
+) -> AiohttpClient:
     """EcoWitt HTTP fixture."""
-    raw_server = event_loop.run_until_complete(
-        aiohttp_raw_server(ecowitt_server.handler)
-    )
-    return event_loop.run_until_complete(aiohttp_client(raw_server))
+    raw_server = await aiohttp_raw_server(ecowitt_server.handler)
+    return await aiohttp_client(raw_server)
diff -pruN 2025.3.1-1/tests/test_calc.py 2025.9.1-1/tests/test_calc.py
--- 2025.3.1-1/tests/test_calc.py	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/tests/test_calc.py	2025-09-06 19:46:05.000000000 +0000
@@ -1,13 +1,13 @@
-"""Test for calulcated values from calc.py"""
+"""Test for calulcated values from calc.py."""
 
 from aioecowitt import calc
 
 from .const import EASYWEATHER_DATA, GW2000A_DATA
 
 
-def test_gw2000a_v2():
-    """Test Calculated values from GW2000A_V2"""
-    values = calc.weather_datapoints(GW2000A_DATA)
+def test_gw2000a_v2() -> None:
+    """Test Calculated values from GW2000A_V2."""
+    values = calc.weather_datapoints(GW2000A_DATA.copy())
 
     assert values == {
         "PASSKEY": "345544D8EAF42E1B8824A86D8250D5A3",
@@ -70,9 +70,9 @@ def test_gw2000a_v2():
     }
 
 
-def test_easyweather():
+def test_easyweather() -> None:
     """Test EasyWeather station."""
-    values = calc.weather_datapoints(EASYWEATHER_DATA)
+    values = calc.weather_datapoints(EASYWEATHER_DATA.copy())
 
     assert values == {
         "PASSKEY": "34271334ED1FADA6D8B988B14267E55D",
diff -pruN 2025.3.1-1/tests/test_sensor.py 2025.9.1-1/tests/test_sensor.py
--- 2025.3.1-1/tests/test_sensor.py	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/tests/test_sensor.py	2025-09-06 19:46:05.000000000 +0000
@@ -1,17 +1,20 @@
 """Test ecowitt sensor module."""
 
+from typing import Any
+
 import pytest
+from pytest_aiohttp import AiohttpClient
 
-from aioecowitt import server
+from aioecowitt.sensor import EcoWittSensor, EcoWittSensorTypes
+from aioecowitt.server import EcoWittListener
 
 from .const import GW2000A_V3_DATA
-from aioecowitt import sensor
 
 
 def test_update_listener() -> None:
     """Test on change get updates from callback."""
-    ecowit_sensor = sensor.EcoWittSensor(
-        "test", "test", sensor.EcoWittSensorTypes.TEMPERATURE_C, "test"
+    ecowit_sensor = EcoWittSensor(
+        "test", "test", EcoWittSensorTypes.TEMPERATURE_C, "test"
     )
 
     called = False
@@ -34,23 +37,35 @@ def test_update_listener() -> None:
     assert called
 
 
-@pytest.mark.asyncio
-async def test_heap_field(ecowitt_server, ecowitt_http) -> None:
-    """Test handling of heap field."""
-    heap_sensor = None
+@pytest.mark.parametrize(
+    ("sensor_key", "expected_value", "request_data"),
+    [
+        ("heap", GW2000A_V3_DATA["heap"], GW2000A_V3_DATA),
+        ("vpd", 0.091, {**GW2000A_V3_DATA, "vpd": "0.091"}),
+    ],
+)
+async def test_sensor(
+    ecowitt_server: EcoWittListener,
+    ecowitt_http: AiohttpClient,
+    sensor_key: str,
+    expected_value: Any,
+    request_data: dict[str, Any],
+) -> None:
+    """Test sensor is correct handled."""
+    target_sensor = None
 
-    def on_change(sensor: server.EcoWittSensor) -> None:
+    def on_change(sensor: EcoWittSensor) -> None:
         """Test callback."""
-        if sensor.key == "heap":
-            nonlocal heap_sensor
-            heap_sensor = sensor
+        if sensor.key == sensor_key:
+            nonlocal target_sensor
+            target_sensor = sensor
 
     ecowitt_server.new_sensor_cb.append(on_change)
 
-    resp = await ecowitt_http.post("/", data=GW2000A_V3_DATA)
+    resp = await ecowitt_http.post("/", data=request_data)
     assert resp.status == 200
     text = await resp.text()
     assert text == "OK"
 
-    assert heap_sensor
-    assert heap_sensor.value == GW2000A_V3_DATA["heap"]
+    assert target_sensor
+    assert target_sensor.value == expected_value
diff -pruN 2025.3.1-1/tests/test_server.py 2025.9.1-1/tests/test_server.py
--- 2025.3.1-1/tests/test_server.py	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/tests/test_server.py	2025-09-06 19:46:05.000000000 +0000
@@ -1,16 +1,17 @@
 """EcoWitt server tests."""
 
-import pytest
+from pytest_aiohttp import AiohttpClient
 
 from aioecowitt import server
 
-from .const import GW2000A_DATA, EASYWEATHER_DATA
+from .const import EASYWEATHER_DATA, GW2000A_DATA
 
 # pylint: disable=redefined-outer-name
 
 
-@pytest.mark.asyncio
-async def test_server_start(ecowitt_server, ecowitt_http) -> None:
+async def test_server_start(
+    ecowitt_server: server.EcoWittListener, ecowitt_http: AiohttpClient
+) -> None:
     """Test server start."""
     sensors = []
 
@@ -32,8 +33,9 @@ async def test_server_start(ecowitt_serv
     assert "PASSKEY" not in ecowitt_server.last_values[GW2000A_DATA["PASSKEY"]]
 
 
-@pytest.mark.asyncio
-async def test_server_token(ecowitt_server, ecowitt_http) -> None:
+async def test_server_token(
+    ecowitt_server: server.EcoWittListener, ecowitt_http: AiohttpClient
+) -> None:
     """Test server start."""
     sensors = []
     path = "/test"
@@ -58,8 +60,9 @@ async def test_server_token(ecowitt_serv
     assert len(ecowitt_server.stations) == 1
 
 
-@pytest.mark.asyncio
-async def test_server_multi_stations(ecowitt_server, ecowitt_http) -> None:
+async def test_server_multi_stations(
+    ecowitt_server: server.EcoWittListener, ecowitt_http: AiohttpClient
+) -> None:
     """Test server start and multiple stations."""
     sensors = []
 
diff -pruN 2025.3.1-1/tests/test_station.py 2025.9.1-1/tests/test_station.py
--- 2025.3.1-1/tests/test_station.py	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/tests/test_station.py	2025-09-06 19:46:05.000000000 +0000
@@ -1,12 +1,12 @@
-"""Test for stations from station.py"""
+"""Test for stations from station.py."""
 
 from aioecowitt import station
 
 from .const import EASYWEATHER_DATA, GW2000A_DATA
 
 
-def test_gw2000a_v2():
-    """Test Calculated values from GW2000A_V2"""
+def test_gw2000a_v2() -> None:
+    """Test Calculated values from GW2000A_V2."""
     ecowitt_station = station.extract_station(GW2000A_DATA)
 
     assert "PASSKEY" not in GW2000A_DATA
@@ -22,7 +22,7 @@ def test_gw2000a_v2():
     assert ecowitt_station.frequence == "868M"
 
 
-def test_easyweather():
+def test_easyweather() -> None:
     """Test EasyWeather station."""
     ecowitt_station = station.extract_station(EASYWEATHER_DATA)
 
diff -pruN 2025.3.1-1/tox.ini 2025.9.1-1/tox.ini
--- 2025.3.1-1/tox.ini	2025-03-05 13:24:27.000000000 +0000
+++ 2025.9.1-1/tox.ini	1970-01-01 00:00:00.000000000 +0000
@@ -1,21 +0,0 @@
-[tox]
-envlist = lint, black, tests
-
-[testenv]
-basepython = python3
-deps =
-    -r{toxinidir}/requirements_tests.txt
-
-[testenv:lint]
-ignore_errors = True
-commands =
-     flake8 aioecowitt
-     pylint aioecowitt
-
-[testenv:black]
-commands =
-    black --target-version py39 --check aioecowitt setup.py tests
-
-[testenv:tests]
-commands =
-    pytest --timeout=10 tests
