import url from 'node:url' import dgram from 'node:dgram' import { EventEmitter } from 'node:events' // @ts-ignore import httpHeaders from 'http-headers' import { YeelightDeviceInfo } from './yeelight.typing' import { YEELIGHT_DEVICE_INFO_DEFAULT, YEELIGHT_DISCOVERY_OPTIONS, YEELIGHT_EVENT } from './yeelight.constant' import { Shared } from '../shared' /** * Handles discovery of yeelight devices using their SSDP multicast protocol */ export class Discovery extends EventEmitter { discoveredInfo: { [id: string]: YeelightDeviceInfo | undefined } = {} socket = dgram.createSocket({ type: 'udp4', reuseAddr: true }) constructor (infos: YeelightDeviceInfo[] = []) { super() // Init infos Shared.log.info('[YeelightDiscoverService] initializing cached device infos') this.discoveredInfo = infos.reduce((acc, info) => ( Object.assign(acc, info.id ? { [info.id]: info } : {}) ), {} as { [id: string]: YeelightDeviceInfo }) // Emit initial discover events // need extra timeout to wait a bit after handler is added) setTimeout(() => { Object.values(this.discoveredInfo).forEach(info => this.emit(YEELIGHT_EVENT.DISCOVERED, info)) }, 0) } public on (event: YEELIGHT_EVENT.DISCOVERED, handler: (info: YeelightDeviceInfo) => void): this public on (e: any, c: any) {return super.on(e, c)} public search () { try { Shared.log.info('[YeelightDiscoverService] initializing socket') // Bind event handlers this.socket.on('listening', this.handleSocketReady) this.socket.on('message', this.handleSocketReceiveResponse) // Connect (Socket 就绪后会自动触发 'listening' 事件) this.socket.bind(YEELIGHT_DISCOVERY_OPTIONS.port, () => { this.socket.setBroadcast(true) this.socket.setMulticastTTL(128) this.socket.addMembership(YEELIGHT_DISCOVERY_OPTIONS.multicastAddr) }) } catch (e) { Shared.log.error('[YeelightDiscoverService] init error', e) } } private handleSocketReady = () => { Shared.log.info('[YeelightDiscoverService] begin discovery multicast procedure') const buffer = Buffer.from(YEELIGHT_DISCOVERY_OPTIONS.discoveryMsg) this.socket.send(buffer, 0, buffer.length, YEELIGHT_DISCOVERY_OPTIONS.port, YEELIGHT_DISCOVERY_OPTIONS.multicastAddr) } private handleSocketReceiveResponse = (response: string) => { // Format device infos Shared.log.info('[YeelightDiscoverService] response received, parsing device infos ...') const headers = httpHeaders(response, true) as YeelightDeviceInfo const info = { ...YEELIGHT_DEVICE_INFO_DEFAULT } as YeelightDeviceInfo for (const [header, value] of Object.entries(headers)) { const key = header as keyof YeelightDeviceInfo switch (typeof YEELIGHT_DEVICE_INFO_DEFAULT[key]) { case 'number': Object.assign(info, { [key]: Number(value) }) break case 'boolean': Object.assign(info, { [key]: value === 'on' }) break case 'string': Object.assign(info, { [key]: value }) break default: Object.assign(info, { [key]: value }) break } } // Parse device address if (info.id && info.location && info.id !== '') { const parsedUrl = url.parse(info.location) info.host = parsedUrl.hostname || '' info.port = Number(parsedUrl.port) } // Guard:invalid id if (!info.id) { Shared.log.info('[YeelightDiscoverService] - invalid id, skipped') return } // Guard: discovered device if (this.discoveredInfo[info.id]?.location === info.location) { Shared.log.info('[YeelightDiscoverService] - same device, skipped') return } // Update device Shared.log.info('[YeelightDiscoverService] - new device founded', info.location) this.discoveredInfo[info.id] = info // Emit event this.emit(YEELIGHT_EVENT.DISCOVERED, info) } }