UNPKG

homebridge-melcloud-control

Version:

Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.

459 lines (385 loc) 18.5 kB
<div class="container mt-3"> <div class="text-center"> <img src="homebridge-melcloud-control.png" alt="Image" height="120" /> </div> <div id="accountManager" class="card card-body mt-2"> <form id="configForm"> <div class="text-center"> <label id="accountName" class="fw-bold" style="font-size: 23px;">Account</label><br> <label id="info" class="d-block" style="font-size: 15px;"></label> <label id="info1" class="d-block" style="font-size: 13px;"></label> <label id="info2" class="d-block" style="font-size: 13px;"></label> </div> <div class="mb-2"> <label for="name" class="form-label">Name</label> <input id="name" type="text" class="form-control" required> </div> <div class="mb-2"> <label for="user" class="form-label">User Name</label> <input id="user" type="text" class="form-control" required> </div> <div class="mb-2 position-relative"> <label for="passwd" class="form-label">Password</label> <div class="input-group"> <input id="passwd" type="password" class="form-control" autocomplete="off" required> <button type="button" id="togglePasswd" class="btn btn-outline-secondary"> <i class="fas fa-eye"></i> </button> </div> </div> <div class="mb-2"> <label for="language" class="form-label">Language</label> <select id="language" class="form-control"> <option value="0">English</option> <option value="1">Български</option> <option value="2">Čeština</option> <option value="3">Dansk</option> <option value="4">Deutsch</option> <option value="5">Eesti</option> <option value="6">Español</option> <option value="7">Français</option> <option value="8">Հայերեն</option> <option value="9">Latviešu</option> <option value="10">Lietuvių</option> <option value="11">Magyar</option> <option value="12">Nederlands</option> <option value="13">Norwegian</option> <option value="14">Polski</option> <option value="15">Português</option> <option value="16">Русский</option> <option value="17">Suomi</option> <option value="18">Svenska</option> <option value="19">Українська</option> <option value="20">Türkçe</option> <option value="21">Ελληνικά</option> <option value="22">Hrvatski</option> <option value="23">Română</option> <option value="24">Slovenščina</option> </select> </div> <div class="mb-2"> <label for="accountType" class="form-label">Account Type</label> <select id="accountType" class="form-control"> <option value="disabled">Disabled</option> <option value="melcloud">MELCloud</option> <option value="melcloudhome">MELCloud Home</option> </select> </div> <div class="text-center"> <button id="logIn" type="button" class="btn btn-primary">Connect to MELCloud</button> <button id="configButton" type="button" class="btn btn-secondary"> <i class="fas fa-gear"></i> </button> </div> </form> <div id="accountButtons" class="d-flex flex-wrap justify-content-center gap-1 mt-3"></div> </div> </div> <script> (async () => { const pluginConfig = await homebridge.getPluginConfig(); if (!pluginConfig.length) { pluginConfig.push({}); await homebridge.updatePluginConfig(pluginConfig); homebridge.showSchemaForm(); return; } const accounts = pluginConfig[0].accounts || []; const accountsCount = accounts.length; const $ = id => document.getElementById(id); // Helpers const setButtonClass = (activeIndex) => { for (let i = 0; i < accountsCount; i++) { $(`button${i}`).className = i === activeIndex ? "btn btn-primary" : "btn btn-secondary"; } }; const updateForm = (account) => { $('accountName').innerText = account.name || ''; $('name').value = account.name || ''; $('user').value = account.user || ''; $('passwd').value = account.passwd || ''; $('language').value = account.language || '0'; $('accountType').value = account.type || 'disabled'; const valid = account.name && account.user && account.passwd && account.type !== 'disabled'; $('logIn').disabled = !valid; const hasDevices = (account.ataDevices?.length ?? 0) > 0 || (account.atwDevices?.length ?? 0) > 0 || (account.ervDevices?.length ?? 0) > 0; $('configButton').disabled = !hasDevices; }; // Buttons const container = $('accountButtons'); accounts.forEach((account, i) => { const button = document.createElement("button"); button.type = "button"; button.id = `button${i}`; button.className = "btn btn-primary"; button.style.textTransform = "none"; button.innerText = account.name || `Account ${i + 1}`; container.appendChild(button); button.addEventListener("click", () => { setButtonClass(i); updateForm(accounts[i]); this.account = accounts[i]; }); if (i === accountsCount - 1) { $("button0").click(); } }); $('accountManager').style.display = 'block'; // Config toggle let configState = false; $('configButton').addEventListener('click', () => { configState = !configState; homebridge[configState ? 'showSchemaForm' : 'hideSchemaForm'](); $('configButton').className = configState ? 'btn btn-primary' : 'btn btn-secondary'; }); // Password toggle $('togglePasswd').addEventListener('click', () => { const input = $('passwd'); const icon = document.querySelector('#togglePasswd i'); if (input.type === 'password') { input.type = 'text'; icon.classList.replace('fa-eye', 'fa-eye-slash'); } else { input.type = 'password'; icon.classList.replace('fa-eye-slash', 'fa-eye'); } }); // Form update $('configForm').addEventListener('input', async () => { const account = this.account; account.name = $('name').value; account.user = $('user').value; account.passwd = $('passwd').value; account.language = $('language').value; account.type = $('accountType').value; updateForm(account); await homebridge.updatePluginConfig(pluginConfig); await homebridge.savePluginConfig(pluginConfig); }); // Update info on page function updateInfo(id, text, color) { const el = $(id); if (el) { el.innerText = text; el.style.color = color; } } // Generic remove function function removeStaleEntities(configEntities, melcloudEntities, getConfigId, getMelcloudId) { const serverIds = new Set((melcloudEntities ?? []).map(item => String(getMelcloudId(item)))); const removedEntities = []; for (let i = configEntities.length - 1; i >= 0; i--) { const entity = configEntities[i]; const entityId = String(getConfigId(entity)); if (!serverIds.has(entityId)) { removedEntities.push(entity); configEntities.splice(i, 1); } } return removedEntities; } // Map UnitId → Scenes for quick lookup function mapUnitIdToScenes(scenesInMelCloud) { const map = new Map(); scenesInMelCloud.forEach(scene => { const allSceneSettings = [...(scene.AtaSceneSettings ?? []), ...(scene.AtwSceneSettings ?? []), ...(scene.ErvSceneSettings ?? [])]; allSceneSettings.forEach(setting => { const unitId = String(setting.UnitId); if (!map.has(unitId)) map.set(unitId, []); map.get(unitId).push(scene); }); }); return map; } // Helper to generate summary string function summarizeChanges(type, ata, atw, erv) { const parts = []; if (ata.length) parts.push(`ATA: ${ata.length}`); if (atw.length) parts.push(`ATW: ${atw.length}`); if (erv.length) parts.push(`ERV: ${erv.length}`); return parts.length ? `${type}: ${parts.join(', ')}` : ''; } // Login & Sync Logic $('logIn').addEventListener('click', async () => { $('logIn').disabled = true; const lightingMode = homebridge.userCurrentLightingMode(); const fontColor = lightingMode === 'dark' ? 'black' : 'white'; updateInfo('info', 'Connecting to MELCloud', fontColor); updateInfo('info1', '', fontColor); updateInfo('info2', '', fontColor); homebridge.showSpinner(); try { const account = this.account; const accountTypeMelcloud = account.type === 'melcloud'; const melCloudDevicesData = await homebridge.request('/connect', account); if (!melCloudDevicesData.State) { updateInfo('info', melCloudDevicesData.Status, 'red'); $('logIn').disabled = false; homebridge.hideSpinner(); return; } // Ensure device arrays exist before handling account.ataDevices = account.ataDevices ?? []; account.atwDevices = account.atwDevices ?? []; account.ervDevices = account.ervDevices ?? []; // Prepare MELCloud data const newInMelCloud = { ata: [], ataPresets: [], ataSchedules: [], ataScenes: [], atw: [], atwPresets: [], atwSchedules: [], atwScenes: [], erv: [], ervPresets: [], ervSchedules: [], ervScenes: [] }; const devicesInMelCloudByType = { ata: [], atw: [], erv: [] }; const scenesInMelCloud = melCloudDevicesData.Scenes ?? []; // Split devices by type const devices = melCloudDevicesData.Devices; for (const device of devices) { if (device.Type === 0) devicesInMelCloudByType.ata.push(device); if (device.Type === 1) devicesInMelCloudByType.atw.push(device); if (device.Type === 3) devicesInMelCloudByType.erv.push(device); }; // Clean up local config const removedFromConfig = { ata: [], atw: [], erv: [], presets: [], schedules: [], scenes: [] }; removedFromConfig.ata = removeStaleEntities(account.ataDevices, devicesInMelCloudByType.ata, d => d.id, d => d.DeviceID); removedFromConfig.atw = removeStaleEntities(account.atwDevices, devicesInMelCloudByType.atw, d => d.id, d => d.DeviceID); removedFromConfig.erv = removeStaleEntities(account.ervDevices, devicesInMelCloudByType.erv, d => d.id, d => d.DeviceID); // Map UnitId → Scenes const unitIdToScenes = mapUnitIdToScenes(scenesInMelCloud); // Generic device handler (handles devices, presets, schedules, and scenes) const handleDevices = (devicesInMelCloud, devicesInConfig, newDevices, newPresets, newSchedules, newScenes) => { const configDevicesMap = new Map(devicesInConfig.map(dev => [String(dev.id), dev])); devicesInMelCloud.forEach(device => { const deviceId = String(device.DeviceID); let deviceInConfig = configDevicesMap.get(deviceId); if (!deviceInConfig) { deviceInConfig = { id: deviceId, type: device.Type, name: device.DeviceName }; devicesInConfig.push(deviceInConfig); newDevices.push(deviceInConfig); configDevicesMap.set(deviceId, deviceInConfig); } // PRESETS (melcloud) if (accountTypeMelcloud) { deviceInConfig.presets = (deviceInConfig.presets ?? []).filter(p => String(p.id) !== '0' && String(p.id) !== null); const presetsInMelCloud = device.Presets ?? []; removedFromConfig.presets.push(...removeStaleEntities(deviceInConfig.presets, presetsInMelCloud, p => p.id, p => p.ID)); const presetIds = new Set(deviceInConfig.presets.map(p => String(p.id))); presetsInMelCloud.forEach((preset, index) => { const presetId = String(preset.ID); if (!presetIds.has(presetId)) { const presetObj = { id: presetId, name: preset.NumberDescription || `Preset ${index}` }; deviceInConfig.presets.push(presetObj); newPresets.push(presetObj); } }); deviceInConfig.schedules = []; deviceInConfig.scenes = []; } // SCHEDULES & SCENES (melcloudhome) if (!accountTypeMelcloud) { // SCHEDULES deviceInConfig.schedules = (deviceInConfig.schedules ?? []).filter(s => String(s.id) !== '0' && String(s.id) !== null); const schedulesInMelCloud = device.Schedule ?? []; removedFromConfig.schedules.push(...removeStaleEntities(deviceInConfig.schedules, schedulesInMelCloud, s => s.id, s => s.Id)); const scheduleIds = new Set(deviceInConfig.schedules.map(s => String(s.id))); schedulesInMelCloud.forEach((schedule, index) => { const scheduleId = String(schedule.Id); if (!scheduleIds.has(scheduleId)) { const scheduleObj = { id: scheduleId, name: `Schedule ${index}`, }; deviceInConfig.schedules.push(scheduleObj); newSchedules.push(scheduleObj); } }); // SCENES deviceInConfig.scenes = (deviceInConfig.scenes ?? []).filter(s => String(s.id) !== '0' && String(s.id) !== null); const scenesForDevice = unitIdToScenes.get(deviceId) ?? []; removedFromConfig.scenes.push(...removeStaleEntities(deviceInConfig.scenes, scenesForDevice, s => s.id, s => s.Id)); const sceneIds = new Set(deviceInConfig.scenes.map(s => String(s.id))); scenesForDevice.forEach((scene, index) => { const sceneId = String(scene.Id); if (!sceneIds.has(sceneId)) { const sceneObj = { id: sceneId, name: scene.Name || `Scene ${index}` }; deviceInConfig.scenes.push(sceneObj); newScenes.push(sceneObj); } }); deviceInConfig.presets = []; } }); return devicesInConfig; }; // Execute device handlers with safe initialization account.ataDevices = handleDevices( devicesInMelCloudByType.ata, account.ataDevices, newInMelCloud.ata, newInMelCloud.ataPresets, newInMelCloud.ataSchedules, newInMelCloud.ataScenes ); account.atwDevices = handleDevices( devicesInMelCloudByType.atw, account.atwDevices, newInMelCloud.atw, newInMelCloud.atwPresets, newInMelCloud.atwSchedules, newInMelCloud.atwScenes ); account.ervDevices = handleDevices( devicesInMelCloudByType.erv, account.ervDevices, newInMelCloud.erv, newInMelCloud.ervPresets, newInMelCloud.ervSchedules, newInMelCloud.ervScenes ); // Summary counts const newDevicesCount = newInMelCloud.ata.length + newInMelCloud.atw.length + newInMelCloud.erv.length; const newPresetsCount = newInMelCloud.ataPresets.length + newInMelCloud.atwPresets.length + newInMelCloud.ervPresets.length; const newSchedulesCount = newInMelCloud.ataSchedules.length + newInMelCloud.atwSchedules.length + newInMelCloud.ervSchedules.length; const newScenesCount = newInMelCloud.ataScenes.length + newInMelCloud.atwScenes.length + newInMelCloud.ervScenes.length; const removedDevicesCount = removedFromConfig.ata.length + removedFromConfig.atw.length + removedFromConfig.erv.length; const removedPresetsCount = removedFromConfig.presets.length; const removedSchedulesCount = removedFromConfig.schedules.length; const removedScenesCount = removedFromConfig.scenes.length; if (!newDevicesCount && !newPresetsCount && !newSchedulesCount && !newScenesCount && !removedDevicesCount && !removedPresetsCount && !removedSchedulesCount && !removedScenesCount) updateInfo('info', 'No changes detected.', fontColor); // New items const newParts = []; if (newDevicesCount) newParts.push(summarizeChanges('Devices', newInMelCloud.ata, newInMelCloud.atw, newInMelCloud.erv)); if (accountTypeMelcloud && newPresetsCount) newParts.push(summarizeChanges('Presets', newInMelCloud.ataPresets, newInMelCloud.atwPresets, newInMelCloud.ervPresets)); if (!accountTypeMelcloud && newSchedulesCount) newParts.push(summarizeChanges('Schedules', newInMelCloud.ataSchedules, newInMelCloud.atwSchedules, newInMelCloud.ervSchedules)); if (!accountTypeMelcloud && newScenesCount) newParts.push(summarizeChanges('Scenes', newInMelCloud.ataScenes, newInMelCloud.atwScenes, newInMelCloud.ervScenes)); if (newParts.length) updateInfo('info', `Found new: ${newParts.join('; ')}`, 'green'); // Removed items const removedParts = []; if (removedDevicesCount) removedParts.push(summarizeChanges('Devices', removedFromConfig.ata, removedFromConfig.atw, removedFromConfig.erv)); if (removedPresetsCount) removedParts.push(`Presets: ${removedPresetsCount}`); if (removedSchedulesCount) removedParts.push(`Schedules: ${removedSchedulesCount}`); if (removedScenesCount) removedParts.push(`Scenes: ${removedScenesCount}`); if (removedParts.length) updateInfo('info1', `Removed old: ${removedParts.join('; ')}`, 'orange'); await homebridge.updatePluginConfig(pluginConfig); await homebridge.savePluginConfig(pluginConfig); } catch (error) { updateInfo('info', `Prepare config error`, "red"); updateInfo('info1', `Error: ${JSON.stringify(error)}`, "red"); } finally { homebridge.hideSpinner(); $('logIn').disabled = false; } }); })(); </script>