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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,6 @@ dmypy.json

test.py

.github/linters/pyproject.toml
.github/linters/pyproject.toml

uv.lock
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ uv pip install -e .

## Usage

Easy mode:

```sh
uv run main.py --ip x.x.x.x --auth-key "ABCD1234WXYZ"
```

Roll your own;

```python
import asyncio
from xcomfort import Bridge
Expand Down Expand Up @@ -46,7 +54,6 @@ async def main():
await runTask

asyncio.run(main())

```

## Development
Expand All @@ -56,24 +63,17 @@ asyncio.run(main())
You can run the tests using uvx without any local dependency management:

```bash
# Run tests with uvx (no local installation needed)
uvx --with aiohttp --with rx --with pycryptodome --with pytest-asyncio pytest tests/ -v

# Or use the convenience script
./run_tests.sh

# Or install dev dependencies and run tests locally
uv pip install -e ".[dev]"
pytest
```

To run Github workflows locally

Install [act](https://nektosact.com/installation/index.html) and run flows locally using `act`.

### Dependencies

The project includes the following dependencies:

- `aiohttp` - For async HTTP client functionality
- `rx` - For reactive programming
- `pycryptodome` - For cryptographic operations

## To run Github workflows locally

Install [act](https://nektosact.com/installation/index.html) and run flows locally using `act`.
Empty file.
57 changes: 57 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Example script demonstrating xComfort Bridge usage."""

import argparse
import asyncio
import logging

from xcomfort import Bridge

_LOGGER = logging.getLogger(__name__)


def observe_device(device):
"""Subscribe to device state changes and log them."""
device.state.subscribe(
lambda state: _LOGGER.info(
"Device state [%s] '%s': %s", device.device_id, device.name, state
)
)


async def main(ip: str, auth_key: str):
"""Run the main example demonstrating bridge connection and device observation."""
bridge = Bridge(ip, auth_key)

runTask = asyncio.create_task(bridge.run())

devices = await bridge.get_devices()

for device in devices.values():
observe_device(device)

# Wait 50 seconds. Try flipping the light switch manually while you wait
await asyncio.sleep(50)

# Turn off all the lights.
# for device in devices.values():
# await device.switch(False)
#
# await asyncio.sleep(5)

await bridge.close()
await runTask

if __name__ == "__main__":
# Configure debug logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

parser = argparse.ArgumentParser(description="Test xComfort Bridge connection")
parser.add_argument("--ip", required=True, help="IP address of the xComfort Bridge")
parser.add_argument("--auth-key", required=True, help="Authentication key for the xComfort Bridge")

args = parser.parse_args()

asyncio.run(main(args.ip, args.auth_key))
9 changes: 8 additions & 1 deletion run_tests.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
#!/bin/bash
# Script to run tests with uvx without local dependency management
# Script to run tests and linting with uvx without local dependency management

set -e # Exit on first error

curl -L -o ./.github/linters/pyproject.toml https://raw.githubusercontent.com/home-assistant/core/refs/heads/dev/pyproject.toml

uvx ruff check --config .github/linters/.ruff.toml main.py
uvx ruff check --config .github/linters/.ruff.toml xcomfort/
uvx --with aiohttp --with rx --with pycryptodome --with pytest-asyncio pytest tests/ -v
9 changes: 3 additions & 6 deletions xcomfort/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ def __init__(self, bridge, device_id, name, comp_id):

def handle_state(self, payload):
"""Handle RcTouch state updates."""
_LOGGER.debug("RcTouch %s: Received payload: %s", self.name, payload)
temperature = None
humidity = None
if "info" in payload:
Expand Down Expand Up @@ -350,12 +351,8 @@ def __init__(self, bridge, device_id, name, comp_id, payload):
self.is_on = bool(payload["curstate"])

# Subscribe to component state updates if this is a multisensor
comp = bridge._comps.get(comp_id) # noqa: SLF001
if comp is not None and comp.comp_type in (
ComponentTypes.PUSH_BUTTON_MULTI_SENSOR_1_CHANNEL,
ComponentTypes.PUSH_BUTTON_MULTI_SENSOR_2_CHANNEL,
ComponentTypes.PUSH_BUTTON_MULTI_SENSOR_4_CHANNEL,
):
if self.has_sensors:
comp = self.bridge._comps.get(self.comp_id) # noqa: SLF001
comp.state.subscribe(lambda _: self._on_component_update())
# Find and subscribe to companion sensor device
self._find_and_subscribe_sensor_device()
Expand Down
3 changes: 3 additions & 0 deletions xcomfort/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def __init__(self, bridge, room_id, name: str):

def handle_state(self, payload):
"""Handle room state updates."""
_LOGGER.debug("Room %s: Received payload: %s", self.name, payload)
old_state = self.state.value

if old_state is not None:
Expand All @@ -89,6 +90,7 @@ def handle_state(self, payload):
humidity = payload.get("humidity", None)
power = payload.get("power", 0.0)

mode = None
if "currentMode" in payload: # When handling from _SET_ALL_DATA
mode = RctMode(payload.get("currentMode", None))
if "mode" in payload: # When handling from _SET_STATE_INFO
Expand All @@ -101,6 +103,7 @@ def handle_state(self, payload):
self.modesetpoints[RctMode(mode_data["mode"])] = float(mode_data["value"])
_LOGGER.debug("Room %s: Loaded mode setpoints: %s", self.name, self.modesetpoints)

currentstate = None
if "state" in payload:
currentstate = RctState(payload.get("state", None))

Expand Down
Loading