UNPKG

node-red-contrib-xmihome

Version:

Node-RED nodes for controlling Xiaomi Mi Home devices using the xmihome library.

120 lines (119 loc) 14 kB
<script type="text/javascript"> var RED=window.RED,node=null,CACHE_TTL=60000,lastCacheTimestamp=0,discoveredDevicesCache=[];function validateDevice(){if(this.deviceType==="msg")return!0;if(this.deviceType==="json")return getIdType(JSON.parse(this.device||"{}"))!==void 0;return!1}function getIdType(device){if(device.address&&device.token&&!device.id?.startsWith("blt."))return"miio";else if(device.mac&&device.model)return"bluetooth";else if(device.id)return"cloud";else if(device.address)return"miio";else if(device.mac)return"bluetooth"}function formatDeviceLabel(device){let label=device.name||device.model||device.mac||device.address||device.id||"Unknown Device",details=[];if(device.model&&label!==device.model)details.push(device.model);if(device.mac)details.push(device.mac);else if(device.address)details.push(device.address);else if(device.id)details.push(`DID: ${device.id}`);if(details.length>0)label+=` (${details.join(", ")})`;return label}function fetchDiscoveredDevices(event){let msg=$("#discovered-msg"),clientId=($("#node-input-settings").val().toString()||"").replace("_ADD_","");if(!clientId){msg.text(node._("device.label.discoveredDeviceConfig")).show(),devicesDropdown();return}msg.text(node._("device.label.discoveredDeviceLoading")).show(),$("#node-input-discovered-device, #node-button-refresh-devices").prop("disabled",!0),$.getJSON(`xmihome/${clientId}/devices?force=${!!event}`).done(function(data){msg.hide(),console.log(`[xmihome-device] Loaded ${data.devices?.length||0} discovered devices for node ${node.id}.`),devicesDropdown(data.devices),lastCacheTimestamp=data.timestamp||0}).fail(function(jqXHR,textStatus,errorThrown){msg.text(`Failed to load devices: ${textStatus}`).show(),devicesDropdown(),lastCacheTimestamp=0,console.error(`[xmihome-device] Error fetching discovered devices for node ${node.id}: ${textStatus}`,errorThrown,jqXHR)}).always(function(){$("#node-input-discovered-device, #node-button-refresh-devices").prop("disabled",!1)})}function devicesDropdown(devices){let select=$("#node-input-discovered-device");if(select.empty(),!devices||devices.length===0)return;discoveredDevicesCache=devices,devices.sort((a,b)=>{let nameA=a.name||a.model||"",nameB=b.name||b.model||"";return nameA.localeCompare(nameB)}),select.append($("<option>",{value:"",text:"-- Select a device --",disabled:!0})),devices.forEach((device)=>{let text=formatDeviceLabel(device),value=JSON.stringify(device);select.append($("<option>",{value,text}))}),select.val("")}function deviceParse(input){let device=JSON.parse(input||$("#node-input-device").typedInput("value")||"{}");return $(".device-config input").val(""),Object.entries(device).forEach(([key,value])=>$("#node-input-device-"+key).val(value)),onchangeidtype(null,device),device}function onchangeidtype(_event,device){let input=$("#node-input-deviceIdType");if(device)input.val(getIdType(device));let value=input.val();if(value)$(".device-config").hide(),$(`.device-config:not([class*="device-config-"]), .device-config-${value}`).show();else $(".device-config").show();$("#node-button-open-model").prop("disabled",value==="bluetooth")}function onchangedevice(event){let input=$("#node-input-device");switch(event.target.id){case"node-input-deviceSource":{let typedinputContainer=$("#node-device-typedinput-container"),discoveredContainer=$("#node-device-discovered-container");switch(event.target.value){case"input":{if(typedinputContainer.show(),discoveredContainer.hide(),input.typedInput("type")!=="json")input.typedInput("type","json"),input.typedInput("value","{}");deviceParse();break}case"discovered":{if(typedinputContainer.hide(),discoveredContainer.show(),discoveredDevicesCache.length===0||Date.now()-lastCacheTimestamp>CACHE_TTL)fetchDiscoveredDevices();else devicesDropdown(discoveredDevicesCache);break}case"msg":{typedinputContainer.hide(),discoveredContainer.hide(),input.typedInput("type","msg"),input.typedInput("value","device");break}}break}case"node-input-discovered-device":{input.typedInput("type","json"),input.typedInput("value",event.target.value),$("#node-input-deviceSource").val("input").trigger("change");break}default:{let device=JSON.parse(input.typedInput("value")||"{}"),key=event.target.id.split("-").pop(),value=event.target.value;device[key]=value,input.typedInput("value",JSON.stringify(device));break}}}function onchangeaction(){let type=$("#node-input-actionType").val(),value=$(this).val();$("#node-config-row-property").toggle(!["getProperties","startMonitoring","stopMonitoring"].includes(value)),$("#node-config-row-value").toggle(type==="msg"||["setProperty","callAction","callMethod"].includes(value))}function onchangeproperty(){let input=$("#node-input-property"),container=$("#node-property-typedinput-container");switch($(this).val()){case"input":{container.show(),input.typedInput("type","str"),input.typedInput("value","");break}case"msg":{container.hide(),input.typedInput("type","msg"),input.typedInput("value","property");break}}}function onclickmodel(){let value=$("#node-input-device-model").val().toString().trim(),url="https://home.miot-spec.com/";if(value!=="")url+=`spec/${value}`;window.open(url,"_blank")}RED.nodes.registerType("xmihome-device",{category:"Xiaomi MiHome",defaults:{settings:{value:null,required:!0,type:"xmihome-config"},name:{value:""},device:{value:"{}",validate:validateDevice},deviceType:{value:"json"},action:{value:"getProperty",required:!0},actionType:{value:"action"},property:{value:""},propertyType:{value:"str"},value:{value:""},valueType:{value:"str"},topic:{value:"topic"},topicType:{value:"msg"}},icon:"font-awesome/fa-power-off",inputs:1,outputs:2,color:"#00BC9C",paletteLabel:"Device",label:function(){return this.name||this.action||"Device"},outputLabels:["Command Result / Notifications","Connection Events"],oneditprepare:function(){if(node=this,$("#node-input-device").typedInput({typeField:"#node-input-deviceType",types:["json","msg"]}),$("#node-input-action").typedInput({types:[{value:"action",options:[{value:"getProperties",label:this._("device.label.actionGetAll")},{value:"getProperty",label:this._("device.label.actionGet")},{value:"setProperty",label:this._("device.label.actionSet")},{value:"callAction",label:this._("device.label.actionCall")},{value:"callMethod",label:this._("device.label.actionCallMethod")},{value:"subscribe",label:this._("device.label.actionSubscribe")},{value:"startMonitoring",label:this._("device.label.actionSubscribeAdvertisements")},{value:"unsubscribe",label:this._("device.label.actionUnsubscribe")},{value:"stopMonitoring",label:this._("device.label.actionUnsubscribeAdvertisements")}]},"msg"],typeField:"#node-input-actionType"}),$("#node-input-property").typedInput({typeField:"#node-input-propertyType",types:["str","msg","flow","global","json"]}),$("#node-input-value").typedInput({typeField:"#node-input-valueType",types:["str","msg","flow","global","num","bool","json","date","jsonata"]}),$("#node-input-topic").typedInput({typeField:"#node-input-topicType",types:["str","msg","flow","global"]}),$("#node-input-deviceSource, #node-input-discovered-device, .device-config input").on("change",onchangedevice),$("#node-button-refresh-devices").on("click",fetchDiscoveredDevices),this.deviceType==="json")deviceParse(this.device);else if(this.deviceType==="msg"&&this.device==="device")$("#node-input-deviceSource").val("msg").trigger("change");if($("#node-input-deviceIdType").on("change",onchangeidtype),$("#node-button-open-model").on("click",onclickmodel),$("#node-input-action").on("change",onchangeaction),$("#node-input-propertySource").on("change",onchangeproperty),this.propertyType==="msg"&&this.property==="property")$("#node-input-propertySource").val("msg").trigger("change")}}); </script> <script type="text/html" data-template-name="xmihome-device"> <div class="form-row"> <label for="node-input-settings"><i class="fa fa-cog"></i> <span data-i18n="device.label.settings"></span></label> <input id="node-input-settings" /> </div> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="device.label.name"></span></label> <input type="text" id="node-input-name" data-i18n="[placeholder]device.placeholder.name" /> </div> <fieldset> <legend data-i18n="device.label.device"></legend> <div class="form-row" id="node-config-row-device"> <label for="node-input-deviceSource"><i class="fa fa-cogs"></i> <span data-i18n="device.label.source"></span></label> <select id="node-input-deviceSource" style="width: 70%"> <option value="input" data-i18n="device.label.deviceSourceInput"></option> <option value="discovered" data-i18n="device.label.deviceSourceDiscovered"></option> <option value="msg">msg.device</option> </select> <div hidden> <input type="text" id="node-input-device" /> <input type="hidden" id="node-input-deviceType" /> </div> </div> <div id="node-device-typedinput-container"> <div class="form-row"> <label for="node-input-deviceIdType"><i class="fa fa-id-badge"></i> <span data-i18n="device.label.deviceIdType"></span></label> <select id="node-input-deviceIdType" style="width: 70%"> <option value="" data-i18n="device.label.deviceIdTypeFull"></option> <option value="cloud">Cloud ID (DID)</option> <option value="miio">MiIO (IP &amp; Token)</option> <option value="bluetooth">Bluetooth (MAC)</option> </select> </div> <div class="form-row device-config device-config-cloud"> <label for="node-input-device-id"><i class="fa fa-cloud"></i> <span data-i18n="device.label.deviceId"></span></label> <input type="text" id="node-input-device-id" data-i18n="[placeholder]device.placeholder.deviceId" /> </div> <div class="form-row device-config device-config-miio"> <label for="node-input-device-address"><i class="fa fa-wifi"></i> <span data-i18n="device.label.deviceAddress"></span></label> <input type="text" id="node-input-device-address" data-i18n="[placeholder]device.placeholder.deviceAddress" /> </div> <div class="form-row device-config device-config-bluetooth"> <label for="node-input-device-mac"><i class="fa fa-bluetooth"></i> <span data-i18n="device.label.deviceMac"></span></label> <input type="text" id="node-input-device-mac" data-i18n="[placeholder]device.placeholder.deviceMac" /> </div> <div class="form-row device-config device-config-miio device-config-bluetooth"> <label for="node-input-device-token"> <i class="fa fa-key"></i> <span data-i18n="device.label.deviceToken"></span> <small class="device-config device-config-bluetooth" data-i18n="device.label.deviceTokenHint"></small> </label> <input type="text" id="node-input-device-token" data-i18n="[placeholder]device.placeholder.deviceToken" /> </div> <div class="form-row device-config device-config-bluetooth"> <label for="node-input-device-bindkey"><i class="fa fa-key"></i> <span data-i18n="device.label.deviceBindkey"></span></label> <input type="text" id="node-input-device-bindkey" data-i18n="[placeholder]device.placeholder.deviceBindkey" /> </div> <div class="form-row device-config"> <label for="node-input-device-model"> <i class="fa fa-cube"></i> <span data-i18n="device.label.deviceModel"></span> <small data-i18n="device.label.deviceModelHint"></small> </label> <div style="width: 70%; display: inline-flex;"> <input type="text" id="node-input-device-model" data-i18n="[placeholder]device.placeholder.deviceModel" style="flex-grow: 1" /> <button id="node-button-open-model" class="red-ui-button" style="margin-left: 10px"> <i class="fa fa-external-link"></i> </button> </div> <div style="font-size: 0.8em; color: #888;" data-i18n="device.label.deviceModelHelp"></div> </div> </div> <div class="form-row" id="node-device-discovered-container" hidden> <label for="node-input-discovered-device"><i class="fa fa-list"></i> <span data-i18n="device.label.discoveredDevice"></span></label> <div style="width: 70%; display: inline-flex;"> <select id="node-input-discovered-device" style="flex-grow: 1"></select> <button id="node-button-refresh-devices" class="red-ui-button" style="margin-left: 10px"> <i class="fa fa-refresh"></i> </button> </div> <div style="font-size: 0.8em; color: #888;" id="discovered-msg"></div> </div> </fieldset> <fieldset> <legend data-i18n="device.label.action"></legend> <div class="form-row"> <label for="node-input-action"><i class="fa fa-tasks"></i> <span data-i18n="device.label.action"></span></label> <input type="text" id="node-input-action" style="width: 70%;" /> <input type="hidden" id="node-input-actionType" /> </div> <div class="form-row" id="node-config-row-property"> <label for="node-input-propertySource"><i class="fa fa-cogs"></i> <span data-i18n="device.label.property"></span></label> <select id="node-input-propertySource" style="width: 110px; margin-right: 5px;"> <option value="input" data-i18n="device.label.propertySourceInput"></option> <option value="msg">msg.property</option> </select> <span id="node-property-typedinput-container"> <input type="text" id="node-input-property" data-i18n="[placeholder]device.placeholder.property" style="width: calc(70% - 120px)" /> <input type="hidden" id="node-input-propertyType" /> </span> </div> <div class="form-row" id="node-config-row-value"> <label for="node-input-value"><i class="fa fa-sign-in"></i> <span data-i18n="device.label.value"></span></label> <input type="text" id="node-input-value" style="width: 70%" /> <input type="hidden" id="node-input-valueType" /> </div> </fieldset> <fieldset> <legend data-i18n="device.label.output"></legend> <div class="form-row"> <label for="node-input-topic"><i class="fa fa-envelope-o"></i> <span data-i18n="device.label.topic"></span></label> <input type="text" id="node-input-topic" data-i18n="[placeholder]device.placeholder.topic" style="width: 70%" /> <input type="hidden" id="node-input-topicType" /> </div> </fieldset> </script>