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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// For format details, see https://aka.ms/devcontainer.json.
{
"name": "Python 3",
"image": "mcr.microsoft.com/devcontainers/python:1-3-bookworm",
"runArgs": [
// Use host network for discovery
"--network=host"
],
"features": {
"ghcr.io/va-h/devcontainers-features/uv:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"charliermarsh.ruff",
"tamasfe.even-better-toml",
]
}
}
}
18 changes: 8 additions & 10 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,28 @@ name: Python lint and packages

on:
push:
branches: [ master ]
branches: [ master, dev ]
pull_request:
branches: [ master ]
branches: [ master, dev ]

jobs:
build:

runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ 3.9, "3.10", "3.11" ]
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python setup.py install
- uses: astral-sh/ruff-action@v3

- name: Black Code Formatter
uses: jpetrucciani/black-check@22.12.0
- uses: astral-sh/ruff-action@v3
with:
args: "format --check --diff"
1 change: 0 additions & 1 deletion MANIFEST.in

This file was deleted.

27 changes: 22 additions & 5 deletions dingz/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
"""Base details for the dingz Python bindings."""

import asyncio
import socket
from typing import Any, Mapping, Optional

import aiohttp
import sys

if sys.version_info >= (3, 11):
import asyncio as async_timeout
else:
import async_timeout

from .constants import TIMEOUT, USER_AGENT, CONTENT_TYPE_JSON, CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN
from .constants import (
TIMEOUT,
USER_AGENT,
CONTENT_TYPE_JSON,
CONTENT_TYPE,
CONTENT_TYPE_TEXT_PLAIN,
)
from .exceptions import DingzConnectionError


Expand Down Expand Up @@ -38,14 +46,23 @@ async def make_call(
self._close_session = True

try:
with async_timeout.timeout(TIMEOUT):
async with async_timeout.timeout(TIMEOUT):
response = await self._session.request(
method, uri, data=data, json=json_data, params=parameters, headers=headers,
method,
uri,
data=data,
json=json_data,
params=parameters,
headers=headers,
)
except asyncio.TimeoutError as exception:
raise DingzConnectionError("Timeout occurred while connecting to dingz unit") from exception
raise DingzConnectionError(
"Timeout occurred while connecting to dingz unit"
) from exception
except (aiohttp.ClientError, socket.gaierror) as exception:
raise DingzConnectionError("Error occurred while communicating with dingz") from exception
raise DingzConnectionError(
"Error occurred while communicating with dingz"
) from exception

if CONTENT_TYPE_JSON in response.headers.get(CONTENT_TYPE, ""):
response_json = await response.json()
Expand Down
1 change: 1 addition & 0 deletions dingz/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Command-line interface to interact with dingz devices."""

import asyncio
from functools import wraps

Expand Down
5 changes: 3 additions & 2 deletions dingz/constants.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Constants used by the Python API for interacting with dingz units."""
import pkg_resources

import importlib.metadata

try:
__version__ = pkg_resources.get_distribution("setuptools").version
__version__ = importlib.metadata.version("dingz")
except Exception:
__version__ = "unknown"

Expand Down
40 changes: 23 additions & 17 deletions dingz/dimmer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ async def toggle(self, brightness_pct=100):
await self.operate_light(action, brightness_pct)

async def turn_on(self, brightness_pct=100):
""" Turn light on.
"""Turn light on.
:param brightness_pct: brightness in percent, or None.
"""
await self.operate_light("on", brightness_pct)

async def turn_off(self):
""" Rurn light off."""
"""Rurn light off."""
await self.operate_light("off")

VALID_OPERATIONS = ('on', 'off')
VALID_OPERATIONS = ("on", "off")

async def operate_light(self, action, brightness_pct=None):
"""
Expand All @@ -46,14 +46,20 @@ async def operate_light(self, action, brightness_pct=None):
:return:
"""
if action not in Dimmer.VALID_OPERATIONS:
raise ValueError("invalid action %s, expected one of %s" %
(repr(action), repr(Dimmer.VALID_OPERATIONS)))
raise ValueError(
"invalid action %s, expected one of %s"
% (repr(action), repr(Dimmer.VALID_OPERATIONS))
)

if brightness_pct is not None and (brightness_pct > 100 or brightness_pct < 0):
raise ValueError("invalid brightness_pct %s, expected value between 0 and 100" %
(repr(brightness_pct)))

url = URL(self.dingz.uri).join(URL("%s/%s/%s" % (DIMMER, self.index_relative, action)))
raise ValueError(
"invalid brightness_pct %s, expected value between 0 and 100"
% (repr(brightness_pct))
)

url = URL(self.dingz.uri).join(
URL("%s/%s/%s" % (DIMMER, self.index_relative, action))
)
params = {}
if brightness_pct is not None:
params["value"] = str(brightness_pct)
Expand All @@ -73,18 +79,18 @@ def _consume_state(self, state_details):
:param state_details:
:return:
"""
assert self.absolute_index == state_details['index']['absolute']
assert self.absolute_index == state_details["index"]["absolute"]
self.seen_state = True
self.index_relative = state_details['index']['relative']
self.on = state_details['on']
self.brightness_pct = state_details['output']
self.index_relative = state_details["index"]["relative"]
self.on = state_details["on"]
self.brightness_pct = state_details["output"]

def _consume_config(self, config):
# "output": "halogen", "name": "Dimmable 3", "feedback": null, "feedback_intensity": 10
self.output = config['output']
self.enabled = config['output'] != 'not_connected'
self.dimmable = config['output'] != 'non_dimmable'
self.name = config['name']
self.output = config["output"]
self.enabled = config["output"] != "not_connected"
self.dimmable = config["output"] != "non_dimmable"
self.name = config["name"]


class DimmerRegistry(BaseRegistry[Dimmer]):
Expand Down
14 changes: 8 additions & 6 deletions dingz/dingz.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Python client/wrapper to interact with dingz devices."""

import logging

import aiohttp
Expand All @@ -25,7 +26,8 @@
STATE,
SYSTEM_CONFIG,
BLIND_CONFIGURATION,
DIMMER_CONFIGURATION, SHADE,
DIMMER_CONFIGURATION,
SHADE,
)
from .dimmer import DimmerRegistry
from .shade import ShadeRegistry
Expand Down Expand Up @@ -154,9 +156,9 @@ async def get_state(self) -> None:
# first fetch the device state
url = URL(self.uri).join(URL(STATE))
device_state = await make_call(self, uri=url)
self._consume_sensor_state(device_state['sensors'])
self._dimmers._consume_dimmer_state(device_state['dimmers'])
self._shades._consume_device_state(device_state['blinds'])
self._consume_sensor_state(device_state["sensors"])
self._dimmers._consume_dimmer_state(device_state["dimmers"])
self._shades._consume_device_state(device_state["blinds"])
self._state = device_state

if len(self._shades.all()) > 0:
Expand All @@ -169,13 +171,13 @@ async def get_blind_config(self) -> None:
"""Get the configuration of the blinds."""
url = URL(self.uri).join(URL(BLIND_CONFIGURATION))
response = await make_call(self, uri=url)
self._blind_config = response['blinds']
self._blind_config = response["blinds"]

async def get_dimmer_config(self) -> None:
"""Get the configuration of the dimmer/lights."""
url = URL(self.uri).join(URL(DIMMER_CONFIGURATION))
response = await make_call(self, uri=url)
self._dimmer_config = response['dimmers']
self._dimmer_config = response["dimmers"]

async def get_system_config(self) -> None:
"""Get the system configuration of a dingz."""
Expand Down
8 changes: 6 additions & 2 deletions dingz/discovery.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Discover dingz devices in a network."""

import asyncio
import logging
from typing import Optional, List
Expand Down Expand Up @@ -66,7 +67,7 @@ class DiscoveryProtocol(asyncio.DatagramProtocol):
"""Representation of the discovery protocol."""

def __init__(self, registry: DeviceRegistry):
""""Initialize the discovery protocol."""
""" "Initialize the discovery protocol."""
super().__init__()
self.registry = registry

Expand Down Expand Up @@ -108,6 +109,9 @@ async def discover_dingz_devices(timeout: int = 7) -> List[DiscoveredDevice]:
devices = registry.devices()
for device in devices:
_LOGGER.debug(
"Discovered dingz %s (%s) (MAC address: %s)", device.host, device.type, device.mac
"Discovered dingz %s (%s) (MAC address: %s)",
device.host,
device.type,
device.mac,
)
return devices
3 changes: 2 additions & 1 deletion dingz/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
def organize_by_absolute_index(items):
result = []
for item in items:
result.append((item['index']['absolute'], item))
result.append((item["index"]["absolute"], item))

return result

Expand All @@ -21,6 +21,7 @@ class BaseRegistry(Generic[T]):
Note, the absolute index remains the same, regardless of deactivation by the dip switch.
So shade 1 is always the shade operated by output 2&3, even if it is the only shade.
"""

_registry: Dict[int, T]

def __init__(self, factory):
Expand Down
31 changes: 14 additions & 17 deletions dingz/shade.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

from dingz import make_call

from .constants import (
SHADE
)
from .constants import SHADE
from .registry import BaseRegistry, organize_by_absolute_index


Expand All @@ -19,7 +17,7 @@ def __init__(self, absolute_index, dingz):
self.lamella = None

def _consume_config(self, config):
self.name = config['name']
self.name = config["name"]

def _consume_device_state(self, state_details):
"""
Expand All @@ -29,12 +27,12 @@ def _consume_device_state(self, state_details):
'index': { 'relative': 0, 'absolute': 0 }
}
"""
assert self.absolute_index == state_details['index']['absolute']
self.index_relative = state_details['index']['relative']
assert self.absolute_index == state_details["index"]["absolute"]
self.index_relative = state_details["index"]["relative"]
self.seen_state = True
self.moving = state_details['moving']
self.position = state_details['position']
self.lamella = state_details['lamella']
self.moving = state_details["moving"]
self.position = state_details["position"]
self.lamella = state_details["lamella"]

def _consume_shade_state(self, state_details):
"""
Expand All @@ -48,9 +46,9 @@ def _consume_shade_state(self, state_details):
"index": { "relative": 1, "absolute": 1 }
}
"""
assert self.absolute_index == state_details['index']['absolute']
self.position = state_details['current']['blind']
self.lamella = state_details['current']['lamella']
assert self.absolute_index == state_details["index"]["absolute"]
self.position = state_details["current"]["blind"]
self.lamella = state_details["current"]["lamella"]

async def operate_shade(self, blind=None, lamella=None) -> None:
"""
Expand Down Expand Up @@ -112,7 +110,9 @@ async def lamella_stop(self) -> None:

async def shade_command(self, verb):
"""Create a command for the shade."""
url = URL(self.dingz.uri).join(URL("%s/%s/%s" % (SHADE, self.absolute_index, verb)))
url = URL(self.dingz.uri).join(
URL("%s/%s/%s" % (SHADE, self.absolute_index, verb))
)
await make_call(self.dingz, uri=url, method="POST")

def current_blind_level(self):
Expand All @@ -126,10 +126,7 @@ def current_lamella_level(self):
def is_shade_closed(self):
"""Get the closed state of a shade."""
# When closed, we care if the lamellas are opened or not
return (
self.current_blind_level() == 0
and self.current_lamella_level() == 0
)
return self.current_blind_level() == 0 and self.current_lamella_level() == 0

def is_shade_opened(self):
"""Get the open state of a shade."""
Expand Down
5 changes: 2 additions & 3 deletions example.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Sample code to use the wrapper for interacting with the dingz device."""

import asyncio
import logging

Expand All @@ -22,7 +23,6 @@ async def main():

# Work with one dingz unit
async with Dingz(IP_ADDRESS) as dingz:

# Collect the data of the current state
await dingz.get_device_info()
print("Device details:", dingz.device_details)
Expand Down Expand Up @@ -63,5 +63,4 @@ async def main():

if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
asyncio.run(main())
Loading