Emergency Stop
This commit is contained in:
@@ -32,6 +32,28 @@ function request(url) {
|
||||
});
|
||||
}
|
||||
|
||||
function post(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
const parsed = new URL(url);
|
||||
const options = {
|
||||
hostname: parsed.hostname,
|
||||
port: parsed.port,
|
||||
path: parsed.pathname,
|
||||
method: 'POST',
|
||||
agent,
|
||||
headers: { 'Content-Length': 0 }
|
||||
};
|
||||
const req = https.request(options, res => {
|
||||
let data = '';
|
||||
res.on('data', c => { data += c.toString(); });
|
||||
res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
describe('InfoServer', () => {
|
||||
let server;
|
||||
let port;
|
||||
@@ -189,4 +211,152 @@ describe('InfoServer', () => {
|
||||
const { statusCode } = await request(`https://127.0.0.1:${port}/api/unknown`);
|
||||
expect(statusCode).toBe(404);
|
||||
});
|
||||
|
||||
// ── Emergency Stop Endpoints ─────────────────────────────────────────────
|
||||
|
||||
test('POST /api/emergency-stop ruft emergencyStop() auf allen Sendern auf', async () => {
|
||||
const key = fs.readFileSync('https/localhost.key');
|
||||
const cert = fs.readFileSync('https/localhost.pem');
|
||||
const httpsOptions = { key, cert, passphrase: 'abcd' };
|
||||
|
||||
const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] };
|
||||
const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 };
|
||||
|
||||
const stopA = jest.fn(() => Promise.resolve({ ok: true }));
|
||||
const stopB = jest.fn(() => Promise.resolve({ ok: true, skipped: true }));
|
||||
const senders = [
|
||||
{ name: 'Base', instance: { emergencyStop: stopA, alarmUnlock: jest.fn(() => Promise.resolve({ ok: true })) } },
|
||||
{ name: 'EmergencyStop', instance: { emergencyStop: stopB, alarmUnlock: jest.fn(() => Promise.resolve({ ok: true, skipped: true })) } }
|
||||
];
|
||||
|
||||
server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders);
|
||||
port = await listen(server);
|
||||
|
||||
const { statusCode, body } = await post(`https://127.0.0.1:${port}/api/emergency-stop`);
|
||||
expect(statusCode).toBe(200);
|
||||
const json = JSON.parse(body);
|
||||
expect(json.ok).toBe(true);
|
||||
expect(stopA).toHaveBeenCalledTimes(1);
|
||||
expect(stopB).toHaveBeenCalledTimes(1);
|
||||
expect(json.results).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('POST /api/alarm-unlock ruft alarmUnlock() auf allen Sendern auf', async () => {
|
||||
const key = fs.readFileSync('https/localhost.key');
|
||||
const cert = fs.readFileSync('https/localhost.pem');
|
||||
const httpsOptions = { key, cert, passphrase: 'abcd' };
|
||||
|
||||
const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] };
|
||||
const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 };
|
||||
|
||||
const unlockA = jest.fn(() => Promise.resolve({ ok: true }));
|
||||
const unlockB = jest.fn(() => Promise.resolve({ ok: true, skipped: true }));
|
||||
const senders = [
|
||||
{ name: 'Base', instance: { emergencyStop: jest.fn(() => Promise.resolve({ ok: true })), alarmUnlock: unlockA } },
|
||||
{ name: 'EmergencyStop', instance: { emergencyStop: jest.fn(() => Promise.resolve({ ok: true })), alarmUnlock: unlockB } }
|
||||
];
|
||||
|
||||
server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders);
|
||||
port = await listen(server);
|
||||
|
||||
const { statusCode, body } = await post(`https://127.0.0.1:${port}/api/alarm-unlock`);
|
||||
expect(statusCode).toBe(200);
|
||||
const json = JSON.parse(body);
|
||||
expect(json.ok).toBe(true);
|
||||
expect(unlockA).toHaveBeenCalledTimes(1);
|
||||
expect(unlockB).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('POST /api/emergency-stop ok=false wenn ein Sender fehlschlägt', async () => {
|
||||
const key = fs.readFileSync('https/localhost.key');
|
||||
const cert = fs.readFileSync('https/localhost.pem');
|
||||
const httpsOptions = { key, cert, passphrase: 'abcd' };
|
||||
|
||||
const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] };
|
||||
const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 };
|
||||
|
||||
const senders = [
|
||||
{ name: 'Base', instance: { emergencyStop: () => Promise.resolve({ ok: false, error: 'not connected' }), alarmUnlock: () => Promise.resolve({ ok: true }) } }
|
||||
];
|
||||
|
||||
server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders);
|
||||
port = await listen(server);
|
||||
|
||||
const { statusCode, body } = await post(`https://127.0.0.1:${port}/api/emergency-stop`);
|
||||
expect(statusCode).toBe(200);
|
||||
const json = JSON.parse(body);
|
||||
expect(json.ok).toBe(false);
|
||||
expect(json.results[0].ok).toBe(false);
|
||||
});
|
||||
|
||||
test('GET /api/power-status gibt armed=true zurück wenn Shelly output:true', async () => {
|
||||
const key = fs.readFileSync('https/localhost.key');
|
||||
const cert = fs.readFileSync('https/localhost.pem');
|
||||
const httpsOptions = { key, cert, passphrase: 'abcd' };
|
||||
|
||||
const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] };
|
||||
const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 };
|
||||
|
||||
const getArmedFn = jest.fn(() => Promise.resolve({ ok: true, armed: true, voltage: 234.9, power: 15.5 }));
|
||||
const senders = [
|
||||
{ name: 'EmergencyStop', instance: { emergencyStop: () => Promise.resolve({ ok: true }), alarmUnlock: () => Promise.resolve({ ok: true }), getArmed: getArmedFn } }
|
||||
];
|
||||
|
||||
server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders);
|
||||
port = await listen(server);
|
||||
|
||||
const { statusCode, body } = await request(`https://127.0.0.1:${port}/api/power-status`);
|
||||
expect(statusCode).toBe(200);
|
||||
const json = JSON.parse(body);
|
||||
expect(json.ok).toBe(true);
|
||||
expect(json.armed).toBe(true);
|
||||
expect(getArmedFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('GET /api/power-status gibt armed=false zurück wenn kein Shelly konfiguriert', async () => {
|
||||
const key = fs.readFileSync('https/localhost.key');
|
||||
const cert = fs.readFileSync('https/localhost.pem');
|
||||
const httpsOptions = { key, cert, passphrase: 'abcd' };
|
||||
|
||||
const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] };
|
||||
const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 };
|
||||
|
||||
const senders = [
|
||||
{ name: 'Base', instance: { emergencyStop: () => Promise.resolve({ ok: true }), alarmUnlock: () => Promise.resolve({ ok: true }) } }
|
||||
];
|
||||
|
||||
server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders);
|
||||
port = await listen(server);
|
||||
|
||||
const { statusCode, body } = await request(`https://127.0.0.1:${port}/api/power-status`);
|
||||
expect(statusCode).toBe(200);
|
||||
const json = JSON.parse(body);
|
||||
expect(json.ok).toBe(false);
|
||||
expect(json.armed).toBe(false);
|
||||
expect(json.error).toMatch(/no shelly/);
|
||||
});
|
||||
|
||||
test('POST /api/power-on ruft powerOn() auf Shelly-Sender auf', async () => {
|
||||
const key = fs.readFileSync('https/localhost.key');
|
||||
const cert = fs.readFileSync('https/localhost.pem');
|
||||
const httpsOptions = { key, cert, passphrase: 'abcd' };
|
||||
|
||||
const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] };
|
||||
const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 };
|
||||
|
||||
const powerOnFn = jest.fn(() => Promise.resolve({ ok: true, status: 200 }));
|
||||
const senders = [
|
||||
{ name: 'Base', instance: { emergencyStop: () => Promise.resolve({ ok: true }), alarmUnlock: () => Promise.resolve({ ok: true }) } },
|
||||
{ name: 'EmergencyStop', instance: { emergencyStop: () => Promise.resolve({ ok: true }), alarmUnlock: () => Promise.resolve({ ok: true }), powerOn: powerOnFn } }
|
||||
];
|
||||
|
||||
server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders);
|
||||
port = await listen(server);
|
||||
|
||||
const { statusCode, body } = await post(`https://127.0.0.1:${port}/api/power-on`);
|
||||
expect(statusCode).toBe(200);
|
||||
const json = JSON.parse(body);
|
||||
expect(json.ok).toBe(true);
|
||||
expect(powerOnFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -177,3 +177,39 @@ describe('RobotConfig.load — heartbeatInterval', () => {
|
||||
expect(cfg.controllers.base.heartbeatInterval).toBe(10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RobotConfig.load — emergencyStop (Shelly)', () => {
|
||||
test('DEFAULTS.controllers.emergencyStop hat protocol=shelly und url=null', () => {
|
||||
expect(DEFAULTS.controllers.emergencyStop.protocol).toBe('shelly');
|
||||
expect(DEFAULTS.controllers.emergencyStop.url).toBeNull();
|
||||
});
|
||||
|
||||
test('emergencyStop.url aus robot.json wird übernommen', () => {
|
||||
const shellyUrl = 'http://shelly.local/rpc/Switch.Set?id=0&on=false';
|
||||
const json = {
|
||||
...FULL_ROBOT_JSON,
|
||||
controllers: {
|
||||
...FULL_ROBOT_JSON.controllers,
|
||||
emergencyStop: { protocol: 'shelly', url: shellyUrl }
|
||||
}
|
||||
};
|
||||
const cfg = load(makeFs(JSON.stringify(json)), {}, log);
|
||||
expect(cfg.controllers.emergencyStop.protocol).toBe('shelly');
|
||||
expect(cfg.controllers.emergencyStop.url).toBe(shellyUrl);
|
||||
});
|
||||
|
||||
test('fehlendes emergencyStop in robot.json → url=null (Default)', () => {
|
||||
// FULL_ROBOT_JSON hat kein emergencyStop → fällt auf Default zurück
|
||||
const cfg = load(makeFs(JSON.stringify(FULL_ROBOT_JSON)), {}, log);
|
||||
expect(cfg.controllers.emergencyStop.protocol).toBe('shelly');
|
||||
expect(cfg.controllers.emergencyStop.url).toBeNull();
|
||||
});
|
||||
|
||||
test('emergencyStop hat keine ip/port/axes/heartbeatInterval Felder', () => {
|
||||
const cfg = load(makeFailFs(), {}, log);
|
||||
expect(cfg.controllers.emergencyStop.ip).toBeUndefined();
|
||||
expect(cfg.controllers.emergencyStop.port).toBeUndefined();
|
||||
expect(cfg.controllers.emergencyStop.axes).toBeUndefined();
|
||||
expect(cfg.controllers.emergencyStop.heartbeatInterval).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
119
test/Sender.Telnet.emergencyStop.test.js
Normal file
119
test/Sender.Telnet.emergencyStop.test.js
Normal file
@@ -0,0 +1,119 @@
|
||||
'use strict';
|
||||
// Tests für TelnetSenderGRBL.emergencyStop() und alarmUnlock()
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const TelnetSenderGRBL = require('../robot/TelnetSenderGRBL');
|
||||
|
||||
function makeTelnetSocket() {
|
||||
const em = new EventEmitter();
|
||||
return Object.assign(em, {
|
||||
write: jest.fn(),
|
||||
end: jest.fn(),
|
||||
destroy: jest.fn(),
|
||||
});
|
||||
}
|
||||
|
||||
/** Sender im Test-Modus (URL "test.test") — tSocket ist gesetzt. */
|
||||
function makeConnectedSender() {
|
||||
const sender = new TelnetSenderGRBL('test.test');
|
||||
sender.tSocket = makeTelnetSocket();
|
||||
return sender;
|
||||
}
|
||||
|
||||
/** Sender ohne tSocket (disconnected). */
|
||||
function makeDisconnectedSender() {
|
||||
const sender = new TelnetSenderGRBL('test.test');
|
||||
sender.tSocket = null;
|
||||
return sender;
|
||||
}
|
||||
|
||||
// ── emergencyStop() ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('TelnetSenderGRBL.emergencyStop()', () => {
|
||||
|
||||
test('sendet "!" wenn verbunden', async () => {
|
||||
const sender = makeConnectedSender();
|
||||
const result = await sender.emergencyStop();
|
||||
expect(result.ok).toBe(true);
|
||||
expect(sender.tSocket.write).toHaveBeenCalledWith('!');
|
||||
});
|
||||
|
||||
test('sendet nur "!" — kein Zeilenende (FluidNC Realtime-Byte)', async () => {
|
||||
const sender = makeConnectedSender();
|
||||
await sender.emergencyStop();
|
||||
const arg = sender.tSocket.write.mock.calls[0][0];
|
||||
expect(arg).toBe('!');
|
||||
expect(arg).not.toContain('\r\n');
|
||||
});
|
||||
|
||||
test('gibt {ok:false} zurück wenn nicht verbunden', async () => {
|
||||
const sender = makeDisconnectedSender();
|
||||
const result = await sender.emergencyStop();
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toBe('not connected');
|
||||
});
|
||||
|
||||
test('gibt {ok:false} zurück wenn write() wirft', async () => {
|
||||
const sender = makeConnectedSender();
|
||||
sender.tSocket.write.mockImplementation(() => { throw new Error('socket closed'); });
|
||||
const result = await sender.emergencyStop();
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toBe('socket closed');
|
||||
});
|
||||
});
|
||||
|
||||
// ── alarmUnlock() ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('TelnetSenderGRBL.alarmUnlock()', () => {
|
||||
|
||||
test('sendet "$X\\r\\n" wenn verbunden', async () => {
|
||||
const sender = makeConnectedSender();
|
||||
const result = await sender.alarmUnlock();
|
||||
expect(result.ok).toBe(true);
|
||||
expect(sender.tSocket.write).toHaveBeenCalledWith('$X\r\n');
|
||||
});
|
||||
|
||||
test('gibt {ok:false} zurück wenn nicht verbunden', async () => {
|
||||
const sender = makeDisconnectedSender();
|
||||
const result = await sender.alarmUnlock();
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toBe('not connected');
|
||||
});
|
||||
|
||||
test('gibt {ok:false} zurück wenn write() wirft', async () => {
|
||||
const sender = makeConnectedSender();
|
||||
sender.tSocket.write.mockImplementation(() => { throw new Error('write error'); });
|
||||
const result = await sender.alarmUnlock();
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toBe('write error');
|
||||
});
|
||||
});
|
||||
|
||||
// ── SenderInterface-Defaults ─────────────────────────────────────────────────
|
||||
|
||||
describe('SenderInterface — Default-Implementierungen (no-op)', () => {
|
||||
test('SenderInterface.emergencyStop() gibt {ok:true, skipped:true}', async () => {
|
||||
const SenderInterface = require('../robot/SenderInterface');
|
||||
// Subclass, die nur emergencyStop von der Basis erbt
|
||||
class MinimalSender extends SenderInterface {
|
||||
async connect() { return this; }
|
||||
send() { return false; }
|
||||
getStatus() { return {}; }
|
||||
disconnect() {}
|
||||
}
|
||||
const s = new MinimalSender();
|
||||
await expect(s.emergencyStop()).resolves.toEqual({ ok: true, skipped: true });
|
||||
});
|
||||
|
||||
test('SenderInterface.alarmUnlock() gibt {ok:true, skipped:true}', async () => {
|
||||
const SenderInterface = require('../robot/SenderInterface');
|
||||
class MinimalSender extends SenderInterface {
|
||||
async connect() { return this; }
|
||||
send() { return false; }
|
||||
getStatus() { return {}; }
|
||||
disconnect() {}
|
||||
}
|
||||
const s = new MinimalSender();
|
||||
await expect(s.alarmUnlock()).resolves.toEqual({ ok: true, skipped: true });
|
||||
});
|
||||
});
|
||||
198
test/ShellyEmergencyStop.test.js
Normal file
198
test/ShellyEmergencyStop.test.js
Normal file
@@ -0,0 +1,198 @@
|
||||
'use strict';
|
||||
|
||||
const ShellyEmergencyStop = require('../robot/ShellyEmergencyStop');
|
||||
|
||||
const OFF_URL = 'http://shelly.local/rpc/Switch.Set?id=0&on=false';
|
||||
const ON_URL = 'http://shelly.local/rpc/Switch.Set?id=0&on=true';
|
||||
const STATUS_URL = 'http://shelly.local/rpc/Switch.GetStatus?id=0';
|
||||
|
||||
function makeHttpGetJson(data, statusCode = 200) {
|
||||
return jest.fn(() => Promise.resolve({
|
||||
ok: statusCode >= 200 && statusCode < 300,
|
||||
status: statusCode,
|
||||
data
|
||||
}));
|
||||
}
|
||||
|
||||
function makeHttpGet(statusCode = 200) {
|
||||
return jest.fn(() => Promise.resolve({
|
||||
ok: statusCode >= 200 && statusCode < 300,
|
||||
status: statusCode
|
||||
}));
|
||||
}
|
||||
|
||||
describe('ShellyEmergencyStop — Konstruktor', () => {
|
||||
test('url wird korrekt gesetzt', () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL);
|
||||
expect(s.url).toBe(OFF_URL);
|
||||
});
|
||||
|
||||
test('_onUrl ersetzt on=false durch on=true', () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL);
|
||||
expect(s._onUrl).toBe(ON_URL);
|
||||
});
|
||||
|
||||
test('_statusUrl zeigt auf Switch.GetStatus', () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL);
|
||||
expect(s._statusUrl).toBe(STATUS_URL);
|
||||
});
|
||||
|
||||
test('null-URL → _offUrl, _onUrl und _statusUrl sind null', () => {
|
||||
const s = new ShellyEmergencyStop(null);
|
||||
expect(s._offUrl).toBeNull();
|
||||
expect(s._onUrl).toBeNull();
|
||||
expect(s._statusUrl).toBeNull();
|
||||
});
|
||||
|
||||
test('Initialzustand ist ready', () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL);
|
||||
expect(s.state).toBe('ready');
|
||||
expect(s.error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ShellyEmergencyStop — SenderInterface', () => {
|
||||
test('connect() gibt Instanz zurück', async () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL);
|
||||
await expect(s.connect()).resolves.toBe(s);
|
||||
});
|
||||
|
||||
test('send() gibt false zurück (kein GCode-Empfänger)', () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL);
|
||||
expect(s.send('G1 X10')).toBe(false);
|
||||
});
|
||||
|
||||
test('getStatus() enthält state und url', () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL);
|
||||
const st = s.getStatus();
|
||||
expect(st.state).toBe('ready');
|
||||
expect(st.url).toBe(OFF_URL);
|
||||
});
|
||||
|
||||
test('alarmUnlock() gibt {ok:true, skipped:true} zurück', async () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL);
|
||||
await expect(s.alarmUnlock()).resolves.toEqual({ ok: true, skipped: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('ShellyEmergencyStop — emergencyStop()', () => {
|
||||
test('ruft _offUrl auf', async () => {
|
||||
const httpGet = makeHttpGet(200);
|
||||
const s = new ShellyEmergencyStop(OFF_URL, { httpGetFn: httpGet });
|
||||
await s.emergencyStop();
|
||||
expect(httpGet).toHaveBeenCalledWith(OFF_URL);
|
||||
});
|
||||
|
||||
test('setzt state=stopped bei HTTP 200', async () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL, { httpGetFn: makeHttpGet(200) });
|
||||
const result = await s.emergencyStop();
|
||||
expect(result.ok).toBe(true);
|
||||
expect(s.state).toBe('stopped');
|
||||
expect(s.error).toBeNull();
|
||||
});
|
||||
|
||||
test('setzt state=error bei HTTP 500', async () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL, { httpGetFn: makeHttpGet(500) });
|
||||
const result = await s.emergencyStop();
|
||||
expect(result.ok).toBe(false);
|
||||
expect(s.state).toBe('error');
|
||||
expect(s.error).toBe('HTTP 500');
|
||||
});
|
||||
|
||||
test('setzt state=error bei Netzwerkfehler', async () => {
|
||||
const httpGet = jest.fn(() => Promise.reject(new Error('ECONNREFUSED')));
|
||||
const s = new ShellyEmergencyStop(OFF_URL, { httpGetFn: httpGet });
|
||||
const result = await s.emergencyStop();
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toBe('ECONNREFUSED');
|
||||
expect(s.state).toBe('error');
|
||||
});
|
||||
|
||||
test('gibt {ok:false} wenn url null ist', async () => {
|
||||
const s = new ShellyEmergencyStop(null);
|
||||
const result = await s.emergencyStop();
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toMatch(/no shelly url/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ShellyEmergencyStop — powerOn()', () => {
|
||||
test('ruft _onUrl auf (on=true)', async () => {
|
||||
const httpGet = makeHttpGet(200);
|
||||
const s = new ShellyEmergencyStop(OFF_URL, { httpGetFn: httpGet });
|
||||
await s.powerOn();
|
||||
expect(httpGet).toHaveBeenCalledWith(ON_URL);
|
||||
});
|
||||
|
||||
test('setzt state=ready bei HTTP 200', async () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL, { httpGetFn: makeHttpGet(200) });
|
||||
const result = await s.powerOn();
|
||||
expect(result.ok).toBe(true);
|
||||
expect(s.state).toBe('ready');
|
||||
});
|
||||
|
||||
test('setzt state=error bei HTTP 503', async () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL, { httpGetFn: makeHttpGet(503) });
|
||||
const result = await s.powerOn();
|
||||
expect(result.ok).toBe(false);
|
||||
expect(s.state).toBe('error');
|
||||
});
|
||||
|
||||
test('gibt {ok:false} wenn url null ist', async () => {
|
||||
const s = new ShellyEmergencyStop(null);
|
||||
const result = await s.powerOn();
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ShellyEmergencyStop — getArmed()', () => {
|
||||
const SHELLY_ON = { id: 0, source: 'WS_in', output: true, apower: 15.5, voltage: 234.9, freq: 50.1, current: 0.144 };
|
||||
const SHELLY_OFF = { id: 0, source: 'WS_in', output: false, apower: 0.0, voltage: 235.2, freq: 50.1, current: 0.000 };
|
||||
|
||||
test('ruft _statusUrl (Switch.GetStatus?id=0) auf', async () => {
|
||||
const httpGetJson = makeHttpGetJson(SHELLY_ON);
|
||||
const s = new ShellyEmergencyStop(OFF_URL, { httpGetJsonFn: httpGetJson });
|
||||
await s.getArmed();
|
||||
expect(httpGetJson).toHaveBeenCalledWith(STATUS_URL);
|
||||
});
|
||||
|
||||
test('armed=true wenn output:true', async () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL, { httpGetJsonFn: makeHttpGetJson(SHELLY_ON) });
|
||||
const result = await s.getArmed();
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.armed).toBe(true);
|
||||
expect(result.voltage).toBe(234.9);
|
||||
expect(result.power).toBe(15.5);
|
||||
});
|
||||
|
||||
test('armed=false wenn output:false', async () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL, { httpGetJsonFn: makeHttpGetJson(SHELLY_OFF) });
|
||||
const result = await s.getArmed();
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.armed).toBe(false);
|
||||
});
|
||||
|
||||
test('armed=false bei HTTP-Fehler (503)', async () => {
|
||||
const s = new ShellyEmergencyStop(OFF_URL, { httpGetJsonFn: makeHttpGetJson(null, 503) });
|
||||
const result = await s.getArmed();
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.armed).toBe(false);
|
||||
});
|
||||
|
||||
test('armed=false bei Netzwerkfehler', async () => {
|
||||
const httpGetJson = jest.fn(() => Promise.reject(new Error('ECONNREFUSED')));
|
||||
const s = new ShellyEmergencyStop(OFF_URL, { httpGetJsonFn: httpGetJson });
|
||||
const result = await s.getArmed();
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.armed).toBe(false);
|
||||
expect(result.error).toBe('ECONNREFUSED');
|
||||
});
|
||||
|
||||
test('armed=false wenn url null ist', async () => {
|
||||
const s = new ShellyEmergencyStop(null);
|
||||
const result = await s.getArmed();
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.armed).toBe(false);
|
||||
expect(result.error).toMatch(/no shelly url/);
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,10 @@ describe('startRobot orchestrator', () => {
|
||||
const createInfoServer = jest.fn(() => infoServerMock);
|
||||
|
||||
const TelnetSenderClass = jest.fn(() => ({ tSocket: null }));
|
||||
// Shelly-Mock: getStatus() liefert state='ready' (kein tSocket, kein isTestMode)
|
||||
const ShellyClass = jest.fn(() => ({
|
||||
getStatus: () => ({ state: 'ready', url: null, error: null })
|
||||
}));
|
||||
|
||||
const robotInstances = [];
|
||||
class RobotClass {
|
||||
@@ -37,6 +41,7 @@ describe('startRobot orchestrator', () => {
|
||||
RobotClass,
|
||||
GCodeModule: { dummy: true },
|
||||
TelnetSenderClass,
|
||||
ShellyClass,
|
||||
initInputWSFn: initInputWS,
|
||||
createInfoServerFn: createInfoServer,
|
||||
setTimeoutFn: (fn) => fn(),
|
||||
@@ -58,9 +63,10 @@ describe('startRobot orchestrator', () => {
|
||||
expect.any(RobotClass),
|
||||
{ dummy: true },
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: 'Base', instance: expect.any(Object) }),
|
||||
expect.objectContaining({ name: 'Elbow', instance: expect.any(Object) }),
|
||||
expect.objectContaining({ name: 'Hand', instance: expect.any(Object) })
|
||||
expect.objectContaining({ name: 'Base', instance: expect.any(Object) }),
|
||||
expect.objectContaining({ name: 'Elbow', instance: expect.any(Object) }),
|
||||
expect.objectContaining({ name: 'Hand', instance: expect.any(Object) }),
|
||||
expect.objectContaining({ name: 'EmergencyStop', instance: expect.any(Object) })
|
||||
]),
|
||||
expect.objectContaining({}) // options: { apiKey }
|
||||
);
|
||||
@@ -71,13 +77,18 @@ describe('startRobot orchestrator', () => {
|
||||
expect(result).toHaveProperty('httpsServer', httpsServerMock);
|
||||
expect(result).toHaveProperty('infoServer', infoServerMock);
|
||||
expect(result).toHaveProperty('senders');
|
||||
expect(result.senders).toHaveLength(3);
|
||||
expect(result.senders).toHaveLength(4); // base, elbow, hand, emergencyStop
|
||||
|
||||
// Nur Telnet-Sender (3) landen in cmdReceivers — Shelly nicht
|
||||
expect(robotInstances[0].cmdReceivers).toHaveLength(3);
|
||||
|
||||
expect(result.startupStatus).toEqual({
|
||||
https: { ok: true },
|
||||
senders: [
|
||||
{ name: 'Base', status: 'disconnected', reason: 'no active socket connection' },
|
||||
{ name: 'Elbow', status: 'disconnected', reason: 'no active socket connection' },
|
||||
{ name: 'Hand', status: 'disconnected', reason: 'no active socket connection' }
|
||||
{ name: 'Base', status: 'disconnected', reason: 'no active socket connection' },
|
||||
{ name: 'Elbow', status: 'disconnected', reason: 'no active socket connection' },
|
||||
{ name: 'Hand', status: 'disconnected', reason: 'no active socket connection' },
|
||||
{ name: 'EmergencyStop', status: 'ready', reason: undefined }
|
||||
]
|
||||
});
|
||||
expect(result.sharedState.connectedClients).toEqual([]);
|
||||
|
||||
Reference in New Issue
Block a user