├── README.md ├── .gitignore ├── .idea └── .gitignore ├── moonraker ├── README.md └── power.py └── klipper ├── shell_command.py ├── htu21d_host.py ├── README.md ├── xiaomi_blue.py └── temperature_fan.py /README.md: -------------------------------------------------------------------------------- 1 | # 3dprinter 2 | My 3dprinter library 3 | 4 | ## Klipper shell command extras 5 | 6 | ## Moonraker power support shell command -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea/3dprinter.iml 3 | .DS_Store 4 | .idea/inspectionProfiles/profiles_settings.xml 5 | .idea/misc.xml 6 | .idea/modules.xml 7 | .idea/vcs.xml 8 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /moonraker/README.md: -------------------------------------------------------------------------------- 1 | # Moonraker 的Power支持系统命令 2 | 把power.py代码覆盖掉原来moonraker/moonraker/components文件夹中的power.py 3 | 4 | 注意:该功能只是临时支持,如果moonraker官方升级后,代码将会不适用 5 | 6 | ```ini 7 | # moonraker.conf 8 | [power device_name] 9 | type: shell_command 10 | # 类型为shell_command 11 | on: 12 | # 打开电源的系统命令 13 | off: 14 | # 关闭电源的系统命令 15 | ``` 16 | 17 | 18 | -------------------------------------------------------------------------------- /klipper/shell_command.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # 命令行指令 3 | # [shell_command test] 4 | # command: ls 5 | import logging 6 | import subprocess 7 | 8 | 9 | class ShellCommand: 10 | def __init__(self, config): 11 | # 获取printer 对象 12 | self.printer = config.get_printer() 13 | name = config.get_name().split()[1] 14 | self.command = config.get('command') 15 | logging.info("run shell command %s", self.command) 16 | self.gcode = self.printer.lookup_object('gcode') 17 | self.gcode.register_mux_command("SHELL_COMMAND", "NAME", name, self.run_cmd, 18 | desc="Run shell command.") 19 | pass 20 | 21 | def run_cmd(self, gcmd): 22 | args = gcmd.get('ARGS', '') 23 | logging.info("args => %s", args) 24 | script = self.command + " " + args 25 | result = subprocess.Popen(script, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 26 | result_message = result.stdout.read().decode('utf-8') 27 | logging.info("run shell script %s => %s", script, result_message) 28 | gcmd.respond_info("result => " + result_message) 29 | pass 30 | 31 | 32 | def load_config_prefix(config): 33 | return ShellCommand(config) 34 | -------------------------------------------------------------------------------- /klipper/htu21d_host.py: -------------------------------------------------------------------------------- 1 | # HTU21D(F)/Si7013/Si7020/Si7021/SHT21 i2c based temperature sensors support 2 | # 3 | # Copyright (C) 2020 Lucio Tarantino 4 | # 5 | # This file may be distributed under the terms of the GNU GPLv3 license. 6 | import logging 7 | from sensor.HTU21D import HTU21D 8 | 9 | HTU21D_I2C_ADDR = 0x40 10 | 11 | class HTU21D_HOST: 12 | def __init__(self, config): 13 | self.printer = config.get_printer() 14 | self.name = config.get_name().split()[-1] 15 | self.reactor = self.printer.get_reactor() 16 | self.report_time = config.getint('htu21d_report_time', 30, minval=5) 17 | i2c_addr = config.getint('htu21d_address', HTU21D_I2C_ADDR) 18 | self.htu = HTU21D(1, i2c_addr) 19 | self.temp = self.min_temp = self.max_temp = self.humidity = 0. 20 | self.sample_timer = self.reactor.register_timer(self._sample_htu21d) 21 | self.printer.add_object("htu21d_host " + self.name, self) 22 | self.printer.register_event_handler("klippy:connect", 23 | self.handle_connect) 24 | 25 | def handle_connect(self): 26 | self.reactor.update_timer(self.sample_timer, self.reactor.NOW) 27 | 28 | def setup_minmax(self, min_temp, max_temp): 29 | self.min_temp = min_temp 30 | self.max_temp = max_temp 31 | 32 | def setup_callback(self, cb): 33 | self._callback = cb 34 | 35 | def get_report_time_delta(self): 36 | return self.report_time 37 | 38 | def _sample_htu21d(self, eventtime): 39 | try: 40 | h = self.htu.humidity() 41 | self.humidity = h.RH 42 | t = self.htu.temperature() 43 | self.temp = t.C 44 | 45 | except Exception: 46 | logging.exception("htu21d: Error reading data") 47 | self.temp = self.humidity = .0 48 | return self.reactor.NEVER 49 | 50 | if self.temp < self.min_temp or self.temp > self.max_temp: 51 | self.printer.invoke_shutdown( 52 | "HTU21D temperature %0.1f outside range of %0.1f:%.01f" 53 | % (self.temp, self.min_temp, self.max_temp)) 54 | 55 | # measured_time = self.reactor.monotonic() 56 | # print_time = self.i2c.get_mcu().estimated_print_time(measured_time) 57 | # self._callback(print_time, self.temp) 58 | 59 | mcu = self.printer.lookup_object('mcu') 60 | measured_time = self.reactor.monotonic() 61 | self._callback(mcu.estimated_print_time(measured_time), self.temp) 62 | return measured_time + self.report_time 63 | 64 | def get_temp(self, eventtime): 65 | return self.temp, 0. 66 | 67 | def get_status(self, eventtime): 68 | return { 69 | 'temperature': round(self.temp, 2), 70 | 'humidity': self.humidity 71 | } 72 | 73 | def load_config(config): 74 | # Register sensor 75 | pheater = config.get_printer().lookup_object("heaters") 76 | pheater.add_sensor_factory('HTU21D_HOST', HTU21D_HOST) 77 | -------------------------------------------------------------------------------- /klipper/README.md: -------------------------------------------------------------------------------- 1 | # 走过路过给我加点star,谢谢 :-) 2 | 3 | 4 | # shell_command.py [Klipper 支持系统命令的扩展] 5 | 6 | 让Klipper的Gcode增加支持运行系统命令的功能 7 | 8 | ## 安装方法 9 | 复制 `shell_command.py` 到 `klipper/klippy/extras` 10 | 11 | ## 使用方法 12 | 在klipper的配置文件`printer.cfg`中加入需要运行的命令,例如 13 | ```ini 14 | [shell_command my_command] 15 | command: date 16 | ``` 17 | 18 | 重启klipper服务 19 | ```shell 20 | sudo service klipper restart 21 | ``` 22 | 23 | 重启成功后,在控制台输入 24 | ``` 25 | SHELL_COMMAND NAME=my_command 26 | ``` 27 | Klipper就会执行系统date这个命令,并且将命令的返回结果当前日期显示在控制台中。 28 | 29 | 30 | # htu21d_host.py [Klipper 增加HTU21D_HOST温湿度传感器支持] 31 | 32 | Klipper似乎对i2c总线的设备非常不稳定,一旦i2c总线的设备通讯发生通讯错误, 33 | 负责i2c总线通讯的mcu就会彻底崩溃,导致klipper服务发生错误,停止工作, 34 | 这对于需要长时间连续工作的3D打印机来说是完全不可接受的, 譬如打印到90%来个i2c timeout的error, 35 | 真的想死的心都有了……也不知道为啥klipper团队为啥不解决这个问:-(。 36 | 所以,我弄了一个通过树莓派系统原生的获取温湿度的功能的插件,来规避这个问题,当然如果这样子的话, 37 | 传感器也就只能接在树莓派的i2c接口上了(树莓派连接HTU21D的方法请自行搜索,网上很多) 38 | 39 | ## 安装方法 40 | 复制 `htu21d_host.py` 到 `klipper/klippy/extras` 41 | 42 | 安装klipper环境的python的传感器支持库(国内安装建议使用国内镜像源) 43 | ```shell 44 | ~/klippy-env/bin/pip install sensor smbus spidev -i https://pypi.tuna.tsinghua.edu.cn/simple 45 | ``` 46 | 47 | 最后重启klipper服务 48 | ```shell 49 | sudo service klipper restart 50 | ``` 51 | 52 | ## 使用方法 53 | 54 | 在klipper的printer.cfg配置文件中增加传感器的配置段落 55 | ```ini 56 | 57 | #加载模块 58 | [htu21d_host] 59 | 60 | 61 | # 传感器配置 62 | [temperature_sensor enclosure] 63 | sensor_type: HTU21D_HOST 64 | #i2c_address: 64 65 | 66 | # 查询的温湿度的宏代码 67 | [gcode_macro QUERY_HTU21D] 68 | gcode: 69 | {% set sensor = printer["htu21d_host enclosure"] %} 70 | {action_respond_info( 71 | "Temperature: %.2f C\n" 72 | "Humidity: %.2f%%" % ( 73 | sensor.temperature, 74 | sensor.humidity))} 75 | ``` 76 | 77 | # temperature_fan.py [Klipper温度风扇的优化扩展,增加reverse配置项] 78 | 79 | Klipper标准的温度风扇temperature_fan默认逻辑是降温逻辑,设置了目标温度后, 80 | 达到目标温度才会启动风扇降温,低于温度不会启动风扇,但是一些内循环风扇目的是增温, 81 | 运行逻辑与降温风扇正好相反,所以我调整了代码,增加了原本温度风扇的reverse工作模式, 82 | 打开reverse工作模式后,低于目标温度时风扇会工作,高于目标温度则会停止风扇 83 | 84 | ## 安装方法 85 | 86 | 复制插件`temperature_fan.py` 到 `klipper/klippy/extras` 覆盖原本的温度风扇扩展 87 | (由于是替换原Klipper扩展,所以升级有可能会覆盖,可能需要重新安装,不过我已经将扩展提交klipper官方PR,如果同意后会成为官方配置,就不需要重新安装) 88 | 89 | ## 使用方法 90 | printer.cfg配置文件中增加传感器的配置段落 91 | ```ini 92 | [temperature_fan enclosure_cyclic_fan] 93 | ## 循环风扇 94 | pin: PD14 95 | sensor_type: HTU21D_HOST 96 | reverse: True # 主要是这个设置为True,温度风扇工作模式将反转,用于增温,其他配置沿用你自己的配置即可 97 | target_temp: 0.0 98 | min_temp: 0 99 | max_temp: 60.0 100 | max_speed: 0.6 101 | min_speed: 0 102 | # control: watermark 103 | control: pid 104 | pid_Kp: 40 105 | pid_Ki: 2 106 | pid_Kd: 1 107 | ``` 108 | 109 | 110 | # xiaomi_blue.py [Klipper 增加小米蓝牙温湿度传感器的温湿度] 111 | 112 | 小米这个传感器便宜大碗,所以很多人用这个传感器来进行打印机的仓温监控,而且这个传感器的协议坊间也公开的差不多了, 113 | 所以就写了个模块用树莓派的蓝牙模块获取这个温度给klipper,不过因为是蓝牙,实效性不是太高就是了,不过用作仓温等应该问题不大 114 | 115 | ## 安装方法 116 | 复制 `xiaomi_blue.py` 到 `klipper/klippy/extras` 117 | 118 | 安装klipper环境的python的蓝牙bluepy库以及系统支持 119 | ```shell 120 | sudo apt install libglib2.0-dev 121 | 122 | ~/klippy-env/bin/pip install bluepy -i https://pypi.tuna.tsinghua.edu.cn/simple 123 | ``` 124 | 125 | 最后重启klipper服务 126 | ```shell 127 | sudo service klipper restart 128 | ``` 129 | 130 | ## 使用方法 131 | 132 | 在klipper的printer.cfg配置文件中增加传感器的配置段落 133 | ```ini 134 | # 加载模块 135 | [xiaomi_blue] 136 | 137 | # 传感器配置 138 | [temperature_sensor xiaomi] 139 | sensor_type: XIAOMI_BLUE # 传感器类型 140 | mac_address: A4:C1:38:10:73:D4 # 蓝牙的传感器mac地址,必须参数,具体可以通过米家连接蓝牙传感器后,通过传感器的关于设备菜单中获得 141 | # report_time: 30 # 默认的30秒读取一次数据(蓝牙不要读取的太频密,最小10秒)非必需 142 | 143 | # 查询的温湿度的宏代码 144 | [gcode_macro QUERY_XIAOMI] 145 | gcode: 146 | {% set sensor = printer["xiaomi_blue xiaomi"] %} 147 | {action_respond_info( 148 | "Temperature: %.2f C\n" 149 | "Humidity: %.2f%%" % ( 150 | sensor.temperature, 151 | sensor.humidity))} 152 | 153 | -------------------------------------------------------------------------------- /klipper/xiaomi_blue.py: -------------------------------------------------------------------------------- 1 | # HTU21D(F)/Si7013/Si7020/Si7021/SHT21 i2c based temperature sensors support 2 | # 3 | # Copyright (C) 2020 Lucio Tarantino 4 | # 5 | # This file may be distributed under the terms of the GNU GPLv3 license. 6 | import logging 7 | from bluepy import btle 8 | import struct 9 | from threading import Thread 10 | from multiprocessing import Process 11 | 12 | 13 | class XIAOMI_BLUE: 14 | _blue_connect = None 15 | 16 | def __init__(self, config): 17 | self.printer = config.get_printer() 18 | self.name = config.get_name().split()[-1] 19 | self.reactor = self.printer.get_reactor() 20 | self.report_time = config.getint('report_time', 20, minval=10) 21 | self.mac = config.get('mac_address') 22 | self.temp = self.min_temp = self.max_temp = self.humidity = self.voltage = self.battery = 0. 23 | self.sample_timer = self.reactor.register_timer(self._sample_read) 24 | self.printer.add_object("xiaomi_blue " + self.name, self) 25 | self.printer.register_event_handler("klippy:connect", 26 | self.handle_connect) 27 | self.printer.register_event_handler("klippy:disconnect", 28 | self.handle_disconnect) 29 | self._thread = None 30 | 31 | def connect(self): 32 | if self._blue_connect is None: 33 | self._blue_connect = btle.Peripheral(self.mac) 34 | return self._blue_connect 35 | 36 | def close(self): 37 | if self._blue_connect is not None: 38 | self._blue_connect.disconnect() 39 | self._blue_connect = self._thread = None 40 | 41 | def handle_connect(self): 42 | self.reactor.update_timer(self.sample_timer, self.reactor.NOW) 43 | 44 | def handle_disconnect(self): 45 | self.close() 46 | 47 | def setup_minmax(self, min_temp, max_temp): 48 | self.min_temp = min_temp 49 | self.max_temp = max_temp 50 | 51 | def setup_callback(self, cb): 52 | self._callback = cb 53 | 54 | def get_report_time_delta(self): 55 | return self.report_time 56 | 57 | def _sample_read(self, eventtime): 58 | # try: 59 | # if self._thread is None or not self._thread.isAlive(): 60 | # self._thread = XiaoMiTempBt(self) 61 | # self._thread.start() 62 | # else: 63 | # logging.info('xiaomi: thread is alive!') 64 | # except Exception as e: 65 | # logging.info("xiaomi error: %s" % e.message) 66 | # self.close() 67 | try: 68 | if self._thread is None or not self._thread.is_alive(): 69 | self._thread = Process(target=self.read()) 70 | self._thread.start() 71 | else: 72 | logging.info('xiaomi: thread is alive!') 73 | except Exception as e: 74 | logging.info("xiaomi error: %s" % e.message) 75 | 76 | mcu = self.printer.lookup_object('mcu') 77 | measured_time = self.reactor.monotonic() 78 | self._callback(mcu.estimated_print_time(measured_time), self.temp) 79 | return measured_time + self.report_time 80 | 81 | def read(self): 82 | try: 83 | p = self.connect() 84 | p.writeCharacteristic(0x0038, b'\x01\x00', True) 85 | p.writeCharacteristic(0x0046, b'\xf4\x01\x00', True) 86 | measure = Measure(self) 87 | p.withDelegate(measure) 88 | 89 | if not p.waitForNotifications(1000): 90 | logging.info('xiaomi: read timeout!') 91 | self.close() 92 | 93 | except Exception as e: 94 | logging.info("xiaomi error: %s" % e.message) 95 | self.close() 96 | 97 | def get_temp(self, eventtime): 98 | return self.temp, 0. 99 | 100 | def get_status(self, eventtime): 101 | return { 102 | 'temperature': round(self.temp, 2), 103 | 'humidity': self.humidity, 104 | 'voltage': self.voltage, 105 | 'battery': self.battery 106 | } 107 | 108 | 109 | class XiaoMiTempBt(Thread): 110 | def __init__(self, obj): 111 | super(XiaoMiTempBt, self).__init__() 112 | self.obj = obj 113 | 114 | def run(self): 115 | try: 116 | p = self.obj.connect() 117 | p.writeCharacteristic(0x0038, b'\x01\x00', True) 118 | p.writeCharacteristic(0x0046, b'\xf4\x01\x00', True) 119 | measure = Measure(self.obj) 120 | p.withDelegate(measure) 121 | 122 | if not p.waitForNotifications(1000): 123 | logging.info('xiaomi: read timeout!') 124 | self.obj.close() 125 | 126 | except Exception as e: 127 | logging.info("xiaomi error: %s" % e.message) 128 | self.obj.close() 129 | 130 | 131 | class Measure(btle.DefaultDelegate): 132 | battery = None # type: float 133 | voltage = None # type: int 134 | humidity = None # type: int 135 | temp = None # type: int 136 | 137 | def __init__(self, obj): 138 | self.obj = obj 139 | btle.DefaultDelegate.__init__(self) 140 | 141 | def handleNotification(self, cHandle, data): 142 | try: 143 | t = data[0:2].encode('hex').decode('hex') 144 | temp = float(struct.unpack(' 0: 146 | self.obj.temp = temp 147 | humidity = int(data[2:3].encode('hex'), 16) 148 | if humidity > 0: 149 | self.obj.humidity = humidity 150 | v = data[3:5].encode('hex').decode('hex') 151 | voltage = float(struct.unpack(' 0: 153 | self.obj.voltage = voltage 154 | battery = round((voltage - 2) / (3.261 - 2) * 100, 2) 155 | if battery > 0: 156 | self.obj.battery = battery 157 | logging.info("xiaomi: temp=%f, humidity=%f, voltage=%f, battery=%f" % (temp, humidity, voltage, battery)) 158 | 159 | except Exception as e: 160 | logging.info("xiaomi error: %s" % e.message) 161 | 162 | 163 | def load_config(config): 164 | # Register sensor 165 | pheater = config.get_printer().lookup_object("heaters") 166 | pheater.add_sensor_factory('XIAOMI_BLUE', XIAOMI_BLUE) 167 | -------------------------------------------------------------------------------- /klipper/temperature_fan.py: -------------------------------------------------------------------------------- 1 | # Support fans that are enabled when temperature exceeds a set threshold 2 | # 3 | # Copyright (C) 2016-2020 Kevin O'Connor 4 | # 5 | # This file may be distributed under the terms of the GNU GPLv3 license. 6 | from . import fan 7 | 8 | KELVIN_TO_CELSIUS = -273.15 9 | MAX_FAN_TIME = 5.0 10 | AMBIENT_TEMP = 25. 11 | PID_PARAM_BASE = 255. 12 | 13 | class TemperatureFan: 14 | def __init__(self, config): 15 | self.name = config.get_name().split()[1] 16 | self.printer = config.get_printer() 17 | self.fan = fan.Fan(config, default_shutdown_speed=1.) 18 | self.min_temp = config.getfloat('min_temp', minval=KELVIN_TO_CELSIUS) 19 | self.max_temp = config.getfloat('max_temp', above=self.min_temp) 20 | pheaters = self.printer.load_object(config, 'heaters') 21 | self.sensor = pheaters.setup_sensor(config) 22 | self.sensor.setup_minmax(self.min_temp, self.max_temp) 23 | self.sensor.setup_callback(self.temperature_callback) 24 | pheaters.register_sensor(config, self) 25 | self.speed_delay = self.sensor.get_report_time_delta() 26 | self.max_speed_conf = config.getfloat( 27 | 'max_speed', 1., above=0., maxval=1.) 28 | self.max_speed = self.max_speed_conf 29 | self.min_speed_conf = config.getfloat( 30 | 'min_speed', 0.3, minval=0., maxval=1.) 31 | self.min_speed = self.min_speed_conf 32 | self.last_temp = 0. 33 | self.last_temp_time = 0. 34 | self.target_temp_conf = config.getfloat( 35 | 'target_temp', 40. if self.max_temp > 40. else self.max_temp, 36 | minval=self.min_temp, maxval=self.max_temp) 37 | self.target_temp = self.target_temp_conf 38 | algos = {'watermark': ControlBangBang, 'pid': ControlPID} 39 | algo = config.getchoice('control', algos) 40 | self.control = algo(self, config) 41 | self.next_speed_time = 0. 42 | self.last_speed_value = 0. 43 | gcode = self.printer.lookup_object('gcode') 44 | gcode.register_mux_command( 45 | "SET_TEMPERATURE_FAN_TARGET", "TEMPERATURE_FAN", self.name, 46 | self.cmd_SET_TEMPERATURE_FAN_TARGET, 47 | desc=self.cmd_SET_TEMPERATURE_FAN_TARGET_help) 48 | 49 | def set_speed(self, read_time, value): 50 | if value <= 0.: 51 | value = 0. 52 | elif value < self.min_speed: 53 | value = self.min_speed 54 | if self.target_temp <= 0.: 55 | value = 0. 56 | if ((read_time < self.next_speed_time or not self.last_speed_value) 57 | and abs(value - self.last_speed_value) < 0.05): 58 | # No significant change in value - can suppress update 59 | return 60 | speed_time = read_time + self.speed_delay 61 | self.next_speed_time = speed_time + 0.75 * MAX_FAN_TIME 62 | self.last_speed_value = value 63 | self.fan.set_speed(speed_time, value) 64 | def temperature_callback(self, read_time, temp): 65 | self.last_temp = temp 66 | self.control.temperature_callback(read_time, temp) 67 | def get_temp(self, eventtime): 68 | return self.last_temp, self.target_temp 69 | def get_min_speed(self): 70 | return self.min_speed 71 | def get_max_speed(self): 72 | return self.max_speed 73 | def get_status(self, eventtime): 74 | status = self.fan.get_status(eventtime) 75 | status["temperature"] = self.last_temp 76 | status["target"] = self.target_temp 77 | return status 78 | cmd_SET_TEMPERATURE_FAN_TARGET_help = \ 79 | "Sets a temperature fan target and fan speed limits" 80 | def cmd_SET_TEMPERATURE_FAN_TARGET(self, gcmd): 81 | temp = gcmd.get_float('TARGET', self.target_temp_conf) 82 | self.set_temp(temp) 83 | min_speed = gcmd.get_float('MIN_SPEED', self.min_speed) 84 | max_speed = gcmd.get_float('MAX_SPEED', self.max_speed) 85 | if min_speed > max_speed: 86 | raise self.printer.command_error( 87 | "Requested min speed (%.1f) is greater than max speed (%.1f)" 88 | % (min_speed, max_speed)) 89 | self.set_min_speed(min_speed) 90 | self.set_max_speed(max_speed) 91 | 92 | def set_temp(self, degrees): 93 | if degrees and (degrees < self.min_temp or degrees > self.max_temp): 94 | raise self.printer.command_error( 95 | "Requested temperature (%.1f) out of range (%.1f:%.1f)" 96 | % (degrees, self.min_temp, self.max_temp)) 97 | self.target_temp = degrees 98 | 99 | def set_min_speed(self, speed): 100 | if speed and (speed < 0. or speed > 1.): 101 | raise self.printer.command_error( 102 | "Requested min speed (%.1f) out of range (0.0 : 1.0)" 103 | % (speed)) 104 | self.min_speed = speed 105 | 106 | def set_max_speed(self, speed): 107 | if speed and (speed < 0. or speed > 1.): 108 | raise self.printer.command_error( 109 | "Requested max speed (%.1f) out of range (0.0 : 1.0)" 110 | % (speed)) 111 | self.max_speed = speed 112 | 113 | ###################################################################### 114 | # Bang-bang control algo 115 | ###################################################################### 116 | 117 | class ControlBangBang: 118 | def __init__(self, temperature_fan, config): 119 | self.temperature_fan = temperature_fan 120 | self.reverse = config.getboolean('reverse', False) 121 | self.max_delta = config.getfloat('max_delta', 2.0, above=0.) 122 | self.heating = False 123 | def temperature_callback(self, read_time, temp): 124 | current_temp, target_temp = self.temperature_fan.get_temp(read_time) 125 | if (self.heating != self.reverse 126 | and temp >= target_temp + self.max_delta): 127 | self.heating = self.reverse 128 | elif (not self.heating == self.reverse 129 | and temp <= target_temp - self.max_delta): 130 | self.heating = not self.reverse 131 | if self.heating: 132 | self.temperature_fan.set_speed(read_time, 0.) 133 | else: 134 | self.temperature_fan.set_speed(read_time, 135 | self.temperature_fan.get_max_speed()) 136 | 137 | ###################################################################### 138 | # Proportional Integral Derivative (PID) control algo 139 | ###################################################################### 140 | 141 | PID_SETTLE_DELTA = 1. 142 | PID_SETTLE_SLOPE = .1 143 | 144 | class ControlPID: 145 | def __init__(self, temperature_fan, config): 146 | self.temperature_fan = temperature_fan 147 | self.reverse = config.getboolean('reverse', False) 148 | self.Kp = config.getfloat('pid_Kp') / PID_PARAM_BASE 149 | self.Ki = config.getfloat('pid_Ki') / PID_PARAM_BASE 150 | self.Kd = config.getfloat('pid_Kd') / PID_PARAM_BASE 151 | self.min_deriv_time = config.getfloat('pid_deriv_time', 2., above=0.) 152 | imax = config.getfloat('pid_integral_max', 153 | self.temperature_fan.get_max_speed(), minval=0.) 154 | self.temp_integ_max = imax / self.Ki 155 | self.prev_temp = AMBIENT_TEMP 156 | self.prev_temp_time = 0. 157 | self.prev_temp_deriv = 0. 158 | self.prev_temp_integ = 0. 159 | def temperature_callback(self, read_time, temp): 160 | current_temp, target_temp = self.temperature_fan.get_temp(read_time) 161 | time_diff = read_time - self.prev_temp_time 162 | # Calculate change of temperature 163 | temp_diff = temp - self.prev_temp 164 | if time_diff >= self.min_deriv_time: 165 | temp_deriv = temp_diff / time_diff 166 | else: 167 | temp_deriv = (self.prev_temp_deriv * (self.min_deriv_time-time_diff) 168 | + temp_diff) / self.min_deriv_time 169 | # Calculate accumulated temperature "error" 170 | temp_err = target_temp - temp 171 | temp_integ = self.prev_temp_integ + temp_err * time_diff 172 | temp_integ = max(0., min(self.temp_integ_max, temp_integ)) 173 | # Calculate output 174 | co = self.Kp*temp_err + self.Ki*temp_integ - self.Kd*temp_deriv 175 | bounded_co = max(0., min(self.temperature_fan.get_max_speed(), co)) 176 | if not self.reverse: 177 | self.temperature_fan.set_speed( 178 | read_time, max( 179 | self.temperature_fan.get_min_speed(), 180 | self.temperature_fan.get_max_speed() - bounded_co)) 181 | else: 182 | self.temperature_fan.set_speed( 183 | read_time, max(self.temperature_fan.get_min_speed(), 184 | bounded_co)) 185 | # Store state for next measurement 186 | self.prev_temp = temp 187 | self.prev_temp_time = read_time 188 | self.prev_temp_deriv = temp_deriv 189 | if co == bounded_co: 190 | self.prev_temp_integ = temp_integ 191 | 192 | def load_config_prefix(config): 193 | return TemperatureFan(config) 194 | -------------------------------------------------------------------------------- /moonraker/power.py: -------------------------------------------------------------------------------- 1 | # Raspberry Pi Power Control 2 | # 3 | # Copyright (C) 2020 Jordan Ruthe 4 | # 5 | # This file may be distributed under the terms of the GNU GPLv3 license. 6 | 7 | import logging 8 | import os 9 | import asyncio 10 | import json 11 | import struct 12 | import socket 13 | import gpiod 14 | import subprocess 15 | from tornado.ioloop import IOLoop 16 | from tornado.iostream import IOStream 17 | from tornado import gen 18 | from tornado.httpclient import AsyncHTTPClient 19 | from tornado.escape import json_decode 20 | 21 | 22 | class PrinterPower: 23 | def __init__(self, config): 24 | self.server = config.get_server() 25 | self.chip_factory = GpioChipFactory() 26 | self.devices = {} 27 | prefix_sections = config.get_prefix_sections("power") 28 | logging.info(f"Power component loading devices: {prefix_sections}") 29 | try: 30 | for section in prefix_sections: 31 | cfg = config[section] 32 | dev_type = cfg.get("type") 33 | if dev_type == "gpio": 34 | dev = GpioDevice(cfg, self.chip_factory) 35 | elif dev_type == "tplink_smartplug": 36 | dev = TPLinkSmartPlug(cfg) 37 | elif dev_type == "tasmota": 38 | dev = Tasmota(cfg) 39 | elif dev_type == "shelly": 40 | dev = Shelly(cfg) 41 | elif dev_type == "homeseer": 42 | dev = HomeSeer(cfg) 43 | elif dev_type == "shell_command": 44 | dev = ShellCommand(cfg) 45 | else: 46 | raise config.error(f"Unsupported Device Type: {dev_type}") 47 | self.devices[dev.get_name()] = dev 48 | except Exception: 49 | self.chip_factory.close() 50 | raise 51 | 52 | self.server.register_endpoint( 53 | "/machine/device_power/devices", ['GET'], 54 | self._handle_list_devices) 55 | self.server.register_endpoint( 56 | "/machine/device_power/status", ['GET'], 57 | self._handle_power_request) 58 | self.server.register_endpoint( 59 | "/machine/device_power/on", ['POST'], 60 | self._handle_power_request) 61 | self.server.register_endpoint( 62 | "/machine/device_power/off", ['POST'], 63 | self._handle_power_request) 64 | self.server.register_remote_method( 65 | "set_device_power", self.set_device_power) 66 | self.server.register_event_handler( 67 | "server:klippy_shutdown", self._handle_klippy_shutdown) 68 | self.server.register_notification("power:power_changed") 69 | IOLoop.current().spawn_callback( 70 | self._initalize_devices, list(self.devices.values())) 71 | 72 | async def _check_klippy_printing(self): 73 | klippy_apis = self.server.lookup_component('klippy_apis') 74 | result = await klippy_apis.query_objects( 75 | {'print_stats': None}, default={}) 76 | pstate = result.get('print_stats', {}).get('state', "").lower() 77 | return pstate == "printing" 78 | 79 | async def _initalize_devices(self, inital_devs): 80 | for dev in inital_devs: 81 | ret = dev.initialize() 82 | if asyncio.iscoroutine(ret): 83 | await ret 84 | 85 | async def _handle_klippy_shutdown(self): 86 | for name, dev in self.devices.items(): 87 | if hasattr(dev, "off_when_shutdown"): 88 | if dev.off_when_shutdown: 89 | logging.info( 90 | f"Powering off device [{name}] due to" 91 | " klippy shutdown") 92 | await self._process_request(dev, "off") 93 | 94 | async def _handle_list_devices(self, web_request): 95 | dev_list = [d.get_device_info() for d in self.devices.values()] 96 | output = {"devices": dev_list} 97 | return output 98 | 99 | async def _handle_power_request(self, web_request): 100 | args = web_request.get_args() 101 | ep = web_request.get_endpoint() 102 | if not args: 103 | raise self.server.error("No arguments provided") 104 | requsted_devs = {k: self.devices.get(k, None) for k in args} 105 | result = {} 106 | req = ep.split("/")[-1] 107 | for name, device in requsted_devs.items(): 108 | if device is not None: 109 | result[name] = await self._process_request(device, req) 110 | else: 111 | result[name] = "device_not_found" 112 | return result 113 | 114 | async def _process_request(self, device, req): 115 | if req in ["on", "off"]: 116 | cur_state = device.get_device_info()['status'] 117 | if req == cur_state: 118 | # device is already in requested state, do nothing 119 | return cur_state 120 | printing = await self._check_klippy_printing() 121 | if device.get_locked_while_printing() and printing: 122 | raise self.server.error( 123 | f"Unable to change power for {device.get_name()} " 124 | "while printing") 125 | ret = device.set_power(req) 126 | if asyncio.iscoroutine(ret): 127 | await ret 128 | dev_info = device.get_device_info() 129 | self.server.send_event("power:power_changed", dev_info) 130 | device.run_power_changed_action() 131 | elif req == "status": 132 | ret = device.refresh_status() 133 | if asyncio.iscoroutine(ret): 134 | await ret 135 | dev_info = device.get_device_info() 136 | else: 137 | raise self.server.error(f"Unsupported power request: {req}") 138 | return dev_info['status'] 139 | 140 | def set_device_power(self, device, state): 141 | status = None 142 | if isinstance(state, bool): 143 | status = "on" if state else "off" 144 | elif isinstance(state, str): 145 | status = state.lower() 146 | if status in ["true", "false"]: 147 | status = "on" if status == "true" else "off" 148 | if status not in ["on", "off"]: 149 | logging.info(f"Invalid state received: {state}") 150 | return 151 | if device not in self.devices: 152 | logging.info(f"No device found: {device}") 153 | return 154 | ioloop = IOLoop.current() 155 | ioloop.spawn_callback( 156 | self._process_request, self.devices[device], status) 157 | 158 | async def add_device(self, name, device): 159 | if name in self.devices: 160 | raise self.server.error( 161 | f"Device [{name}] already configured") 162 | ret = device.initialize() 163 | if asyncio.iscoroutine(ret): 164 | await ret 165 | self.devices[name] = device 166 | 167 | async def close(self): 168 | for device in self.devices.values(): 169 | if hasattr(device, "close"): 170 | ret = device.close() 171 | if asyncio.iscoroutine(ret): 172 | await ret 173 | self.chip_factory.close() 174 | 175 | 176 | class PowerDevice: 177 | def __init__(self, config): 178 | name_parts = config.get_name().split(maxsplit=1) 179 | if len(name_parts) != 2: 180 | raise config.error(f"Invalid Section Name: {config.get_name()}") 181 | self.server = config.get_server() 182 | self.name = name_parts[1] 183 | self.state = "init" 184 | self.locked_while_printing = config.getboolean( 185 | 'locked_while_printing', False) 186 | self.off_when_shutdown = config.getboolean('off_when_shutdown', False) 187 | self.restart_delay = 1. 188 | self.klipper_restart = config.getboolean( 189 | 'restart_klipper_when_powered', False) 190 | if self.klipper_restart: 191 | self.restart_delay = config.getfloat('restart_delay', 1.) 192 | if self.restart_delay < .000001: 193 | raise config.error("Option 'restart_delay' must be above 0.0") 194 | 195 | def get_name(self): 196 | return self.name 197 | 198 | def get_device_info(self): 199 | return { 200 | 'device': self.name, 201 | 'status': self.state, 202 | 'locked_while_printing': self.locked_while_printing 203 | } 204 | 205 | def get_locked_while_printing(self): 206 | return self.locked_while_printing 207 | 208 | def run_power_changed_action(self): 209 | if self.state == "on" and self.klipper_restart: 210 | ioloop = IOLoop.current() 211 | klippy_apis = self.server.lookup_component("klippy_apis") 212 | ioloop.call_later(self.restart_delay, klippy_apis.do_restart, 213 | "FIRMWARE_RESTART") 214 | 215 | 216 | class GpioChipFactory: 217 | def __init__(self): 218 | self.chips = {} 219 | 220 | def get_gpio_chip(self, chip_name): 221 | if chip_name in self.chips: 222 | return self.chips[chip_name] 223 | chip = gpiod.Chip(chip_name, gpiod.Chip.OPEN_BY_NAME) 224 | self.chips[chip_name] = chip 225 | return chip 226 | 227 | def close(self): 228 | for chip in self.chips.values(): 229 | chip.close() 230 | 231 | 232 | class GpioDevice(PowerDevice): 233 | def __init__(self, config, chip_factory): 234 | super().__init__(config) 235 | pin, chip_id, invert = self._parse_pin(config) 236 | try: 237 | chip = chip_factory.get_gpio_chip(chip_id) 238 | self.line = chip.get_line(pin) 239 | if invert: 240 | self.line.request( 241 | consumer="moonraker", type=gpiod.LINE_REQ_DIR_OUT, 242 | flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW) 243 | else: 244 | self.line.request( 245 | consumer="moonraker", type=gpiod.LINE_REQ_DIR_OUT) 246 | except Exception: 247 | self.state = "error" 248 | logging.exception( 249 | f"Unable to init {pin}. Make sure the gpio is not in " 250 | "use by another program or exported by sysfs.") 251 | raise config.error("Power GPIO Config Error") 252 | self.initial_state = config.getboolean('initial_state', False) 253 | 254 | def _parse_pin(self, config): 255 | pin = cfg_pin = config.get("pin") 256 | invert = False 257 | if pin[0] == "!": 258 | pin = pin[1:] 259 | invert = True 260 | chip_id = "gpiochip0" 261 | pin_parts = pin.split("/") 262 | if len(pin_parts) == 2: 263 | chip_id, pin = pin_parts 264 | elif len(pin_parts) == 1: 265 | pin = pin_parts[0] 266 | # Verify pin 267 | if not chip_id.startswith("gpiochip") or \ 268 | not chip_id[-1].isdigit() or \ 269 | not pin.startswith("gpio") or \ 270 | not pin[4:].isdigit(): 271 | raise config.error( 272 | f"Invalid Power Pin configuration: {cfg_pin}") 273 | pin = int(pin[4:]) 274 | return pin, chip_id, invert 275 | 276 | def initialize(self): 277 | self.set_power("on" if self.initial_state else "off") 278 | 279 | def get_device_info(self): 280 | return { 281 | **super().get_device_info(), 282 | 'type': "gpio" 283 | } 284 | 285 | def refresh_status(self): 286 | try: 287 | val = self.line.get_value() 288 | except Exception: 289 | self.state = "error" 290 | msg = f"Error Refeshing Device Status: {self.name}" 291 | logging.exception(msg) 292 | raise self.server.error(msg) from None 293 | self.state = "on" if val else "off" 294 | 295 | def set_power(self, state): 296 | try: 297 | self.line.set_value(int(state == "on")) 298 | except Exception: 299 | self.state = "error" 300 | msg = f"Error Toggling Device Power: {self.name}" 301 | logging.exception(msg) 302 | raise self.server.error(msg) from None 303 | self.state = state 304 | 305 | def close(self): 306 | self.line.release() 307 | 308 | 309 | # This implementation based off the work tplink_smartplug 310 | # script by Lubomir Stroetmann available at: 311 | # 312 | # https://github.com/softScheck/tplink-smartplug 313 | # 314 | # Copyright 2016 softScheck GmbH 315 | class TPLinkSmartPlug(PowerDevice): 316 | START_KEY = 0xAB 317 | 318 | def __init__(self, config): 319 | super().__init__(config) 320 | self.server = config.get_server() 321 | self.addr = config.get("address").split('/') 322 | self.port = config.getint("port", 9999) 323 | 324 | async def _send_tplink_command(self, command): 325 | out_cmd = {} 326 | if command in ["on", "off"]: 327 | out_cmd = { 328 | 'system': {'set_relay_state': {'state': int(command == "on")}} 329 | } 330 | if len(self.addr) == 2: # TPLink device controls multiple devices 331 | sysinfo = await self._send_tplink_command("info") 332 | out_cmd["context"] = { 333 | 'child_ids': 334 | [ 335 | sysinfo["system"]["get_sysinfo"]["deviceId"] 336 | + '%02d' % int(self.addr[1]) 337 | ] 338 | } 339 | elif command == "info": 340 | out_cmd = {'system': {'get_sysinfo': {}}} 341 | else: 342 | raise self.server.error(f"Invalid tplink command: {command}") 343 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 344 | stream = IOStream(s) 345 | try: 346 | await stream.connect((self.addr[0], self.port)) 347 | await stream.write(self._encrypt(out_cmd)) 348 | data = await stream.read_bytes(2048, partial=True) 349 | length = struct.unpack(">I", data[:4])[0] 350 | data = data[4:] 351 | retries = 5 352 | remaining = length - len(data) 353 | while remaining and retries: 354 | data += await stream.read_bytes(remaining) 355 | remaining = length - len(data) 356 | retries -= 1 357 | if not retries: 358 | raise self.server.error("Unable to read tplink packet") 359 | except Exception: 360 | msg = f"Error sending tplink command: {command}" 361 | logging.exception(msg) 362 | raise self.server.error(msg) 363 | finally: 364 | stream.close() 365 | return json.loads(self._decrypt(data)) 366 | 367 | def _encrypt(self, data): 368 | data = json.dumps(data) 369 | key = self.START_KEY 370 | res = struct.pack(">I", len(data)) 371 | for c in data: 372 | val = key ^ ord(c) 373 | key = val 374 | res += bytes([val]) 375 | return res 376 | 377 | def _decrypt(self, data): 378 | key = self.START_KEY 379 | res = "" 380 | for c in data: 381 | val = key ^ c 382 | key = c 383 | res += chr(val) 384 | return res 385 | 386 | async def initialize(self): 387 | await self.refresh_status() 388 | 389 | def get_device_info(self): 390 | return { 391 | **super().get_device_info(), 392 | 'type': "tplink_smartplug" 393 | } 394 | 395 | async def refresh_status(self): 396 | try: 397 | res = await self._send_tplink_command("info") 398 | if len(self.addr) == 2: # TPLink device controls multiple devices 399 | state = res['system']['get_sysinfo']['children'][ 400 | int(self.addr[1])]['state'] 401 | else: 402 | state = res['system']['get_sysinfo']['relay_state'] 403 | except Exception: 404 | self.state = "error" 405 | msg = f"Error Refeshing Device Status: {self.name}" 406 | logging.exception(msg) 407 | raise self.server.error(msg) from None 408 | self.state = "on" if state else "off" 409 | 410 | async def set_power(self, state): 411 | try: 412 | res = await self._send_tplink_command(state) 413 | err = res['system']['set_relay_state']['err_code'] 414 | except Exception: 415 | err = 1 416 | logging.exception(f"Power Toggle Error: {self.name}") 417 | if err: 418 | self.state = "error" 419 | raise self.server.error( 420 | f"Error Toggling Device Power: {self.name}") 421 | self.state = state 422 | 423 | 424 | class Tasmota(PowerDevice): 425 | def __init__(self, config): 426 | super().__init__(config) 427 | self.server = config.get_server() 428 | self.addr = config.get("address") 429 | self.output_id = config.getint("output_id", 1) 430 | self.password = config.get("password", "") 431 | 432 | async def _send_tasmota_command(self, command, password=None): 433 | if command in ["on", "off"]: 434 | out_cmd = f"Power{self.output_id}%20{command}" 435 | elif command == "info": 436 | out_cmd = f"Power{self.output_id}" 437 | else: 438 | raise self.server.error(f"Invalid tasmota command: {command}") 439 | 440 | url = f"http://{self.addr}/cm?user=admin&password=" \ 441 | f"{self.password}&cmnd={out_cmd}" 442 | data = "" 443 | http_client = AsyncHTTPClient() 444 | try: 445 | response = await http_client.fetch(url) 446 | data = json_decode(response.body) 447 | except Exception: 448 | msg = f"Error sending tplink command: {command}" 449 | logging.exception(msg) 450 | raise self.server.error(msg) 451 | return data 452 | 453 | async def initialize(self): 454 | await self.refresh_status() 455 | 456 | def get_device_info(self): 457 | return { 458 | **super().get_device_info(), 459 | 'type': "tasmota" 460 | } 461 | 462 | async def refresh_status(self): 463 | try: 464 | res = await self._send_tasmota_command("info") 465 | state = res[f"POWER{self.output_id}"].lower() 466 | except Exception: 467 | self.state = "error" 468 | msg = f"Error Refeshing Device Status: {self.name}" 469 | logging.exception(msg) 470 | raise self.server.error(msg) from None 471 | self.state = state 472 | 473 | async def set_power(self, state): 474 | try: 475 | res = await self._send_tasmota_command(state) 476 | state = res[f"POWER{self.output_id}"].lower() 477 | except Exception: 478 | self.state = "error" 479 | msg = f"Error Setting Device Status: {self.name} to {state}" 480 | logging.exception(msg) 481 | raise self.server.error(msg) from None 482 | self.state = state 483 | 484 | 485 | class Shelly(PowerDevice): 486 | def __init__(self, config): 487 | super().__init__(config) 488 | self.server = config.get_server() 489 | self.addr = config.get("address") 490 | self.output_id = config.getint("output_id", 0) 491 | self.user = config.get("user", "admin") 492 | self.password = config.get("password", "") 493 | 494 | async def _send_shelly_command(self, command): 495 | if command in ["on", "off"]: 496 | out_cmd = f"relay/{self.output_id}?turn={command}" 497 | elif command == "info": 498 | out_cmd = f"relay/{self.output_id}" 499 | else: 500 | raise self.server.error(f"Invalid shelly command: {command}") 501 | if self.password != "": 502 | out_pwd = f"{self.user}:{self.password}@" 503 | else: 504 | out_pwd = f"" 505 | url = f"http://{out_pwd}{self.addr}/{out_cmd}" 506 | data = "" 507 | http_client = AsyncHTTPClient() 508 | try: 509 | response = await http_client.fetch(url) 510 | data = json_decode(response.body) 511 | except Exception: 512 | msg = f"Error sending shelly command: {command}" 513 | logging.exception(msg) 514 | raise self.server.error(msg) 515 | return data 516 | 517 | async def initialize(self): 518 | await self.refresh_status() 519 | 520 | def get_device_info(self): 521 | return { 522 | **super().get_device_info(), 523 | 'type': "shelly" 524 | } 525 | 526 | async def refresh_status(self): 527 | try: 528 | res = await self._send_shelly_command("info") 529 | state = res[f"ison"] 530 | except Exception: 531 | self.state = "error" 532 | msg = f"Error Refeshing Device Status: {self.name}" 533 | logging.exception(msg) 534 | raise self.server.error(msg) from None 535 | self.state = "on" if state else "off" 536 | 537 | async def set_power(self, state): 538 | try: 539 | res = await self._send_shelly_command(state) 540 | state = res[f"ison"] 541 | except Exception: 542 | self.state = "error" 543 | msg = f"Error Setting Device Status: {self.name} to {state}" 544 | logging.exception(msg) 545 | raise self.server.error(msg) from None 546 | self.state = "on" if state else "off" 547 | 548 | 549 | class HomeSeer(PowerDevice): 550 | def __init__(self, config): 551 | super().__init__(config) 552 | self.server = config.get_server() 553 | self.addr = config.get("address") 554 | self.device = config.getint("device") 555 | self.user = config.get("user", "admin") 556 | self.password = config.get("password", "") 557 | 558 | async def _send_homeseer(self, request, additional=""): 559 | url = (f"http://{self.user}:{self.password}@{self.addr}" 560 | f"/JSON?user={self.user}&pass={self.password}" 561 | f"&request={request}&ref={self.device}&{additional}") 562 | data = "" 563 | http_client = AsyncHTTPClient() 564 | try: 565 | response = await http_client.fetch(url) 566 | data = json_decode(response.body) 567 | except Exception: 568 | msg = f"Error sending HomeSeer command: {request}" 569 | logging.exception(msg) 570 | raise self.server.error(msg) 571 | return data 572 | 573 | async def initialize(self): 574 | await self.refresh_status() 575 | 576 | def get_device_info(self): 577 | return { 578 | **super().get_device_info(), 579 | 'type': "homeseer" 580 | } 581 | 582 | async def refresh_status(self): 583 | try: 584 | res = await self._send_homeseer("getstatus") 585 | state = res[f"Devices"][0]["status"].lower() 586 | except Exception: 587 | self.state = "error" 588 | msg = f"Error Refeshing Device Status: {self.name}" 589 | logging.exception(msg) 590 | raise self.server.error(msg) from None 591 | self.state = state 592 | 593 | async def set_power(self, state): 594 | try: 595 | if state == "on": 596 | state_hs = "On" 597 | elif state == "off": 598 | state_hs = "Off" 599 | res = await self._send_homeseer("controldevicebylabel", 600 | f"label={state_hs}") 601 | except Exception: 602 | self.state = "error" 603 | msg = f"Error Setting Device Status: {self.name} to {state}" 604 | logging.exception(msg) 605 | raise self.server.error(msg) from None 606 | self.state = state 607 | 608 | 609 | class ShellCommand(PowerDevice): 610 | def __init__(self, config): 611 | super().__init__(config) 612 | self.on = config.get("on") # 开机命令 613 | self.off = config.get("off") # 关机命令 614 | 615 | def initialize(self): 616 | pass 617 | 618 | def get_device_info(self): 619 | return { 620 | **super().get_device_info(), 621 | 'type': "shell_command" 622 | } 623 | 624 | # def refresh_status(self): 625 | # # 通过命令查询状态 626 | # # 如果失败的话,提示错误 627 | # # raise self.server.error(msg) from None 628 | # 629 | # # 根据查询状态定义state的值 630 | # # self.state = "on" if val else "off" 631 | # 632 | # pass 633 | 634 | def set_power(self, state): 635 | # state == "on" 就打开电源,state == "off" 就关闭电源 636 | if state == "on": 637 | result = subprocess.Popen(self.on, shell=True, 638 | stdout=subprocess.PIPE, 639 | stderr=subprocess.STDOUT) 640 | msg = f"shell command run result: {result.stdout.read().decode('utf-8')}" 641 | logging.exception(msg) 642 | elif state == "off": 643 | result = subprocess.Popen(self.on, shell=True, 644 | stdout=subprocess.PIPE, 645 | stderr=subprocess.STDOUT) 646 | msg = f"shell command run result: {result.stdout.read().decode('utf-8')}" 647 | logging.exception(msg) 648 | 649 | self.state = state 650 | pass 651 | 652 | 653 | # The power component has multiple configuration sections 654 | def load_component_multi(config): 655 | return PrinterPower(config) 656 | --------------------------------------------------------------------------------