diff --git a/gpio_new.coffee b/gpio_new.coffee new file mode 100644 index 0000000..6a5454c --- /dev/null +++ b/gpio_new.coffee @@ -0,0 +1,455 @@ +module.exports = (env) -> + + # pimatic imports. + Promise = env.require 'bluebird' + assert = env.require 'cassert' + _ = env.require 'lodash' + + # env in Closure speichern - DAS ist der Schlüssel! + _env = env + + # Für Prozess-Kommunikation + spawn = require('child_process').spawn + { EventEmitter } = require 'events' + exec = require('child_process').exec + util = require('util') + execPromise = util.promisify(exec) + fs = require 'fs' + + # ================================================== + # Python-Script mit ECHTER Entprellung + # ================================================== + scriptPath = __dirname + '/gpio_poller.py' + + try + fs.accessSync scriptPath, fs.constants.F_OK + _env.logger.debug "GPIO-Poller: Python-Script existiert bereits" + catch + _env.logger.info "GPIO-Poller: Erstelle Python-Script mit Entprellung" + + pythonScript = '''#!/usr/bin/env python3 +import sys +import time +import json +import gpiod +import argparse +import signal +from collections import defaultdict + +parser = argparse.ArgumentParser() +parser.add_argument('--pins', nargs='+', type=int, required=True) +parser.add_argument('--debounce', nargs='+', type=str, required=True) +args = parser.parse_args() + +GPIO_PINS = args.pins +POLL_INTERVAL = 0.01 +CHIP_PATH = '/dev/gpiochip0' + +debounce_config = {} +for entry in args.debounce: + try: + pin, ms = entry.split('=') + debounce_config[int(pin)] = int(ms) + except: + pass + +for pin in GPIO_PINS: + if pin not in debounce_config: + debounce_config[pin] = 50 + +debounce_state = {} + +def signal_handler(sig, frame): + sys.stderr.write(json.dumps({"info": "Python-Poller wird beendet"}) + "\\n") + sys.exit(0) + +signal.signal(signal.SIGINT, signal_handler) +signal.signal(signal.SIGTERM, signal_handler) + +def main(): + sys.stderr.write(json.dumps({"info": f"Poller für Pins: {GPIO_PINS}, Debounce: {debounce_config}"}) + "\\n") + + chip = gpiod.Chip(CHIP_PATH) + line_settings = gpiod.LineSettings() + line_settings.direction = gpiod.line.Direction.INPUT + + config = {pin: line_settings for pin in GPIO_PINS} + request = chip.request_lines(consumer="pimatic-poller", config=config) + + try: + while True: + values = request.get_values() + current_time = time.time() * 1000 + + for i, pin in enumerate(GPIO_PINS): + if i < len(values): + raw_value = 1 if values[i] == gpiod.line.Value.ACTIVE else 0 + debounce_ms = debounce_config[pin] + + if pin not in debounce_state: + debounce_state[pin] = { + 'last_value': raw_value, + 'stable_value': raw_value, + 'last_change_time': current_time, + 'last_output_time': 0 + } + status = "active" if raw_value == 1 else "inactive" + print(f'"{pin}"={status}') + sys.stdout.flush() + continue + + state = debounce_state[pin] + + if raw_value != state['last_value']: + state['last_value'] = raw_value + state['last_change_time'] = current_time + else: + if (raw_value != state['stable_value'] and + current_time - state['last_change_time'] >= debounce_ms): + state['stable_value'] = raw_value + + if current_time - state['last_output_time'] >= 50: + state['last_output_time'] = current_time + status = "active" if raw_value == 1 else "inactive" + print(f'"{pin}"={status}') + sys.stdout.flush() + + time.sleep(POLL_INTERVAL) + + except Exception as e: + sys.stderr.write(json.dumps({"error": str(e)}) + "\\n") + finally: + request.release() + chip.close() + +if __name__ == "__main__": + main() +''' + + fs.writeFileSync scriptPath, pythonScript + fs.chmodSync scriptPath, '755' + _env.logger.info "GPIO-Poller: Python-Script mit Entprellung erstellt unter #{scriptPath}" + + # ================================================== + # GpioPoller Klasse + # ================================================== + class GpioPoller extends EventEmitter + constructor: (@env) -> + @process = null + @activePins = [] + @listeners = {} + @debounceConfig = {} + @restartTimeout = null + @env.logger.debug "GPIO-Poller: Initialisiert" + + addPin: (pin, chip = 'gpiochip0', deviceId, deviceName, debounceMs = 50) -> + pinStr = pin.toString() + + if not @debounceConfig[pinStr]? or debounceMs < @debounceConfig[pinStr] + @debounceConfig[pinStr] = debounceMs + @env.logger.debug "GPIO-Poller: Pin #{pin} Debounce auf #{debounceMs}ms gesetzt" + + if not @listeners[pinStr]? + @listeners[pinStr] = [] + @activePins.push(pin) + @_restartPoller() + + @listeners[pinStr].push { deviceId, deviceName } + @env.logger.debug "GPIO-Poller: Pin #{pin} wird für '#{deviceName}' überwacht (Debounce: #{debounceMs}ms)" + + removePin: (pin, deviceId) -> + pinStr = pin.toString() + + if @listeners[pinStr]? + @listeners[pinStr] = @listeners[pinStr].filter (l) -> l.deviceId isnt deviceId + + if @listeners[pinStr].length is 0 + delete @listeners[pinStr] + delete @debounceConfig[pinStr] + @activePins = @activePins.filter (p) -> p.toString() isnt pinStr + @env.logger.debug "GPIO-Poller: Pin #{pin} wird nicht mehr überwacht" + @_restartPoller() + + _restartPoller: -> + clearTimeout(@restartTimeout) if @restartTimeout? + @restartTimeout = setTimeout(=> + @_startPoller() + , 100) + + _startPoller: -> + @_stopPoller() + + if @activePins.length is 0 + @env.logger.debug "GPIO-Poller: Keine aktiven Pins, Prozess wird nicht gestartet" + return + + @env.logger.debug "GPIO-Poller: Starte Python-Prozess für Pins: #{@activePins.join(', ')}" + + debounceArgs = [] + for pin in @activePins + pinStr = pin.toString() + debounceMs = @debounceConfig[pinStr] or 50 + debounceArgs.push("#{pin}=#{debounceMs}") + + args = ['--pins'].concat(@activePins).concat(['--debounce']).concat(debounceArgs) + @process = spawn('python3', [scriptPath].concat(args)) + + @process.stdout.on 'data', (data) => + lines = data.toString().split('\n') + for line in lines + line = line.trim() + if line.length > 0 + @_parseOutput(line) + + @process.stderr.on 'data', (data) => + try + jsonData = JSON.parse(data.toString()) + if jsonData.debug + @env.logger.debug "GPIO-Poller: #{jsonData.debug}" + else if jsonData.info + @env.logger.debug "GPIO-Poller: #{jsonData.info}" + else if jsonData.error + @env.logger.error "GPIO-Poller: #{jsonData.error}" + catch + msg = data.toString().trim() + if msg.length > 0 + @env.logger.debug "GPIO-Poller: #{msg}" + + @process.on 'exit', (code, signal) => + @env.logger.debug "GPIO-Poller: Python-Prozess beendet (Code: #{code}, Signal: #{signal})" + @process = null + + if @activePins.length > 0 + @env.logger.debug "GPIO-Poller: Neustart in 1 Sekunde..." + setTimeout(=> + @_startPoller() if @activePins.length > 0 + , 1000) + + _stopPoller: -> + if @process + @env.logger.debug "GPIO-Poller: Stoppe Python-Prozess" + @process.kill() + @process = null + + _parseOutput: (line) -> + parts = line.split(' ') + for part in parts + match = part.match(/"(\d+)"=(\w+)/) + if match + pin = match[1] + status = match[2] is 'active' + @emit "pin:#{pin}", status + + destroy: -> + @env.logger.debug "GPIO-Poller: Wird zerstört" + @_stopPoller() + clearTimeout(@restartTimeout) if @restartTimeout? + + # ================================================== + # GLOBALEN Poller INSTANZIEREN + # ================================================== + globalGpioPoller = new GpioPoller(env) + + # ================================================== + # GpioSwitch - Optimale v2.2.1 Lösung mit --mode=signal + # ================================================== + class GpioSwitch extends env.devices.PowerSwitch + + constructor: (@config, lastState) -> + @name = @config.name + @id = @config.id + @gpio = @config.gpio + @chip = @config.chip or 'gpiochip0' + + @_state = lastState?.state?.value or @config.defaultState or false + @_gpioProcess = null + + @_setGpioState(@_state) + super() + + destroy: () -> + @_stopGpioProcess() + if globalGpioPoller? and typeof globalGpioPoller.removePin is 'function' + globalGpioPoller.removePin(@gpio, @id) if @gpio? + super() + + _stopGpioProcess: -> + if @_gpioProcess + _env.logger.debug "GpioSwitch: Beende GPIO-Prozess für Pin #{@gpio}" + try + @_gpioProcess.kill('SIGTERM') + catch e + _env.logger.error "GpioSwitch: Fehler beim Beenden von Pin #{@gpio}: #{e.message}" + @_gpioProcess = null + + _setGpioState: (state) -> + valueToWrite = if state then 1 else 0 + valueToWrite = (if @config.inverted then 1 - valueToWrite else valueToWrite) + + @_stopGpioProcess() + + @_gpioProcess = spawn('gpioset', [ + '--chip', @chip, + '--mode=signal', + '--background', + "#{@gpio}=#{valueToWrite}" + ]) + + @_gpioProcess.on 'error', (error) => + _env.logger.error "GpioSwitch: Fehler Pin #{@gpio}: #{error.message}" + @_gpioProcess = null + + @_gpioProcess.unref() + + _env.logger.debug "GpioSwitch: GPIO #{@gpio} auf #{valueToWrite} gesetzt (state: #{state})" + + getState: () -> + Promise.resolve(@_state) + + changeStateTo: (state) -> + assert state is on or state is off + @_setGpioState(state) + @_setState(state) + Promise.resolve() + + # ================================================== + # GpioContact - MIT ECHTER ENTRELLUNG + # ================================================== + class GpioContact extends env.devices.ContactSensor + + constructor: (@config, lastState) -> + @id = @config.id + @name = @config.name + @gpio = @config.gpio + @chip = @config.chip or 'gpiochip0' + + @debounceMs = @config.debounceTimeout + if not @debounceMs? + @debounceMs = 50 + else if @debounceMs < 5 + @debounceMs = 5 + + @_currentContactState = lastState?.contact?.value or false + + # _env.logger verwenden! + _env.logger.debug "GpioContact: Erstelle '#{@name}' mit Pin #{@gpio}, Debounce #{@debounceMs}ms" + + # Beim Poller anmelden + if globalGpioPoller? and typeof globalGpioPoller.addPin is 'function' + globalGpioPoller.addPin(@gpio, @chip, @id, @name, @debounceMs) + else + _env.logger.error "GpioContact: globalGpioPoller nicht verfügbar für Pin #{@gpio}" + + @_pinHandler = (status) => + newCalculatedState = (if @config.inverted then not status else status) + + if newCalculatedState isnt @_currentContactState + @_currentContactState = newCalculatedState + @_setContact(@_currentContactState) + # _env.logger verwenden! + _env.logger.debug "GpioContact '#{@name}': Zustand geändert zu #{if @_currentContactState then 'geschlossen' else 'offen'}" + + if globalGpioPoller? + globalGpioPoller.on "pin:#{@gpio}", @_pinHandler + + super() + + destroy: () -> + if globalGpioPoller? + globalGpioPoller.removePin(@gpio, @id) if typeof globalGpioPoller.removePin is 'function' + globalGpioPoller.removeListener("pin:#{@gpio}", @_pinHandler) if @_pinHandler? + super() + + getContact: () -> + Promise.resolve(@_currentContactState) + + # ================================================== + # GpioPresence - MIT ECHTER ENTRELLUNG + # ================================================== + class GpioPresence extends env.devices.PresenceSensor + + constructor: (@config, lastState) -> + @id = @config.id + @name = @config.name + @gpio = @config.gpio + @chip = @config.chip or 'gpiochip0' + + @debounceMs = @config.debounceTimeout + if not @debounceMs? + @debounceMs = 50 + else if @debounceMs < 5 + @debounceMs = 5 + + @_presence = lastState?.presence?.value or false + + # _env.logger verwenden! + _env.logger.debug "GpioPresence: Erstelle '#{@name}' mit Pin #{@gpio}, Debounce #{@debounceMs}ms" + + # Beim Poller anmelden + if globalGpioPoller? and typeof globalGpioPoller.addPin is 'function' + globalGpioPoller.addPin(@gpio, @chip, @id, @name, @debounceMs) + else + _env.logger.error "GpioPresence: globalGpioPoller nicht verfügbar für Pin #{@gpio}" + + @_pinHandler = (status) => + newState = (if @config.inverted then not status else status) + + if newState isnt @_presence + @_presence = newState + @_setPresence(@_presence) + # _env.logger verwenden! + _env.logger.debug "GpioPresence '#{@name}': Zustand geändert zu #{@_presence}" + + if globalGpioPoller? + globalGpioPoller.on "pin:#{@gpio}", @_pinHandler + + super() + + destroy: () -> + if globalGpioPoller? + globalGpioPoller.removePin(@gpio, @id) if typeof globalGpioPoller.removePin is 'function' + globalGpioPoller.removeListener("pin:#{@gpio}", @_pinHandler) if @_pinHandler? + super() + + getPresence: () -> Promise.resolve(@_presence) + + # ================================================== + # Plugin-Klasse + # ================================================== + class GpioPlugin extends env.plugins.Plugin + + init: (app, @framework, @config) -> + + deviceConfigDef = require("./device-config-schema") + + # Poller im Framework verfügbar machen + @framework.gpioPoller = globalGpioPoller + + @framework.deviceManager.registerDeviceClass("GpioPresence", { + configDef: deviceConfigDef.GpioPresence, + createCallback: (config, lastState) => new GpioPresence(config, lastState) + }) + + @framework.deviceManager.registerDeviceClass("GpioContact", { + configDef: deviceConfigDef.GpioContact, + createCallback: (config, lastState) => new GpioContact(config, lastState) + }) + + @framework.deviceManager.registerDeviceClass("GpioSwitch", { + configDef: deviceConfigDef.GpioSwitch, + createCallback: (config, lastState) => new GpioSwitch(config, lastState) + }) + + destroy: () -> + globalGpioPoller.destroy() + super() + + plugin = new GpioPlugin + + # For testing... + plugin.GpioSwitch = GpioSwitch + plugin.GpioPresence = GpioPresence + plugin.GpioContact = GpioContact + plugin.GpioPoller = GpioPoller + + return plugin