Skip to content

Commit

Permalink
refactor: setup & command handler, prepare release (#5)
Browse files Browse the repository at this point in the history
refactor: entity command handler on the base Entity
  This will simplify the client command handling significantly:
- configured entity is passed as argument
- basic error checking is done in the library
- websocket stays hidden
- command acknowledgment is implemented in the library

refactor: driver setup process
  Don't expose websocket or request ids. Setup flow is controlled by new data classes:
- Return object is the next step in the setup flow.
- The client should not have to know or call driver_setup_error, request_driver_setup_user_input, etc.

fix: mDNS service publishing
- Publish service with local hostname.
- Cleanup interface and port handling.

refactor: logging
- Remove logging.basicConfig() calls.
  Must be done by client.
- Make logging instances private.
- Move entity creation log statement to base class.

fix: improve websocket handling
- best effort in broadcast_ws_event
- add more type information
- add docstrings

fix: config_dir_path is always set
- Use ENV var UC_CONFIG_HOME for configuration directory, fallback to HOME directory or local path.

Prepare project for public release:
- add CONTRIBUTING.md
- add CHANGELOG.md
- add a minimal example
- enhance README

Closes #1
Fixes #2
Fixes #3
  • Loading branch information
zehnm authored Oct 31, 2023
1 parent cf0f65a commit b2083c4
Show file tree
Hide file tree
Showing 22 changed files with 1,189 additions and 448 deletions.
19 changes: 2 additions & 17 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -1,18 +1,3 @@
[MAIN]
# Specify a score threshold to be exceeded before program exits with error.
fail-under=9.5

# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=
logging-fstring-interpolation,
logging-not-lazy,
unspecified-encoding,
consider-using-from-import,
consider-using-with,
invalid-name

[FORMAT]

# Maximum number of characters on a single line.
Expand All @@ -34,8 +19,8 @@ disable=
too-many-instance-attributes,
global-statement,
too-many-arguments,
unused-argument,
too-few-public-methods
too-few-public-methods,
fixme

[STRING]

Expand Down
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# UC Integration API Python wrapper Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

_Changes in the next release_

### Added
- Type information
- Simple example and initial developer documentation

### Fixed
- mDNS service publishing announces local hostname.
- ENV var handling: `UC_INTEGRATION_INTERFACE` and `UC_INTEGRATION_HTTP_PORT` are optional (#2, #3)
- config_dir_path is always set

### Changed
- driver setup process
- entity command handler
- don't expose AsyncIOEventEmitter for event callbacks
- invalid names in public classes
- logging configuration, configuration must be done in client code


---
72 changes: 72 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Contributing

First off, thanks for taking the time to contribute!

Found a bug, typo, missing feature or a description that doesn't make sense or needs clarification?
Great, please let us know!

### Bug Reports :bug:

If you find a bug, please search for it first in the [Issues](https://github.com/unfoldedcircle/integration-python-library/issues),
and if it isn't already tracked, [create a new issue](https://github.com/unfoldedcircle/integration-python-library/issues/new).

### New Features :bulb:

If you'd like to see or add new functionality to the library, describe the problem you want to solve in a
[new Issue](https://github.com/unfoldedcircle/integration-python-library/issues/new).

### Pull Requests

**Any pull request needs to be reviewed and approved by the Unfolded Circle development team.**

We love contributions from everyone.

⚠️ If you plan to make substantial changes, we kindly ask you, that you please reach out to us first.
Either by opening a feature request describing your proposed changes before submitting code, or by contacting us on
one of the other [feedback channels](#feedback-speech_balloon).

Since this library is being used in integration drivers running on the embedded Remote Two device,
we have to make sure it remains compatible with the embedded runtime environment and runs smoothly.

Submitting pull requests for typos, formatting issues etc. are happily accepted and usually approved relatively quick.

With that out of the way, here's the process of creating a pull request and making sure it passes the automated tests:

### Contributing Code :bulb:

1. Fork the repo.

2. Make your changes or enhancements (preferably on a feature-branch).

Contributed code must be licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0/),
or a compatible license, if existing parts of other projects are reused (e.g. MIT licensed code).
It is required to add a boilerplate copyright notice to the top of each file:

```
"""
{fileheader}
:copyright: (c) {year} {person OR org} <{email}>
:license: MPL-2.0, see LICENSE for more details.
"""
```
3. Make sure your changes follow the project's code style and the lints pass described in [Code Style](docs/code_guidelines.md).
4. Push to your fork.
5. Submit a pull request.
At this point we will review the PR and give constructive feedback.
This is a time for discussion and improvements, and making the necessary changes will be required before we can
merge the contribution.
### Feedback :speech_balloon:
There are a few different ways to provide feedback:
- [Create a new issue](https://github.com/unfoldedcircle/integration-python-library/issues/new)
- [Reach out to us on Twitter](https://twitter.com/unfoldedcircle)
- [Visit our community forum](http://unfolded.community/)
- [Chat with us in our Discord channel](http://unfolded.chat/)
- [Send us a message on our website](https://unfoldedcircle.com/contact)
60 changes: 49 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,57 @@
# Python API wrapper for the UC Integration API

This is a Python library that can be used for Python based integrations. It wraps the UC Integration API.
This library simplifies writing Python based integrations for the [Unfolded Circle Remote Two](https://www.unfoldedcircle.com/)
by wrapping the [WebSocket Integration API](https://github.com/unfoldedcircle/core-api/tree/main/integration-api).

It's a pre-alpha release. Missing features will be added continuously. Based on the NodeJS implementation.
It's a pre-alpha release (in our eyes). Missing features will be added continuously.
Based on our [Node.js integration library](https://github.com/unfoldedcircle/integration-node-library).

Not supported:
❗️**Attention:**
> This is our first Python project, and we don't see ourselves as Python professionals.
> Therefore, the library is most likely not yet that Pythonic!
> We are still learning and value your feedback on how to improve it :-)
- secure WebSocket
Not yet supported:

Requires Python 3.10 or newer
- Secure WebSocket
- Token based authentication

---
Requirements:
- Python 3.10 or newer

### Local testing:
```console
python3 setup.py bdist_wheel
pip3 install dist/ucapi-$VERSION-py3-none-any.whl
```
## Usage

See [examples directory](examples) for a minimal integration driver example.

More examples will be published.

### Environment Variables

Certain features can be configured by environment variables:

| Variable | Values | Description |
|--------------------------|------------------|----------------------------------------------------------------------------------------------------------------------|
| UC_CONFIG_HOME | _directory path_ | Configuration directory to save the user configuration from the driver setup.<br>Default: $HOME or current directory |
| UC_INTEGRATION_INTERFACE | _address_ | Listening interface for WebSocket server.<br>Default: `0.0.0.0` |
| UC_INTEGRATION_HTTP_PORT | _number_ | WebSocket listening port.<br>Default: `port` field in driver metadata json file, if not specified: `9090` |
| UC_MDNS_LOCAL_HOSTNAME | _hostname_ | Published local hostname in mDNS service announcement.<br>Default: _short hostname_ with `.local` domain. |
| UC_DISABLE_MDNS_PUBLISH | `true` / `false` | Disables mDNS service advertisement.<br>Default: `false` |

## Versioning

We use [SemVer](http://semver.org/) for versioning. For the versions available, see the
[tags and releases on this repository](https://github.com/unfoldedcircle/integration-python-library/releases).

## Changelog

The major changes found in each new release are listed in the [changelog](CHANGELOG.md) and
under the GitHub [releases](https://github.com/unfoldedcircle/integration-python-library/releases).

## Contributions

Please read our [contribution guidelines](./CONTRIBUTING.md) before opening a pull request.

## License

This project is licensed under the [**Mozilla Public License 2.0**](https://choosealicense.com/licenses/mpl-2.0/).
See the [LICENSE](LICENSE) file for details.
63 changes: 63 additions & 0 deletions docs/code_guidelines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Code Style

This project uses the [PEP 8 – Style Guide for Python Code](https://peps.python.org/pep-0008/) as coding convention, with the
following customization:

- Code line length: 120
- Use double quotes as default (don't mix and match for simple quoting, checked with pylint).

## Tooling

Install all code linting tools:

```shell
pip3 install -r test-requirements.txt
```

### Linting

```shell
python -m pylint ucapi
```

- The tool is configured in `.pylintrc`.

Linting integration in PyCharm/IntelliJ IDEA:
1. Install plugin [Pylint](https://plugins.jetbrains.com/plugin/11084-pylint)
2. Open Pylint window and run a scan: `Check Module` or `Check Current File`

### Sort Imports

Import statements must be sorted with [isort](https://pycqa.github.io/isort/):

```shell
python -m isort ucapi/.
```

- The tool is configured in `pyproject.toml` to use the `black` code-formatting profile.

### Format Code

Source code is formatted with the [Black code formatting tool](https://github.com/psf/black):

```shell
python -m black ucapi --line-length 120
```

PyCharm/IntelliJ IDEA integration:
1. Go to `Preferences or Settings -> Tools -> Black`
2. Configure:
- Python interpreter
- Use Black formatter: `On code reformat` & optionally `On save`
- Arguments: `--line-length 120`

## Verify

The following tests are run as GitHub action for each push on the main branch and for pull requests.
They can also be run anytime on a local developer machine:
```shell
python -m pylint ucapi
python -m flake8 ucapi --count --show-source --statistics
python -m isort ucapi/. --check --verbose
python -m black ucapi --check --verbose --line-length 120
```
11 changes: 11 additions & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Development Setup

This library requires Python 3.10 or newer.

## Local installation

```shell
python3 setup.py bdist_wheel
pip3 install --force-reinstall dist/ucapi-$VERSION-py3-none-any.whl
```

18 changes: 18 additions & 0 deletions examples/hello_integration.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"driver_id": "hello_integration",
"version": "0.0.1",
"min_core_api": "0.20.0",
"name": { "en": "Hello Python integration" },
"icon": "uc:integration",
"description": {
"en": "Minimal Python integration driver example."
},
"port": 9080,
"developer": {
"name": "Unfolded Circle ApS",
"email": "[email protected]",
"url": "https://www.unfoldedcircle.com"
},
"home_page": "https://www.unfoldedcircle.com",
"release_date": "2023-10-30"
}
43 changes: 43 additions & 0 deletions examples/hello_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Hello world integration example. Bare minimum of an integration driver."""
import asyncio
import logging
from typing import Any

import ucapi

loop = asyncio.get_event_loop()
api = ucapi.IntegrationAPI(loop)


async def cmd_handler(entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None) -> ucapi.StatusCodes:
"""
Push button command handler.
Called by the integration-API if a command is sent to a configured button-entity.
:param entity: button entity
:param cmd_id: command
:param _params: optional command parameters
:return: status of the command
"""
print(f"Got {entity.id} command request: {cmd_id}")

return ucapi.StatusCodes.OK


if __name__ == "__main__":
logging.basicConfig()

button = ucapi.Button(
"button1",
"Push the button",
cmd_handler=cmd_handler,
)
api.available_entities.add(button)

# We are ready all the time! Otherwise, use @api.listens_to(ucapi.Events.CONNECT) & DISCONNECT
api.set_device_state(ucapi.DeviceStates.CONNECTED)

loop.run_until_complete(api.init("hello_integration.json"))
loop.run_forever()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "ucapi"
version = "0.0.11"
version = "0.1.0"
authors = [
{name = "Unfolded Circle ApS", email = "[email protected]"}
]
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

PACKAGE_NAME = "ucapi"
HERE = path.abspath(path.dirname(__file__))
VERSION = "0.0.11"
VERSION = "0.1.0"

with open(path.join(HERE, "README.md"), encoding="utf-8") as f:
long_description = f.read()
Expand Down
35 changes: 34 additions & 1 deletion ucapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,38 @@
Integration driver library for Remote Two.
:copyright: (c) 2023 by Unfolded Circle ApS.
:license: MPL 2.0, see LICENSE for more details.
:license: MPL-2.0, see LICENSE for more details.
"""

# Set default logging handler to avoid "No handler found" warnings.
import logging # isort:skip

from .api_definitions import ( # isort:skip # noqa: F401
DeviceStates,
DriverSetupRequest,
Events,
IntegrationSetupError,
RequestUserConfirmation,
RequestUserInput,
SetupAction,
SetupComplete,
SetupDriver,
SetupError,
StatusCodes,
UserConfirmationResponse,
UserDataResponse,
)
from .entity import Entity, EntityTypes # isort:skip # noqa: F401
from .entities import Entities # isort:skip # noqa: F401
from .api import IntegrationAPI # isort:skip # noqa: F401

# Entity types
from .button import Button # noqa: F401
from .climate import Climate # noqa: F401
from .cover import Cover # noqa: F401
from .light import Light # noqa: F401
from .media_player import MediaPlayer # noqa: F401
from .sensor import Sensor # noqa: F401
from .switch import Switch # noqa: F401

logging.getLogger(__name__).addHandler(logging.NullHandler())
Loading

0 comments on commit b2083c4

Please sign in to comment.