Emergency Stop
This commit is contained in:
166
robot/ShellyEmergencyStop.js
Normal file
166
robot/ShellyEmergencyStop.js
Normal file
@@ -0,0 +1,166 @@
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const SenderInterface = require('./SenderInterface');
|
||||
|
||||
/**
|
||||
* Steuert einen Shelly Smart Plug als Emergency-Stop-Aktor.
|
||||
*
|
||||
* emergencyStop() → Switch.Set?id=0&on=false (Strom abschalten)
|
||||
* powerOn() → Switch.Set?id=0&on=true (Strom einschalten)
|
||||
* alarmUnlock() → no-op / skipped (kein FluidNC-Alarm)
|
||||
*
|
||||
* Implementiert SenderInterface — empfängt aber keinen GCode (send() ist no-op).
|
||||
* startRobot.js trägt diese Klasse NICHT in robot.cmdReceivers ein.
|
||||
*/
|
||||
module.exports = class ShellyEmergencyStop extends SenderInterface {
|
||||
|
||||
/**
|
||||
* @param {string|null} url Shelly-RPC-URL für Switch.Set?on=false,
|
||||
* z.B. "http://shelly.local/rpc/Switch.Set?id=0&on=false".
|
||||
* null → kein Shelly konfiguriert (alle Methoden liefern {ok:false}).
|
||||
* @param {object} options
|
||||
* @param {function} [options.httpGetFn] DI für Tests; Standard: Node http.get
|
||||
*/
|
||||
constructor(url, options = {}) {
|
||||
super();
|
||||
this._offUrl = url || null;
|
||||
this._onUrl = url ? url.replace('on=false', 'on=true') : null;
|
||||
// Switch.GetStatus?id=0 — leitet Status-URL aus der Switch.Set-URL ab
|
||||
this._statusUrl = url ? url.replace(/\/rpc\/.*$/, '/rpc/Switch.GetStatus?id=0') : null;
|
||||
this.url = url || null; // für getStatus / InfoServer-Anzeige
|
||||
this.state = 'ready';
|
||||
this.error = null;
|
||||
this._httpGet = options.httpGetFn || ShellyEmergencyStop._defaultHttpGet;
|
||||
this._httpGetJson = options.httpGetJsonFn || ShellyEmergencyStop._defaultHttpGetJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard-HTTP-GET über Node's eingebautes http-Modul (kein npm-Paket nötig).
|
||||
* Response-Body wird verworfen (nur Status-Code relevant).
|
||||
* @private
|
||||
*/
|
||||
static _defaultHttpGet(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(url, res => {
|
||||
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode });
|
||||
res.resume(); // Response-Body verwerfen
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard-HTTP-GET mit JSON-Body-Parsing.
|
||||
* Für Switch.GetStatus — Response-Body enthält { output, apower, voltage, ... }.
|
||||
* @private
|
||||
*/
|
||||
static _defaultHttpGetJson(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(url, res => {
|
||||
let body = '';
|
||||
res.on('data', chunk => { body += chunk; });
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, data });
|
||||
} catch {
|
||||
resolve({ ok: false, status: res.statusCode, data: null, error: 'JSON parse error' });
|
||||
}
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
// ── SenderInterface ──────────────────────────────────────────────────────
|
||||
|
||||
/** Shelly braucht kein persistentes Socket — sofort ready. */
|
||||
async connect() { return this; }
|
||||
|
||||
/** Legt keine Ressourcen frei (kein Socket), aber aktualisiert den State. */
|
||||
disconnect() { this.state = 'disconnected'; }
|
||||
|
||||
/** Kein GCode-Empfänger — wird immer ignoriert. */
|
||||
send(/* cmd */) { return false; }
|
||||
|
||||
getStatus() {
|
||||
return { state: this.state, url: this.url, error: this.error };
|
||||
}
|
||||
|
||||
// ── Emergency Stop / Power ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Strom abschalten.
|
||||
* Wird von POST /api/emergency-stop im InfoServer aufgerufen.
|
||||
* @returns {{ ok: boolean, status?: number, error?: string }}
|
||||
*/
|
||||
async emergencyStop() {
|
||||
if (!this._offUrl) return { ok: false, error: 'no shelly url configured' };
|
||||
try {
|
||||
const result = await this._httpGet(this._offUrl);
|
||||
this.state = result.ok ? 'stopped' : 'error';
|
||||
this.error = result.ok ? null : `HTTP ${result.status}`;
|
||||
console.log(`[Shelly] power OFF → ${result.ok ? 'OK' : `HTTP ${result.status}`}`);
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.state = 'error';
|
||||
this.error = err.message;
|
||||
console.error(`[Shelly] power OFF failed: ${err.message}`);
|
||||
return { ok: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strom wieder einschalten.
|
||||
* Wird von POST /api/power-on im InfoServer aufgerufen.
|
||||
* @returns {{ ok: boolean, status?: number, error?: string }}
|
||||
*/
|
||||
async powerOn() {
|
||||
if (!this._onUrl) return { ok: false, error: 'no shelly url configured' };
|
||||
try {
|
||||
const result = await this._httpGet(this._onUrl);
|
||||
this.state = result.ok ? 'ready' : 'error';
|
||||
this.error = result.ok ? null : `HTTP ${result.status}`;
|
||||
console.log(`[Shelly] power ON → ${result.ok ? 'OK' : `HTTP ${result.status}`}`);
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.state = 'error';
|
||||
this.error = err.message;
|
||||
console.error(`[Shelly] power ON failed: ${err.message}`);
|
||||
return { ok: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shelly hat keine FluidNC-Alarme — no-op, damit InfoServer-Sammeldurchlauf
|
||||
* den Shelly einfach überspringen kann.
|
||||
*/
|
||||
async alarmUnlock() { return { ok: true, skipped: true }; }
|
||||
|
||||
// ── Power Status ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Liest den aktuellen Schaltzustand vom Shelly (Switch.GetStatus?id=0).
|
||||
* `armed = true` → output:true → Strom AN → Roboter bestromt
|
||||
* `armed = false` → output:false → Strom AUS → Roboter stromlos
|
||||
*
|
||||
* Gibt zusätzlich Spannung und Wirkleistung zurück (für Diagnosezwecke).
|
||||
* @returns {{ ok: boolean, armed: boolean, voltage?: number, power?: number, error?: string }}
|
||||
*/
|
||||
async getArmed() {
|
||||
if (!this._statusUrl) return { ok: false, armed: false, error: 'no shelly url configured' };
|
||||
try {
|
||||
const result = await this._httpGetJson(this._statusUrl);
|
||||
if (!result.ok || !result.data) {
|
||||
return { ok: false, armed: false, error: result.error || `HTTP ${result.status}` };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
armed: result.data.output === true,
|
||||
voltage: result.data.voltage,
|
||||
power: result.data.apower,
|
||||
};
|
||||
} catch (err) {
|
||||
return { ok: false, armed: false, error: err.message };
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user