node-red-contrib-xmihome
Version:
Node-RED nodes for controlling Xiaomi Mi Home devices using the xmihome library.
2 lines (1 loc) • 9.57 kB
JavaScript
import X from"xmihome/device.js";class W{#z;#q;#w;settings;values=new Map;constructor(q,w,z){this.#q=q,this.#w=w,this.#z=z;let J=z.nodes.getNode(this.#w.settings);if(J)this.settings=J.instance;else this.#q.warn("Config node not found or configured.");this.#q.on("input",this.#O.bind(this)),this.#q.on("close",this.#Q.bind(this)),this.#q.status({})}get client(){if(!this.settings)throw new Error("Client is not initialized. Check configuration.");return this.settings.client}getDeviceId(q){return X.getDeviceId(q)}getDeviceConfig(q){let w=this.#z.util.evaluateNodeProperty(this.#w.device,this.#w.deviceType,this.#q,q);if(!w||typeof w!=="object")throw new Error("Device configuration is missing or not an object");let z=Object.fromEntries(Object.entries(w).filter(([J,G])=>G!=null&&G!==""));if(!z.id&&!z.address&&!z.mac)throw new Error("Device configuration must contain at least id, address, or mac");if(!z.model&&z.mac&&!z.name)this.#q.warn("Device configuration for Bluetooth is missing 'model', device might not work correctly without a specific class.");return z}truncate(q,w=20){if(!q||typeof q==="object")return null;if(typeof q!=="string")q=String(q);return q.length>w?q.slice(0,w-3)+"...":q}formatPropertyForStatus(q,w=20){if(q===null||q===void 0)return"null";if(typeof q==="boolean")return String(q);if(typeof q==="string"||typeof q==="number")return this.truncate(String(q),w);if(Array.isArray(q)){if(q.length===0)return"[]";if(q.every((z)=>typeof z==="string"||typeof z==="number"))return`[${this.truncate(q.join(", "),w)}]`;return`[${q.length} items]`}if(typeof q==="object"){let z=Object.keys(q);if(z.length===0)return"{}";let J=z.map((G)=>`${G}: ${this.truncate(String(q[G]),10)}`).join(", ");return this.truncate(J,w)}return String(q)}#H(q){if(this.settings.disconnectTimers.has(q))clearTimeout(this.settings.disconnectTimers.get(q)),this.settings.disconnectTimers.delete(q);let w=this.settings.devices.get(q);if(w)this.#q.status({fill:"green",shape:"dot",text:`Connected: ${w.connectionType}`}),this.#q.send([null,{_msgid:this.#z.util.generateId(),topic:`connection/${q}/connected`,payload:{device:w.config,connectionType:w.connectionType,event:"connected"}}])}#B(q,w=200){let z=this.values.get(q)||"Disconnected";if(this.settings.devices.has(q))this.settings.disconnectTimers.set(q,setTimeout(()=>{this.#q.status({fill:"grey",shape:"ring",text:z});let J=this.settings.devices.get(q);if(J)this.#q.send([null,{_msgid:this.#z.util.generateId(),topic:`connection/${q}/disconnected`,payload:{device:J.config,event:"disconnected"}}]),this.#G(q);this.settings.disconnectTimers.delete(q)},w))}#J(q,{reason:w}){if(this.settings.disconnectTimers.has(q))clearTimeout(this.settings.disconnectTimers.get(q)),this.settings.disconnectTimers.delete(q);let z=this.settings.devices.get(q);if(z)this.#q.status({fill:"yellow",shape:"dot",text:"Reconnecting"}),this.#q.send([null,{_msgid:this.#z.util.generateId(),topic:`connection/${q}/reconnecting`,payload:{reason:w,device:z.config,event:"reconnecting"}}])}#M(q,{attempts:w,error:z}){if(this.settings.disconnectTimers.has(q))clearTimeout(this.settings.disconnectTimers.get(q)),this.settings.disconnectTimers.delete(q);let J=this.settings.devices.get(q);if(J)this.#q.status({fill:"red",shape:"ring",text:"Reconnect failed"}),this.#q.send([null,{_msgid:this.#z.util.generateId(),topic:`connection/${q}/reconnect_failed`,payload:{attempts:w,error:z,device:J.config,event:"reconnect_failed"}}]),this.#G(q)}async#G(q){let w=this.settings.devices.get(q);if(!w)return;this.#q.debug(`Cleaning up and disconnecting device: ${q}`);try{await w.disconnect()}catch(z){this.#q.error(`Error during cleanup disconnect for ${q}: ${z}`)}w.removeAllListeners(),this.settings.devices.delete(q),this.values.delete(q)}async#O(q,w,z){let J,G,T,H;this.#q.status({fill:"blue",shape:"dot",text:"Getting device..."});let U=this.#z.util.evaluateNodeProperty(this.#w.topic,this.#w.topicType,this.#q,q),Q=this.#z.util.evaluateNodeProperty(this.#w.value,this.#w.valueType,this.#q,q),B=this.#z.util.evaluateNodeProperty(this.#w.property,this.#w.propertyType,this.#q,q),S=this.formatPropertyForStatus(B);try{if(T=this.getDeviceConfig(q),H=this.getDeviceId(T),this.settings.disconnectTimers.has(H))this.#q.debug(`Cancelling pending disconnect for ${H} due to new command.`),clearTimeout(this.settings.disconnectTimers.get(H)),this.settings.disconnectTimers.delete(H);if(!["getProperties","getProperty","setProperty","callAction","callMethod","subscribe","unsubscribe"].includes(this.#w.action))throw new Error(`Invalid action specified: ${this.#w.action}`);if(this.#w.action!=="getProperties"&&!B)throw new Error("Property name is missing (configure node or provide msg.property)");if(this.settings.devices.has(H))G=this.settings.devices.get(H),this.#q.debug(`Using existing device instance for key: ${H}`);else G=await this.client.getDevice(T),G.on("connected",this.#H.bind(this,H)),G.on("disconnect",this.#B.bind(this,H)),G.on("reconnecting",this.#J.bind(this,H)),G.on("reconnect_failed",this.#M.bind(this,H)),this.settings.devices.set(H,G),this.#q.debug(`Created and stored new device instance for key: ${H}`);if(this.#w.action!=="unsubscribe")this.#q.status({fill:"blue",shape:"dot",text:`Connecting (${this.client.config.connectionType||"auto"})...`}),await G.connect();switch(this.#q.debug(`Action: ${this.#w.action}, Property: ${B}`),this.#w.action){case"getProperties":case"getProperty":{this.#q.status({fill:"blue",shape:"dot",text:`Getting ${S}...`});let M=this.#w.action==="getProperties"?await G.getProperties():await G.getProperty(B),O=this.formatPropertyForStatus(M);this.#q.debug(`Got property/ies: ${JSON.stringify(M)}`),q.payload=M,q.topic=U||q.topic||`property/${this.#w.action==="getProperties"?"":B}`,w([q,null]),this.values.set(H,O),this.#q.status({fill:"green",shape:"dot",text:O});break}case"setProperty":{this.#q.status({fill:"blue",shape:"dot",text:`Setting ${S}...`}),this.#q.debug(`Value to set for ${B}: ${JSON.stringify(Q)}`),await G.setProperty(B,Q),q.payload={property:B,value:Q},q.topic=U||q.topic||`property/${B}`,w([q,null]),this.#q.log(`Property ${B} set to ${JSON.stringify(Q)} successfully.`),this.#q.status({fill:"green",shape:"dot",text:"Done"});break}case"callAction":{this.#q.status({fill:"blue",shape:"dot",text:`Calling ${S}...`}),this.#q.debug(`Calling action ${B} with params: ${JSON.stringify(Q)}`);let M=await G.callAction(B,Q);q.payload=M,q.topic=U||q.topic||`action/${B}`,w([q,null]),this.#q.log(`Action ${B} called successfully.`),this.#q.status({fill:"green",shape:"dot",text:"Done"});break}case"callMethod":{let M=Array.isArray(Q)?Q:!Q?[]:[Q];if(this.#q.status({fill:"blue",shape:"dot",text:`Calling ${S}()...`}),this.#q.debug(`Calling method ${B} with params: ${JSON.stringify(M)}`),typeof G[B]!=="function")throw new Error(`Device object does not have a method named "${B}"`);let O=await G[B](...M);q.payload=O,q.topic=U||q.topic||`method/${B}`,w([q,null]),this.#q.log(`Method ${B} called successfully.`),this.#q.status({fill:"green",shape:"dot",text:"Done"});break}case"subscribe":{this.#q.status({fill:"yellow",shape:"dot",text:`Subscribing to ${S}...`});let M=`${H}_${S}`;if(this.settings.subscriptions.has(M))this.#q.warn(`Already subscribed to ${B} for device ${H}. Ignoring.`),this.#q.status({fill:"yellow",shape:"ring",text:`Subscribed: ${B}`});else{let O=(V)=>{this.#q.debug(`Notification received for ${B}: ${JSON.stringify(V)}`),w([{_msgid:this.#z.util.generateId(),payload:V,property:B,device:T,topic:U||q.topic||`notify/${B}`},null]),this.#q.status({fill:"yellow",shape:"ring",text:`Subscribed: ${B}`})};this.settings.subscriptions.set(M,{device:G,property:B,callback:O}),await G.startNotify(B,O),this.#q.log(`Successfully subscribed to ${B} for device ${H}`),this.#q.status({fill:"yellow",shape:"ring",text:`Subscribed: ${B}`})}break}case"unsubscribe":{this.#q.status({fill:"blue",shape:"dot",text:`Unsubscribing from ${S}...`});let M=`${H}_${S}`;if(this.settings.subscriptions.has(M)){let O=this.settings.subscriptions.get(M);await O.device.stopNotify(O.property),this.settings.subscriptions.delete(M),this.#q.log(`Successfully unsubscribed from ${B} for device ${H}`),this.#q.status({fill:"grey",shape:"ring",text:"Unsubscribed"})}else this.#q.warn(`Not subscribed to ${B} for device ${H}. Cannot unsubscribe.`),this.#q.status({});break}}}catch(M){if(J=M,this.#q.status({fill:"red",shape:"ring",text:"Error"}),H)w([null,{_msgid:q._msgid,topic:`error/${H}`,payload:{event:"error",error:M.message,action:this.#w.action,sourceMessage:q,device:T}}])}finally{if(G&&this.#w.action!=="subscribe"){if(![...this.settings.subscriptions.keys()].some((O)=>O.startsWith(H)))this.#B(H,this.#w.action==="unsubscribe"?0:30000)}}z(J)}async#Q(q,w){let z=[];this.#q.debug(`Node closing, cleaning up all active connections and subscriptions... (removed: ${!!q})`);for(let J of this.settings.disconnectTimers.values())clearTimeout(J);for(let[J,G]of this.settings.devices.entries())this.#q.debug(`Disconnecting device instance for key: ${J}`),z.push(G.disconnect().catch((T)=>this.#q.error(`Error during cleanup disconnect for ${J}: ${T}`)));try{await Promise.all(z),this.#q.log("All active device connections closed.")}catch(J){this.#q.error("Error during connection cleanup on node close.")}this.settings.disconnectTimers.clear(),this.settings.subscriptions.clear(),this.settings.devices.clear(),this.values.clear(),this.#q.status({}),w()}}function Y(q){q.nodes.registerType("xmihome-device",function(w){q.nodes.createNode(this,w);let z=this;z.instance=new W(z,w,q)})}export{Y as default,W as DeviceNode};