Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
fail-fast: false
steps:
- uses: actions/checkout@v4
Expand Down
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ include *.md

# Include the license file
include LICENSE

# Include the py.typed marker for PEP 561 type checking
include pynobo/py.typed
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,35 @@ not perform any I/O, and can safely be called from the event loop.
* get_current_zone_temperature - Get the current temperature from (the first component in) a zone
* get_zone_override_mode - Get the override mode for the zone

## Exceptions

Errors raised by pynobo inherit from `PynoboError`:

* `PynoboConnectionError` — TCP connection to the hub failed or was lost
* `PynoboHandshakeError` — the hub rejected the handshake (bad serial, wrong API version, etc.)
* `PynoboValidationError` — invalid parameters. Also inherits `ValueError` for backwards compatibility with callers
written against earlier versions.

## Backwards compatibility

Synchronous wrapper methods are available for compatibility with v1.1.2, but it is recommended to
switch to the async methods by initializing the hub with `synchronous=False`. Otherwise, initializing
will start the async event loop in a daemon thread, discover and connect to hub before returning as before.
**Deprecated as of 1.9.0, to be removed in 2.0.** The synchronous wrapper API is still available for
compatibility with v1.1.2, but every sync entry point now emits a `DeprecationWarning`. Migrate to
the async API — initialize with `synchronous=False` and call the `async_*` methods from an event
loop (or `asyncio.run(...)`).

> The following APIs emit a `DeprecationWarning`:
>
> - `synchronous=True` in `nobo(...)` — the daemon-thread wrapper. Use the async API and
> `asyncio.run(hub.connect())` (or await from an existing event loop) instead.
> - `nobo.connect_hub(ip, serial)` — use `await hub.async_connect_hub(ip, serial)`.
> - `nobo.discover_hubs(...)` — use `await nobo.async_discover_hubs(...)`.
> - `hub.send_command(commands)` — use `await hub.async_send_command(commands)`.
> - `hub.create_override(...)` — use `await hub.async_create_override(...)`.
> - `hub.update_zone(...)` — use `await hub.async_update_zone(...)`.
> - `loop=` parameter in `nobo(...)` and `nobo.async_discover_hubs(...)`.

While `synchronous=True` remains supported in 1.x, initializing this way starts the async event loop
in a daemon thread, discovers and connects to the hub before returning as before.

import time
from pynobo import nobo
Expand Down
321 changes: 230 additions & 91 deletions pynobo.py → pynobo/__init__.py

Large diffs are not rendered by default.

Empty file added pynobo/py.typed
Empty file.
37 changes: 37 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "pynobo"
version = "1.8.1"
description = "Nobø Hub / Nobø Energy Control TCP/IP Interface"
readme = "README.md"
license = { text = "GPL-3.0-or-later" }
authors = [
{ name = "echoromeo, capelevy, oyvindwe" },
]
keywords = ["hvac", "nobø", "heating", "automation"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Topic :: Home Automation",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Typing :: Typed",
]
requires-python = ">=3.10, <4"

[project.urls]
Homepage = "https://github.com/echoromeo/pynobo"
Source = "https://github.com/echoromeo/pynobo"
"Bug Reports" = "https://github.com/echoromeo/pynobo/issues"

[tool.setuptools]
packages = ["pynobo"]
zip-safe = false
include-package-data = true

[tool.setuptools.package-data]
pynobo = ["py.typed"]
8 changes: 5 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,16 @@
#
# py_modules=["my_module"],
#
# packages=find_packages(exclude=['contrib', 'docs', 'tests']), # Required
py_modules=["pynobo"],
packages=["pynobo"],
package_data={"pynobo": ["py.typed"]},
include_package_data=True,
zip_safe=False,

# Specify which Python versions you support. In contrast to the
# 'Programming Language' classifiers above, 'pip install' will check this
# and refuse to install the project if the version does not match. See
# https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
python_requires='>=3.7, <4',
python_requires='>=3.10, <4',

# This field lists other packages that your project depends on to run.
# Any package you put here will be installed by pip when your project is
Expand Down
41 changes: 40 additions & 1 deletion test_pynobo.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import pathlib
import unittest

from pynobo import nobo
from pynobo import (
PynoboConnectionError,
PynoboError,
PynoboHandshakeError,
PynoboValidationError,
nobo,
)

class TestValidation(unittest.TestCase):

Expand Down Expand Up @@ -45,5 +52,37 @@ def test_validate_week_profile(self):
with self.assertRaisesRegex(ValueError, "not in whole quarters"):
nobo.API.validate_week_profile(['00000','01231','00000','00000','00000','00000','00000','00000'])


class TestExceptionHierarchy(unittest.TestCase):

def test_validation_error_raised_and_inherits_value_error(self):
with self.assertRaises(PynoboValidationError):
nobo.API.validate_temperature(6)
# back-compat: callers catching ValueError still work
with self.assertRaises(ValueError):
nobo.API.validate_temperature(6)

def test_validation_error_raised_for_type_check(self):
with self.assertRaises(PynoboValidationError):
nobo.API.validate_temperature(0.0)
# back-compat: callers catching TypeError still work
with self.assertRaises(TypeError):
nobo.API.validate_temperature(0.0)

def test_all_errors_inherit_base(self):
self.assertTrue(issubclass(PynoboConnectionError, PynoboError))
self.assertTrue(issubclass(PynoboHandshakeError, PynoboError))
self.assertTrue(issubclass(PynoboValidationError, PynoboError))
self.assertTrue(issubclass(PynoboValidationError, ValueError))
self.assertTrue(issubclass(PynoboValidationError, TypeError))


class TestPyTypedMarker(unittest.TestCase):

def test_py_typed_file_is_present_in_source(self):
marker = pathlib.Path(__file__).parent / "pynobo" / "py.typed"
self.assertTrue(marker.is_file(), f"{marker} is missing")


if __name__ == '__main__':
unittest.main()
Loading