Skip to content

Commit

Permalink
Updated beoremote-halo CLI (#16)
Browse files Browse the repository at this point in the history
Updated beoremote-halo CLI
- Better handlig for CTRL+C on windows
- Using queues to communicate between processes in cli demo
- Changed arguments for demo/listen to use --ip or --serial instead of --hostname
- Updated animation in readme to reflect new cli usage
- beoremote-halo now has logging default set to DEBUG
- Added downloads per month badge
- Bumped beoremote package to 1.0.2
  • Loading branch information
anedergaard committed Jan 14, 2022
1 parent 22b783f commit e8c7f0d
Show file tree
Hide file tree
Showing 16 changed files with 554 additions and 218 deletions.
15 changes: 0 additions & 15 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,3 @@ jobs:
with:
name: beoremote-halo
path: dist/*halo*

verify:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v2
with:
name: beoremote-halo
- name: Verify package - Connect Demo
run: |
wget https://github.com/vi/websocat/releases/download/v1.9.0/websocat_linux32_nossl && chmod +x websocat_linux32_nossl
python3 -m venv env && source env/bin/activate
python -m pip install beoremote_halo-*-py3-none-any.whl
timeout --preserve-status 5 echo '{"event":{"type":"system","state":"active"}}' | ./websocat_linux32_nossl -s 8080 --one-message --oneshot &
timeout --preserve-status 5 beoremote-halo demo --hostname localhost
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ repos:
- id: pylint
args:
- -j0
- --disable=C0209,C0116,C0115,E0401,R0801,C0103,R0903,W1202
- --disable=C0209,C0116,C0115,E0401,R0801,C0103,R0903,W1202,W0703
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![build](https://github.com/bang-olufsen/beoremote-halo/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/bang-olufsen/beoremote-halo/actions/workflows/ci.yaml)
[![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
[![Version](https://img.shields.io/pypi/v/beoremote-halo?color=g)](https://pypi.org/project/beoremote-halo)
[![Downloads](https://img.shields.io/pypi/dm/beoremote-halo)](https://pypi.org/project/beoremote-halo)


The [Beoremote Halo](https://www.bang-olufsen.com/en/us/accessories/beoremote-halo) Open API is an open source async API that allows you to interact with a Beoremote Halo from a home automation system.
Expand Down Expand Up @@ -36,7 +37,7 @@ def on_system_event(client: Halo, event: SystemEvent):
print(event)


remote = Halo("BeoremoteHalo-XXXXXXXX.local")
remote = Halo("192.168.1.57")
remote.set_on_system_event_callback(on_system_event)
remote.connect()
```
Expand All @@ -46,7 +47,15 @@ Use the `beoremote-halo` CLI tool to discover and then run a demo by connecting

<img src="https://github.com/bang-olufsen/beoremote-halo/raw/main/docs/images/beoremote-halo-demo.gif">

In the above demo the CLI is used to locate Beoremote-Halo on the network. Afterwards the CLI demo is run by passing the host name of a Beoremote-Halo. The demo configures the Beoremote Halo and reacts to events received from Halo. The callbacks each handle a specific type of [event](https://bang-olufsen.github.io/beoremote-halo/#message-event).
In the above demo the CLI is used to locate Beoremote Halo on the network.
```
beoremote-halo scan
```
Afterwards the CLI demo is run by passing the serial number of the discovered Beoremote Halo.
```
beoremote-halo demo --serial xxxxxxxx
```
The demo configures the Beoremote Halo and reacts to events received from Halo. The callbacks each handle a specific type of [event](https://bang-olufsen.github.io/beoremote-halo/#message-event).

`on_system_event` is provided but unused in this example.

Expand Down
2 changes: 1 addition & 1 deletion beoremote/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@
SOFTWARE.
"""

__version__ = "1.0.1"
__version__ = "1.0.2"
from beoremote.configuration import Configuration
from beoremote.halo import Halo
2 changes: 1 addition & 1 deletion beoremote/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
SOFTWARE.
"""

__version__ = "1.0.0"
__version__ = "1.0.1"
20 changes: 15 additions & 5 deletions beoremote/cli/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
SOFTWARE.
"""
import logging
import queue
import signal
import sys
import time
from multiprocessing import Process, Semaphore

Expand Down Expand Up @@ -104,10 +107,9 @@ def on_wheel_event(client: Halo, event: WheelEvent):
client.send(update)


def oven_timer_function(client: Halo, button_id: str, content: Text):
def oven_timer_function(send_queue: queue.Queue, button_id: str, content: Text):
"""
:param client: Client handle to communicate with Beoremote Halo
:param send_queue: queue handle to communicate with Beoremote Halo
:param button_id: Uuid of button to update
:param content: Text content of the button to update
"""
Expand All @@ -128,7 +130,7 @@ def oven_timer_function(client: Halo, button_id: str, content: Text):
content=content,
)
)
client.send(update)
send_queue.put(str(update))
time.sleep(1)
seconds = 59
except KeyboardInterrupt:
Expand Down Expand Up @@ -167,7 +169,7 @@ def on_button_event(client: Halo, event: ButtonEvent):
else:
proc = Process(
target=oven_timer_function,
args=(client, event.id, button.content),
args=(client.sendQueue, event.id, button.content),
)
oven_timer["running"] = True
processes.append(proc)
Expand All @@ -182,6 +184,14 @@ def backend(hostname: str):
remote.set_on_button_event_callback(on_button_event)
remote.set_on_wheel_event_callback(on_wheel_event)
remote.set_configuration(config)
remote.set_on_connected(lambda: print("Demo connected to: {}".format(hostname)))

def signal_handler(sig, frame):
del sig, frame
remote.close_connection()
sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)

remote.connect()
for process in processes:
Expand Down
149 changes: 130 additions & 19 deletions beoremote/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,84 @@
"""

import logging
import re
import socket
import sys

import click
from click import Option, UsageError

import beoremote
from beoremote import halo
from beoremote.cli import backend, discover
from beoremote.cli import __version__, backend, discover
from beoremote.configuration import Configuration


class MutuallyExclusiveOptions(Option):
def __init__(self, *args, **kwargs):
self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", []))
help_string = kwargs.get("help", "")
if self.mutually_exclusive:
ex_str = ", ".join(self.mutually_exclusive)
kwargs["help"] = help_string + (
" - NOTE: This argument is mutually exclusive with"
" arguments: [" + ex_str + "]"
)
super().__init__(*args, **kwargs)

def handle_parse_result(self, ctx, opts, args):
if self.mutually_exclusive.intersection(opts) and self.name in opts:
raise UsageError(
"Illegal usage: `{}` is mutually exclusive with "
"arguments `{}`.".format(self.name, ", ".join(self.mutually_exclusive))
)

return super().handle_parse_result(ctx, opts, args)


def serial_to_ip(serial: str) -> str:
ip = None
if serial and re.match(R"\d{8}", serial):
hostname = "BeoremoteHalo-{}.local".format(serial)
try:
click.echo(
"Searching for Beoremote Halo with serial {} on the network".format(
serial
)
)
ip = socket.gethostbyname(hostname)
except socket.gaierror:
click.echo(
"Unable to locate serial {} on network, try specifying ip address".format(
serial
)
)
sys.exit(0)
else:
click.echo("{} is not a valid serial number".format(serial))
sys.exit(0)
return ip


@click.group()
@click.option(
"--log",
help="Set logging level, default INFO",
default="INFO",
type=click.Choice(
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False
default="DEBUG",
type=click.Choice(["DEBUG", "INFO"], case_sensitive=False),
)
@click.version_option(
prog_name="beoremote-halo",
version="beoremote-halo package version: {}"
"\nbeoremote-halo cli version: {}"
"\nAPI version: {}".format(
beoremote.__version__, __version__, Configuration.APIVersion
),
message="%(version)s",
)
@click.group()
def cli(log):
"""Cli provided by Bang & Olufsen a/s used to scan the network for Beoremote Halo on local
@click.pass_context
def cli(ctx, log):
"""cli provided by Bang & Olufsen a/s used to scan the network for Beoremote Halo on local
network
\b
Expand All @@ -55,9 +113,15 @@ def cli(log):
Please report bugs or issues at:
https://github.com/bang-olufsen/beoremote-halo/issues
\b
For command specific help message type:
beoremote-halo [command] --help
"""
del ctx
numeric_level = getattr(logging, log.upper(), None)
logging.basicConfig(format="%(levelname)-8s | %(message)s", level=numeric_level)
logging.basicConfig(
format="%(levelname)-5s | %(message)s",
level=numeric_level,
)


@cli.command(help="Scan the network for active Beoremote Halo.")
Expand All @@ -66,19 +130,66 @@ def scan():


@cli.command(help="Interactive Home Automation Demo")
@click.option("--hostname", required=True)
def demo(hostname):
backend.backend(hostname)
@click.option(
"--ip",
"ip",
help="ip address of Beoremote Halo",
type=str,
cls=MutuallyExclusiveOptions,
mutually_exclusive=["serial"],
)
@click.option(
"--serial",
"serial",
type=str,
help="Serial number of Beoremote Halo",
cls=MutuallyExclusiveOptions,
mutually_exclusive=["ip"],
)
def demo(ip, serial):
if not ip and not serial:
click.echo(demo.get_help(click.get_current_context()))
sys.exit(0)

if ip and not re.match(R"\d+\.\d+\.\d+\.\d+", ip):
click.echo("Incorrect IPv4 format: {}".format(ip))
sys.exit(0)

if serial:
ip = serial_to_ip(serial)

backend.backend(ip)


@cli.command(help="Connect to a Halo and listen for events")
@click.option("--hostname", required=True)
def listen(hostname):
remote = halo.Halo(hostname)
remote.connect()
@click.option(
"--ip",
"ip",
help="ip address of Beoremote Halo",
type=str,
cls=MutuallyExclusiveOptions,
mutually_exclusive=["serial"],
)
@click.option(
"--serial",
"serial",
type=str,
help="Serial number of Beoremote Halo",
cls=MutuallyExclusiveOptions,
mutually_exclusive=["ip"],
)
def listen(ip, serial):
if not ip and not serial:
click.echo(demo.get_help(click.get_current_context()))
sys.exit(0)

if ip and not re.match(R"\d+\.\d+\.\d+\.\d+", ip):
click.echo("Incorrect IPv4 format: {}".format(ip))
sys.exit(0)

@cli.command(help="Version")
def version():
print("beoremote-halo package version: {}".format(beoremote.__version__))
print("API version: {}".format(Configuration.APIVersion))
if serial:
ip = serial_to_ip(serial)

remote = halo.Halo(ip)
remote.set_on_connected(lambda: print("Listening on events from: {}".format(ip)))
remote.connect()
Loading

0 comments on commit e8c7f0d

Please sign in to comment.