Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplement CLI using Click #150

Closed
wants to merge 6 commits into from
Closed
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
60 changes: 60 additions & 0 deletions LGTV/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import logging
import sys
from typing import List, Optional

import click

from LGTV import cli_static
from LGTV.conf import read_config


class CLI(click.Group):
def list_commands(self, ctx: click.Context) -> List[str]:
commands = []

for command in cli_static.__all__:
commands.append(command.replace("_", "-"))

return sorted(commands)

def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Command]:
fun_name = cmd_name.replace("-", "_")
return getattr(cli_static, fun_name, None)


@click.group(cls=CLI)
@click.option("-d", "--debug", is_flag=True, help="Enable debug output.")
@click.option("-n", "--name", help="Name of the TV to manage.")
@click.pass_context
def cli(ctx: click.Context, debug: bool = False, name: Optional[str] = None) -> None:
"""Command line webOS remote for LG TVs."""
logging.basicConfig(level=logging.DEBUG if debug else logging.INFO)

config_path, full_config = read_config()
name = name or full_config.get("_default")

possible_tvs = list(full_config.keys())
try:
possible_tvs.remove("_default")
except ValueError:
pass

if name and name not in possible_tvs:
click.secho(
f"No entry with the name '{name}' was found in the configuration at {config_path}. Names found: {', '.join(possible_tvs)}",
fg="red",
err=True,
)
sys.exit(1)

tv_config = full_config.get(name, {})
ctx.obj = {
"config_path": config_path,
"full_config": full_config,
"tv_name": name,
"tv_config": tv_config,
}


if __name__ == "__main__":
cli()
85 changes: 85 additions & 0 deletions LGTV/cli_static.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import json
import sys
from time import sleep
from typing import Dict, Tuple

import click

from LGTV.auth import LGTVAuth
from LGTV.conf import write_config
from LGTV.cursor import LGTVCursor
from LGTV.remote import LGTVRemote
from LGTV.scan import LGTVScan


__all__ = ["scan", "auth", "set_default", "send_button", "on"]


@click.command
def scan() -> None:
"""Scan the local network for LG TVs."""
results = LGTVScan()

if len(results) > 0:
click.echo(json.dumps({"result": "ok", "count": len(results), "list": results}))
sys.exit(0)
else:
click.echo(json.dumps({"result": "failed", "count": len(results)}))
sys.exit(1)


@click.command
@click.argument("host")
@click.argument("name")
@click.option("-s", "--ssl", is_flag=True, help="Connect to TV using SSL.")
@click.pass_obj
def auth(obj: Dict, host: str, name: str, ssl: bool = False) -> None:
"""Connect to a new TV."""
if name.startswith("_"):
click.secho(
"TV names are not allowed to start with an underscore", fg="red", err=True
)
sys.exit(1)

ws = LGTVAuth(name, host, ssl=ssl)
ws.connect()
ws.run_forever()
sleep(1)
config = obj["full_config"]
config[name] = ws.serialise()
write_config(obj["config_path"], config)
click.echo(f"Wrote config file: {obj['config_path']}")


@click.command
@click.argument("name")
@click.pass_obj
def set_default(obj: Dict, name: str) -> None:
"""Change the default TV to interact with."""
config = obj["full_config"]
if name == "_default" or name not in config:
click.secho("TV not found in config", fg="red", err=True)
sys.exit(1)

config["_default"] = name
write_config(obj["config_path"], config)
click.echo(f"Default TV set to '{name}'")


@click.command
@click.argument("buttons", nargs=-1)
@click.option("-s", "--ssl", is_flag=True, help="Connect to TV using SSL.")
@click.pass_obj
def send_button(obj: Dict, buttons: Tuple[str], ssl: bool = False) -> None:
"""Sends button presses from the remote."""
cursor = LGTVCursor(obj["tv_name"], **obj["tv_config"], ssl=ssl)
cursor.connect()
cursor.execute(buttons)


@click.command
@click.pass_obj
def on(obj: Dict) -> None:
"""Turn on TV using Wake-on-LAN."""
remote = LGTVRemote(obj["tv_name"], **obj["tv_config"])
remote.on()
41 changes: 41 additions & 0 deletions LGTV/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import json
import sys
from pathlib import Path
from typing import Dict, Tuple

# TODO: Should this be replaced with click.get_app_dir()?
search_paths = [
"/etc/lgtv/config.json",
"~/.lgtv/config.json",
"/opt/venvs/lgtv/config/config.json",
]


def read_config() -> Tuple[Path, Dict]:
# Check for existing config files
for path in map(Path, search_paths):
path = path.expanduser()
if path.exists():
with path.open() as fp:
return path, json.load(fp)

# Attempt to find place to write new config file
for path in map(Path, search_paths):
path = path.expanduser()

try:
path.parent.mkdir(parents=True, exist_ok=True)
return path, {}
except (FileExistsError, PermissionError):
pass

print(
"Cannot find suitable config path to write, create one in",
" or ".join(search_paths),
)
sys.exit(1)


def write_config(path: Path, config: Dict) -> None:
with path.open("w") as fp:
json.dump(config, fp)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ wakeonlan==1.1.6
ws4py==0.5.1
requests==2.31.0
getmac==0.9.2
click==8.1.7
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'ws4py',
'requests',
'getmac',
'click>=8.1.0',
],
data_files=[
('config', ['data/config.json'])
Expand Down