UNPKG

node-red-contrib-xmihome

Version:

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

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