node-red-contrib-xmihome
Version:
Node-RED nodes for controlling Xiaomi Mi Home devices using the xmihome library.
123 lines (122 loc) • 11.9 kB
HTML
<script type="text/javascript">
var N=window.RED,_=60000,M=null,U=0,V=[];function j(){if(this.deviceType==="msg")return!0;if(this.deviceType==="json")return W(JSON.parse(this.device||"{}"))!==void 0;return!1}function W(z){if(z.address&&z.token)return"miio";else if(z.mac&&z.model)return"bluetooth";else if(z.id)return"cloud";else if(z.address)return"miio";else if(z.mac)return"bluetooth"}function q(z){let F=z.name||z.model||z.mac||z.address||z.id||"Unknown Device",G=[];if(z.model&&F!==z.model)G.push(z.model);if(z.mac)G.push(z.mac);else if(z.address)G.push(z.address);else if(z.id)G.push(`DID: ${z.id}`);if(G.length>0)F+=` (${G.join(", ")})`;return F}function Y(z){let F=$("#discovered-msg"),G=($("#node-input-settings").val().toString()||"").replace("_ADD_","");if(!G){F.text(M._("device.label.discoveredDeviceConfig")).show(),O();return}F.text(M._("device.label.discoveredDeviceLoading")).show(),$("#node-input-discovered-device, #node-button-refresh-devices").prop("disabled",!0),$.getJSON(`xmihome/${G}/devices?force=${!!z}`).done(function(J){F.hide(),console.log(`[xmihome-device] Loaded ${J.devices?.length||0} discovered devices for node ${M.id}.`),O(J.devices),U=J.timestamp||0}).fail(function(J,K,Q){F.text(`Failed to load devices: ${K}`).show(),O(),U=0,console.error(`[xmihome-device] Error fetching discovered devices for node ${M.id}: ${K}`,Q,J)}).always(function(){$("#node-input-discovered-device, #node-button-refresh-devices").prop("disabled",!1)})}function O(z){let F=$("#node-input-discovered-device");if(F.empty(),!z||z.length===0)return;V=z,z.sort((G,J)=>{let K=G.name||G.model||"",Q=J.name||J.model||"";return K.localeCompare(Q)}),F.append($("<option>",{value:"",text:"-- Select a device --",disabled:!0})),z.forEach((G)=>{let J=q(G),K=JSON.stringify(G);F.append($("<option>",{value:K,text:J}))}),F.val("")}function Z(z){let F=JSON.parse(z||$("#node-input-device").typedInput("value")||"{}");return $(".device-config input").val(""),Object.entries(F).forEach(([G,J])=>$("#node-input-device-"+G).val(J)),B(null,F),F}function B(z,F){let G=$("#node-input-deviceIdType");if(F)G.val(W(F));let J=G.val();if(J)$(".device-config").hide(),$(`.device-config:not([class*="device-config-"]), .device-config-${J}`).show();else $(".device-config").show();$("#node-button-open-model").prop("disabled",J==="bluetooth")}function A(z){let F=$("#node-input-device");switch(z.target.id){case"node-input-deviceSource":{let G=$("#node-device-typedinput-container"),J=$("#node-device-discovered-container");switch(z.target.value){case"input":{if(G.show(),J.hide(),F.typedInput("type")!=="json")F.typedInput("type","json"),F.typedInput("value","{}");Z();break}case"discovered":{if(G.hide(),J.show(),V.length===0||Date.now()-U>_)Y();else O(V);break}case"msg":{G.hide(),J.hide(),F.typedInput("type","msg"),F.typedInput("value","device");break}}break}case"node-input-discovered-device":{F.typedInput("type","json"),F.typedInput("value",z.target.value),$("#node-input-deviceSource").val("input").change();break}default:{let G=JSON.parse(F.typedInput("value")||"{}"),J=z.target.id.split("-").pop(),K=z.target.value;G[J]=K,F.typedInput("value",JSON.stringify(G));break}}}function L(z){let F=z.target.value;$("#node-config-row-property").toggle(F!=="getProperties"),$("#node-config-row-value").toggle(["setProperty","callAction","callMethod"].includes(F))}function X(z){let F=$("#node-input-property"),G=$("#node-property-typedinput-container");switch(z.target.value){case"input":{G.show(),F.typedInput("type","str"),F.typedInput("value","");break}case"msg":{G.hide(),F.typedInput("type","msg"),F.typedInput("value","property");break}}}function E(z){let F=$("#node-input-device-model").val().toString().trim(),G="https://home.miot-spec.com/";if(F!=="")G+=`spec/${F}`;window.open(G,"_blank")}N.nodes.registerType("xmihome-device",{category:"Xiaomi MiHome",defaults:{settings:{value:null,required:!0,type:"xmihome-config"},name:{value:""},device:{value:"{}",validate:j},deviceType:{value:"json"},action:{value:"getProperty",required:!0},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(M=this,$("#node-input-device").typedInput({typeField:"#node-input-deviceType",types:["json","msg"]}),$("#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",A),$("#node-button-refresh-devices").on("click",Y),this.deviceType==="json")Z(this.device);else if(this.deviceType==="msg"&&this.device==="device")$("#node-input-deviceSource").val("msg").change();if($("#node-input-deviceIdType").on("change",B),$("#node-button-open-model").on("click",E),$("#node-input-action").on("change",L),$("#node-input-propertySource").on("change",X),this.propertyType==="msg"&&this.property==="property")$("#node-input-propertySource").val("msg").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 & 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">
<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>
<select id="node-input-action" style="width: 70%">
<option value="getProperties" data-i18n="device.label.actionGetAll"></option>
<option value="getProperty" data-i18n="device.label.actionGet"></option>
<option value="setProperty" data-i18n="device.label.actionSet"></option>
<option value="callAction" data-i18n="device.label.actionCall"></option>
<option value="callMethod" data-i18n="device.label.actionCallMethod"></option>
<option value="subscribe" data-i18n="device.label.actionSubscribe"></option>
<option value="unsubscribe" data-i18n="device.label.actionUnsubscribe"></option>
</select>
</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>