@switchbot/homebridge-switchbot
Version:
The SwitchBot plugin allows you to access your SwitchBot device(s) from HomeKit.
308 lines • 12.7 kB
JavaScript
// Fetch the list of configured devices from the Homebridge UI API
import { isValidDeviceType, normalizeDeviceType } from '../../../device-types.js';
import './types.js';
import { uiLog } from './logger.js';
import { toastError } from './toast.js';
export async function fetchDevices() {
try {
if (typeof homebridge.getPluginConfig !== 'function') {
throw new TypeError('Homebridge UI API not available');
}
const configArr = await homebridge.getPluginConfig();
const config = Array.isArray(configArr) && configArr.length > 0 ? configArr.find(isSwitchBotPlatformConfig) : null;
if (!config || !Array.isArray(config.devices)) {
return [];
}
return config.devices;
}
catch (e) {
const msg = e instanceof Error ? e.message : String(e);
uiLog.error('Error fetching devices:', msg);
return [];
}
}
/**
* Validate and auto-correct device types in the config array before saving.
* Returns an array of errors for devices that cannot be fixed.
*/
export function validateAndFixDeviceTypes(devices) {
const errors = [];
for (const d of devices) {
if (!isValidDeviceType(d.configDeviceType)) {
const fixed = normalizeDeviceType(d.configDeviceType);
if (fixed) {
d.configDeviceType = fixed;
}
else {
errors.push({
deviceId: d.deviceId,
name: d.configDeviceName,
type: d.configDeviceType,
});
}
}
}
return errors;
}
// API wrapper functions for communicating with the Homebridge UI server
function isSwitchBotPlatformConfig(block) {
const platformName = String(block?.platform || block?.name || '').toLowerCase();
return (platformName === 'switchbot'
|| platformName === '@switchbot/homebridge-switchbot'
|| platformName.includes('switchbot'));
}
export async function syncParentPluginConfigFromDisk(autoSave = false) {
try {
if (typeof homebridge.getPluginConfig !== 'function'
|| typeof homebridge.updatePluginConfig !== 'function') {
uiLog.warn('Parent config sync API not available');
return false;
}
// Use Homebridge UI API to fetch and update config
const pluginConfigBlocks = await homebridge.getPluginConfig();
if (!Array.isArray(pluginConfigBlocks) || !pluginConfigBlocks.length) {
uiLog.warn('No plugin config blocks returned from Homebridge');
return false;
}
const index = pluginConfigBlocks.findIndex(block => isSwitchBotPlatformConfig(block));
if (index < 0) {
uiLog.warn('SwitchBot platform block not found in Homebridge plugin config');
return false;
}
// Validate and fix device types before saving
const errors = validateAndFixDeviceTypes(pluginConfigBlocks[index].devices || []);
if (errors.length > 0) {
toastError(`Invalid device types found: ${errors.map(e => `${e.name} (${e.type})`).join(', ')}`);
return false;
}
// pluginConfigBlocks[index] is already up to date
await homebridge.updatePluginConfig(pluginConfigBlocks);
// Auto-save to disk if requested - prevents parent UI from overwriting with stale cache
if (autoSave && typeof homebridge.savePluginConfig === 'function') {
uiLog.info('Auto-saving config to disk...');
await homebridge.savePluginConfig();
uiLog.info('Config saved successfully');
}
return true;
}
catch (e) {
uiLog.warn('Failed to sync parent plugin config cache:', e);
return false;
}
}
export async function fetchCredentialStatus() {
try {
const resp = await homebridge.request('/credentials', {});
uiLog.info('Load credentials response:', resp);
if (!resp || resp.success === false) {
uiLog.error('Failed to load credentials:', resp);
return null;
}
return resp.data || {};
}
catch (e) {
uiLog.error('Error loading credentials:', e);
return null;
}
}
export async function saveCredentials(token, secret) {
uiLog.info('Saving credentials...');
const resp = await homebridge.request('/credentials', { token, secret });
uiLog.info('Save response:', resp);
if (!resp || resp.success === false) {
throw new Error(resp?.message || 'Save failed');
}
return resp.data || resp;
}
export async function discoverDevices(mode = 'all', options) {
const resp = await homebridge.request('/discover', { mode, ...options });
uiLog.info('Discover response:', resp);
if (!resp || resp.success === false) {
throw new Error(resp?.data?.message || 'Discovery failed');
}
return resp.data || [];
}
export async function fetchBluetoothStatus() {
try {
const resp = await homebridge.request('/ble-status', {});
if (!resp || resp.success === false) {
return { available: false, message: 'Bluetooth status unavailable' };
}
return resp.data || { available: false, message: 'Bluetooth status unavailable' };
}
catch (_e) {
return { available: false, message: 'Bluetooth status unavailable' };
}
}
export async function testDeviceConnection(payload) {
const resp = await homebridge.request('/test-connection', payload);
if (!resp || resp.success === false) {
throw new Error(resp?.data?.message || 'Connection test failed');
}
return resp.data || {
success: false,
deviceId: payload.deviceId,
method: 'Auto',
latencyMs: 0,
message: 'Connection test failed',
};
}
export async function addDevice(deviceId, name, type, options) {
if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
throw new TypeError('Homebridge UI API not available');
}
const configArr = await homebridge.getPluginConfig();
const idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1;
if (idx === -1) {
throw new Error('SwitchBot config not found');
}
const config = configArr[idx];
if (!Array.isArray(config.devices)) {
config.devices = [];
}
const normalizedDeviceId = String(deviceId).trim().toLowerCase();
const exists = config.devices.some((d) => String(d.deviceId ?? d.id ?? '').trim().toLowerCase() === normalizedDeviceId);
if (exists) {
return { alreadyExists: true, message: 'Device already in config' };
}
const newDevice = { deviceId, configDeviceName: name, configDeviceType: type };
if (options?.address) {
newDevice.address = options.address;
}
if (options?.model) {
newDevice.model = options.model;
}
if (options?.rssi !== undefined && options?.rssi !== null && options?.rssi !== 0) {
newDevice.rssi = options.rssi;
}
if (options?.encryptionKey) {
newDevice.encryptionKey = options.encryptionKey;
}
if (options?.keyId) {
newDevice.keyId = options.keyId;
}
config.devices.push(newDevice);
await homebridge.updatePluginConfig(configArr);
if (typeof homebridge.savePluginConfig === 'function') {
await homebridge.savePluginConfig();
}
return { added: true, message: `Device "${name}" added successfully` };
}
export async function addDevicesInBulk(devices) {
const resp = await homebridge.request('/add-devices', { devices });
uiLog.info('Bulk add response:', resp);
if (!resp || resp.success === false) {
throw new Error(resp?.data?.message || 'Bulk add failed');
}
return normalizeBulkAddDevicesResponse(resp);
}
export function normalizeBulkAddDevicesResponse(resp) {
const payload = resp?.data && typeof resp.data === 'object' ? resp.data : resp;
const addedCount = Number(payload?.addedCount ?? payload?.added ?? 0);
const skippedCount = Number(payload?.skippedCount ?? payload?.skipped ?? 0);
const updatedCount = Number(payload?.updatedCount ?? payload?.updated ?? 0);
return {
...payload,
success: resp?.success ?? true,
addedCount,
skippedCount,
updatedCount,
};
}
export async function updateDevice(deviceId, configDeviceName, configDeviceType, options) {
if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
throw new TypeError('Homebridge UI API not available');
}
const configArr = await homebridge.getPluginConfig();
const idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1;
if (idx === -1) {
throw new Error('SwitchBot config not found');
}
const config = configArr[idx];
if (!Array.isArray(config.devices)) {
throw new TypeError('No devices array in config');
}
const normalizedDeviceId = String(deviceId).trim().toLowerCase();
const device = config.devices.find((d) => String(d.deviceId ?? d.id ?? '').trim().toLowerCase() === normalizedDeviceId);
if (!device) {
throw new Error('Device not found in config');
}
if (configDeviceName) {
device.configDeviceName = configDeviceName;
}
if (configDeviceType) {
device.configDeviceType = configDeviceType;
}
if (options) {
Object.assign(device, options);
}
await homebridge.updatePluginConfig(configArr);
if (typeof homebridge.savePluginConfig === 'function') {
await homebridge.savePluginConfig();
}
return { updated: true, message: `Device updated successfully` };
}
export async function deleteDevice(deviceId) {
if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
throw new TypeError('Homebridge UI API not available');
}
const configArr = await homebridge.getPluginConfig();
const idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1;
if (idx === -1) {
throw new Error('SwitchBot config not found');
}
const config = configArr[idx];
if (!Array.isArray(config.devices)) {
throw new TypeError('No devices array in config');
}
const normalizedDeviceId = String(deviceId).trim().toLowerCase();
const before = config.devices.length;
// Remove the target device
config.devices = config.devices.filter(d => String(d.deviceId ?? d.id ?? '').trim().toLowerCase() !== normalizedDeviceId);
// Defensive: filter out any invalid device entries (missing required fields)
config.devices = config.devices.filter(d => d && typeof d === 'object' && d.deviceId && d.configDeviceType);
if (config.devices.length === before) {
throw new Error('Device not found in config');
}
await homebridge.updatePluginConfig(configArr);
if (typeof homebridge.savePluginConfig === 'function') {
await homebridge.savePluginConfig();
}
return { deleted: true, message: `Device removed from config` };
}
export async function deleteAllDevices() {
if (typeof homebridge.getPluginConfig !== 'function' || typeof homebridge.updatePluginConfig !== 'function') {
throw new TypeError('Homebridge UI API not available');
}
const configArr = await homebridge.getPluginConfig();
// Find or create the SwitchBot config block
let idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1;
if (idx === -1) {
// If not found, create a new config block for SwitchBot
const newBlock = { platform: 'SwitchBot', devices: [] };
configArr.push(newBlock);
idx = configArr.length - 1;
}
const config = configArr[idx];
// Always ensure devices is an array
if (!Array.isArray(config.devices)) {
config.devices = [];
}
const deletedCount = config.devices.length;
config.devices = [];
// Defensive: ensure required fields for schema compliance
if (!config.platform) {
config.platform = 'SwitchBot';
}
// Ensure 'name' property is present (required by schema for Homebridge platform blocks)
if (!config.name) {
config.name = 'SwitchBot';
}
// Save updated config
await homebridge.updatePluginConfig(configArr);
if (typeof homebridge.savePluginConfig === 'function') {
await homebridge.savePluginConfig();
}
return { deleted: true, deletedCount, message: `Removed ${deletedCount} device(s) from config` };
}
//# sourceMappingURL=api.js.map