2020-07-27 21:08:48 +03:00
|
|
|
# Raspberry Pi Power Control
|
|
|
|
#
|
|
|
|
# Copyright (C) 2020 Jordan Ruthe <jordanruthe@gmail.com>
|
|
|
|
#
|
|
|
|
# This file may be distributed under the terms of the GNU GPLv3 license.
|
|
|
|
|
|
|
|
import logging
|
|
|
|
import os
|
2020-11-14 16:24:54 +03:00
|
|
|
import asyncio
|
2020-11-16 20:36:28 +03:00
|
|
|
import json
|
|
|
|
import struct
|
|
|
|
import socket
|
2020-11-15 19:08:25 +03:00
|
|
|
import gpiod
|
2020-07-30 01:18:54 +03:00
|
|
|
from tornado.ioloop import IOLoop
|
2020-11-16 20:36:28 +03:00
|
|
|
from tornado.iostream import IOStream
|
2020-07-30 01:18:54 +03:00
|
|
|
from tornado import gen
|
2020-11-21 00:44:16 +03:00
|
|
|
from tornado.httpclient import AsyncHTTPClient
|
|
|
|
from tornado.escape import json_decode
|
2020-07-27 21:08:48 +03:00
|
|
|
|
|
|
|
class PrinterPower:
|
2020-08-06 04:06:52 +03:00
|
|
|
def __init__(self, config):
|
|
|
|
self.server = config.get_server()
|
2021-01-06 22:39:33 +03:00
|
|
|
self.chip_factory = GpioChipFactory()
|
|
|
|
self.devices = {}
|
|
|
|
prefix_sections = config.get_prefix_sections("power")
|
2021-03-18 15:23:40 +03:00
|
|
|
logging.info(f"Power component loading devices: {prefix_sections}")
|
2021-01-06 22:39:33 +03:00
|
|
|
try:
|
|
|
|
for section in prefix_sections:
|
|
|
|
cfg = config[section]
|
|
|
|
dev_type = cfg.get("type")
|
|
|
|
if dev_type == "gpio":
|
|
|
|
dev = GpioDevice(cfg, self.chip_factory)
|
|
|
|
elif dev_type == "tplink_smartplug":
|
|
|
|
dev = TPLinkSmartPlug(cfg)
|
|
|
|
elif dev_type == "tasmota":
|
|
|
|
dev = Tasmota(cfg)
|
2021-02-07 13:02:09 +03:00
|
|
|
elif dev_type == "shelly":
|
|
|
|
dev = Shelly(cfg)
|
2021-03-12 02:54:16 +03:00
|
|
|
elif dev_type == "homeseer":
|
2021-03-18 15:28:32 +03:00
|
|
|
dev = HomeSeer(cfg)
|
2021-01-06 22:39:33 +03:00
|
|
|
else:
|
|
|
|
raise config.error(f"Unsupported Device Type: {dev_type}")
|
|
|
|
self.devices[dev.get_name()] = dev
|
|
|
|
except Exception:
|
|
|
|
self.chip_factory.close()
|
|
|
|
raise
|
|
|
|
|
2020-07-27 21:08:48 +03:00
|
|
|
self.server.register_endpoint(
|
2020-11-16 16:46:05 +03:00
|
|
|
"/machine/device_power/devices", ['GET'],
|
2020-07-27 21:08:48 +03:00
|
|
|
self._handle_list_devices)
|
|
|
|
self.server.register_endpoint(
|
2020-11-16 16:46:05 +03:00
|
|
|
"/machine/device_power/status", ['GET'],
|
2020-07-27 21:08:48 +03:00
|
|
|
self._handle_power_request)
|
|
|
|
self.server.register_endpoint(
|
2020-11-16 16:46:05 +03:00
|
|
|
"/machine/device_power/on", ['POST'],
|
2020-07-27 21:08:48 +03:00
|
|
|
self._handle_power_request)
|
|
|
|
self.server.register_endpoint(
|
2020-11-16 16:46:05 +03:00
|
|
|
"/machine/device_power/off", ['POST'],
|
2020-07-27 21:08:48 +03:00
|
|
|
self._handle_power_request)
|
2020-11-01 19:44:54 +03:00
|
|
|
self.server.register_remote_method(
|
|
|
|
"set_device_power", self.set_device_power)
|
2021-01-03 03:38:24 +03:00
|
|
|
self.server.register_event_handler(
|
|
|
|
"server:klippy_shutdown", self._handle_klippy_shutdown)
|
2021-02-17 16:16:49 +03:00
|
|
|
self.server.register_notification("power:power_changed")
|
2021-01-06 22:39:33 +03:00
|
|
|
IOLoop.current().spawn_callback(
|
|
|
|
self._initalize_devices, list(self.devices.values()))
|
2020-07-27 21:08:48 +03:00
|
|
|
|
2021-01-22 04:40:09 +03:00
|
|
|
async def _check_klippy_printing(self):
|
2021-03-18 15:23:40 +03:00
|
|
|
klippy_apis = self.server.lookup_component('klippy_apis')
|
2021-01-22 04:40:09 +03:00
|
|
|
result = await klippy_apis.query_objects(
|
|
|
|
{'print_stats': None}, default={})
|
|
|
|
pstate = result.get('print_stats', {}).get('state', "").lower()
|
|
|
|
return pstate == "printing"
|
|
|
|
|
2021-01-06 22:39:33 +03:00
|
|
|
async def _initalize_devices(self, inital_devs):
|
|
|
|
for dev in inital_devs:
|
|
|
|
ret = dev.initialize()
|
|
|
|
if asyncio.iscoroutine(ret):
|
|
|
|
await ret
|
2020-07-27 21:08:48 +03:00
|
|
|
|
2021-01-03 03:38:24 +03:00
|
|
|
async def _handle_klippy_shutdown(self):
|
|
|
|
for name, dev in self.devices.items():
|
|
|
|
if hasattr(dev, "off_when_shutdown"):
|
|
|
|
if dev.off_when_shutdown:
|
|
|
|
logging.info(
|
|
|
|
f"Powering off device [{name}] due to"
|
|
|
|
" klippy shutdown")
|
|
|
|
await self._process_request(dev, "off")
|
|
|
|
|
2020-11-09 15:02:22 +03:00
|
|
|
async def _handle_list_devices(self, web_request):
|
2020-11-16 16:27:09 +03:00
|
|
|
dev_list = [d.get_device_info() for d in self.devices.values()]
|
|
|
|
output = {"devices": dev_list}
|
2020-07-27 21:08:48 +03:00
|
|
|
return output
|
|
|
|
|
2020-11-09 15:02:22 +03:00
|
|
|
async def _handle_power_request(self, web_request):
|
|
|
|
args = web_request.get_args()
|
|
|
|
ep = web_request.get_endpoint()
|
2020-11-16 16:27:09 +03:00
|
|
|
if not args:
|
|
|
|
raise self.server.error("No arguments provided")
|
|
|
|
requsted_devs = {k: self.devices.get(k, None) for k in args}
|
2020-07-27 21:08:48 +03:00
|
|
|
result = {}
|
2020-11-09 15:02:22 +03:00
|
|
|
req = ep.split("/")[-1]
|
2020-11-16 16:27:09 +03:00
|
|
|
for name, device in requsted_devs.items():
|
|
|
|
if device is not None:
|
|
|
|
result[name] = await self._process_request(device, req)
|
2020-11-07 21:00:59 +03:00
|
|
|
else:
|
2020-11-16 16:27:09 +03:00
|
|
|
result[name] = "device_not_found"
|
2020-07-27 21:08:48 +03:00
|
|
|
return result
|
|
|
|
|
2020-11-16 16:27:09 +03:00
|
|
|
async def _process_request(self, device, req):
|
2020-11-01 19:44:54 +03:00
|
|
|
if req in ["on", "off"]:
|
2021-01-27 14:31:29 +03:00
|
|
|
cur_state = device.get_device_info()['status']
|
|
|
|
if req == cur_state:
|
|
|
|
# device is already in requested state, do nothing
|
|
|
|
return cur_state
|
2021-01-22 04:40:09 +03:00
|
|
|
printing = await self._check_klippy_printing()
|
|
|
|
if device.get_locked_while_printing() and printing:
|
2021-01-22 19:20:36 +03:00
|
|
|
raise self.server.error(
|
|
|
|
f"Unable to change power for {device.get_name()} "
|
|
|
|
"while printing")
|
2020-11-16 16:27:09 +03:00
|
|
|
ret = device.set_power(req)
|
2020-11-15 19:08:25 +03:00
|
|
|
if asyncio.iscoroutine(ret):
|
|
|
|
await ret
|
2020-11-16 16:27:09 +03:00
|
|
|
dev_info = device.get_device_info()
|
2021-02-17 16:16:49 +03:00
|
|
|
self.server.send_event("power:power_changed", dev_info)
|
2021-01-27 14:31:29 +03:00
|
|
|
device.run_power_changed_action()
|
2020-11-16 16:27:09 +03:00
|
|
|
elif req == "status":
|
|
|
|
ret = device.refresh_status()
|
|
|
|
if asyncio.iscoroutine(ret):
|
|
|
|
await ret
|
|
|
|
dev_info = device.get_device_info()
|
|
|
|
else:
|
|
|
|
raise self.server.error(f"Unsupported power request: {req}")
|
|
|
|
return dev_info['status']
|
2020-10-20 05:22:39 +03:00
|
|
|
|
2020-11-01 19:44:54 +03:00
|
|
|
def set_device_power(self, device, state):
|
|
|
|
status = None
|
|
|
|
if isinstance(state, bool):
|
|
|
|
status = "on" if state else "off"
|
|
|
|
elif isinstance(state, str):
|
|
|
|
status = state.lower()
|
|
|
|
if status in ["true", "false"]:
|
|
|
|
status = "on" if status == "true" else "off"
|
|
|
|
if status not in ["on", "off"]:
|
|
|
|
logging.info(f"Invalid state received: {state}")
|
|
|
|
return
|
2020-11-16 16:27:09 +03:00
|
|
|
if device not in self.devices:
|
|
|
|
logging.info(f"No device found: {device}")
|
|
|
|
return
|
2020-11-01 19:44:54 +03:00
|
|
|
ioloop = IOLoop.current()
|
2020-11-16 16:27:09 +03:00
|
|
|
ioloop.spawn_callback(
|
|
|
|
self._process_request, self.devices[device], status)
|
2020-11-01 19:44:54 +03:00
|
|
|
|
2020-11-14 16:24:54 +03:00
|
|
|
async def add_device(self, name, device):
|
|
|
|
if name in self.devices:
|
|
|
|
raise self.server.error(
|
|
|
|
f"Device [{name}] already configured")
|
2020-11-15 19:08:25 +03:00
|
|
|
ret = device.initialize()
|
|
|
|
if asyncio.iscoroutine(ret):
|
|
|
|
await ret
|
2020-11-14 16:24:54 +03:00
|
|
|
self.devices[name] = device
|
2020-11-07 21:00:59 +03:00
|
|
|
|
2020-11-15 19:08:25 +03:00
|
|
|
async def close(self):
|
|
|
|
for device in self.devices.values():
|
|
|
|
if hasattr(device, "close"):
|
|
|
|
ret = device.close()
|
|
|
|
if asyncio.iscoroutine(ret):
|
|
|
|
await ret
|
|
|
|
self.chip_factory.close()
|
2020-11-07 21:00:59 +03:00
|
|
|
|
2020-07-27 21:08:48 +03:00
|
|
|
|
2021-01-22 04:40:09 +03:00
|
|
|
class PowerDevice:
|
|
|
|
def __init__(self, config):
|
|
|
|
name_parts = config.get_name().split(maxsplit=1)
|
|
|
|
if len(name_parts) != 2:
|
|
|
|
raise config.error(f"Invalid Section Name: {config.get_name()}")
|
2021-01-27 14:31:29 +03:00
|
|
|
self.server = config.get_server()
|
2021-01-22 04:40:09 +03:00
|
|
|
self.name = name_parts[1]
|
|
|
|
self.state = "init"
|
2021-01-22 19:20:36 +03:00
|
|
|
self.locked_while_printing = config.getboolean(
|
|
|
|
'locked_while_printing', False)
|
2021-01-22 04:40:09 +03:00
|
|
|
self.off_when_shutdown = config.getboolean('off_when_shutdown', False)
|
2021-01-27 14:31:29 +03:00
|
|
|
self.restart_delay = 1.
|
|
|
|
self.klipper_restart = config.getboolean(
|
2021-01-28 15:36:21 +03:00
|
|
|
'restart_klipper_when_powered', False)
|
2021-01-27 14:31:29 +03:00
|
|
|
if self.klipper_restart:
|
|
|
|
self.restart_delay = config.getfloat('restart_delay', 1.)
|
|
|
|
if self.restart_delay < .000001:
|
|
|
|
raise config.error("Option 'restart_delay' must be above 0.0")
|
2021-01-22 04:40:09 +03:00
|
|
|
|
|
|
|
def get_name(self):
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
def get_device_info(self):
|
|
|
|
return {
|
|
|
|
'device': self.name,
|
2021-01-22 04:43:34 +03:00
|
|
|
'status': self.state,
|
|
|
|
'locked_while_printing': self.locked_while_printing
|
2021-01-22 04:40:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
def get_locked_while_printing(self):
|
|
|
|
return self.locked_while_printing
|
|
|
|
|
2021-01-27 14:31:29 +03:00
|
|
|
def run_power_changed_action(self):
|
|
|
|
if self.state == "on" and self.klipper_restart:
|
|
|
|
ioloop = IOLoop.current()
|
2021-03-18 15:23:40 +03:00
|
|
|
klippy_apis = self.server.lookup_component("klippy_apis")
|
2021-01-27 14:31:29 +03:00
|
|
|
ioloop.call_later(self.restart_delay, klippy_apis.do_restart,
|
|
|
|
"FIRMWARE_RESTART")
|
|
|
|
|
2020-11-15 19:08:25 +03:00
|
|
|
class GpioChipFactory:
|
|
|
|
def __init__(self):
|
|
|
|
self.chips = {}
|
2020-07-27 21:08:48 +03:00
|
|
|
|
2020-11-15 19:08:25 +03:00
|
|
|
def get_gpio_chip(self, chip_name):
|
|
|
|
if chip_name in self.chips:
|
|
|
|
return self.chips[chip_name]
|
|
|
|
chip = gpiod.Chip(chip_name, gpiod.Chip.OPEN_BY_NAME)
|
|
|
|
self.chips[chip_name] = chip
|
|
|
|
return chip
|
2020-07-27 21:08:48 +03:00
|
|
|
|
2020-11-15 19:08:25 +03:00
|
|
|
def close(self):
|
|
|
|
for chip in self.chips.values():
|
|
|
|
chip.close()
|
2020-07-27 21:08:48 +03:00
|
|
|
|
2021-01-22 04:40:09 +03:00
|
|
|
class GpioDevice(PowerDevice):
|
2020-11-15 19:08:25 +03:00
|
|
|
def __init__(self, config, chip_factory):
|
2021-01-22 04:40:09 +03:00
|
|
|
super().__init__(config)
|
2020-11-15 19:08:25 +03:00
|
|
|
pin, chip_id, invert = self._parse_pin(config)
|
|
|
|
try:
|
|
|
|
chip = chip_factory.get_gpio_chip(chip_id)
|
|
|
|
self.line = chip.get_line(pin)
|
|
|
|
if invert:
|
|
|
|
self.line.request(
|
|
|
|
consumer="moonraker", type=gpiod.LINE_REQ_DIR_OUT,
|
|
|
|
flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW)
|
|
|
|
else:
|
|
|
|
self.line.request(
|
|
|
|
consumer="moonraker", type=gpiod.LINE_REQ_DIR_OUT)
|
|
|
|
except Exception:
|
|
|
|
self.state = "error"
|
|
|
|
logging.exception(
|
|
|
|
f"Unable to init {pin}. Make sure the gpio is not in "
|
|
|
|
"use by another program or exported by sysfs.")
|
|
|
|
raise config.error("Power GPIO Config Error")
|
2021-01-06 22:39:33 +03:00
|
|
|
self.initial_state = config.getboolean('initial_state', False)
|
2020-11-15 19:08:25 +03:00
|
|
|
|
|
|
|
def _parse_pin(self, config):
|
|
|
|
pin = cfg_pin = config.get("pin")
|
|
|
|
invert = False
|
2020-11-14 16:24:54 +03:00
|
|
|
if pin[0] == "!":
|
|
|
|
pin = pin[1:]
|
2020-11-15 19:08:25 +03:00
|
|
|
invert = True
|
|
|
|
chip_id = "gpiochip0"
|
2020-11-14 16:24:54 +03:00
|
|
|
pin_parts = pin.split("/")
|
|
|
|
if len(pin_parts) == 2:
|
2020-11-15 19:08:25 +03:00
|
|
|
chip_id, pin = pin_parts
|
2020-11-14 16:24:54 +03:00
|
|
|
elif len(pin_parts) == 1:
|
2020-11-15 19:08:25 +03:00
|
|
|
pin = pin_parts[0]
|
2020-11-14 16:24:54 +03:00
|
|
|
# Verify pin
|
2020-11-15 19:08:25 +03:00
|
|
|
if not chip_id.startswith("gpiochip") or \
|
|
|
|
not chip_id[-1].isdigit() or \
|
|
|
|
not pin.startswith("gpio") or \
|
|
|
|
not pin[4:].isdigit():
|
2020-11-14 16:24:54 +03:00
|
|
|
raise config.error(
|
|
|
|
f"Invalid Power Pin configuration: {cfg_pin}")
|
2020-11-15 19:08:25 +03:00
|
|
|
pin = int(pin[4:])
|
|
|
|
return pin, chip_id, invert
|
2020-11-07 21:00:59 +03:00
|
|
|
|
2020-11-15 19:08:25 +03:00
|
|
|
def initialize(self):
|
2021-01-06 22:39:33 +03:00
|
|
|
self.set_power("on" if self.initial_state else "off")
|
2020-11-07 21:00:59 +03:00
|
|
|
|
2020-11-16 16:27:09 +03:00
|
|
|
def get_device_info(self):
|
|
|
|
return {
|
2021-01-22 04:40:09 +03:00
|
|
|
**super().get_device_info(),
|
2020-11-16 16:27:09 +03:00
|
|
|
'type': "gpio"
|
|
|
|
}
|
2020-11-15 19:08:25 +03:00
|
|
|
|
|
|
|
def refresh_status(self):
|
|
|
|
try:
|
|
|
|
val = self.line.get_value()
|
|
|
|
except Exception:
|
|
|
|
self.state = "error"
|
|
|
|
msg = f"Error Refeshing Device Status: {self.name}"
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.server.error(msg) from None
|
|
|
|
self.state = "on" if val else "off"
|
|
|
|
|
|
|
|
def set_power(self, state):
|
|
|
|
try:
|
|
|
|
self.line.set_value(int(state == "on"))
|
|
|
|
except Exception:
|
|
|
|
self.state = "error"
|
|
|
|
msg = f"Error Toggling Device Power: {self.name}"
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.server.error(msg) from None
|
|
|
|
self.state = state
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
self.line.release()
|
2020-11-07 21:00:59 +03:00
|
|
|
|
2020-11-16 20:36:28 +03:00
|
|
|
|
|
|
|
# This implementation based off the work tplink_smartplug
|
|
|
|
# script by Lubomir Stroetmann available at:
|
|
|
|
#
|
|
|
|
# https://github.com/softScheck/tplink-smartplug
|
|
|
|
#
|
|
|
|
# Copyright 2016 softScheck GmbH
|
2021-01-22 04:40:09 +03:00
|
|
|
class TPLinkSmartPlug(PowerDevice):
|
2020-11-16 20:36:28 +03:00
|
|
|
START_KEY = 0xAB
|
|
|
|
def __init__(self, config):
|
2021-01-22 04:40:09 +03:00
|
|
|
super().__init__(config)
|
2020-11-16 20:36:28 +03:00
|
|
|
self.server = config.get_server()
|
2021-03-27 21:53:28 +03:00
|
|
|
self.addr = config.get("address").split('/')
|
2020-11-16 20:36:28 +03:00
|
|
|
self.port = config.getint("port", 9999)
|
|
|
|
|
|
|
|
async def _send_tplink_command(self, command):
|
|
|
|
out_cmd = {}
|
|
|
|
if command in ["on", "off"]:
|
2021-03-27 21:53:28 +03:00
|
|
|
out_cmd = {
|
|
|
|
'system': {'set_relay_state': {'state': int(command == "on")}}
|
|
|
|
}
|
2021-04-01 03:32:57 +03:00
|
|
|
# TPLink device controls multiple devices
|
|
|
|
if len(self.addr) == 2:
|
|
|
|
sysinfo = await self._send_tplink_command("info")
|
|
|
|
dev_id = sysinfo["system"]["get_sysinfo"]["deviceId"]
|
|
|
|
out_cmd["context"] = {
|
|
|
|
'child_ids': [f"{dev_id}{int(self.addr[1]):02}"]
|
|
|
|
}
|
2020-11-16 20:36:28 +03:00
|
|
|
elif command == "info":
|
|
|
|
out_cmd = {'system': {'get_sysinfo': {}}}
|
|
|
|
else:
|
|
|
|
raise self.server.error(f"Invalid tplink command: {command}")
|
|
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
stream = IOStream(s)
|
|
|
|
try:
|
2021-03-27 21:53:28 +03:00
|
|
|
await stream.connect((self.addr[0], self.port))
|
2020-11-16 20:36:28 +03:00
|
|
|
await stream.write(self._encrypt(out_cmd))
|
|
|
|
data = await stream.read_bytes(2048, partial=True)
|
|
|
|
length = struct.unpack(">I", data[:4])[0]
|
|
|
|
data = data[4:]
|
|
|
|
retries = 5
|
|
|
|
remaining = length - len(data)
|
|
|
|
while remaining and retries:
|
|
|
|
data += await stream.read_bytes(remaining)
|
|
|
|
remaining = length - len(data)
|
|
|
|
retries -= 1
|
|
|
|
if not retries:
|
|
|
|
raise self.server.error("Unable to read tplink packet")
|
|
|
|
except Exception:
|
|
|
|
msg = f"Error sending tplink command: {command}"
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.server.error(msg)
|
|
|
|
finally:
|
|
|
|
stream.close()
|
|
|
|
return json.loads(self._decrypt(data))
|
|
|
|
|
|
|
|
def _encrypt(self, data):
|
|
|
|
data = json.dumps(data)
|
|
|
|
key = self.START_KEY
|
|
|
|
res = struct.pack(">I", len(data))
|
|
|
|
for c in data:
|
|
|
|
val = key ^ ord(c)
|
|
|
|
key = val
|
|
|
|
res += bytes([val])
|
|
|
|
return res
|
|
|
|
|
|
|
|
def _decrypt(self, data):
|
|
|
|
key = self.START_KEY
|
|
|
|
res = ""
|
|
|
|
for c in data:
|
|
|
|
val = key ^ c
|
|
|
|
key = c
|
|
|
|
res += chr(val)
|
|
|
|
return res
|
|
|
|
|
|
|
|
async def initialize(self):
|
|
|
|
await self.refresh_status()
|
|
|
|
|
|
|
|
def get_device_info(self):
|
|
|
|
return {
|
2021-01-22 04:40:09 +03:00
|
|
|
**super().get_device_info(),
|
2020-11-16 20:36:28 +03:00
|
|
|
'type': "tplink_smartplug"
|
|
|
|
}
|
|
|
|
|
|
|
|
async def refresh_status(self):
|
|
|
|
try:
|
|
|
|
res = await self._send_tplink_command("info")
|
2021-04-01 03:32:57 +03:00
|
|
|
if len(self.addr) == 2:
|
|
|
|
# TPLink device controls multiple devices
|
|
|
|
children = res['system']['get_sysinfo']['children']
|
|
|
|
state = children[int(self.addr[1])]['state']
|
2021-03-27 21:53:28 +03:00
|
|
|
else:
|
2021-04-01 03:32:57 +03:00
|
|
|
state = res['system']['get_sysinfo']['relay_state']
|
2020-11-16 20:36:28 +03:00
|
|
|
except Exception:
|
|
|
|
self.state = "error"
|
|
|
|
msg = f"Error Refeshing Device Status: {self.name}"
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.server.error(msg) from None
|
|
|
|
self.state = "on" if state else "off"
|
|
|
|
|
|
|
|
async def set_power(self, state):
|
|
|
|
try:
|
|
|
|
res = await self._send_tplink_command(state)
|
|
|
|
err = res['system']['set_relay_state']['err_code']
|
|
|
|
except Exception:
|
|
|
|
err = 1
|
|
|
|
logging.exception(f"Power Toggle Error: {self.name}")
|
|
|
|
if err:
|
|
|
|
self.state = "error"
|
|
|
|
raise self.server.error(
|
|
|
|
f"Error Toggling Device Power: {self.name}")
|
|
|
|
self.state = state
|
|
|
|
|
2020-11-21 00:44:16 +03:00
|
|
|
|
2021-01-22 04:40:09 +03:00
|
|
|
class Tasmota(PowerDevice):
|
2020-11-21 00:44:16 +03:00
|
|
|
def __init__(self, config):
|
2021-01-23 01:16:51 +03:00
|
|
|
super().__init__(config)
|
2020-11-21 00:44:16 +03:00
|
|
|
self.server = config.get_server()
|
|
|
|
self.addr = config.get("address")
|
|
|
|
self.output_id = config.getint("output_id", 1)
|
|
|
|
self.password = config.get("password", "")
|
|
|
|
|
|
|
|
async def _send_tasmota_command(self, command, password=None):
|
|
|
|
if command in ["on", "off"]:
|
|
|
|
out_cmd = f"Power{self.output_id}%20{command}"
|
|
|
|
elif command == "info":
|
2020-12-26 03:30:21 +03:00
|
|
|
out_cmd = f"Power{self.output_id}"
|
2020-11-21 00:44:16 +03:00
|
|
|
else:
|
|
|
|
raise self.server.error(f"Invalid tasmota command: {command}")
|
2020-12-26 03:30:21 +03:00
|
|
|
|
|
|
|
url = f"http://{self.addr}/cm?user=admin&password=" \
|
|
|
|
f"{self.password}&cmnd={out_cmd}"
|
2020-11-21 00:44:16 +03:00
|
|
|
data = ""
|
|
|
|
http_client = AsyncHTTPClient()
|
|
|
|
try:
|
|
|
|
response = await http_client.fetch(url)
|
|
|
|
data = json_decode(response.body)
|
|
|
|
except Exception:
|
|
|
|
msg = f"Error sending tplink command: {command}"
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.server.error(msg)
|
|
|
|
return data
|
|
|
|
|
|
|
|
async def initialize(self):
|
|
|
|
await self.refresh_status()
|
|
|
|
|
|
|
|
def get_device_info(self):
|
|
|
|
return {
|
2021-01-22 04:40:09 +03:00
|
|
|
**super().get_device_info(),
|
2020-11-21 00:44:16 +03:00
|
|
|
'type': "tasmota"
|
|
|
|
}
|
|
|
|
|
|
|
|
async def refresh_status(self):
|
|
|
|
try:
|
|
|
|
res = await self._send_tasmota_command("info")
|
2021-04-02 14:45:45 +03:00
|
|
|
try:
|
|
|
|
state = res[f"POWER{self.output_id}"].lower()
|
|
|
|
except KeyError as e:
|
|
|
|
if self.output_id == 1 :
|
|
|
|
state = res[f"POWER"].lower()
|
|
|
|
else:
|
|
|
|
raise KeyError(e)
|
2020-11-21 00:44:16 +03:00
|
|
|
except Exception:
|
|
|
|
self.state = "error"
|
|
|
|
msg = f"Error Refeshing Device Status: {self.name}"
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.server.error(msg) from None
|
|
|
|
self.state = state
|
|
|
|
|
|
|
|
async def set_power(self, state):
|
|
|
|
try:
|
|
|
|
res = await self._send_tasmota_command(state)
|
2021-04-02 14:45:45 +03:00
|
|
|
try:
|
|
|
|
state = res[f"POWER{self.output_id}"].lower()
|
|
|
|
except KeyError as e:
|
|
|
|
if self.output_id == 1 :
|
|
|
|
state = res[f"POWER"].lower()
|
|
|
|
else:
|
|
|
|
raise KeyError(e)
|
2020-11-21 00:44:16 +03:00
|
|
|
except Exception:
|
|
|
|
self.state = "error"
|
|
|
|
msg = f"Error Setting Device Status: {self.name} to {state}"
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.server.error(msg) from None
|
|
|
|
self.state = state
|
|
|
|
|
2021-02-07 13:02:09 +03:00
|
|
|
|
|
|
|
class Shelly(PowerDevice):
|
|
|
|
def __init__(self, config):
|
|
|
|
super().__init__(config)
|
|
|
|
self.server = config.get_server()
|
|
|
|
self.addr = config.get("address")
|
|
|
|
self.output_id = config.getint("output_id", 0)
|
|
|
|
self.user = config.get("user", "admin")
|
|
|
|
self.password = config.get("password", "")
|
|
|
|
|
|
|
|
async def _send_shelly_command(self, command):
|
|
|
|
if command in ["on", "off"]:
|
|
|
|
out_cmd = f"relay/{self.output_id}?turn={command}"
|
|
|
|
elif command == "info":
|
|
|
|
out_cmd = f"relay/{self.output_id}"
|
|
|
|
else:
|
|
|
|
raise self.server.error(f"Invalid shelly command: {command}")
|
|
|
|
if self.password != "":
|
|
|
|
out_pwd = f"{self.user}:{self.password}@"
|
|
|
|
else:
|
|
|
|
out_pwd = f""
|
|
|
|
url = f"http://{out_pwd}{self.addr}/{out_cmd}"
|
|
|
|
data = ""
|
|
|
|
http_client = AsyncHTTPClient()
|
|
|
|
try:
|
|
|
|
response = await http_client.fetch(url)
|
|
|
|
data = json_decode(response.body)
|
|
|
|
except Exception:
|
|
|
|
msg = f"Error sending shelly command: {command}"
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.server.error(msg)
|
|
|
|
return data
|
|
|
|
|
|
|
|
async def initialize(self):
|
|
|
|
await self.refresh_status()
|
|
|
|
|
|
|
|
def get_device_info(self):
|
|
|
|
return {
|
|
|
|
**super().get_device_info(),
|
|
|
|
'type': "shelly"
|
|
|
|
}
|
|
|
|
|
|
|
|
async def refresh_status(self):
|
|
|
|
try:
|
|
|
|
res = await self._send_shelly_command("info")
|
|
|
|
state = res[f"ison"]
|
|
|
|
except Exception:
|
|
|
|
self.state = "error"
|
|
|
|
msg = f"Error Refeshing Device Status: {self.name}"
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.server.error(msg) from None
|
|
|
|
self.state = "on" if state else "off"
|
|
|
|
|
|
|
|
async def set_power(self, state):
|
|
|
|
try:
|
|
|
|
res = await self._send_shelly_command(state)
|
|
|
|
state = res[f"ison"]
|
|
|
|
except Exception:
|
|
|
|
self.state = "error"
|
|
|
|
msg = f"Error Setting Device Status: {self.name} to {state}"
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.server.error(msg) from None
|
|
|
|
self.state = "on" if state else "off"
|
|
|
|
|
2021-03-12 02:54:16 +03:00
|
|
|
class HomeSeer(PowerDevice):
|
|
|
|
def __init__(self, config):
|
|
|
|
super().__init__(config)
|
|
|
|
self.server = config.get_server()
|
|
|
|
self.addr = config.get("address")
|
|
|
|
self.device = config.getint("device")
|
|
|
|
self.user = config.get("user", "admin")
|
|
|
|
self.password = config.get("password", "")
|
|
|
|
|
2021-03-18 15:28:32 +03:00
|
|
|
async def _send_homeseer(self, request, additional=""):
|
2021-03-12 02:54:16 +03:00
|
|
|
url = (f"http://{self.user}:{self.password}@{self.addr}"
|
|
|
|
f"/JSON?user={self.user}&pass={self.password}"
|
|
|
|
f"&request={request}&ref={self.device}&{additional}")
|
|
|
|
data = ""
|
|
|
|
http_client = AsyncHTTPClient()
|
|
|
|
try:
|
|
|
|
response = await http_client.fetch(url)
|
|
|
|
data = json_decode(response.body)
|
|
|
|
except Exception:
|
|
|
|
msg = f"Error sending HomeSeer command: {request}"
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.server.error(msg)
|
|
|
|
return data
|
|
|
|
|
|
|
|
async def initialize(self):
|
|
|
|
await self.refresh_status()
|
|
|
|
|
|
|
|
def get_device_info(self):
|
|
|
|
return {
|
|
|
|
**super().get_device_info(),
|
|
|
|
'type': "homeseer"
|
|
|
|
}
|
|
|
|
|
|
|
|
async def refresh_status(self):
|
|
|
|
try:
|
|
|
|
res = await self._send_homeseer("getstatus")
|
|
|
|
state = res[f"Devices"][0]["status"].lower()
|
|
|
|
except Exception:
|
|
|
|
self.state = "error"
|
|
|
|
msg = f"Error Refeshing Device Status: {self.name}"
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.server.error(msg) from None
|
|
|
|
self.state = state
|
|
|
|
|
|
|
|
async def set_power(self, state):
|
|
|
|
try:
|
|
|
|
if state == "on":
|
|
|
|
state_hs = "On"
|
|
|
|
elif state == "off":
|
|
|
|
state_hs = "Off"
|
|
|
|
res = await self._send_homeseer("controldevicebylabel",
|
|
|
|
f"label={state_hs}")
|
|
|
|
except Exception:
|
|
|
|
self.state = "error"
|
|
|
|
msg = f"Error Setting Device Status: {self.name} to {state}"
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.server.error(msg) from None
|
|
|
|
self.state = state
|
|
|
|
|
2021-03-18 15:23:40 +03:00
|
|
|
# The power component has multiple configuration sections
|
|
|
|
def load_component_multi(config):
|
2020-08-06 04:06:52 +03:00
|
|
|
return PrinterPower(config)
|