UNPKG

matterbridge

Version:
423 lines • 19.8 kB
/** * This file contains the shelly api functions. * * @file shelly.ts * @author Luca Liguori * @date 2025-02-19 * @version 1.0.3 * * Copyright 2025, 2026, 2027 Luca Liguori. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ import { WS_ID_SHELLY_MAIN_UPDATE, WS_ID_SHELLY_SYS_UPDATE } from './frontend.js'; import { debugStringify } from './logger/export.js'; /** * Fetches Shelly system updates. If available: logs the result, sends a snackbar message, and broadcasts the message. * * @param {Matterbridge} matterbridge - The Matterbridge instance. * @returns {Promise<void>} A promise that resolves when the operation is complete. */ export async function getShellySysUpdate(matterbridge) { getShelly('/api/updates/sys/check', 60 * 1000) .then(async (data) => { if (data.length > 0) { matterbridge.matterbridgeInformation.shellySysUpdate = true; matterbridge.frontend.wssBroadcastMessage(WS_ID_SHELLY_SYS_UPDATE, 'shelly-sys-update', { available: true }); for (const update of data) { if (update.name) matterbridge.log.notice(`Shelly system update available: ${update.name}`); if (update.name) matterbridge.frontend.wssSendSnackbarMessage(`Shelly system update available: ${update.name}`, 10); } } }) .catch((error) => { matterbridge.log.warn(`Error getting Shelly system updates: ${error instanceof Error ? error.message : error}`); }); } /** * Triggers Shelly system updates. * * @param {Matterbridge} matterbridge - The Matterbridge instance. * @returns {Promise<void>} A promise that resolves when the operation is complete. */ export async function triggerShellySysUpdate(matterbridge) { getShelly('/api/updates/sys/perform', 10 * 1000) .then(async () => { matterbridge.log.debug(`Triggered Shelly system updates`); }) .catch((error) => { matterbridge.log.debug(`****Error triggering Shelly system updates: ${error instanceof Error ? error.message : error}`); }) .finally(() => { matterbridge.matterbridgeInformation.shellySysUpdate = false; matterbridge.log.notice(`Installing Shelly system update...`); matterbridge.frontend.wssSendSnackbarMessage('Installing Shelly system update...', 15); matterbridge.frontend.wssBroadcastMessage(WS_ID_SHELLY_SYS_UPDATE, 'shelly-sys-update', { available: false }); verifyShellyUpdate(matterbridge, '/api/updates/sys/status', 'Shelly system update'); }); } /** * Fetches Shelly main updates. If available: logs the result, sends a snackbar message, and broadcasts the message. * * @param {Matterbridge} matterbridge - The Matterbridge instance. * @returns {Promise<void>} A promise that resolves when the operation is complete. */ export async function getShellyMainUpdate(matterbridge) { getShelly('/api/updates/main/check', 60 * 1000) .then(async (data) => { if (data.length > 0) { matterbridge.matterbridgeInformation.shellyMainUpdate = true; matterbridge.frontend.wssBroadcastMessage(WS_ID_SHELLY_MAIN_UPDATE, 'shelly-main-update', { available: true }); for (const update of data) { if (update.name) matterbridge.log.notice(`Shelly software update available: ${update.name}`); if (update.name) matterbridge.frontend.wssSendSnackbarMessage(`Shelly software update available: ${update.name}`, 10); } } }) .catch((error) => { matterbridge.log.warn(`Error getting Shelly main updates: ${error instanceof Error ? error.message : error}`); }); } /** * Triggers Shelly main updates. * @param {Matterbridge} matterbridge - The Matterbridge instance. * @returns {Promise<void>} A promise that resolves when the operation is complete. */ export async function triggerShellyMainUpdate(matterbridge) { getShelly('/api/updates/main/perform', 10 * 1000) .then(async () => { // {"updatingInProgress":true} or {"updatingInProgress":false} matterbridge.log.debug(`Triggered Shelly main updates`); }) .catch((error) => { matterbridge.log.debug(`****Error triggering Shelly main updates: ${error instanceof Error ? error.message : error}`); }) .finally(() => { matterbridge.matterbridgeInformation.shellyMainUpdate = false; matterbridge.log.notice(`Installing Shelly software update...`); matterbridge.frontend.wssSendSnackbarMessage('Installing Shelly software update...', 15); matterbridge.frontend.wssBroadcastMessage(WS_ID_SHELLY_MAIN_UPDATE, 'shelly-main-update', { available: false }); verifyShellyUpdate(matterbridge, '/api/updates/main/status', 'Shelly software update'); }); } /** * Verifies Shelly update. * @param {Matterbridge} matterbridge - The Matterbridge instance. * @param {string} api - The api to call: /api/updates/sys/status or /api/updates/main/status * @param {string} name - The name of the update. * @returns {Promise<void>} A promise that resolves when the operation is complete. */ async function verifyShellyUpdate(matterbridge, api, name) { return new Promise((resolve) => { const timeout = setTimeout(() => { matterbridge.log.warn(`${name} check timed out`); clearInterval(interval); resolve(); }, 600 * 1000); // 10 minutes const interval = setInterval(() => { getShelly(api, 10 * 1000) .then(async (data) => { if (data.updatingInProgress) { matterbridge.log.debug(`${name} in progress...`); matterbridge.frontend.wssSendSnackbarMessage(`${name} in progress...`, 20); } else { matterbridge.log.notice(`${name} installed`); matterbridge.frontend.wssSendSnackbarMessage(`${name} installed`, 20); clearInterval(interval); clearTimeout(timeout); resolve(); } }) .catch((error) => { matterbridge.log.warn(`Error getting status of ${name}: ${error instanceof Error ? error.message : error}`); clearInterval(interval); clearTimeout(timeout); resolve(); }); }, 15 * 1000); // 15 seconds }); } /** * Triggers Shelly change network configuration. * @param {Matterbridge} matterbridge - The Matterbridge instance. * @param {object} config - The network configuration. * @returns {Promise<void>} A promise that resolves when the operation is complete. */ export async function triggerShellyChangeIp(matterbridge, config) { const api = config.type === 'static' ? '/api/network/connection/static' : '/api/network/connection/dynamic'; const data = { interface: 'end0' }; if (config.type === 'static') { data['addr'] = config.ip; data['mask'] = config.subnet; data['gw'] = config.gateway; data['dns'] = config.dns; } matterbridge.log.debug(`Triggering Shelly network configuration change: ${debugStringify(config)}`); postShelly(api, data, 60 * 1000) .then(async () => { matterbridge.log.debug(`Triggered Shelly network configuration change: ${debugStringify(config)}`); matterbridge.log.notice(`Changed Shelly network configuration`); matterbridge.frontend.wssSendSnackbarMessage('Changed Shelly network configuration'); }) .catch((error) => { matterbridge.log.debug(`****Error triggering Shelly network configuration change: ${error instanceof Error ? error.message : error}`); matterbridge.log.error(`Error changing Shelly network configuration: ${error instanceof Error ? error.message : error}`); matterbridge.frontend.wssSendSnackbarMessage('Error changing Shelly network configuration', 10, 'error'); }) .finally(() => { // matterbridge.log.notice(`Changed Shelly network configuration`); // matterbridge.frontend.wssSendSnackbarMessage('Changed Shelly network configuration'); }); } /** * Triggers Shelly system reboot. * @param {Matterbridge} matterbridge - The Matterbridge instance. * @returns {Promise<void>} A promise that resolves when the operation is complete. */ export async function triggerShellyReboot(matterbridge) { matterbridge.log.debug(`Triggering Shelly system reboot`); postShelly('/api/system/reboot', {}, 60 * 1000) .then(async () => { matterbridge.log.debug(`Triggered Shelly system reboot`); matterbridge.log.notice(`Rebooting Shelly board...`); matterbridge.frontend.wssSendSnackbarMessage('Rebooting Shelly board...'); }) .catch((error) => { matterbridge.log.debug(`****Error triggering Shelly system reboot: ${error instanceof Error ? error.message : error}`); matterbridge.log.error(`Error rebooting Shelly board: ${error instanceof Error ? error.message : error}`); matterbridge.frontend.wssSendSnackbarMessage('Error rebooting Shelly board', 10, 'error'); }) .finally(() => { // matterbridge.log.notice(`Rebooting Shelly board...`); // matterbridge.frontend.wssSendSnackbarMessage('Rebooting Shelly board...'); }); } /** * Triggers Shelly soft reset. * It will replaces network config with default one (edn0 on dhcp). * * @param {Matterbridge} matterbridge - The Matterbridge instance. * @returns {Promise<void>} A promise that resolves when the operation is complete. */ export async function triggerShellySoftReset(matterbridge) { matterbridge.log.debug(`Triggering Shelly soft reset`); getShelly('/api/reset/soft', 60 * 1000) .then(async () => { matterbridge.log.debug(`Triggered Shelly soft reset`); matterbridge.log.notice(`Resetting the network parameters on Shelly board...`); matterbridge.frontend.wssSendSnackbarMessage('Resetting the network parameters on Shelly board...'); }) .catch((error) => { matterbridge.log.debug(`****Error triggering Shelly soft reset: ${error instanceof Error ? error.message : error}`); matterbridge.log.error(`Error resetting the network parameters on Shelly board: ${error instanceof Error ? error.message : error}`); matterbridge.frontend.wssSendSnackbarMessage('Error resetting the network parameters on Shelly board', 10, 'error'); }) .finally(() => { // matterbridge.log.notice(`Resetting the network parameters on Shelly board...`); // matterbridge.frontend.wssSendSnackbarMessage('Resetting the network parameters on Shelly board...'); }); } /** * Triggers Shelly soft reset. * It will do a soft reset and will remove both directories .matterbridge Matterbridge. * @param {Matterbridge} matterbridge - The Matterbridge instance. * @returns {Promise<void>} A promise that resolves when the operation is complete. */ export async function triggerShellyHardReset(matterbridge) { matterbridge.log.debug(`Triggering Shelly hard reset`); getShelly('/api/reset/hard', 60 * 1000) .then(async () => { matterbridge.log.debug(`Triggered Shelly hard reset`); matterbridge.log.notice(`Factory resetting Shelly board...`); matterbridge.frontend.wssSendSnackbarMessage('Factory resetting Shelly board...'); }) .catch((error) => { matterbridge.log.debug(`****Error triggering Shelly hard reset: ${error instanceof Error ? error.message : error}`); matterbridge.log.error(`Error while factory resetting the Shelly board: ${error instanceof Error ? error.message : error}`); matterbridge.frontend.wssSendSnackbarMessage('Error while factory resetting the Shelly board', 10, 'error'); }) .finally(() => { // matterbridge.log.notice(`Factory resetting Shelly board...`); // matterbridge.frontend.wssSendSnackbarMessage('Factory resetting Shelly board...'); }); } /** * Fetches Shelly system log and write it to shelly.log. * * @param {Matterbridge} matterbridge - The Matterbridge instance. * @returns {Promise<void>} A promise that resolves when the operation is complete. */ export async function createShellySystemLog(matterbridge) { const { promises: fs } = await import('node:fs'); const path = await import('node:path'); matterbridge.log.debug(`Downloading Shelly system log...`); getShelly('/api/logs/system', 60 * 1000) .then(async (data) => { fs.writeFile(path.join(matterbridge.matterbridgeDirectory, 'shelly.log'), data) .then(() => { matterbridge.log.notice(`Shelly system log ready for download`); matterbridge.frontend.wssSendSnackbarMessage('Shelly system log ready for download'); }) .catch((error) => { matterbridge.log.warn(`Error writing Shelly system log to file: ${error instanceof Error ? error.message : error}`); }); }) .catch((error) => { matterbridge.log.warn(`Error getting Shelly system log: ${error instanceof Error ? error.message : error}`); }); } /** * Perform a GET to Shelly board apis. * @param {string} api - The api to call: * * /api/updates/sys/check => [{name:string; ...}] * /api/updates/sys/perform => {"updatingInProgress":true} or {"updatingInProgress":false} * /api/updates/sys/status => {"updatingInProgress":true} or {"updatingInProgress":false} * /api/updates/main/check => [{name:string; ...}] * /api/updates/main/perform => {"updatingInProgress":true} or {"updatingInProgress":false} * /api/updates/main/status => {"updatingInProgress":true} or {"updatingInProgress":false} * * /api/logs/system => text * * /api/reset/soft => "ok" Replaces network config with default one (edn0 on dhcp) * /api/reset/hard => reboot on success Hard reset makes soft reset + removing both directories .matterbridge Matterbridge + reboot * * * @param {number} [timeout=5000] - The timeout duration in milliseconds (default is 60000ms). * @returns {Promise<any>} A promise that resolves to the response. * @throws {Error} If the request fails. */ async function getShelly(api, timeout = 60000) { const http = await import('node:http'); return new Promise((resolve, reject) => { const url = `http://127.0.0.1:8101${api}`; const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); reject(new Error(`Request timed out after ${timeout / 1000} seconds`)); }, timeout).unref(); const req = http.get(url, { signal: controller.signal }, (res) => { let data = ''; if (res.statusCode !== 200) { clearTimeout(timeoutId); res.resume(); // Discard response data to close the socket properly req.destroy(); // Forcefully close the request reject(new Error(`Failed to fetch data. Status code: ${res.statusCode}`)); return; } res.on('data', (chunk) => { // console.log(chunk); data += chunk; }); res.on('end', () => { clearTimeout(timeoutId); if (api !== '/api/logs/system') { try { const jsonData = JSON.parse(data); resolve(jsonData); } catch (error) { reject(new Error(`Failed to parse response JSON: ${error instanceof Error ? error.message : error}`)); } } else { // console.log(data); resolve(data); } }); }); req.on('error', (error) => { clearTimeout(timeoutId); reject(new Error(`Request failed: ${error instanceof Error ? error.message : error}`)); }); }); } /** * Perform a POST request to Shelly board apis. * @param {string} api - The api to call: * * Set static ip * /api/network/connection/static -d '{"interface": "end0", "addr": "10.11.12.101", "mask": "255.255.255.0", "gw": "10.11.12.1", "dns": "1.1.1.1"}' => {} * * Set dhcp * /api/network/connection/dynamic -d '{"interface": "end0"}' => {} * * Reboot * /api/system/reboot => {"success":true} * * curl -H "Content-Type: application/json" -X POST http://127.0.0.1:8101/api/network/connection/dynamic * -d '{"interface": "end0"}' * * curl -H "Content-Type: application/json" -X POST http://127.0.0.1:8101/api/network/connection/static * -d '{"interface": "end0", "addr": "192.168.1.64", "mask": "255.255.255.0", "gw": "192.168.1.1", "dns": "192.168.1.1"}' * * @param {number} [timeout=5000] - The timeout duration in milliseconds (default is 60000ms). * @returns {Promise<any>} A promise that resolves to the response. * @throws {Error} If the request fails. */ async function postShelly(api, data, timeout = 60000) { const http = await import('node:http'); return new Promise((resolve, reject) => { const url = `http://127.0.0.1:8101${api}`; const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); reject(new Error(`Request timed out after ${timeout / 1000} seconds`)); }, timeout).unref(); const jsonData = JSON.stringify(data); const options = { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(jsonData), }, signal: controller.signal, }; const req = http.request(url, options, (res) => { let responseData = ''; // Check for non-success status codes (e.g., 300+) if (res.statusCode && res.statusCode >= 300) { clearTimeout(timeoutId); res.resume(); // Discard response data to free up memory req.destroy(); // Close the request return reject(new Error(`Failed to post data. Status code: ${res.statusCode}`)); } res.on('data', (chunk) => { responseData += chunk; }); res.on('end', () => { clearTimeout(timeoutId); try { const jsonResponse = JSON.parse(responseData); resolve(jsonResponse); } catch (err) { reject(new Error(`Failed to parse response JSON: ${err instanceof Error ? err.message : err}`)); } }); }); req.on('error', (error) => { clearTimeout(timeoutId); reject(new Error(`Request failed: ${error instanceof Error ? error.message : error}`)); }); // Send the JSON data req.write(jsonData); req.end(); }); } //# sourceMappingURL=shelly.js.map