feat: initial myprimal MQTT bridge service
Dynamic MySolem/MyIndygo IoT bridge with MQTT, Home Assistant autodiscovery, and Homebridge EasyMQTT compatibility. - Device discovery via MySolem API on startup (all modules/inputs/outputs) - Configurable poll engine (fast/normal/slow buckets, per-input overrides) - MQTT retained state publishing + HA autodiscovery payloads - Unified manual command routing (linesControl vs sendManualModuleCommand) - Runtime config via MQTT $config/set, persisted to config.json - Expression override system (API-provided > hardcoded > passthrough) - Minimal REST API: /health, /state, /control, /rediscover - Docker deployment (Dockerfile + docker-compose.yml) - 38 unit tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
function makeTempPath() {
|
||||
return path.join(os.tmpdir(), `myprimal-test-config-${process.pid}-${Date.now()}.json`);
|
||||
}
|
||||
|
||||
function freshConfig(tmpPath) {
|
||||
process.env.MYPRIMAL_CONFIG_PATH = tmpPath;
|
||||
jest.resetModules();
|
||||
return require('../src/config');
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.MYPRIMAL_CONFIG_PATH;
|
||||
delete process.env.POLL_FAST;
|
||||
});
|
||||
|
||||
describe('config', () => {
|
||||
test('loadConfig uses env defaults when no file', () => {
|
||||
process.env.POLL_FAST = '45';
|
||||
const tmp = makeTempPath();
|
||||
const { loadConfig, getConfig } = freshConfig(tmp);
|
||||
loadConfig();
|
||||
expect(getConfig().poll.fast).toBe(45);
|
||||
});
|
||||
|
||||
test('patchConfig deep merges and persists', () => {
|
||||
const tmp = makeTempPath();
|
||||
const { loadConfig, getConfig, patchConfig } = freshConfig(tmp);
|
||||
loadConfig();
|
||||
patchConfig({ poll: { fast: 20 } });
|
||||
expect(getConfig().poll.fast).toBe(20);
|
||||
expect(getConfig().poll.normal).toBeGreaterThan(0);
|
||||
|
||||
const saved = JSON.parse(fs.readFileSync(tmp, 'utf8'));
|
||||
expect(saved.poll.fast).toBe(20);
|
||||
fs.unlinkSync(tmp);
|
||||
});
|
||||
|
||||
test('patchConfig emits change event', done => {
|
||||
const tmp = makeTempPath();
|
||||
const { loadConfig, patchConfig, on } = freshConfig(tmp);
|
||||
loadConfig();
|
||||
on('change', cfg => {
|
||||
if (cfg.poll.slow === 1800) {
|
||||
fs.existsSync(tmp) && fs.unlinkSync(tmp);
|
||||
done();
|
||||
}
|
||||
});
|
||||
patchConfig({ poll: { slow: 1800 } });
|
||||
});
|
||||
|
||||
test('overrides preserved across patches', () => {
|
||||
const tmp = makeTempPath();
|
||||
const { loadConfig, getConfig, patchConfig } = freshConfig(tmp);
|
||||
loadConfig();
|
||||
patchConfig({ poll: { overrides: { 'lrps_0565c0/ph': 30 } } });
|
||||
patchConfig({ poll: { fast: 10 } });
|
||||
expect(getConfig().poll.overrides['lrps_0565c0/ph']).toBe(30);
|
||||
expect(getConfig().poll.fast).toBe(10);
|
||||
fs.unlinkSync(tmp);
|
||||
});
|
||||
|
||||
test('loadConfig reads persisted values', () => {
|
||||
const tmp = makeTempPath();
|
||||
fs.writeFileSync(tmp, JSON.stringify({ poll: { fast: 99, normal: 500, slow: 1000, overrides: {} } }));
|
||||
const { loadConfig, getConfig } = freshConfig(tmp);
|
||||
loadConfig();
|
||||
expect(getConfig().poll.fast).toBe(99);
|
||||
expect(getConfig().poll.normal).toBe(500);
|
||||
fs.unlinkSync(tmp);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user