├── openhab
├── py.typed
├── __init__.py
├── exceptions.py
├── rules.py
├── config.py
├── oauth2_helper.py
├── client.py
├── command_types.py
└── items.py
├── tests
├── __init__.py
├── conftest.py
├── test_oauth.py
└── test_basic.py
├── .github
├── FUNDING.yml
└── workflows
│ ├── pypi_publish.yml
│ └── test_lint.yml
├── setup.py
├── docs
├── api_items.md
├── api_client.md
├── api_types.md
└── index.md
├── docker
├── openhab_conf
│ ├── transform
│ │ ├── de.map
│ │ ├── en.map
│ │ └── readme.txt
│ ├── sounds
│ │ ├── barking.mp3
│ │ └── doorbell.mp3
│ ├── rules
│ │ └── readme.txt
│ ├── items
│ │ ├── readme.txt
│ │ └── basic.items
│ ├── things
│ │ └── readme.txt
│ ├── scripts
│ │ └── readme.txt
│ ├── sitemaps
│ │ └── readme.txt
│ ├── persistence
│ │ └── readme.txt
│ ├── html
│ │ ├── readme.txt
│ │ └── index.html
│ ├── icons
│ │ └── classic
│ │ │ └── readme.txt
│ └── services
│ │ ├── readme.txt
│ │ ├── addons.cfg
│ │ └── runtime.cfg
└── test_connectivity.py
├── MANIFEST.in
├── .editorconfig
├── mkdocs.yml
├── .readthedocs.yaml
├── .gitignore
├── test_groups.py
├── test.py
├── README.md
├── pyproject.toml
└── LICENSE
/openhab/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: sim0n
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | setuptools.setup()
4 |
--------------------------------------------------------------------------------
/docs/api_items.md:
--------------------------------------------------------------------------------
1 | # API Documentation - Items
2 |
3 | ::: openhab.items
4 |
--------------------------------------------------------------------------------
/docs/api_client.md:
--------------------------------------------------------------------------------
1 | # API Documentation - Client
2 |
3 | ::: openhab.client
4 |
--------------------------------------------------------------------------------
/docker/openhab_conf/transform/de.map:
--------------------------------------------------------------------------------
1 | CLOSED=zu
2 | OPEN=offen
3 | NULL=undefiniert
4 |
--------------------------------------------------------------------------------
/docker/openhab_conf/transform/en.map:
--------------------------------------------------------------------------------
1 | CLOSED=closed
2 | OPEN=open
3 | NULL=unknown
4 |
--------------------------------------------------------------------------------
/docs/api_types.md:
--------------------------------------------------------------------------------
1 | # API Documentation - Types
2 |
3 | ::: openhab.command_types
4 |
--------------------------------------------------------------------------------
/openhab/__init__.py:
--------------------------------------------------------------------------------
1 | """Module entry point."""
2 |
3 | from .client import OpenHAB
4 |
5 | __all__ = ['OpenHAB']
6 |
--------------------------------------------------------------------------------
/docker/openhab_conf/sounds/barking.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sim0nx/python-openhab/HEAD/docker/openhab_conf/sounds/barking.mp3
--------------------------------------------------------------------------------
/docker/openhab_conf/sounds/doorbell.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sim0nx/python-openhab/HEAD/docker/openhab_conf/sounds/doorbell.mp3
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | exclude .editorconfig
2 | exclude .gitignore
3 | exclude .pylintrc
4 | exclude .readthedocs.yaml
5 | exclude .travis.yml
6 | exclude mkdocs.yml
7 |
8 |
9 | prune .github
10 | prune docker
11 | prune docs
--------------------------------------------------------------------------------
/docker/openhab_conf/rules/readme.txt:
--------------------------------------------------------------------------------
1 | Your rules go here.
2 | All rule files have to have the ".rules" file extension and must follow a special syntax.
3 |
4 | Check out the openHAB documentation for more details:
5 | https://www.openhab.org/docs/configuration/rules-dsl.html
6 |
--------------------------------------------------------------------------------
/docker/openhab_conf/items/readme.txt:
--------------------------------------------------------------------------------
1 | Your item definitions go here.
2 | All items files have to have the ".items" file extension and must follow a special syntax.
3 |
4 | Check out the openHAB documentation for more details:
5 | https://www.openhab.org/docs/configuration/items.html
6 |
--------------------------------------------------------------------------------
/docker/openhab_conf/things/readme.txt:
--------------------------------------------------------------------------------
1 | Your thing definitions go here.
2 | All thing files have to have the ".things" file extension and must follow a special syntax.
3 |
4 | Check out the openHAB documentation for more details:
5 | https://www.openhab.org/docs/configuration/things.html
6 |
--------------------------------------------------------------------------------
/docker/openhab_conf/scripts/readme.txt:
--------------------------------------------------------------------------------
1 | Your scripts go here.
2 | All script files have to have the ".script" file extension and must follow a special syntax.
3 |
4 | Check out the openHAB documentation for more details:
5 | https://www.openhab.org/docs/configuration/rules-dsl.html#scripts
6 |
--------------------------------------------------------------------------------
/docker/openhab_conf/sitemaps/readme.txt:
--------------------------------------------------------------------------------
1 | Your sitemap definitions go here.
2 | All sitemap files have to have the ".sitemap" file extension and must follow a special syntax.
3 |
4 | Check out the openHAB documentation for more details:
5 | https://www.openhab.org/docs/configuration/sitemaps.html
6 |
--------------------------------------------------------------------------------
/openhab/exceptions.py:
--------------------------------------------------------------------------------
1 | """python-openhab exceptions."""
2 |
3 |
4 | class OpenHABException(Exception):
5 | """Base of all python-openhab exceptions."""
6 |
7 |
8 | class InvalidReturnException(OpenHABException):
9 | """The openHAB server returned an invalid or unparsable result."""
10 |
--------------------------------------------------------------------------------
/docker/openhab_conf/persistence/readme.txt:
--------------------------------------------------------------------------------
1 | Your persistence configuration goes here.
2 | All persistence files have to have the ".persist" file extension and must follow a special syntax.
3 |
4 | Check out the openHAB documentation for more details:
5 | https://www.openhab.org/docs/configuration/persistence.html
6 |
--------------------------------------------------------------------------------
/docker/openhab_conf/html/readme.txt:
--------------------------------------------------------------------------------
1 | Serve your own static html pages or resources from here.
2 | Files stored in this folder will be available through the HTTP server of openHAB, e.g. "http://device-address:8080/static/image.png".
3 | Resources for sitemap elements (image, video,...) can also be provided though this folder.
4 |
--------------------------------------------------------------------------------
/docker/openhab_conf/transform/readme.txt:
--------------------------------------------------------------------------------
1 | Transformations like map or jsonpath can utilize configuration files with data definitions.
2 | These files have their specific file extensions and syntax definition.
3 |
4 | Check out the openHAB documentation for more details:
5 | https://www.openhab.org/docs/configuration/transformations.html
6 |
--------------------------------------------------------------------------------
/docker/openhab_conf/icons/classic/readme.txt:
--------------------------------------------------------------------------------
1 | Your additional icons go here.
2 | Icons can be provided as png (32x32) or preferably as svg files.
3 | ClassicUI and BasicUI can be configured to accept svg (default) or png icons.
4 |
5 | Check out the openHAB documentation for more details:
6 | https://www.openhab.org/docs/configuration/items.html#icons
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_style = space
7 |
8 | [{*.pyw,*.py}]
9 | indent_size = 2
10 | tab_width = 2
11 | max_line_length = 240
12 | ij_continuation_indent_size = 2
13 | ij_python_blank_line_at_file_end = true
14 |
15 | [{*.yml_sample,*.yaml_sample,*.yml,*.yaml,*.toml}]
16 | indent_size = 2
17 |
--------------------------------------------------------------------------------
/docker/openhab_conf/services/readme.txt:
--------------------------------------------------------------------------------
1 | Your service configurations will reside here.
2 | All configuration files have to have the ".cfg" file extension.
3 | Service configuration files are automatically created as soon as you install an add-on that can be configured.
4 |
5 | Check out the openHAB documentation for more details:
6 | https://www.openhab.org/docs/configuration/services.html
7 |
--------------------------------------------------------------------------------
/docker/openhab_conf/html/index.html:
--------------------------------------------------------------------------------
1 |
2 |
openHAB user provided static content
3 | Serve your own static html pages or resources from here. Files stored in the openHAB configuration subfolder html will be available through the HTTP server of openHAB, e.g. http://device-address:8080/static/image.png.
4 | Resources for sitemap elements (image, video,...) can also be provided though this folder.
5 |
6 |
--------------------------------------------------------------------------------
/docker/test_connectivity.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | """Tries to connect to OpenHAB and fetch items. Loop until successful."""
4 |
5 | import json
6 | import time
7 |
8 | import httpx
9 |
10 | base_url = 'http://localhost:8080/rest'
11 |
12 | while True:
13 | try:
14 | req = httpx.get(base_url + '/items')
15 | items = req.json()
16 | except (httpx.RequestError, json.JSONDecodeError) as exc:
17 | print(str(exc))
18 | else:
19 | if req.status_code == 200:
20 | break
21 |
22 | time.sleep(0.5)
23 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: python-openhab
2 | site_url: https://github.com/sim0nx/python-openhab
3 | repo_url: https://github.com/sim0nx/python-openhab
4 |
5 |
6 | theme:
7 | name: "material"
8 | font: false
9 |
10 | plugins:
11 | - search
12 | - mkdocstrings:
13 | watch:
14 | - openhab
15 |
16 | markdown_extensions:
17 | - pymdownx.highlight:
18 | linenums: true
19 | linenums_style: pymdownx-inline
20 | - pymdownx.superfences
21 | - admonition
22 |
23 | nav:
24 | - Home: 'index.md'
25 | - API Documentation:
26 | - 'Client': 'api_client.md'
27 | - 'Items': 'api_items.md'
28 | - 'Types': 'api_types.md'
29 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yaml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | # Required
6 | version: 2
7 |
8 | # Set the version of Python and other tools you might need
9 | build:
10 | os: ubuntu-lts-latest
11 | tools:
12 | python: "3"
13 | jobs:
14 | pre_create_environment:
15 | - asdf plugin add uv
16 | - asdf install uv latest
17 | - asdf global uv latest
18 | create_environment:
19 | - uv venv "${READTHEDOCS_VIRTUALENV_PATH}"
20 | install:
21 | - UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --group docs
22 |
23 | mkdocs:
24 | configuration: mkdocs.yml
25 |
--------------------------------------------------------------------------------
/docker/openhab_conf/items/basic.items:
--------------------------------------------------------------------------------
1 | Group Home "Our Home" ["Building"]
2 |
3 | Group GF "Ground Floor" (Home) ["GroundFloor"]
4 |
5 | Group Dining "Dining" (Home) ["Room"]
6 |
7 | Number:Temperature Dining_Temperature "Temperature [%.1f °C]" (Dining, gTemperature) ["Temperature"]
8 |
9 | Group:Number:AVG gTemperature "Temperature" (Home) ["Temperature"]
10 |
11 | Contact TheContact "contact test" (Home)
12 |
13 | DateTime TheDateTime "datetime test [%s]" (Home)
14 |
15 | String stringtest
16 |
17 | Number floattest
18 |
19 | Color color_item "Color item test"
20 |
21 | Location location_item
22 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pathlib
3 |
4 | import pytest
5 |
6 | import openhab.oauth2_helper
7 |
8 | # ruff: noqa: S106
9 |
10 |
11 | @pytest.fixture(scope='session')
12 | def oh() -> 'openhab.OpenHAB':
13 | """Setup a generic connection."""
14 | base_url = 'http://localhost:8080/rest'
15 | return openhab.OpenHAB(base_url)
16 |
17 |
18 | @pytest.fixture(scope='session')
19 | def oh_oauth2() -> 'openhab.OpenHAB':
20 | """Setup a generic connection."""
21 | url_base = 'http://localhost:8080'
22 | url_rest = f'{url_base}/rest'
23 |
24 | # this must be set for oauthlib to work on http
25 | os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
26 |
27 | oauth2_token = openhab.oauth2_helper.get_oauth2_token(url_base, username='admin', password='admin')
28 |
29 | oauth2_config = {
30 | 'client_id': r'http://127.0.0.1/auth',
31 | 'token_cache': str(pathlib.Path(__file__).resolve().parent.parent / '.oauth2_token_test'),
32 | 'token': oauth2_token,
33 | }
34 |
35 | return openhab.OpenHAB(url_rest, oauth2_config=oauth2_config)
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | uv.lock
6 |
7 | # C extensions
8 | *.so
9 |
10 | # Distribution / packaging
11 | .Python
12 | env/
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *,cover
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 |
55 | # Sphinx documentation
56 | docs/_build/
57 |
58 | # PyBuilder
59 | target/
60 |
61 | *.swp
62 | *.swo
63 |
64 | .oauth2_token*
65 |
66 | .pydevproject
67 | .project
68 | .settings
69 | .idea
70 | .mypy_cache
71 | .vscode
72 | venv
73 |
--------------------------------------------------------------------------------
/.github/workflows/pypi_publish.yml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | release-build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-python@v5
14 | with:
15 | python-version: '3.x'
16 | - name: Install dependencies and build wheel
17 | run: |
18 | python -m pip install uv
19 | uv build --wheel
20 | - name: Upload distributions
21 | uses: actions/upload-artifact@v4
22 | with:
23 | name: release-dists
24 | path: dist/
25 |
26 | pypi-publish:
27 | name: upload release to PyPI
28 | runs-on: ubuntu-latest
29 | needs:
30 | - release-build
31 | permissions:
32 | # IMPORTANT: this permission is mandatory for trusted publishing
33 | id-token: write
34 | # Specifying a GitHub environment is optional, but strongly encouraged
35 | environment:
36 | name: release
37 | url: https://pypi.org/project/python-openhab/
38 |
39 | steps:
40 | - name: Retrieve release distributions
41 | uses: actions/download-artifact@v4
42 | with:
43 | name: release-dists
44 | path: dist/
45 |
46 | - name: Publish package distributions to PyPI
47 | uses: pypa/gh-action-pypi-publish@release/v1
48 |
--------------------------------------------------------------------------------
/test_groups.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | #
5 | # Łukasz Matuszek (c) 2020-present
6 | #
7 | # python-openhab is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # python-openhab is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU General Public License
18 | # along with python-openhab. If not, see .
19 | #
20 |
21 | import openhab
22 |
23 | base_url = 'http://localhost:8080/rest'
24 | openhab = openhab.OpenHAB(base_url)
25 |
26 | # fetch all items
27 | print(' - Print all items:')
28 | all_items = openhab.fetch_all_items()
29 | for i in all_items.values():
30 | print(i)
31 |
32 | # fetch some group
33 | lights_group = openhab.get_item('Lights')
34 |
35 | print(' - Send command to group')
36 | lights_group.on()
37 |
38 | print(' - Update all lights to OFF')
39 | for v in lights_group.members.values():
40 | v.update('OFF')
41 |
42 | print(' - Print all lights:')
43 | for v in lights_group.members.values():
44 | print(v)
45 |
--------------------------------------------------------------------------------
/openhab/rules.py:
--------------------------------------------------------------------------------
1 | """python library for accessing the openHAB REST API."""
2 |
3 | #
4 | # Georges Toth (c) 2016-present
5 | #
6 | # python-openhab is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # python-openhab is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with python-openhab. If not, see .
18 | #
19 |
20 | # pylint: disable=bad-indentation
21 |
22 | import logging
23 | import typing
24 |
25 | if typing.TYPE_CHECKING:
26 | import openhab.client
27 |
28 | __author__ = 'Georges Toth '
29 | __license__ = 'AGPLv3+'
30 |
31 |
32 | class Rules:
33 | """Base rule class."""
34 |
35 | def __init__(self, openhab_conn: 'openhab.client.OpenHAB') -> None:
36 | """Constructor.
37 |
38 | Args:
39 | openhab_conn (openhab.OpenHAB): openHAB object.
40 | """
41 | self.openhab = openhab_conn
42 | self.logger = logging.getLogger(__name__)
43 |
44 | def get(self) -> list[dict[str, typing.Any]]:
45 | """Get all rules."""
46 | return self.openhab.req_get('/rules')
47 |
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | #
4 | # Georges Toth (c) 2016-present
5 | #
6 | # python-openhab is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # python-openhab is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with python-openhab. If not, see .
18 | #
19 |
20 |
21 | import datetime
22 | import json
23 |
24 | import openhab
25 |
26 | base_url = 'http://localhost:8080/rest'
27 | openhab = openhab.OpenHAB(base_url)
28 |
29 | # fetch all items
30 | items = openhab.fetch_all_items()
31 |
32 | # fetch other items, show how to toggle a switch
33 | sunset = items.get('Sunset')
34 | sunrise = items.get('Sunrise')
35 | knx_day_night = items.get('KNX_day_night')
36 |
37 | now = datetime.datetime.now(datetime.timezone.utc)
38 |
39 | if now > sunrise.state and now < sunset.state:
40 | knx_day_night.on()
41 | else:
42 | knx_day_night.off()
43 |
44 | print(knx_day_night.state)
45 |
46 | # start_time for fetching persistence data
47 | start_time = datetime.datetime.fromtimestamp(1695504300123 / 1000, tz=datetime.UTC)
48 |
49 | # fetch persistence data using the OpenHAB client object
50 | for k in openhab.get_item_persistence(knx_day_night.name, page_length=20, start_time=start_time):
51 | print(json.dumps(k, indent=4))
52 |
53 | # fetch persistence data using the item directly
54 | for k in knx_day_night.persistence(page_length=20, start_time=start_time):
55 | print(json.dumps(k, indent=4))
56 |
--------------------------------------------------------------------------------
/openhab/config.py:
--------------------------------------------------------------------------------
1 | """python library for accessing the openHAB REST API."""
2 |
3 | #
4 | # Georges Toth (c) 2016-present
5 | #
6 | # python-openhab is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # python-openhab is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with python-openhab. If not, see .
18 | #
19 |
20 | import pathlib
21 | import time
22 | import typing
23 |
24 | import pydantic
25 |
26 | """Considering a oauth2 token config is expected to look like the following:
27 |
28 | ```
29 | {"client_id": "http://127.0.0.1/auth",
30 | "token_cache": "///.oauth2_token",
31 | "token": {
32 | "access_token": "adsafdasfasfsafasfsafasfasfasfsa....",
33 | "expires_in": 3600,
34 | "refresh_token": "312e21e21e32112",
35 | "scope": "admin",
36 | "token_type": "bearer",
37 | "user": {
38 | "name": "admin",
39 | "roles": [
40 | "administrator"
41 | ]
42 | }
43 | }
44 | }
45 | ```
46 |
47 | , the following classes model that structure for validation.
48 | """
49 |
50 |
51 | class Oauth2User(pydantic.BaseModel):
52 | """Nested user structure within an oauth2 token."""
53 |
54 | name: str
55 | roles: list[str]
56 |
57 |
58 | class Oauth2Token(pydantic.BaseModel):
59 | """Structure as returned by openHAB when generating a new oauth2 token."""
60 |
61 | access_token: str
62 | expires_in: int
63 | expires_at: float = time.time() - 10
64 | refresh_token: str
65 | scope: typing.Union[str, list[str]] = 'admin'
66 | token_type: str
67 | user: Oauth2User
68 |
69 |
70 | class Oauth2Config(pydantic.BaseModel):
71 | """Structure expected for a full oauth2 config."""
72 |
73 | client_id: str = 'http://127.0.0.1/auth'
74 | token_cache: pathlib.Path
75 | token: Oauth2Token
76 |
--------------------------------------------------------------------------------
/docker/openhab_conf/services/addons.cfg:
--------------------------------------------------------------------------------
1 | # The installation package of this openHAB instance
2 | # Note: This is only regarded at the VERY FIRST START of openHAB
3 | # Note: If you want to specify your add-ons yourself through entries below, set the package to "minimal"
4 | # as otherwise your definition might be in conflict with what the installation package defines.
5 | #
6 | # Optional. If not set, the dashboard (https://:8080/) will ask you to choose a package.
7 | #
8 | # Valid options:
9 | # - minimal : Installation only with dashboard, but no UIs or other add-ons. Use this for custom setups.
10 | # - simple : Setup for using openHAB purely through UIs - you need to expect MANY constraints in functionality!
11 | # - standard : Default setup for normal users, best for textual setup
12 | # - expert : Setup for expert users, especially for people migrating from openHAB 1.x
13 | # - demo : A demo setup which includes UIs, a few bindings, config files etc.
14 | #
15 | # See https://www.openhab.org/docs/configuration/packages.html for a detailed explanation of these packages.
16 | #
17 | #package = minimal
18 |
19 | # Access Remote Add-on Repository
20 | # Defines whether the remote openHAB add-on repository should be used for browsing and installing add-ons.
21 | # This not only makes latest snapshots of add-ons available, it is also required for the installation of
22 | # any legacy 1.x add-on. (default is true)
23 | #
24 | #remote = true
25 |
26 | # Include legacy 1.x bindings. If set to true, it also allows the installation of 1.x bindings for which there is
27 | # already a 2.x version available (requires remote repo access, see above). (default is false)
28 | #
29 | #legacy = true
30 |
31 | # A comma-separated list of bindings to install (e.g. "binding = sonos,knx,zwave")
32 | #binding =
33 |
34 | # A comma-separated list of UIs to install (e.g. "ui = basic,paper")
35 | #ui =
36 |
37 | # A comma-separated list of persistence services to install (e.g. "persistence = rrd4j,jpa")
38 | #persistence =
39 |
40 | # A comma-separated list of actions to install (e.g. "action = mail,pushover")
41 | #action =
42 |
43 | # A comma-separated list of transformation services to install (e.g. "transformation = map,jsonpath")
44 | #transformation =
45 |
46 | # A comma-separated list of voice services to install (e.g. "voice = marytts,freetts")
47 | #voice =
48 |
49 | # A comma-separated list of miscellaneous services to install (e.g. "misc = myopenhab")
50 | #misc =
51 |
--------------------------------------------------------------------------------
/.github/workflows/test_lint.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | tests:
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
18 |
19 | steps:
20 | - uses: actions/checkout@v4
21 | - name: Set up Python ${{ matrix.python-version }}
22 | uses: actions/setup-python@v5
23 | with:
24 | python-version: ${{ matrix.python-version }}
25 |
26 | - name: Start container
27 | run: |
28 | docker run -d --name openhab -p 8080:8080 -e OPENHAB_HTTP_PORT=8080 -v ${{ github.workspace }}/docker/openhab_conf:/openhab/conf "openhab/openhab:latest"
29 |
30 | - name: Run tests
31 | run: |
32 | python3 -m pip install uv
33 | uv sync --group test
34 | uv run docker/test_connectivity.py
35 |
36 | docker exec -i openhab /openhab/runtime/bin/client -v -p habopen users add admin admin administrator || true
37 | sleep 2
38 |
39 | uv run coverage run --omit tests --parallel-mode -m pytest --junitxml=junit.xml -o junit_family=legacy -v tests
40 |
41 | - name: Upload coverage data
42 | uses: actions/upload-artifact@v4
43 | with:
44 | name: coverage-data-${{ matrix.python-version }}
45 | path: .coverage.*
46 | if-no-files-found: error
47 | include-hidden-files: true
48 |
49 | - name: Upload test results to Codecov
50 | if: ${{ !cancelled() }}
51 | uses: codecov/test-results-action@v1
52 | with:
53 | token: ${{ secrets.CODECOV_TOKEN }}
54 |
55 |
56 | coverage:
57 | name: Combine & check coverage.
58 | runs-on: ubuntu-latest
59 | needs: tests
60 |
61 | steps:
62 | - uses: actions/checkout@v4
63 | - uses: actions/setup-python@v5
64 | with:
65 | python-version: ${{env.PYTHON_LATEST}}
66 | - uses: actions/download-artifact@v4
67 | with:
68 | pattern: coverage-data-*
69 | merge-multiple: true
70 |
71 | - run: |
72 | python3 -m pip install uv
73 | uv venv
74 | uv pip install coverage[toml]
75 | uv run coverage combine
76 | uv run coverage xml
77 | - name: Upload Coverage to Codecov
78 | uses: codecov/codecov-action@v4
79 | with:
80 | fail_ci_if_error: true
81 | files: ./coverage.xml
82 | token: ${{ secrets.CODECOV_TOKEN }}
83 | verbose: true
84 |
85 |
86 | lint_python:
87 | runs-on: ubuntu-latest
88 | steps:
89 | - uses: actions/checkout@v4
90 | - uses: actions/setup-python@v5
91 | with:
92 | python-version: ${{env.PYTHON_LATEST}}
93 |
94 | - run: |
95 | python3 -m pip install uv
96 | uv sync --group dev
97 | uv run mypy --config-file pyproject.toml openhab
98 |
--------------------------------------------------------------------------------
/openhab/oauth2_helper.py:
--------------------------------------------------------------------------------
1 | """OAuth2 helper method for generating and fetching an OAuth2 token."""
2 |
3 | import typing
4 |
5 | import bs4
6 | import httpx
7 |
8 |
9 | def get_oauth2_token(
10 | base_url: str,
11 | username: str,
12 | password: str,
13 | client_id: typing.Optional[str] = None,
14 | redirect_url: typing.Optional[str] = None,
15 | scope: typing.Optional[str] = None,
16 | ) -> dict:
17 | """Method for generating an OAuth2 token.
18 |
19 | Args:
20 | base_url: openHAB base URL
21 | username: Admin account username
22 | password: Admin account password
23 | client_id: OAuth2 client ID; does not need to be specified
24 | redirect_url: OAuth2 redirect URL; does not need to be specified
25 | scope: Do not change unless you know what you are doing
26 |
27 | Returns:
28 | *dict* with the generated OAuth2 token details
29 | """
30 | if client_id is not None:
31 | oauth2_client_id = client_id
32 | else:
33 | oauth2_client_id = 'http://127.0.0.1/auth'
34 |
35 | if redirect_url is not None:
36 | oauth2_redirect_url = redirect_url
37 | else:
38 | oauth2_redirect_url = 'http://127.0.0.1/auth'
39 |
40 | if scope is not None:
41 | oauth2_scope = scope
42 | else:
43 | oauth2_scope = 'admin'
44 |
45 | oauth2_auth_endpoint = f'{base_url}/rest/auth/token'
46 | url_generate_token = f'{base_url}/auth?response_type=code&redirect_uri={oauth2_redirect_url}&client_id={oauth2_client_id}&scope={oauth2_scope}'
47 |
48 | res = httpx.get(url_generate_token, timeout=30)
49 | res.raise_for_status()
50 |
51 | soup = bs4.BeautifulSoup(res.content, 'html.parser')
52 | submit_form = soup.find('form')
53 |
54 | action = submit_form.attrs.get('action').lower()
55 | url_submit_generate_token = f'{base_url}{action}'
56 |
57 | data = {}
58 |
59 | for input_tag in submit_form.find_all('input'):
60 | input_name = input_tag.attrs.get('name')
61 |
62 | if input_name is None:
63 | continue
64 |
65 | input_value = input_tag.attrs.get('value', '')
66 |
67 | data[input_name] = input_value
68 |
69 | data['username'] = username
70 | data['password'] = password
71 |
72 | res = httpx.post(url_submit_generate_token, data=data, timeout=30)
73 | if not 200 < res.status_code <= 302:
74 | res.raise_for_status()
75 |
76 | if 'location' not in res.headers:
77 | raise KeyError('Token generation failed!')
78 |
79 | oauth_redirect_location = res.headers['location']
80 |
81 | if '?code=' not in oauth_redirect_location:
82 | raise ValueError('Token generation failed!')
83 |
84 | oauth2_registration_code = oauth_redirect_location.split('?code=', 1)[1]
85 |
86 | data = {
87 | 'grant_type': 'authorization_code',
88 | 'code': oauth2_registration_code,
89 | 'redirect_uri': oauth2_redirect_url,
90 | 'client_id': oauth2_client_id,
91 | 'refresh_token': None,
92 | 'code_verifier': None,
93 | }
94 |
95 | res = httpx.post(oauth2_auth_endpoint, data=data, timeout=30)
96 | res.raise_for_status()
97 |
98 | return res.json()
99 |
--------------------------------------------------------------------------------
/docker/openhab_conf/services/runtime.cfg:
--------------------------------------------------------------------------------
1 | ##################### LOCALE ####################
2 |
3 | # The default language that should be used. If not specified, the system default locale is used.
4 | # The ISO 639 alpha-2 or alpha-3 language code (if there is no alpha-2 one).
5 | # Example: "en" (English), "de" (German), "ja" (Japanese), "kok" (Konkani)
6 | #
7 | #org.eclipse.smarthome.core.localeprovider:language=
8 |
9 | # The region that should be used.
10 | # ISO 3166 alpha-2 country code or UN M.49 numeric-3 area code.
11 | # Example: "US" (United States), "DE" (Germany), "FR" (France), "029" (Caribbean)
12 | #
13 | #org.eclipse.smarthome.core.localeprovider:region=
14 |
15 | ################ PERSISTENCE ####################
16 |
17 | # The persistence service to use if no other is specified.
18 | #
19 | #org.eclipse.smarthome.persistence:default=
20 |
21 | ################### AUDIO #######################
22 |
23 | # This parameter defines the default audio source to use (if not set, the first available one will be used.
24 | #
25 | #org.eclipse.smarthome.audio:defaultSource=
26 |
27 | # This parameter defines the default audio sink to use (if not set, the first available one will be used.
28 | #
29 | #org.eclipse.smarthome.audio:defaultSink=
30 |
31 | ##################### VOICE ####################
32 |
33 | # This parameter defines the default text-to-speech service to use (if not set, the first available one will be used.
34 | #
35 | #org.eclipse.smarthome.voice:defaultTTS=
36 |
37 | # This parameter defines the default speech-to-text service to use (if not set, the first available one will be used.
38 | #
39 | #org.eclipse.smarthome.voice:defaultSTT=
40 |
41 | # The default voice to use if no specific TTS service or voice is specified.
42 | #
43 | #org.eclipse.smarthome.voice:defaultVoice=
44 |
45 | # The default human language interpreter to use if no other is specified.
46 | #
47 | #org.eclipse.smarthome.voice:defaultHLI=
48 |
49 | ################ MISCELLANOUS ####################
50 |
51 | # The karaf sshHost parameter configures the bind address for the ssh login to karaf.
52 | # Default is 127.0.0.1 (localhost), so it is only possible to login from the local machine.
53 | #
54 | # Setting this to the address of another network interfaces will allow login from this network.
55 | # Setting this to 0.0.0.0 will allow login from all network interfaces.
56 | #
57 | # !!! Security warning !!!
58 | # Remember to change default login/password, if you allow external login.
59 | # See https://www.openhab.org/docs/administration/console.html for details.
60 | #
61 | #org.apache.karaf.shell:sshHost = 0.0.0.0
62 |
63 | # Setting this to true will automatically approve all inbox entries and create Things for them,
64 | # so that they are immediately available in the system (default is false)
65 | #
66 | #org.eclipse.smarthome.inbox:autoApprove=true
67 |
68 | # This setting allows to switch between a "simple" and an "advanced" mode for item management.
69 | # In simple mode (autoLinks=true), links and their according items are automatically created for new Things.
70 | # In advanced mode (autoLinks=false), the user has the full control about which items channels are linked to.
71 | # Existing links will remain untouched. (default is true)
72 | #
73 | #org.eclipse.smarthome.links:autoLinks=false
74 |
--------------------------------------------------------------------------------
/tests/test_oauth.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import os
3 | import time
4 |
5 | import pytest
6 |
7 | import openhab.oauth2_helper
8 |
9 | # ruff: noqa: S101, ANN201, T201
10 |
11 | pytestmark = pytest.mark.skipif('CI' in os.environ, reason='oauth2 tests currently not working in github CI')
12 |
13 |
14 | def test_fetch_all_items(oh_oauth2: openhab.OpenHAB):
15 | items = oh_oauth2.fetch_all_items()
16 |
17 | assert len(items)
18 |
19 |
20 | def test_datetime_update(oh_oauth2: openhab.OpenHAB):
21 | dt_obj = oh_oauth2.get_item('TheDateTime')
22 | dt_utc_now = datetime.datetime.now(tz=datetime.timezone.utc)
23 | dt_obj.state = dt_utc_now
24 |
25 | assert dt_obj.state.isoformat(timespec='seconds') == dt_utc_now.isoformat(timespec='seconds')
26 |
27 |
28 | def test_datetime_command(oh_oauth2: openhab.OpenHAB):
29 | dt_obj = oh_oauth2.get_item('TheDateTime')
30 | dt_utc_now = datetime.datetime.now(tz=datetime.timezone.utc)
31 | dt_obj.command(dt_utc_now)
32 |
33 | assert dt_obj.state.isoformat(timespec='seconds') == dt_utc_now.isoformat(timespec='seconds')
34 |
35 |
36 | def test_null_undef(oh_oauth2: openhab.OpenHAB):
37 | float_obj = oh_oauth2.get_item('floattest')
38 |
39 | float_obj.update_state_null()
40 | assert float_obj.is_state_null()
41 |
42 | float_obj.update_state_undef()
43 | assert float_obj.is_state_undef()
44 |
45 |
46 | def test_float(oh_oauth2: openhab.OpenHAB):
47 | float_obj = oh_oauth2.get_item('floattest')
48 |
49 | float_obj.state = 1.0
50 | assert float_obj.state == 1.0
51 |
52 |
53 | def test_scientific_notation(oh_oauth2: openhab.OpenHAB):
54 | float_obj = oh_oauth2.get_item('floattest')
55 |
56 | float_obj.state = 1e-10
57 | time.sleep(1) # Allow time for OpenHAB test instance to process state update
58 | assert float_obj.state == 1e-10
59 |
60 |
61 | def test_non_ascii_string(oh_oauth2: openhab.OpenHAB):
62 | string_obj = oh_oauth2.get_item('stringtest')
63 |
64 | string_obj.state = 'שלום'
65 | assert string_obj.state == 'שלום'
66 |
67 | string_obj.state = '°F'
68 | assert string_obj.state == '°F'
69 |
70 |
71 | def test_color_item(oh_oauth2: openhab.OpenHAB):
72 | coloritem = oh_oauth2.get_item('color_item')
73 |
74 | coloritem.update_state_null()
75 | assert coloritem.is_state_null()
76 |
77 | coloritem.state = 1
78 | assert coloritem.state == (0.0, 0.0, 1.0)
79 |
80 | coloritem.state = '1.1, 1.2, 1.3'
81 | assert coloritem.state == (1.1, 1.2, 1.3)
82 |
83 | coloritem.state = 'OFF'
84 | assert coloritem.state == (1.1, 1.2, 0.0)
85 |
86 | coloritem.state = 'ON'
87 | assert coloritem.state == (1.1, 1.2, 100.0)
88 |
89 |
90 | def test_number_temperature(oh_oauth2: openhab.OpenHAB):
91 | # Tests below require the OpenHAB test instance to be configured with '°C' as
92 | # the unit of measure for the 'Dining_Temperature' item
93 | temperature_item = oh_oauth2.get_item('Dining_Temperature')
94 |
95 | temperature_item.state = 1.0
96 | time.sleep(1) # Allow time for OpenHAB test instance to process state update
97 | assert temperature_item.state == 1.0
98 | assert temperature_item.unit_of_measure == '°C'
99 |
100 | temperature_item.state = '2 °C'
101 | time.sleep(1)
102 | assert temperature_item.state == 2
103 | assert temperature_item.unit_of_measure == '°C'
104 |
105 | temperature_item.state = (3, '°C')
106 | time.sleep(1)
107 | assert temperature_item.state == 3
108 | assert temperature_item.unit_of_measure == '°C'
109 |
110 | # Unit of measure conversion (performed by OpenHAB server)
111 | temperature_item.state = (32, '°F')
112 | assert round(temperature_item.state, 2) == 0
113 | temperature_item.state = (212, '°F')
114 | time.sleep(1)
115 | assert temperature_item.state == 100
116 | assert temperature_item.unit_of_measure == '°C'
117 |
118 |
119 | # def test_session_logout(oh_oauth2: openhab.OpenHAB):
120 | # assert oh_oauth2.logout() is True
121 |
--------------------------------------------------------------------------------
/tests/test_basic.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import time
3 |
4 | import pytest
5 |
6 | import openhab
7 |
8 | # ruff: noqa: S101, ANN201, T201
9 |
10 |
11 | def test_fetch_all_items(oh: openhab.OpenHAB):
12 | items = oh.fetch_all_items()
13 |
14 | assert len(items)
15 |
16 |
17 | def test_datetime_update(oh: openhab.OpenHAB):
18 | dt_obj = oh.get_item('TheDateTime')
19 | dt_utc_now = datetime.datetime.now(tz=datetime.timezone.utc)
20 | dt_obj.state = dt_utc_now
21 |
22 | time.sleep(0.5)
23 | assert dt_obj.state.isoformat(timespec='seconds') == dt_utc_now.isoformat(timespec='seconds')
24 |
25 |
26 | def test_datetime_command(oh: openhab.OpenHAB):
27 | dt_obj = oh.get_item('TheDateTime')
28 | dt_utc_now = datetime.datetime.now(tz=datetime.timezone.utc)
29 | dt_obj.command(dt_utc_now)
30 |
31 | time.sleep(0.5)
32 | assert dt_obj.state.isoformat(timespec='seconds') == dt_utc_now.isoformat(timespec='seconds')
33 |
34 |
35 | def test_null_undef(oh: openhab.OpenHAB):
36 | float_obj = oh.get_item('floattest')
37 |
38 | float_obj.update_state_null()
39 | assert float_obj.is_state_null()
40 |
41 | float_obj.update_state_undef()
42 | assert float_obj.is_state_undef()
43 |
44 |
45 | def test_float(oh: openhab.OpenHAB):
46 | float_obj = oh.get_item('floattest')
47 |
48 | float_obj.state = 1.0
49 | assert float_obj.state == 1.0
50 |
51 |
52 | def test_scientific_notation(oh: openhab.OpenHAB):
53 | float_obj = oh.get_item('floattest')
54 |
55 | float_obj.state = 1e-10
56 | time.sleep(1) # Allow time for OpenHAB test instance to process state update
57 | assert float_obj.state == 1e-10
58 |
59 |
60 | def test_non_ascii_string(oh: openhab.OpenHAB):
61 | string_obj = oh.get_item('stringtest')
62 |
63 | string_obj.state = 'שלום'
64 | assert string_obj.state == 'שלום'
65 |
66 | string_obj.state = '°F'
67 | assert string_obj.state == '°F'
68 |
69 |
70 | def test_color_item(oh: openhab.OpenHAB):
71 | coloritem = oh.get_item('color_item')
72 |
73 | coloritem.update_state_null()
74 | assert coloritem.is_state_null()
75 |
76 | coloritem.state = 1
77 | assert coloritem.state == (0.0, 0.0, 1.0)
78 |
79 | coloritem.state = '1.1, 1.2, 1.3'
80 | assert coloritem.state == (1.1, 1.2, 1.3)
81 |
82 | coloritem.state = 'OFF'
83 | assert coloritem.state == (1.1, 1.2, 0.0)
84 |
85 | coloritem.state = 'ON'
86 | assert coloritem.state == (1.1, 1.2, 100.0)
87 |
88 |
89 | def test_number_temperature(oh: openhab.OpenHAB):
90 | # Tests below require the OpenHAB test instance to be configured with '°C' as
91 | # the unit of measure for the 'Dining_Temperature' item
92 | temperature_item = oh.get_item('Dining_Temperature')
93 |
94 | temperature_item.state = 1.0
95 | time.sleep(1) # Allow time for OpenHAB test instance to process state update
96 | assert temperature_item.state == 1.0
97 | assert temperature_item.unit_of_measure == '°C'
98 |
99 | temperature_item.state = '2 °C'
100 | time.sleep(1)
101 | assert temperature_item.state == 2
102 | assert temperature_item.unit_of_measure == '°C'
103 |
104 | temperature_item.state = (3, '°C')
105 | time.sleep(1)
106 | assert temperature_item.state == 3
107 | assert temperature_item.unit_of_measure == '°C'
108 |
109 | # Unit of measure conversion (performed by OpenHAB server)
110 | temperature_item.state = (32, '°F')
111 | time.sleep(1)
112 | assert round(temperature_item.state, 2) == 0
113 | temperature_item.state = (212, '°F')
114 | time.sleep(1)
115 | assert temperature_item.state == 100
116 | assert temperature_item.unit_of_measure == '°C'
117 |
118 |
119 | def test_location_item(oh: openhab.OpenHAB):
120 | locationitem = oh.get_item('location_item')
121 |
122 | locationitem.update_state_null()
123 | assert locationitem.is_state_null()
124 |
125 | locationitem.state = '1.1, 1.2, 1.3'
126 | assert locationitem.state == (1.1, 1.2, 1.3)
127 |
128 | locationitem.state = (1.1, 1.2, 1.3)
129 | assert locationitem.state == (1.1, 1.2, 1.3)
130 |
131 | locationitem.state = '30.1, -50.2, 7325.456'
132 | assert locationitem.state == (30.1, -50.2, 7325.456)
133 |
134 | locationitem.state = (30.1, -50.2, 7325.456)
135 | assert locationitem.state == (30.1, -50.2, 7325.456)
136 |
137 | with pytest.raises(ValueError):
138 | locationitem.state = (91, 181, -10)
139 |
140 | with pytest.raises(ValueError):
141 | locationitem.state = '90 10 10'
142 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](http://python-openhab.readthedocs.io/en/latest/?badge=latest)
2 | [](https://pypi.org/project/python-openhab/)
3 | 
4 |
5 |
6 | # python library for accessing the openHAB REST API
7 |
8 | This library allows for easily accessing the openHAB REST API. A number of features are implemented but not all, this is
9 | work in progress.
10 |
11 | # Requirements
12 |
13 | - python >= 3.9
14 | - python :: dateutil
15 | - python :: httpx
16 | - python :: authlib
17 | - openHAB version 3 / 4
18 |
19 | # Installation
20 |
21 |
22 | Install the latest version using pip:
23 |
24 | ```shell
25 | pip install python-openhab
26 | ```
27 |
28 | # Example
29 |
30 | Example usage of the library:
31 |
32 | ```python
33 |
34 | from openhab import OpenHAB
35 |
36 | base_url = 'http://localhost:8080/rest'
37 | openhab = OpenHAB(base_url)
38 |
39 | # fetch all items
40 | items = openhab.fetch_all_items()
41 |
42 | sunset = items.get('Sunset')
43 | print(sunset.state)
44 |
45 | # fetch a single item
46 | item = openhab.get_item('light_switch')
47 |
48 | # turn a switch on
49 | item.on()
50 |
51 | # send a state update (this only update the state)
52 | item.state = 'OFF'
53 |
54 | # send a command
55 | item.command('ON')
56 |
57 | # check if item state is NULL
58 | if item.state is None and item.is_state_null():
59 | pass
60 |
61 | # check if item state is UNDEF
62 | if item.state is None and item.is_state_undef():
63 | pass
64 |
65 | # fetch some group
66 | lights_group = openhab.get_item('lights_group')
67 |
68 | # send command to group
69 | lights_group.on()
70 |
71 | # send update to each member
72 | for v in lights_group.members.values():
73 | v.update('OFF')
74 | ```
75 |
76 | # Note on NULL and UNDEF
77 |
78 | In openHAB items may have two states named NULL and UNDEF, which have distinct meanings but basically indicate that an
79 | item has no usable value. This library sets the state of an item, regardless of their openHAB value being NULL or UNDEF,
80 | to None. This in order to ease working with the library as we do cast certain types to native types.
81 |
82 | In order to check if an item's state is either NULL or UNDEF, you can use the helper functions:
83 |
84 | ```python
85 | item.is_state_null()
86 | item.is_state_undef()
87 | ```
88 |
89 | # Experimental OAuth2 Support
90 |
91 | In order to try out OAuth2 authentication, you first need to register with the openHAB endpoint in order to retrieve a
92 | token and refresh token.
93 |
94 | Assuming your openHAB instance runs at *http://127.0.0.1:8080* (replace with the correct one), use the following snippet
95 | to retrieve a token:
96 |
97 | ```python
98 | import pathlib
99 | import openhab.oauth2_helper
100 | import os
101 | import json
102 |
103 | url_base = 'http://127.0.0.1:8080'
104 | api_username = 'admin'
105 | api_password = 'admin'
106 | oauth2_client_id = 'http://127.0.0.1/auth'
107 | oauth2_token_cache = pathlib.Path(__file__).resolve().parent / '.oauth2_token_test'
108 |
109 | # this must be set for oauthlib to work on http (do not set for https!)
110 | os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
111 |
112 | oauth2_token = openhab.oauth2_helper.get_oauth2_token(url_base, username=api_username, password=api_password)
113 |
114 | with oauth2_token_cache.open('w') as fhdl:
115 | json.dump(oauth2_token, fhdl, indent=2, sort_keys=True)
116 | ```
117 |
118 | The JSON that is returned is required for authenticating to openHAB using OAuth2 as well as a refresh token which is
119 | used for refreshing a session.
120 |
121 | Next try connecting to openHAB using this library as follows:
122 |
123 | ```python
124 | import openhab
125 | import pathlib
126 | import json
127 | import os
128 |
129 | url_base = 'http://127.0.0.1:8080'
130 | url_rest = f'{url_base}/rest'
131 | oauth2_client_id = 'http://127.0.0.1/auth'
132 | oauth2_token_cache = pathlib.Path(__file__).resolve().parent / '.oauth2_token_test'
133 |
134 | # this must be set for oauthlib to work on http (do not set for https!)
135 | os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
136 |
137 | oauth2_config = {'client_id': oauth2_client_id,
138 | 'token_cache': str(oauth2_token_cache)
139 | }
140 |
141 | with oauth2_token_cache.open('r') as fhdl:
142 | oauth2_config['token'] = json.load(fhdl)
143 |
144 | oh = openhab.OpenHAB(base_url=url_rest, oauth2_config=oauth2_config)
145 |
146 | o = oh.get_item('test_item')
147 | print(o)
148 | ```
149 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | [](http://python-openhab.readthedocs.io/en/latest/?badge=latest)
2 | [](https://pypi.org/project/python-openhab/)
3 | 
4 |
5 | # python library for accessing the openHAB REST API
6 |
7 | This library allows for easily accessing the openHAB REST API. A number of features are implemented but not all, this is
8 | work in progress.
9 |
10 | # Requirements
11 |
12 | - python >= 3.9
13 | - python :: dateutil
14 | - python :: httpx
15 | - python :: authlib
16 | - openHAB version 3 / 4
17 |
18 | # Installation
19 |
20 | Install the latest version using pip:
21 |
22 | ```shell
23 | pip install python-openhab
24 | ```
25 |
26 | # Example
27 |
28 | Example usage of the library:
29 |
30 | ```python
31 |
32 | import datetime
33 | import json
34 |
35 | from openhab import OpenHAB
36 |
37 | base_url = 'http://localhost:8080/rest'
38 | openhab = OpenHAB(base_url)
39 |
40 | # fetch all items
41 | items = openhab.fetch_all_items()
42 |
43 | sunset = items.get('Sunset')
44 | print(sunset.state)
45 |
46 | # fetch a single item
47 | item = openhab.get_item('light_switch')
48 |
49 | # turn a switch on
50 | item.on()
51 |
52 | # send a state update (this only update the state)
53 | item.state = 'OFF'
54 |
55 | # send a command
56 | item.command('ON')
57 |
58 | # check if item state is NULL
59 | if item.state is None and item.is_state_null():
60 | pass
61 |
62 | # check if item state is UNDEF
63 | if item.state is None and item.is_state_undef():
64 | pass
65 |
66 | # fetch some group
67 | lights_group = openhab.get_item('lights_group')
68 |
69 | # send command to group
70 | lights_group.on()
71 |
72 | # send update to each member
73 | for v in lights_group.members.values():
74 | v.update('OFF')
75 |
76 | # start_time for fetching persistence data
77 | start_time = datetime.datetime.fromtimestamp(1695504300123 / 1000, tz=datetime.UTC)
78 |
79 | # fetch persistence data using the OpenHAB client object
80 | for k in openhab.get_item_persistence(knx_day_night.name,
81 | page_length=20,
82 | start_time=start_time
83 | ):
84 | print(json.dumps(k, indent=4))
85 |
86 | # fetch persistence data using the item directly
87 | for k in item.persistence(page_length=20,
88 | start_time=start_time
89 | ):
90 | print(json.dumps(k, indent=4))
91 | ```
92 |
93 | # Note on NULL and UNDEF
94 |
95 | In openHAB items may have two states named NULL and UNDEF, which have distinct meanings but basically indicate that an
96 | item has no usable value. This library sets the state of an item, regardless of their openHAB value being NULL or UNDEF,
97 | to None. This in order to ease working with the library as we do cast certain types to native types.
98 |
99 | In order to check if an item's state is either NULL or UNDEF, you can use the helper functions:
100 |
101 | ```python
102 | item.is_state_null()
103 | item.is_state_undef()
104 | ```
105 |
106 | # Experimental OAuth2 Support
107 |
108 | In order to try out OAuth2 authentication, you first need to register with the openHAB endpoint in order to retrieve a
109 | token and refresh token.
110 |
111 | Assuming your openHAB instance runs at *http://127.0.0.1:8080* (replace with the correct one), use the following snippet
112 | to retrieve a token:
113 |
114 | ```python
115 | import pathlib
116 | import openhab.oauth2_helper
117 | import os
118 | import json
119 |
120 | url_base = 'http://127.0.0.1:8080'
121 | api_username = 'admin'
122 | api_password = 'admin'
123 | oauth2_client_id = 'http://127.0.0.1/auth'
124 | oauth2_token_cache = pathlib.Path(__file__).resolve().parent / '.oauth2_token_test'
125 |
126 | # this must be set for oauthlib to work on http (do not set for https!)
127 | os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
128 |
129 | oauth2_token = openhab.oauth2_helper.get_oauth2_token(url_base, username=api_username, password=api_password)
130 |
131 | with oauth2_token_cache.open('w') as fhdl:
132 | json.dump(oauth2_token, fhdl, indent=2, sort_keys=True)
133 | ```
134 |
135 | The JSON that is returned is required for authenticating to openHAB using OAuth2 as well as a refresh token which is
136 | used for refreshing a session.
137 |
138 | Next try connecting to openHAB using this library as follows:
139 |
140 | ```python
141 | import openhab
142 | import pathlib
143 | import json
144 | import os
145 |
146 | url_base = 'http://127.0.0.1:8080'
147 | url_rest = f'{url_base}/rest'
148 | oauth2_client_id = 'http://127.0.0.1/auth'
149 | oauth2_token_cache = pathlib.Path(__file__).resolve().parent / '.oauth2_token_test'
150 |
151 | # this must be set for oauthlib to work on http (do not set for https!)
152 | os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
153 |
154 | oauth2_config = {'client_id': oauth2_client_id,
155 | 'token_cache': str(oauth2_token_cache)
156 | }
157 |
158 | with oauth2_token_cache.open('r') as fhdl:
159 | oauth2_config['token'] = json.load(fhdl)
160 |
161 | oh = openhab.OpenHAB(base_url=url_rest, oauth2_config=oauth2_config)
162 |
163 | o = oh.get_item('test_item')
164 | print(o)
165 | ```
166 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling", "hatch-vcs"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "python-openhab"
7 | description = "python library for accessing the openHAB REST API"
8 | authors = [{ name = "Georges Toth", email = "georges@trypill.org" }]
9 | license = { text = "AGPLv3+" }
10 | classifiers = [
11 | "Development Status :: 5 - Production/Stable",
12 | "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
13 | "Intended Audience :: Developers",
14 | "Operating System :: OS Independent",
15 | "Programming Language :: Python :: 3 :: Only",
16 | "Programming Language :: Python :: 3.9",
17 | "Programming Language :: Python :: 3.10",
18 | "Programming Language :: Python :: 3.11",
19 | "Programming Language :: Python :: 3.12",
20 | "Programming Language :: Python :: 3.13",
21 | ]
22 | keywords = ["openHAB"]
23 | requires-python = ">=3.9"
24 | dependencies = [
25 | "python-dateutil~=2.8",
26 | "pydantic<3",
27 | "Authlib~=1.2",
28 | "httpx~=0.24",
29 | ]
30 | dynamic = ["version"]
31 |
32 | [project.readme]
33 | file = "README.md"
34 | content-type = "text/markdown"
35 |
36 | [project.urls]
37 | Homepage = "https://github.com/sim0nx/python-openhab"
38 | Download = "https://github.com/sim0nx/python-openhab"
39 | Tracker = "https://github.com/sim0nx/python-openhab/issues"
40 | Documentation = "http://python-openhab.readthedocs.io/en/latest/?badge=latest"
41 | Source = "https://github.com/sim0nx/python-openhab"
42 |
43 | [dependency-groups]
44 | docs = [
45 | "mkdocs-material",
46 | "mkdocstrings[crystal,python]",
47 | ]
48 | dev = [
49 | "pip",
50 | "mypy",
51 | "ruff",
52 | "types-python-dateutil",
53 | "typeguard",
54 | ]
55 | test = [
56 | "pytest",
57 | "pytest-sugar",
58 | "coverage",
59 | "beautifulsoup4",
60 | ]
61 |
62 | [tool.hatch.envs.default]
63 | installer = "uv"
64 |
65 | [tool.hatch.build.targets.wheel]
66 | packages = [
67 | "/openhab",
68 | ]
69 |
70 | [tool.hatch.build.targets.sdist]
71 | include = [
72 | "/openhab",
73 | "/tests",
74 | ]
75 | exclude = []
76 |
77 | [tool.hatch.version]
78 | source = "vcs"
79 |
80 | [tool.mypy]
81 | show_error_context = true
82 | show_column_numbers = true
83 | ignore_missing_imports = true
84 | disallow_incomplete_defs = true
85 | disallow_untyped_defs = true
86 | disallow_untyped_calls = false
87 | warn_no_return = true
88 | warn_redundant_casts = true
89 | warn_unused_ignores = true
90 | strict_optional = true
91 | check_untyped_defs = false
92 |
93 | files = [
94 | "openhab/**/*.py",
95 | ]
96 |
97 | [tool.pytest.ini_options]
98 | minversion = "8.0"
99 | testpaths = [
100 | "tests"
101 | ]
102 |
103 | [tool.ruff]
104 | line-length = 160
105 | indent-width = 2
106 | target-version = "py39"
107 | include = [
108 | "pyproject.toml",
109 | "openhab/**/*.py",
110 | ]
111 |
112 | [tool.ruff.lint]
113 | select = [
114 | "E", # pycodestyle errors
115 | "W", # pycodestyle warnings
116 | "F", # pyflakes
117 | "I", # isort
118 | "C", # flake8-comprehensions
119 | "B", # flake8-bugbear
120 | "D", # pydocstyle
121 | "N", # pep8-naming
122 | "UP", # pyupgrade
123 | "YTT", # flake8-2020
124 | "ANN", # flake8-annotations
125 | "ASYNC", # flake8-async
126 | "S", # flake8-bandit
127 | "BLE", # flake8-blind-except
128 | "B", # flake8-bugbear
129 | "A", # flake8-builtins
130 | "COM", # flake8-commas
131 | "C4", # flake8-comprehensions
132 | "DTZ", # flake8-datetimez
133 | "EM103", # flake8-errmsg - dot-format-in-exception
134 | "EXE", # flake8-executable
135 | "ISC", # flake8-implicit-str-concat
136 | "ICN", # flake8-import-conventions
137 | "G", # flake8-logging-format
138 | "INP", # flake8-no-pep420
139 | "PIE", # flake8-pie
140 | "T20", # flake8-print
141 | "PYI", # flake8-pyi
142 | "RSE", # flake8-raise
143 | "RET", # flake8-return
144 | "SLF", # flake8-self
145 | "SLOT", # flake8-slots
146 | # "SIM", # flake8-simplify
147 | "TID", # flake8-tidy-imports
148 | "TCH", # flake8-type-checking
149 | "PTH", # flake8-use-pathlib
150 | "TD", # flake8-todos
151 | "FIX", # flake8-fixme
152 | "ERA", # eradicate
153 | "PL", # Pylint
154 | "PLC", # Convention
155 | "PLE", # Error
156 | "PLR", # Refactor
157 | "PLW", # Warning
158 | "B904", # reraise-no-cause
159 | "FLY", # flynt
160 | # "PERF", # Perflint
161 | "RUF013", # implicit-optional
162 | ]
163 | unfixable = ['ERA001']
164 | extend-select = ['Q', 'RUF100', 'C90']
165 | flake8-quotes = { inline-quotes = 'single', multiline-quotes = 'single' }
166 | ignore = [
167 | "C901", # too complex
168 | "E501", # line too long
169 | "B008", # do not perform function call in argument defaults
170 | "ANN401", # any-type
171 | "ANN002", # missing-type-args
172 | "ANN003", # missing-type-kwargs
173 | "PLR0913", # Too many arguments to function call
174 | "PLR0915", # Too many statements
175 | "PLR2004", # Magic value used in comparison
176 | "PLW0603", # Using the global statement
177 | "PLR0912", # Too many branches
178 | "COM812", # missing-trailing-comma
179 | "ISC001", # single-line-implicit-string-concatenation
180 | "Q001", # bad-quotes-multiline-string
181 | ]
182 |
183 | [tool.ruff.lint.per-file-ignores]
184 | "tests/*" = [
185 | "S101", # Use of `assert` detected
186 | "D", # docstring
187 | "RET504", # Unnecessary assignment before `return` statement
188 | ]
189 |
190 | [tool.ruff.format]
191 | quote-style = "single"
192 |
193 | [tool.ruff.lint.pydocstyle]
194 | convention = "google"
195 |
--------------------------------------------------------------------------------
/openhab/client.py:
--------------------------------------------------------------------------------
1 | """python library for accessing the openHAB REST API."""
2 |
3 | #
4 | # Georges Toth (c) 2016-present
5 | #
6 | # python-openhab is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # python-openhab is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with python-openhab. If not, see .
18 | #
19 |
20 | import datetime
21 | import logging
22 | import typing
23 |
24 | import authlib.integrations.httpx_client
25 | import httpx
26 |
27 | import openhab.items
28 | import openhab.rules
29 |
30 | from .config import Oauth2Config, Oauth2Token
31 |
32 | __author__ = 'Georges Toth '
33 | __license__ = 'AGPLv3+'
34 |
35 |
36 | class OpenHAB:
37 | """openHAB REST API client."""
38 |
39 | def __init__(
40 | self,
41 | base_url: str,
42 | username: typing.Optional[str] = None,
43 | password: typing.Optional[str] = None,
44 | http_auth: typing.Optional[httpx.Auth] = None,
45 | timeout: typing.Optional[float] = None,
46 | oauth2_config: typing.Optional[dict[str, typing.Any]] = None,
47 | ) -> None:
48 | """Class constructor.
49 |
50 | The format of the optional *oauth2_config* dictionary is as follows:
51 | ```python
52 | {"client_id": "http://127.0.0.1/auth",
53 | "token_cache": "///.oauth2_token",
54 | "token":
55 | {"access_token": "adsafdasfasfsafasfsafasfasfasfsa....",
56 | "expires_in": 3600,
57 | "refresh_token": "312e21e21e32112",
58 | "scope": "admin",
59 | "token_type": "bearer",
60 | "user": {
61 | "name": "admin",
62 | "roles": [
63 | "administrator"
64 | ]
65 | }
66 | }
67 | ```
68 |
69 | Args:
70 | base_url (str): The openHAB REST URL, e.g. http://example.com/rest
71 | username (str, optional): A optional username, used in conjunction with a optional
72 | provided password, in case openHAB requires authentication.
73 | password (str, optional): A optional password, used in conjunction with a optional
74 | provided username, in case openHAB requires authentication.
75 | http_auth (Auth, optional): An alternative to username/password pair, is to
76 | specify a custom http authentication object of type :class:`requests.Auth`.
77 | timeout (float, optional): An optional timeout for REST transactions
78 | oauth2_config: Optional OAuth2 configuration dictionary
79 |
80 | Returns:
81 | OpenHAB: openHAB class instance.
82 | """
83 | self.url_rest = base_url
84 | self.url_base = base_url.rsplit('/', 1)[0]
85 |
86 | self.oauth2_config: typing.Optional[Oauth2Config] = None
87 |
88 | if oauth2_config is not None:
89 | self.oauth2_config = Oauth2Config(**oauth2_config)
90 |
91 | self.session = authlib.integrations.httpx_client.OAuth2Client(
92 | client_id=self.oauth2_config.client_id,
93 | token=self.oauth2_config.token.model_dump(),
94 | update_token=self._oauth2_token_updater,
95 | )
96 |
97 | self.session.metadata['token_endpoint'] = f'{self.url_rest}/auth/token'
98 |
99 | if not self.oauth2_config.token_cache.is_file():
100 | self._oauth2_token_updater(self.oauth2_config.token.model_dump())
101 |
102 | else:
103 | self.session = httpx.Client(timeout=timeout)
104 |
105 | if http_auth is not None:
106 | self.session.auth = http_auth
107 | elif not (username is None or password is None):
108 | self.session.auth = httpx.BasicAuth(username, password)
109 |
110 | self.logger = logging.getLogger(__name__)
111 |
112 | self._rules: typing.Optional[openhab.rules.Rules] = None
113 |
114 | @property
115 | def rules(self) -> openhab.rules.Rules:
116 | """Get object for managing rules."""
117 | if self._rules is None:
118 | self._rules = openhab.rules.Rules(self)
119 |
120 | return self._rules
121 |
122 | @staticmethod
123 | def _check_req_return(req: httpx.Response) -> None:
124 | """Internal method for checking the return value of a REST HTTP request.
125 |
126 | Args:
127 | req (requests.Response): A requests Response object.
128 |
129 | Returns:
130 | None: Returns None if no error occurred; else raises an exception.
131 |
132 | Raises:
133 | ValueError: Raises a ValueError exception in case of a non-successful
134 | REST request.
135 | """
136 | if not 200 <= req.status_code < 300:
137 | req.raise_for_status()
138 |
139 | def req_get(self, uri_path: str, params: typing.Optional[typing.Union[dict[str, typing.Any], list, tuple]] = None) -> typing.Any:
140 | """Helper method for initiating a HTTP GET request.
141 |
142 | Besides doing the actual request, it also checks the return value and returns the resulting decoded
143 | JSON data.
144 |
145 | Args:
146 | uri_path (str): The path to be used in the GET request.
147 |
148 | Returns:
149 | dict: Returns a dict containing the data returned by the OpenHAB REST server.
150 | """
151 | r = self.session.get(f'{self.url_rest}{uri_path}', params=params)
152 | self._check_req_return(r)
153 | return r.json()
154 |
155 | def req_post(
156 | self,
157 | uri_path: str,
158 | data: typing.Optional[typing.Union[str, bytes, typing.Mapping[str, typing.Any], typing.Iterable[tuple[str, typing.Optional[str]]]]] = None,
159 | ) -> None:
160 | """Helper method for initiating a HTTP POST request.
161 |
162 | Besides doing the actual request, it also checks the return value and returns the resulting decoded
163 | JSON data.
164 |
165 | Args:
166 | uri_path (str): The path to be used in the POST request.
167 | data (dict, optional): A optional dict with data to be submitted as part of the POST request.
168 |
169 | Returns:
170 | None: No data is returned.
171 | """
172 | headers = self.session.headers
173 | headers['Content-Type'] = 'text/plain'
174 |
175 | r = self.session.post(self.url_rest + uri_path, content=data, headers=headers)
176 | self._check_req_return(r)
177 |
178 | def req_put(
179 | self,
180 | uri_path: str,
181 | data: typing.Optional[dict] = None,
182 | json_data: typing.Optional[dict] = None,
183 | headers: typing.Optional[dict] = None,
184 | ) -> None:
185 | """Helper method for initiating a HTTP PUT request.
186 |
187 | Besides doing the actual request, it also checks the return value and returns the resulting decoded
188 | JSON data.
189 |
190 | Args:
191 | uri_path (str): The path to be used in the PUT request.
192 | data (dict, optional): A optional dict with data to be submitted as part of the PUT request.
193 | json_data: Data to be submitted as json.
194 | headers: Specify optional custom headers.
195 |
196 | Returns:
197 | None: No data is returned.
198 | """
199 | if headers is None:
200 | headers = {'Content-Type': 'text/plain'}
201 | content = data
202 | data = None
203 | else:
204 | content = None
205 |
206 | r = self.session.put(self.url_rest + uri_path, content=content, data=data, json=json_data, headers=headers)
207 | self._check_req_return(r)
208 |
209 | # fetch all items
210 | def fetch_all_items(self) -> dict[str, openhab.items.Item]:
211 | """Returns all items defined in openHAB.
212 |
213 | Returns:
214 | dict: Returns a dict with item names as key and item class instances as value.
215 | """
216 | items = {} # type: dict
217 | res = self.req_get('/items/')
218 |
219 | for i in res:
220 | if i['name'] not in items:
221 | items[i['name']] = self.json_to_item(i)
222 |
223 | return items
224 |
225 | def get_item(self, name: str) -> openhab.items.Item:
226 | """Returns an item with its state and type as fetched from openHAB.
227 |
228 | Args:
229 | name (str): The name of the item to fetch from openHAB.
230 |
231 | Returns:
232 | Item: A corresponding Item class instance with the state of the requested item.
233 | """
234 | json_data = self.get_item_raw(name)
235 |
236 | return self.json_to_item(json_data)
237 |
238 | def json_to_item(self, json_data: dict) -> openhab.items.Item: # noqa: PLR0911
239 | """This method takes as argument the RAW (JSON decoded) response for an openHAB item.
240 |
241 | It checks of what type the item is and returns a class instance of the
242 | specific item filled with the item's state.
243 |
244 | Args:
245 | json_data (dict): The JSON decoded data as returned by the openHAB server.
246 |
247 | Returns:
248 | Item: A corresponding Item class instance with the state of the item.
249 | """
250 | _type = json_data['type']
251 |
252 | if _type == 'Group' and 'groupType' in json_data:
253 | _type = json_data['groupType']
254 |
255 | if _type == 'Group' and 'groupType' not in json_data:
256 | return openhab.items.GroupItem(self, json_data)
257 |
258 | if _type == 'String':
259 | return openhab.items.StringItem(self, json_data)
260 |
261 | if _type == 'Switch':
262 | return openhab.items.SwitchItem(self, json_data)
263 |
264 | if _type == 'DateTime':
265 | return openhab.items.DateTimeItem(self, json_data)
266 |
267 | if _type == 'Contact':
268 | return openhab.items.ContactItem(self, json_data)
269 |
270 | if _type.startswith('Number'):
271 | return openhab.items.NumberItem(self, json_data)
272 |
273 | if _type == 'Dimmer':
274 | return openhab.items.DimmerItem(self, json_data)
275 |
276 | if _type == 'Color':
277 | return openhab.items.ColorItem(self, json_data)
278 |
279 | if _type == 'Rollershutter':
280 | return openhab.items.RollershutterItem(self, json_data)
281 |
282 | if _type == 'Player':
283 | return openhab.items.PlayerItem(self, json_data)
284 |
285 | if _type == 'Location':
286 | return openhab.items.LocationItem(self, json_data)
287 |
288 | return openhab.items.Item(self, json_data)
289 |
290 | def get_item_raw(self, name: str) -> typing.Any:
291 | """Private method for fetching a json configuration of an item.
292 |
293 | Args:
294 | name (str): The item name to be fetched.
295 |
296 | Returns:
297 | dict: A JSON decoded dict.
298 | """
299 | return self.req_get(f'/items/{name}')
300 |
301 | def logout(self) -> bool:
302 | """OAuth2 session logout method.
303 |
304 | Returns:
305 | True or False depending on if the logout did succeed.
306 | """
307 | if self.oauth2_config is None or not isinstance(self.session, authlib.integrations.httpx_client.OAuth2Client):
308 | raise ValueError('You are trying to logout from a non-OAuth2 session. This is not supported!')
309 |
310 | data = {
311 | 'refresh_token': self.oauth2_config.token.refresh_token,
312 | 'id': self.oauth2_config.client_id,
313 | }
314 | url_logout = f'{self.url_rest}/auth/logout'
315 |
316 | res = self.session.post(url_logout, data=data)
317 |
318 | return res.status_code == 200
319 |
320 | def _oauth2_token_updater(self, token: dict[str, typing.Any], refresh_token: typing.Any = None, access_token: typing.Any = None) -> None:
321 | if self.oauth2_config is None:
322 | raise ValueError('OAuth2 configuration is not set; invalid action!')
323 |
324 | self.oauth2_config.token = Oauth2Token(**token)
325 |
326 | with self.oauth2_config.token_cache.open('w', encoding='utf-8') as fhdl:
327 | fhdl.write(self.oauth2_config.token.model_dump_json())
328 |
329 | def create_or_update_item(
330 | self,
331 | name: str,
332 | _type: typing.Union[str, type[openhab.items.Item]],
333 | quantity_type: typing.Optional[str] = None,
334 | label: typing.Optional[str] = None,
335 | category: typing.Optional[str] = None,
336 | tags: typing.Optional[list[str]] = None,
337 | group_names: typing.Optional[list[str]] = None,
338 | group_type: typing.Optional[typing.Union[str, type[openhab.items.Item]]] = None,
339 | function_name: typing.Optional[str] = None,
340 | function_params: typing.Optional[list[str]] = None,
341 | ) -> None:
342 | """Creates a new item in openHAB if there is no item with name 'name' yet.
343 |
344 | If there is an item with 'name' already in openHAB, the item gets updated with the infos provided. be aware that not provided fields will be deleted in openHAB.
345 | Consider to get the existing item via 'getItem' and then read out existing fields to populate the parameters here.
346 |
347 | Args:
348 | name: unique name of the item
349 | _type: the data_type used in openHAB (like Group, Number, Contact, DateTime, Rollershutter, Color, Dimmer, Switch, Player)
350 | server.
351 | To create groups use 'GroupItem'!
352 | quantity_type: optional quantity_type ( like Angle, Temperature, Illuminance (see https://www.openhab.org/docs/concepts/units-of-measurement.html))
353 | label: optional openHAB label (see https://www.openhab.org/docs/configuration/items.html#label)
354 | category: optional category. no documentation found
355 | tags: optional list of tags (see https://www.openhab.org/docs/configuration/items.html#tags)
356 | group_names: optional list of groups this item belongs to.
357 | group_type: Optional group_type (e.g. NumberItem, SwitchItem, etc).
358 | function_name: Optional function_name. no documentation found.
359 | Can be one of ['EQUALITY', 'AND', 'OR', 'NAND', 'NOR', 'AVG', 'SUM', 'MAX', 'MIN', 'COUNT', 'LATEST', 'EARLIEST']
360 | function_params: Optional list of function params (no documentation found), depending on function name.
361 | """
362 | paramdict: dict[str, typing.Union[str, list[str], dict[str, typing.Union[str, list[str]]]]] = {}
363 |
364 | if isinstance(_type, type):
365 | if issubclass(_type, openhab.items.Item):
366 | itemtypename = _type.TYPENAME
367 | else:
368 | raise ValueError(f'_type parameter must be a valid subclass of type *Item* or a string name of such a class; given value is "{str(_type)}"')
369 | else:
370 | itemtypename = _type
371 |
372 | if quantity_type is None:
373 | paramdict['type'] = itemtypename
374 | else:
375 | paramdict['type'] = f'{itemtypename}:{quantity_type}'
376 |
377 | paramdict['name'] = name
378 |
379 | if label is not None:
380 | paramdict['label'] = label
381 |
382 | if category is not None:
383 | paramdict['category'] = category
384 |
385 | if tags is not None:
386 | paramdict['tags'] = tags
387 |
388 | if group_names is not None:
389 | paramdict['groupNames'] = group_names
390 |
391 | if group_type is not None:
392 | if isinstance(group_type, type):
393 | if issubclass(group_type, openhab.items.Item):
394 | paramdict['groupType'] = group_type.TYPENAME
395 | else:
396 | raise ValueError(f'group_type parameter must be a valid subclass of type *Item* or a string name of such a class; given value is "{str(group_type)}"')
397 | else:
398 | paramdict['groupType'] = group_type
399 |
400 | if function_name is not None:
401 | if function_name not in ('EQUALITY', 'AND', 'OR', 'NAND', 'NOR', 'AVG', 'SUM', 'MAX', 'MIN', 'COUNT', 'LATEST', 'EARLIEST'):
402 | raise ValueError(f'Invalid function name "{function_name}')
403 |
404 | if function_name in ('AND', 'OR', 'NAND', 'NOR') and (not function_params or len(function_params) != 2):
405 | raise ValueError(f'Group function "{function_name}" requires two arguments')
406 |
407 | if function_name == 'COUNT' and (not function_params or len(function_params) != 1):
408 | raise ValueError(f'Group function "{function_name}" requires one arguments')
409 |
410 | if function_params:
411 | paramdict['function'] = {'name': function_name, 'params': function_params}
412 | else:
413 | paramdict['function'] = {'name': function_name}
414 |
415 | self.logger.debug('About to create item with PUT request:\n%s', str(paramdict))
416 |
417 | self.req_put(f'/items/{name}', json_data=paramdict, headers={'Content-Type': 'application/json'})
418 |
419 | def get_item_persistence(
420 | self,
421 | name: str,
422 | service_id: typing.Optional[str] = None,
423 | start_time: typing.Optional[datetime.datetime] = None,
424 | end_time: typing.Optional[datetime.datetime] = None,
425 | page: int = 0,
426 | page_length: int = 0,
427 | boundary: bool = False,
428 | ) -> typing.Iterator[dict[str, typing.Union[str, int]]]:
429 | """Method for fetching persistence data for a given item.
430 |
431 | Args:
432 | name: The item name persistence data should be fetched for.
433 | service_id: ID of the persistence service. If not provided the default service will be used.
434 | start_time: Start time of the data to return. Will default to 1 day before end_time.
435 | end_time: End time of the data to return. Will default to current time.
436 | page: Page number of data to return. Defaults to 0 if not provided.
437 | page_length: The length of each page. Defaults to 0 which disabled paging.
438 | boundary: Gets one value before and after the requested period.
439 |
440 | Returns:
441 | Iterator over dict values containing time and state value, e.g.
442 | {"time": 1695588900122,
443 | "state": "23"
444 | }
445 | """
446 | params: dict[str, typing.Any] = {
447 | 'boundary': str(boundary).lower(),
448 | 'page': page,
449 | 'pagelength': page_length,
450 | }
451 |
452 | if service_id is not None:
453 | params['serviceId'] = service_id
454 |
455 | if start_time is not None:
456 | params['starttime'] = start_time.isoformat()
457 |
458 | if end_time is not None:
459 | params['endtime'] = end_time.isoformat()
460 |
461 | if start_time == end_time:
462 | raise ValueError('start_time must differ from end_time')
463 |
464 | res = self.req_get(f'/persistence/items/{name}', params=params)
465 |
466 | yield from res['data']
467 |
468 | while page_length > 0 and int(res['datapoints']) > 0:
469 | params['page'] += 1
470 | res = self.req_get(f'/persistence/items/{name}', params=params)
471 | yield from res['data']
472 |
--------------------------------------------------------------------------------
/openhab/command_types.py:
--------------------------------------------------------------------------------
1 | """python library for accessing the openHAB REST API."""
2 |
3 | #
4 | # Georges Toth (c) 2016-present
5 | #
6 | # python-openhab is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # python-openhab is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with python-openhab. If not, see .
18 | #
19 |
20 | # pylint: disable=bad-indentation
21 |
22 | import abc
23 | import datetime
24 | import re
25 | import typing
26 |
27 | import dateutil.parser
28 |
29 | __author__ = 'Georges Toth '
30 | __license__ = 'AGPLv3+'
31 |
32 |
33 | class CommandType(metaclass=abc.ABCMeta):
34 | """Base command type class."""
35 |
36 | TYPENAME = ''
37 | SUPPORTED_TYPENAMES: list[str] = []
38 | UNDEF = 'UNDEF'
39 | NULL = 'NULL'
40 | UNDEFINED_STATES = [UNDEF, NULL]
41 |
42 | @classmethod
43 | def is_undefined(cls, value: typing.Any) -> bool:
44 | """Return true if given value is an undefined value in openHAB (i.e. UNDEF/NULL)."""
45 | return value in CommandType.UNDEFINED_STATES
46 |
47 | @classmethod
48 | def get_type_for(
49 | cls,
50 | typename: str,
51 | parent_cls: typing.Optional[type['CommandType']] = None,
52 | ) -> typing.Union[type['CommandType'], None]:
53 | """Get a class type for a given typename."""
54 | if parent_cls is None:
55 | parent_cls = CommandType
56 | for a_type in parent_cls.__subclasses__():
57 | if typename in a_type.SUPPORTED_TYPENAMES:
58 | return a_type
59 |
60 | # maybe a subclass of a subclass
61 | result = a_type.get_type_for(typename, a_type)
62 | if result is not None:
63 | return result
64 |
65 | return None
66 |
67 | @classmethod
68 | @abc.abstractmethod
69 | def parse(cls, value: str) -> typing.Optional[typing.Any]:
70 | """Parse a given value."""
71 | raise NotImplementedError
72 |
73 | @classmethod
74 | @abc.abstractmethod
75 | def validate(cls, value: typing.Any) -> None:
76 | """Value validation method.
77 |
78 | As this is the base class which should not be used
79 | directly, we throw a NotImplementedError exception.
80 |
81 | Args:
82 | value (Object): The value to validate. The data_type of the value depends on the item
83 | data_type and is checked accordingly.
84 |
85 | Raises:
86 | NotImplementedError: Raises NotImplementedError as the base class should never
87 | be used directly.
88 | """
89 | raise NotImplementedError
90 |
91 |
92 | class UndefType(CommandType):
93 | """Undefined type."""
94 |
95 | TYPENAME = 'UnDef'
96 | SUPPORTED_TYPENAMES = [TYPENAME]
97 |
98 | @classmethod
99 | def parse(cls, value: str) -> typing.Optional[str]:
100 | """Parse a given value."""
101 | if value in UndefType.UNDEFINED_STATES:
102 | return None
103 | return value
104 |
105 | @classmethod
106 | def validate(cls, value: str) -> None:
107 | """Value validation method."""
108 |
109 |
110 | class GroupType(CommandType):
111 | """Group type."""
112 |
113 | TYPENAME = 'Group'
114 | SUPPORTED_TYPENAMES = [TYPENAME]
115 |
116 | @classmethod
117 | def parse(cls, value: str) -> typing.Optional[str]:
118 | """Parse a given value."""
119 | if value in GroupType.UNDEFINED_STATES:
120 | return None
121 | return value
122 |
123 | @classmethod
124 | def validate(cls, value: str) -> None:
125 | """Value validation method."""
126 |
127 |
128 | class StringType(CommandType):
129 | """StringType data_type class."""
130 |
131 | TYPENAME = 'String'
132 | SUPPORTED_TYPENAMES = [TYPENAME]
133 |
134 | @classmethod
135 | def parse(cls, value: str) -> typing.Optional[str]:
136 | """Parse a given value."""
137 | if value in StringType.UNDEFINED_STATES:
138 | return None
139 | if not isinstance(value, str):
140 | raise ValueError
141 | return value
142 |
143 | @classmethod
144 | def validate(cls, value: str) -> None:
145 | """Value validation method.
146 |
147 | Valid values are any of data_type string.
148 |
149 | Args:
150 | value (str): The value to validate.
151 |
152 | Raises:
153 | ValueError: Raises ValueError if an invalid value has been specified.
154 | """
155 | StringType.parse(value)
156 |
157 |
158 | class OnOffType(StringType):
159 | """OnOffType data_type class."""
160 |
161 | TYPENAME = 'OnOff'
162 | SUPPORTED_TYPENAMES = [TYPENAME]
163 | ON = 'ON'
164 | OFF = 'OFF'
165 | POSSIBLE_VALUES = [ON, OFF]
166 |
167 | @classmethod
168 | def parse(cls, value: str) -> typing.Optional[str]:
169 | """Parse a given value."""
170 | if value in OnOffType.UNDEFINED_STATES:
171 | return None
172 | if value not in OnOffType.POSSIBLE_VALUES:
173 | raise ValueError
174 | return value
175 |
176 | @classmethod
177 | def validate(cls, value: str) -> None:
178 | """Value validation method.
179 |
180 | Valid values are ``ON`` and ``OFF``.
181 |
182 | Args:
183 | value (str): The value to validate.
184 |
185 | Raises:
186 | ValueError: Raises ValueError if an invalid value has been specified.
187 | """
188 | super().validate(value)
189 | OnOffType.parse(value)
190 |
191 |
192 | class OpenCloseType(StringType):
193 | """OpenCloseType data_type class."""
194 |
195 | TYPENAME = 'OpenClosed'
196 | SUPPORTED_TYPENAMES = [TYPENAME]
197 | OPEN = 'OPEN'
198 | CLOSED = 'CLOSED'
199 | POSSIBLE_VALUES = [OPEN, CLOSED]
200 |
201 | @classmethod
202 | def parse(cls, value: str) -> typing.Optional[str]:
203 | """Parse a given value."""
204 | if value in OpenCloseType.UNDEFINED_STATES:
205 | return None
206 | if value not in OpenCloseType.POSSIBLE_VALUES:
207 | raise ValueError
208 | return value
209 |
210 | @classmethod
211 | def validate(cls, value: str) -> None:
212 | """Value validation method.
213 |
214 | Valid values are ``OPEN`` and ``CLOSED``.
215 |
216 | Args:
217 | value (str): The value to validate.
218 |
219 | Raises:
220 | ValueError: Raises ValueError if an invalid value has been specified.
221 | """
222 | super().validate(value)
223 | OpenCloseType.parse(value)
224 |
225 |
226 | class ColorType(CommandType):
227 | """ColorType data_type class."""
228 |
229 | TYPENAME = 'HSB'
230 | SUPPORTED_TYPENAMES = [TYPENAME]
231 |
232 | @classmethod
233 | def parse(cls, value: str) -> typing.Optional[tuple[float, float, float]]:
234 | """Parse a given value."""
235 | if value in ColorType.UNDEFINED_STATES:
236 | return None
237 |
238 | if not isinstance(value, str):
239 | raise ValueError
240 |
241 | str_split = value.split(',')
242 | if len(str_split) != 3:
243 | raise ValueError
244 |
245 | hs, ss, bs = value.split(',', 3)
246 | h = float(hs)
247 | s = float(ss)
248 | b = float(bs)
249 | if not ((0 <= h <= 360) and (0 <= s <= 100) and (0 <= b <= 100)):
250 | raise ValueError
251 | return h, s, b
252 |
253 | @classmethod
254 | def validate(cls, value: typing.Union[str, tuple[float, float, float]]) -> None:
255 | """Value validation method.
256 |
257 | Valid values are in format H,S,B.
258 | Value ranges:
259 | H(ue): 0-360
260 | S(aturation): 0-100
261 | B(rightness): 0-100
262 |
263 | Args:
264 | value (str): The value to validate.
265 |
266 | Raises:
267 | ValueError: Raises ValueError if an invalid value has been specified.
268 | """
269 | if isinstance(value, str):
270 | str_value = str(value)
271 | elif isinstance(value, tuple) and len(value) == 3:
272 | str_value = f'{value[0]},{value[1]},{value[2]}'
273 | else:
274 | raise ValueError
275 |
276 | ColorType.parse(str_value)
277 |
278 |
279 | class DecimalType(CommandType):
280 | """DecimalType data_type class."""
281 |
282 | TYPENAME = 'Decimal'
283 | SUPPORTED_TYPENAMES = [TYPENAME, 'Quantity']
284 |
285 | @classmethod
286 | def parse(cls, value: str) -> typing.Union[None, tuple[typing.Union[int, float], str]]:
287 | """Parse a given value."""
288 | if value in DecimalType.UNDEFINED_STATES:
289 | return None
290 |
291 | m = re.match(r'(-?[0-9.]+(?:[eE]-?[0-9]+)?)\s?(.*)?$', value)
292 | if m:
293 | value_value = m.group(1)
294 | value_unit_of_measure = m.group(2)
295 |
296 | try:
297 | if '.' in value:
298 | return_value: typing.Union[int, float] = float(value_value)
299 | else:
300 | return_value = int(value_value)
301 | except ArithmeticError as exc:
302 | raise ValueError(exc) from exc
303 |
304 | return return_value, value_unit_of_measure
305 |
306 | raise ValueError
307 |
308 | @classmethod
309 | def validate(cls, value: typing.Union[float, tuple[float, str], str]) -> None:
310 | """Value validation method.
311 |
312 | Valid values are any of data_type:
313 | - ``int``
314 | - ``float``
315 | - a tuple of (``int`` or ``float``, ``str``) for numeric value, unit of measure
316 | - a ``str`` that can be parsed to one of the above by ``DecimalType.parse``
317 |
318 | Args:
319 | value (int, float, tuple, str): The value to validate.
320 |
321 | Raises:
322 | ValueError: Raises ValueError if an invalid value has been specified.
323 | """
324 | if isinstance(value, str):
325 | DecimalType.parse(value)
326 | elif isinstance(value, tuple) and len(value) == 2:
327 | DecimalType.parse(f'{value[0]} {value[1]}')
328 | elif not isinstance(value, (int, float)):
329 | raise ValueError
330 |
331 |
332 | class PercentType(CommandType):
333 | """PercentType data_type class."""
334 |
335 | TYPENAME = 'Percent'
336 | SUPPORTED_TYPENAMES = [TYPENAME]
337 |
338 | @classmethod
339 | def parse(cls, value: str) -> typing.Optional[float]:
340 | """Parse a given value."""
341 | if value in PercentType.UNDEFINED_STATES:
342 | return None
343 | try:
344 | f = float(value)
345 | if not 0 <= f <= 100:
346 | raise ValueError
347 | return f
348 | except Exception as e:
349 | raise ValueError(e) from e
350 |
351 | @classmethod
352 | def validate(cls, value: float) -> None:
353 | """Value validation method.
354 |
355 | Valid values are any of data_type ``float`` or ``int`` and must be greater of equal to 0
356 | and smaller or equal to 100.
357 |
358 | Args:
359 | value (float): The value to validate.
360 |
361 | Raises:
362 | ValueError: Raises ValueError if an invalid value has been specified.
363 | """
364 | if not (isinstance(value, (float, int)) and 0 <= value <= 100):
365 | raise ValueError
366 |
367 |
368 | class IncreaseDecreaseType(StringType):
369 | """IncreaseDecreaseType data_type class."""
370 |
371 | TYPENAME = 'IncreaseDecrease'
372 | SUPPORTED_TYPENAMES = [TYPENAME]
373 |
374 | INCREASE = 'INCREASE'
375 | DECREASE = 'DECREASE'
376 |
377 | POSSIBLE_VALUES = [INCREASE, DECREASE]
378 |
379 | @classmethod
380 | def parse(cls, value: str) -> typing.Optional[str]:
381 | """Parse a given value."""
382 | if value in IncreaseDecreaseType.UNDEFINED_STATES:
383 | return None
384 | if value not in IncreaseDecreaseType.POSSIBLE_VALUES:
385 | raise ValueError
386 | return value
387 |
388 | @classmethod
389 | def validate(cls, value: str) -> None:
390 | """Value validation method.
391 |
392 | Valid values are ``INCREASE`` and ``DECREASE``.
393 |
394 | Args:
395 | value (str): The value to validate.
396 |
397 | Raises:
398 | ValueError: Raises ValueError if an invalid value has been specified.
399 | """
400 | super().validate(value)
401 | IncreaseDecreaseType.parse(value)
402 |
403 |
404 | class DateTimeType(CommandType):
405 | """DateTimeType data_type class."""
406 |
407 | TYPENAME = 'DateTime'
408 | SUPPORTED_TYPENAMES = [TYPENAME]
409 |
410 | @classmethod
411 | def parse(cls, value: str) -> typing.Optional[datetime.datetime]:
412 | """Parse a given value."""
413 | if value in DateTimeType.UNDEFINED_STATES:
414 | return None
415 | return dateutil.parser.parse(value)
416 |
417 | @classmethod
418 | def validate(cls, value: datetime.datetime) -> None:
419 | """Value validation method.
420 |
421 | Valid values are any of data_type ``datetime.datetime``.
422 |
423 | Args:
424 | value (datetime.datetime): The value to validate.
425 |
426 | Raises:
427 | ValueError: Raises ValueError if an invalid value has been specified.
428 | """
429 | if not isinstance(value, datetime.datetime):
430 | raise ValueError
431 |
432 |
433 | class UpDownType(StringType):
434 | """UpDownType data_type class."""
435 |
436 | TYPENAME = 'UpDown'
437 | SUPPORTED_TYPENAMES = [TYPENAME]
438 | UP = 'UP'
439 | DOWN = 'DOWN'
440 | POSSIBLE_VALUES = [UP, DOWN]
441 |
442 | @classmethod
443 | def parse(cls, value: str) -> typing.Optional[str]:
444 | """Parse a given value."""
445 | if value in UpDownType.UNDEFINED_STATES:
446 | return None
447 | if value not in UpDownType.POSSIBLE_VALUES:
448 | raise ValueError
449 | return value
450 |
451 | @classmethod
452 | def validate(cls, value: str) -> None:
453 | """Value validation method.
454 |
455 | Valid values are ``UP`` and ``DOWN``.
456 |
457 | Args:
458 | value (str): The value to validate.
459 |
460 | Raises:
461 | ValueError: Raises ValueError if an invalid value has been specified.
462 | """
463 | super().validate(value)
464 |
465 | UpDownType.parse(value)
466 |
467 |
468 | class StopMoveType(StringType):
469 | """UpDownType data_type class."""
470 |
471 | TYPENAME = 'StopMove'
472 | SUPPORTED_TYPENAMES = [TYPENAME]
473 | STOP = 'STOP'
474 | POSSIBLE_VALUES = [STOP]
475 |
476 | @classmethod
477 | def parse(cls, value: str) -> typing.Optional[str]:
478 | """Parse a given value."""
479 | if value in StopMoveType.UNDEFINED_STATES:
480 | return None
481 | if value not in StopMoveType.POSSIBLE_VALUES:
482 | raise ValueError
483 | return value
484 |
485 | @classmethod
486 | def validate(cls, value: str) -> None:
487 | """Value validation method.
488 |
489 | Valid values are ``UP`` and ``DOWN``.
490 |
491 | Args:
492 | value (str): The value to validate.
493 |
494 | Raises:
495 | ValueError: Raises ValueError if an invalid value has been specified.
496 | """
497 | super().validate(value)
498 |
499 | StopMoveType.parse(value)
500 |
501 |
502 | class PlayPauseType(StringType):
503 | """PlayPauseType data_type class."""
504 |
505 | TYPENAME = 'PlayPause'
506 | SUPPORTED_TYPENAMES = [TYPENAME]
507 | PLAY = 'PLAY'
508 | PAUSE = 'PAUSE'
509 | POSSIBLE_VALUES = [PLAY, PAUSE]
510 |
511 | @classmethod
512 | def parse(cls, value: str) -> typing.Optional[str]:
513 | """Parse a given value."""
514 | if value in PlayPauseType.UNDEFINED_STATES:
515 | return None
516 | if value not in PlayPauseType.POSSIBLE_VALUES:
517 | raise ValueError
518 | return value
519 |
520 | @classmethod
521 | def validate(cls, value: str) -> None:
522 | """Value validation method.
523 |
524 | Valid values are ``PLAY``, ``PAUSE``
525 |
526 | Args:
527 | value (str): The value to validate.
528 |
529 | Raises:
530 | ValueError: Raises ValueError if an invalid value has been specified.
531 | """
532 | super().validate(value)
533 |
534 | PlayPauseType.parse(value)
535 |
536 |
537 | class NextPrevious(StringType):
538 | """NextPrevious data_type class."""
539 |
540 | TYPENAME = 'NextPrevious'
541 | SUPPORTED_TYPENAMES = [TYPENAME]
542 | NEXT = 'NEXT'
543 | PREVIOUS = 'PREVIOUS'
544 | POSSIBLE_VALUES = [NEXT, PREVIOUS]
545 |
546 | @classmethod
547 | def parse(cls, value: str) -> typing.Optional[str]:
548 | """Parse a given value."""
549 | if value in NextPrevious.UNDEFINED_STATES:
550 | return None
551 | if value not in NextPrevious.POSSIBLE_VALUES:
552 | raise ValueError
553 | return value
554 |
555 | @classmethod
556 | def validate(cls, value: str) -> None:
557 | """Value validation method.
558 |
559 | Valid values are ``PLAY``, ``PAUSE``
560 |
561 | Args:
562 | value (str): The value to validate.
563 |
564 | Raises:
565 | ValueError: Raises ValueError if an invalid value has been specified.
566 | """
567 | super().validate(value)
568 |
569 | NextPrevious.parse(value)
570 |
571 |
572 | class RewindFastforward(StringType):
573 | """RewindFastforward data_type class."""
574 |
575 | TYPENAME = 'RewindFastforward'
576 | SUPPORTED_TYPENAMES = [TYPENAME]
577 | REWIND = 'REWIND'
578 | FASTFORWARD = 'FASTFORWARD'
579 | POSSIBLE_VALUES = [REWIND, FASTFORWARD]
580 |
581 | @classmethod
582 | def parse(cls, value: str) -> typing.Optional[str]:
583 | """Parse a given value."""
584 | if value in RewindFastforward.UNDEFINED_STATES:
585 | return None
586 | if value not in RewindFastforward.POSSIBLE_VALUES:
587 | raise ValueError
588 | return value
589 |
590 | @classmethod
591 | def validate(cls, value: str) -> None:
592 | """Value validation method.
593 |
594 | Valid values are ``REWIND``, ``FASTFORWARD``
595 |
596 | Args:
597 | value (str): The value to validate.
598 |
599 | Raises:
600 | ValueError: Raises ValueError if an invalid value has been specified.
601 | """
602 | super().validate(value)
603 |
604 | RewindFastforward.parse(value)
605 |
606 |
607 | class PointType(CommandType):
608 | """PointType data_type class."""
609 |
610 | TYPENAME = 'Point'
611 | SUPPORTED_TYPENAMES = [TYPENAME]
612 |
613 | @classmethod
614 | def parse(cls, value: str) -> typing.Optional[tuple[float, float, float]]:
615 | """Parse a given value."""
616 | if value in PercentType.UNDEFINED_STATES:
617 | return None
618 |
619 | value_split = value.split(',', maxsplit=2)
620 | if not (1 < len(value_split) < 4):
621 | raise ValueError
622 |
623 | try:
624 | latitude = float(value_split[0])
625 | longitude = float(value_split[1])
626 | altitude = 0.0
627 |
628 | if len(value_split) == 3:
629 | altitude = float(value_split[2])
630 | except ArithmeticError as exc:
631 | raise ValueError(exc) from exc
632 |
633 | return latitude, longitude, altitude
634 |
635 | @classmethod
636 | def validate(cls, value: typing.Optional[typing.Union[str, tuple[typing.Union[float, int], typing.Union[float, int], typing.Union[float, int]]]]) -> None:
637 | """Value validation method.
638 |
639 | A valid PointType is a tuple of three decimal values representing:
640 | - latitude
641 | - longitude
642 | - altitude
643 |
644 | Valid values are:
645 | - a tuple of (``int`` or ``float``, ``int`` or ``float``, ``int`` or ``float``)
646 | - a ``str`` that can be parsed to one of the above by ``DecimalType.parse``
647 |
648 | Args:
649 | value: The value to validate.
650 |
651 | Raises:
652 | ValueError: Raises ValueError if an invalid value has been specified.
653 | """
654 | if isinstance(value, str):
655 | result = PointType.parse(value)
656 | if result is None:
657 | return
658 |
659 | latitude, longitude, altitude = result
660 |
661 | elif not (isinstance(value, tuple) and len(value) == 3):
662 | raise ValueError
663 |
664 | elif not (isinstance(value[0], (float, int)) and isinstance(value[1], (float, int)) and isinstance(value[2], (float, int))):
665 | raise ValueError
666 |
667 | else:
668 | latitude, longitude, altitude = value
669 |
670 | if not (-90 <= latitude <= 90):
671 | msg = 'Latitude must be between -90 and 90, inclusive.'
672 | raise ValueError(msg)
673 |
674 | if not (-180 <= longitude <= 180):
675 | msg = 'Longitude must be between -180 and 180, inclusive.'
676 | raise ValueError(msg)
677 |
--------------------------------------------------------------------------------
/openhab/items.py:
--------------------------------------------------------------------------------
1 | """python library for accessing the openHAB REST API."""
2 |
3 | #
4 | # Georges Toth (c) 2016-present
5 | #
6 | # python-openhab is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # python-openhab is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with python-openhab. If not, see .
18 | #
19 |
20 | import datetime
21 | import logging
22 | import re
23 | import typing
24 |
25 | import dateutil.parser
26 |
27 | import openhab.command_types
28 | import openhab.exceptions
29 |
30 | __author__ = 'Georges Toth '
31 | __license__ = 'AGPLv3+'
32 |
33 |
34 | class Item:
35 | """Base item class."""
36 |
37 | types: typing.Sequence[type[openhab.command_types.CommandType]] = []
38 | state_types: typing.Sequence[type[openhab.command_types.CommandType]] = []
39 | command_event_types: typing.Sequence[type[openhab.command_types.CommandType]] = []
40 | state_event_types: typing.Sequence[type[openhab.command_types.CommandType]] = []
41 | state_changed_event_types: typing.Sequence[type[openhab.command_types.CommandType]] = []
42 |
43 | TYPENAME = 'unknown'
44 |
45 | def __init__(self, openhab_conn: 'openhab.client.OpenHAB', json_data: dict) -> None:
46 | """Constructor.
47 |
48 | Args:
49 | openhab_conn (openhab.OpenHAB): openHAB object.
50 | json_data (dic): A dict converted from the JSON data returned by the openHAB
51 | server.
52 | """
53 | self.openhab = openhab_conn
54 | self.type_: typing.Optional[str] = None
55 | self.quantityType: typing.Optional[str] = None
56 | self.editable = None
57 | self.label = ''
58 | self.category = ''
59 | self.tags = ''
60 | self.groupNames = ''
61 | self.group = False
62 | self.name = ''
63 | self._state = None # type: typing.Optional[typing.Any]
64 | self._unitOfMeasure = ''
65 | self._raw_state = None # type: typing.Optional[typing.Any] # raw state as returned by the server
66 | self._members = {} # type: typing.Dict[str, typing.Any] # group members (key = item name), for none-group items it's empty
67 | self.function_name: typing.Optional[str] = None
68 | self.function_params: typing.Optional[typing.Sequence[str]] = None
69 |
70 | self.logger = logging.getLogger(__name__)
71 |
72 | self.init_from_json(json_data)
73 |
74 | def init_from_json(self, json_data: dict) -> None:
75 | """Initialize this object from a json configuration as fetched from openHAB.
76 |
77 | Args:
78 | json_data (dict): A dict converted from the JSON data returned by the openHAB
79 | server.
80 | """
81 | self.name = json_data['name']
82 | if json_data['type'] == 'Group':
83 | self.group = True
84 | if 'groupType' in json_data:
85 | self.type_ = json_data['groupType']
86 |
87 | if 'function' in json_data:
88 | self.function_name = json_data['function']['name']
89 |
90 | if 'params' in json_data['function']:
91 | self.function_params = json_data['function']['params']
92 |
93 | # init members
94 | for i in json_data['members']:
95 | self.members[i['name']] = self.openhab.json_to_item(i)
96 |
97 | else:
98 | self.type_ = json_data.get('type', None)
99 |
100 | if self.type_ is None:
101 | raise openhab.exceptions.InvalidReturnException('Item did not return a type attribute.')
102 |
103 | parts = self.type_.split(':')
104 | if len(parts) == 2:
105 | self.quantityType = parts[1]
106 |
107 | if 'editable' in json_data:
108 | self.editable = json_data['editable']
109 | if 'label' in json_data:
110 | self.label = json_data['label']
111 | if 'category' in json_data:
112 | self.category = json_data['category']
113 | if 'tags' in json_data:
114 | self.tags = json_data['tags']
115 | if 'groupNames' in json_data:
116 | self.groupNames = json_data['groupNames']
117 |
118 | self._raw_state = json_data['state']
119 |
120 | if self.is_undefined(self._raw_state):
121 | self._state = None
122 | else:
123 | self._state, self._unitOfMeasure = self._parse_rest(self._raw_state)
124 |
125 | @property
126 | def state(self) -> typing.Any:
127 | """The state property represents the current state of the item.
128 |
129 | The state is automatically refreshed from openHAB on reading it.
130 | Updating the value via this property send an update to the event bus.
131 | """
132 | json_data = self.openhab.get_item_raw(self.name)
133 | self.init_from_json(json_data)
134 |
135 | return self._state
136 |
137 | @state.setter
138 | def state(self, value: typing.Any) -> None:
139 | self.update(value)
140 |
141 | @property
142 | def unit_of_measure(self) -> str:
143 | """Return the unit of measure. Returns an empty string if there is none defined."""
144 | return self._unitOfMeasure
145 |
146 | @property
147 | def members(self) -> dict[str, typing.Any]:
148 | """If item is a type of Group, it will return all member items for this group.
149 |
150 | For none group item empty dictionary will be returned.
151 |
152 | Returns:
153 | dict: Returns a dict with item names as key and `Item` class instances as value.
154 |
155 | """
156 | return self._members
157 |
158 | def _validate_value(self, value: typing.Union[str, type[openhab.command_types.CommandType]]) -> None:
159 | """Private method for verifying the new value before modifying the state of the item."""
160 | if self.type_ == 'String':
161 | if not isinstance(value, (str, bytes)):
162 | raise ValueError
163 | elif self.types:
164 | validation = False
165 |
166 | for type_ in self.types:
167 | try:
168 | type_.validate(value)
169 | except ValueError:
170 | pass
171 | else:
172 | validation = True
173 |
174 | if not validation:
175 | raise ValueError(f'Invalid value "{value}"')
176 | else:
177 | raise ValueError
178 |
179 | def _parse_rest(self, value: str) -> tuple[str, str]:
180 | """Parse a REST result into a native object."""
181 | return value, ''
182 |
183 | def _rest_format(self, value: str) -> typing.Union[str, bytes]:
184 | """Format a value before submitting to openHAB."""
185 | _value = value # type: typing.Union[str, bytes]
186 |
187 | # Only ascii encoding is supported by default. If non-ascii characters were provided, convert them to bytes.
188 | try:
189 | _ = value.encode('ascii')
190 | except UnicodeError:
191 | _value = value.encode('utf-8')
192 |
193 | return _value
194 |
195 | def is_undefined(self, value: str) -> bool:
196 | """Check if value is undefined."""
197 | for a_state_type in self.state_types:
198 | if not a_state_type.is_undefined(value):
199 | return False
200 |
201 | return True
202 |
203 | def __str__(self) -> str:
204 | """String representation."""
205 | state = self._state
206 | if self._unitOfMeasure and not isinstance(self._state, tuple):
207 | state = f'{self._state} {self._unitOfMeasure}'
208 | return f'<{self.type_} - {self.name} : {state}>'
209 |
210 | def _update(self, value: typing.Any) -> None:
211 | """Updates the state of an item, input validation is expected to be already done.
212 |
213 | Args:
214 | value (object): The value to update the item with. The type of the value depends
215 | on the item type and is checked accordingly.
216 | """
217 | # noinspection PyTypeChecker
218 | self.openhab.req_put(f'/items/{self.name}/state', data=value)
219 |
220 | def update(self, value: typing.Any) -> None:
221 | """Updates the state of an item.
222 |
223 | Args:
224 | value (object): The value to update the item with. The type of the value depends
225 | on the item type and is checked accordingly.
226 | """
227 | self._validate_value(value)
228 |
229 | v = self._rest_format(value)
230 |
231 | self._state = value
232 |
233 | self._update(v)
234 |
235 | def command(self, value: typing.Any) -> None:
236 | """Sends the given value as command to the event bus.
237 |
238 | Args:
239 | value (object): The value to send as command to the event bus. The type of the
240 | value depends on the item type and is checked accordingly.
241 | """
242 | self._validate_value(value)
243 |
244 | v = self._rest_format(value)
245 |
246 | self._state = value
247 |
248 | self.openhab.req_post(f'/items/{self.name}', data=v)
249 |
250 | def update_state_null(self) -> None:
251 | """Update the state of the item to *NULL*."""
252 | self._update('NULL')
253 |
254 | def update_state_undef(self) -> None:
255 | """Update the state of the item to *UNDEF*."""
256 | self._update('UNDEF')
257 |
258 | def is_state_null(self) -> bool:
259 | """If the item state is None, use this method for checking if the remote value is NULL."""
260 | if self.state is None:
261 | # we need to query the current remote state as else this method will not work correctly if called after
262 | # either update_state method
263 | if self._raw_state is None:
264 | # This should never happen
265 | raise ValueError('Invalid internal (raw) state.')
266 |
267 | return self._raw_state == 'NULL'
268 |
269 | return False
270 |
271 | def is_state_undef(self) -> bool:
272 | """If the item state is None, use this method for checking if the remote value is UNDEF."""
273 | if self.state is None:
274 | # we need to query the current remote state as else this method will not work correctly if called after
275 | # either update_state method
276 | if self._raw_state is None:
277 | # This should never happen
278 | raise ValueError('Invalid internal (raw) state.')
279 |
280 | return self._raw_state == 'UNDEF'
281 |
282 | return False
283 |
284 | def persistence(
285 | self,
286 | service_id: typing.Optional[str] = None,
287 | start_time: typing.Optional[datetime.datetime] = None,
288 | end_time: typing.Optional[datetime.datetime] = None,
289 | page: int = 0,
290 | page_length: int = 0,
291 | boundary: bool = False,
292 | ) -> typing.Iterator[dict[str, typing.Union[str, int]]]:
293 | """Method for fetching persistence data for a given item.
294 |
295 | Args:
296 | service_id: ID of the persistence service. If not provided the default service will be used.
297 | start_time: Start time of the data to return. Will default to 1 day before end_time.
298 | end_time: End time of the data to return. Will default to current time.
299 | page: Page number of data to return. Defaults to 0 if not provided.
300 | page_length: The length of each page. Defaults to 0 which disabled paging.
301 | boundary: Gets one value before and after the requested period.
302 |
303 | Returns:
304 | Iterator over dict values containing time and state value, e.g.
305 | {"time": 1695588900122,
306 | "state": "23"
307 | }
308 | """
309 | yield from self.openhab.get_item_persistence(
310 | name=self.name,
311 | service_id=service_id,
312 | start_time=start_time,
313 | end_time=end_time,
314 | page=page,
315 | page_length=page_length,
316 | boundary=boundary,
317 | )
318 |
319 |
320 | class GroupItem(Item):
321 | """String item type."""
322 |
323 | TYPENAME = 'Group'
324 | types: list[type[openhab.command_types.CommandType]] = []
325 | state_types: list[type[openhab.command_types.CommandType]] = []
326 |
327 |
328 | class StringItem(Item):
329 | """String item type."""
330 |
331 | TYPENAME = 'String'
332 | types = [openhab.command_types.StringType]
333 | state_types = types
334 |
335 |
336 | class DateTimeItem(Item):
337 | """DateTime item type."""
338 |
339 | TYPENAME = 'DateTime'
340 | types = [openhab.command_types.DateTimeType]
341 | state_types = types
342 |
343 | def __gt__(self, other: datetime.datetime) -> bool:
344 | """Greater than comparison."""
345 | if self._state is None or not isinstance(other, datetime.datetime):
346 | raise NotImplementedError('You can only compare two DateTimeItem objects.')
347 |
348 | return self._state > other
349 |
350 | def __ge__(self, other: datetime.datetime) -> bool:
351 | """Greater or equal comparison."""
352 | if self._state is None or not isinstance(other, datetime.datetime):
353 | raise NotImplementedError('You can only compare two DateTimeItem objects.')
354 |
355 | return self._state >= other
356 |
357 | def __lt__(self, other: object) -> bool:
358 | """Less than comparison."""
359 | if not isinstance(other, datetime.datetime):
360 | raise NotImplementedError('You can only compare two DateTimeItem objects.')
361 |
362 | return not self.__gt__(other)
363 |
364 | def __le__(self, other: object) -> bool:
365 | """Less or equal comparison."""
366 | if self._state is None or not isinstance(other, datetime.datetime):
367 | raise NotImplementedError('You can only compare two DateTimeItem objects.')
368 |
369 | return self._state <= other
370 |
371 | def __eq__(self, other: object) -> bool:
372 | """Equality comparison."""
373 | if not isinstance(other, datetime.datetime):
374 | raise NotImplementedError('You can only compare two DateTimeItem objects.')
375 |
376 | return self._state == other
377 |
378 | def __ne__(self, other: object) -> bool:
379 | """Not equal comparison."""
380 | if not isinstance(other, datetime.datetime):
381 | raise NotImplementedError('You can only compare two DateTimeItem objects.')
382 |
383 | return not self.__eq__(other)
384 |
385 | def _parse_rest(self, value: str) -> tuple[datetime.datetime, str]: # type: ignore[override]
386 | """Parse a REST result into a native object.
387 |
388 | Args:
389 | value (str): A string argument to be converted into a datetime.datetime object.
390 |
391 | Returns:
392 | datetime.datetime: The datetime.datetime object as converted from the string
393 | parameter.
394 | """
395 | return dateutil.parser.parse(value), ''
396 |
397 | def _rest_format(self, value: datetime.datetime) -> str: # type: ignore[override]
398 | """Format a value before submitting to openHAB.
399 |
400 | Args:
401 | value (datetime.datetime): A datetime.datetime argument to be converted
402 | into a string.
403 |
404 | Returns:
405 | str: The string as converted from the datetime.datetime parameter.
406 | """
407 | # openHAB supports only up to milliseconds as of this writing
408 | return value.isoformat(timespec='milliseconds')
409 |
410 |
411 | class PlayerItem(Item):
412 | """PlayerItem item type."""
413 |
414 | TYPENAME = 'Player'
415 | types = [openhab.command_types.PlayPauseType, openhab.command_types.NextPrevious, openhab.command_types.RewindFastforward]
416 | state_types = [openhab.command_types.PlayPauseType, openhab.command_types.RewindFastforward]
417 |
418 | def play(self) -> None:
419 | """Send the command PLAY."""
420 | self.command(openhab.command_types.PlayPauseType.PLAY)
421 |
422 | def pause(self) -> None:
423 | """Send the command PAUSE."""
424 | self.command(openhab.command_types.PlayPauseType.PAUSE)
425 |
426 | def next(self) -> None:
427 | """Send the command NEXT."""
428 | self.command(openhab.command_types.NextPrevious.NEXT)
429 |
430 | def previous(self) -> None:
431 | """Send the command PREVIOUS."""
432 | self.command(openhab.command_types.NextPrevious.PREVIOUS)
433 |
434 | def fastforward(self) -> None:
435 | """Send the command FASTFORWARD."""
436 | self.command(openhab.command_types.RewindFastforward.FASTFORWARD)
437 |
438 | def rewind(self) -> None:
439 | """Send the command REWIND."""
440 | self.command(openhab.command_types.RewindFastforward.REWIND)
441 |
442 |
443 | class SwitchItem(Item):
444 | """SwitchItem item type."""
445 |
446 | TYPENAME = 'Switch'
447 | types = [openhab.command_types.OnOffType]
448 | state_types = types
449 |
450 | def on(self) -> None:
451 | """Set the state of the switch to ON."""
452 | self.command(openhab.command_types.OnOffType.ON)
453 |
454 | def off(self) -> None:
455 | """Set the state of the switch to OFF."""
456 | self.command(openhab.command_types.OnOffType.OFF)
457 |
458 | def toggle(self) -> None:
459 | """Toggle the state of the switch to OFF to ON and vice versa."""
460 | if self.state == openhab.command_types.OnOffType.ON:
461 | self.off()
462 | elif self.state == openhab.command_types.OnOffType.OFF:
463 | self.on()
464 |
465 |
466 | class NumberItem(Item):
467 | """NumberItem item type."""
468 |
469 | TYPENAME = 'Number'
470 | types = [openhab.command_types.DecimalType]
471 | state_types = types
472 |
473 | def _parse_rest(self, value: str) -> tuple[typing.Union[float, None], str]: # type: ignore[override]
474 | """Parse a REST result into a native object.
475 |
476 | Args:
477 | value (str): A string argument to be converted into a float object.
478 |
479 | Returns:
480 | float: The float object as converted from the string parameter.
481 | str: The unit Of Measure or empty string
482 | """
483 | if value in ('UNDEF', 'NULL'):
484 | return None, ''
485 | # m = re.match(r'''^(-?[0-9.]+)''', value)
486 | try:
487 | m = re.match(r'(-?[0-9.]+(?:[eE]-?[0-9]+)?)\s?(.*)?$', value)
488 |
489 | if m:
490 | value = m.group(1)
491 | unit_of_measure = m.group(2)
492 |
493 | return float(value), unit_of_measure
494 |
495 | return float(value), ''
496 |
497 | except (ArithmeticError, ValueError) as exc:
498 | self.logger.error('Error in parsing new value "%s" for "%s" - "%s"', value, self.name, exc)
499 |
500 | raise ValueError(f'{self.__class__}: unable to parse value "{value}"')
501 |
502 | def _rest_format(self, value: typing.Union[float, tuple[float, str], str]) -> typing.Union[str, bytes]:
503 | """Format a value before submitting to openHAB.
504 |
505 | Args:
506 | value: Either a float, a tuple of (float, str), or string; in the first two cases we have to cast it to a string.
507 |
508 | Returns:
509 | str or bytes: A string or bytes as converted from the value parameter.
510 | """
511 | if isinstance(value, tuple) and len(value) == 2:
512 | return super()._rest_format(f'{value[0]:G} {value[1]}')
513 | if not isinstance(value, str):
514 | return super()._rest_format(f'{value:G}')
515 | return super()._rest_format(value)
516 |
517 |
518 | class ContactItem(Item):
519 | """Contact item type."""
520 |
521 | TYPENAME = 'Contact'
522 | types = [openhab.command_types.OpenCloseType]
523 | state_types = types
524 |
525 | def command(self, *args: typing.Any, **kwargs: typing.Any) -> None:
526 | """This overrides the `Item` command method.
527 |
528 | Note: Commands are not accepted for items of type contact.
529 | """
530 | raise ValueError(f'This item ({self.__class__}) only supports updates, not commands!')
531 |
532 | def open(self) -> None:
533 | """Set the state of the contact item to OPEN."""
534 | self.state = openhab.command_types.OpenCloseType.OPEN
535 |
536 | def closed(self) -> None:
537 | """Set the state of the contact item to CLOSED."""
538 | self.state = openhab.command_types.OpenCloseType.CLOSED
539 |
540 |
541 | class DimmerItem(Item):
542 | """DimmerItem item type."""
543 |
544 | TYPENAME = 'Dimmer'
545 | types = [openhab.command_types.OnOffType, openhab.command_types.PercentType, openhab.command_types.IncreaseDecreaseType]
546 | state_types = [openhab.command_types.PercentType]
547 |
548 | def _parse_rest(self, value: str) -> tuple[float, str]: # type: ignore[override]
549 | """Parse a REST result into a native object.
550 |
551 | Args:
552 | value (str): A string argument to be converted into a int object.
553 |
554 | Returns:
555 | float: The int object as converted from the string parameter.
556 | str: Possible UoM
557 | """
558 | return float(value), ''
559 |
560 | def _rest_format(self, value: typing.Union[str, int]) -> str:
561 | """Format a value before submitting to OpenHAB.
562 |
563 | Args:
564 | value: Either a string or an integer; in the latter case we have to cast it to a string.
565 |
566 | Returns:
567 | str: The string as possibly converted from the parameter.
568 | """
569 | if not isinstance(value, str):
570 | return str(value)
571 |
572 | return value
573 |
574 | def on(self) -> None:
575 | """Set the state of the dimmer to ON."""
576 | self.command(openhab.command_types.OnOffType.ON)
577 |
578 | def off(self) -> None:
579 | """Set the state of the dimmer to OFF."""
580 | self.command(openhab.command_types.OnOffType.OFF)
581 |
582 | def increase(self) -> None:
583 | """Increase the state of the dimmer."""
584 | self.command(openhab.command_types.IncreaseDecreaseType.INCREASE)
585 |
586 | def decrease(self) -> None:
587 | """Decrease the state of the dimmer."""
588 | self.command(openhab.command_types.IncreaseDecreaseType.DECREASE)
589 |
590 |
591 | class ColorItem(DimmerItem):
592 | """ColorItem item type."""
593 |
594 | TYPENAME = 'Color'
595 | types = [openhab.command_types.OnOffType, openhab.command_types.PercentType, openhab.command_types.IncreaseDecreaseType, openhab.command_types.ColorType]
596 | state_types = [openhab.command_types.ColorType]
597 |
598 | def _parse_rest(self, value: str) -> tuple[typing.Optional[tuple[float, float, float]], str]: # type: ignore[override]
599 | """Parse a REST result into a native object.
600 |
601 | Args:
602 | value (str): A string argument to be converted into a str object.
603 |
604 | Returns:
605 | HSB components
606 | Optional UoM
607 | """
608 | result = openhab.command_types.ColorType.parse(value)
609 | return result, ''
610 |
611 | def _rest_format(self, value: typing.Union[tuple[int, int, float], str, int]) -> str:
612 | """Format a value before submitting to openHAB.
613 |
614 | Args:
615 | value: Either a string, an integer or a tuple of HSB components (int, int, float); in the latter two cases we have to cast it to a string.
616 |
617 | Returns:
618 | str: The string as possibly converted from the parameter.
619 | """
620 | if isinstance(value, tuple):
621 | if len(value) == 3:
622 | return f'{value[0]},{value[1]},{value[2]}'
623 |
624 | if not isinstance(value, str):
625 | return str(value)
626 |
627 | return value
628 |
629 |
630 | class RollershutterItem(Item):
631 | """RollershutterItem item type."""
632 |
633 | TYPENAME = 'Rollershutter'
634 | types = [openhab.command_types.UpDownType, openhab.command_types.PercentType, openhab.command_types.StopMoveType]
635 | state_types = [openhab.command_types.PercentType]
636 |
637 | def _parse_rest(self, value: str) -> tuple[int, str]: # type: ignore[override]
638 | """Parse a REST result into a native object.
639 |
640 | Args:
641 | value (str): A string argument to be converted into a int object.
642 |
643 | Returns:
644 | int: The int object as converted from the string parameter.
645 | str: Possible UoM
646 | """
647 | return int(float(value)), ''
648 |
649 | def _rest_format(self, value: typing.Union[str, int]) -> str:
650 | """Format a value before submitting to openHAB.
651 |
652 | Args:
653 | value: Either a string or an integer; in the latter case we have to cast it to a string.
654 |
655 | Returns:
656 | str: The string as possibly converted from the parameter.
657 | """
658 | if not isinstance(value, str):
659 | return str(value)
660 |
661 | return value
662 |
663 | def up(self) -> None:
664 | """Set the state of the dimmer to ON."""
665 | self.command(openhab.command_types.UpDownType.UP)
666 |
667 | def down(self) -> None:
668 | """Set the state of the dimmer to OFF."""
669 | self.command(openhab.command_types.UpDownType.DOWN)
670 |
671 | def stop(self) -> None:
672 | """Set the state of the dimmer to OFF."""
673 | self.command(openhab.command_types.StopMoveType.STOP)
674 |
675 |
676 | class LocationItem(Item):
677 | """LocationItem item type."""
678 |
679 | TYPENAME = 'Location'
680 | types = [openhab.command_types.PointType]
681 | state_types = [openhab.command_types.PointType]
682 |
683 | def _parse_rest(self, value: str) -> tuple[typing.Optional[tuple[float, float, float]], str]: # type: ignore[override]
684 | """Parse a REST result into a native object.
685 |
686 | Args:
687 | value (str): A string argument to be converted into a str object.
688 |
689 | Returns:
690 | Latitude, longitude and altitude components
691 | Optional UoM
692 | """
693 | return openhab.command_types.PointType.parse(value), ''
694 |
695 | def _rest_format(self, value: typing.Union[tuple[float, float, float], str]) -> str:
696 | """Format a value before submitting to openHAB.
697 |
698 | Args:
699 | value: Either a string, an integer or a tuple of HSB components (int, int, float); in the latter two cases we have to cast it to a string.
700 |
701 | Returns:
702 | str: The string as possibly converted from the parameter.
703 | """
704 | if isinstance(value, tuple):
705 | if len(value) == 3:
706 | return f'{value[0]},{value[1]},{value[2]}'
707 |
708 | if not isinstance(value, str):
709 | return str(value)
710 |
711 | return value
712 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
663 |
--------------------------------------------------------------------------------