All files / src discover.ts

67.24% Statements 78/116
39.66% Branches 23/58
63.64% Functions 14/22
66.67% Lines 74/111

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 2361x 1x 1x   1x 1x       1x             1x     1x                   1x 1x             1x 1x 1x 1x 1x 1x 1x 1x 1x           1x                                       1x                           1x   1x 1x 1x 1x 1x                     1x 1x   1x 1x 1x       1x 1x   1x 1x 1x 1x 1x   1x     1x     1x 1x       1x 1x 1x 1x 1x                                                                 1x 1x 1x     1x 1x 1x 1x               1x             1x   1x 1x             1x 1x 1x 1x   1x 1x 1x               1x 1x       1x           1x 1x 1x   1x     1x  
import { createSocket, Socket } from "dgram";
import { EventEmitter } from "events";
import { address, toBuffer } from "ip";
import { AddressInfo } from "net";
import { checkPortStatus } from "portscanner";
import { defaultLogger } from "./logger";
import { IDevice } from "./models/device";
import { IDiscoverConfig } from "./models/discover-config";
import { ILogger } from "./models/logger";
import { Utils } from "./utils";
/**
 * The class to discover yeelight device on wifi network using UDP package
 * @constructor
 * @param {string} title - Yeelight Discover
 * @param {string} author - samuraitruong@hotmail.com
 */
export class Discover extends EventEmitter {
    private devices: IDevice[];
    private timer: any;
    private options: IDiscoverConfig = {
        debug: true,
        fallback: true,
        host: null,
        limit: 1,
        multicastHost: "239.255.255.250",
        port: 1982,
        timeout: 10000,
    };
    private client: Socket;
    private clientBound = false;
    private isDestroyed = false;
    /**
     * @constructor
     * @param {IDiscoverConfig } options discover object include the port and multicast host.
     * see {@link IDiscoverConfig} for more detail
     * @param {ILogger} logger  the application logger which implement of log, info, debug and error function
     */
    constructor(options: IDiscoverConfig, private logger?: ILogger) {
        super();
        this.devices = [];
        this.options = { ...this.options, ...options };
        this.client = createSocket("udp4");
        this.client.on("message", this.onSocketMessage.bind(this));
        this.client.on("error", this.onError.bind(this));
        this.clientBound = false;
        this.logger = logger || defaultLogger;
    }
    /**
     * Try to verify if the light on and listening on the know ip address
     * @param ipAddress : know IP Address of the light.
     */
    public async detectLightIP(ipAddress: string): Promise<IDevice> {
        const device: Partial<IDevice> = {
            host: ipAddress,
            port: 55443,
        };
        return new Promise<IDevice>((resolve, reject) => {
            checkPortStatus(55443, ipAddress, (err: any, status: any) => {
                if (err || status === "closed") {
                    return resolve(null);
                } else {
                    this.addDevice(device as IDevice);
                    return resolve(device as IDevice);
                }
            });
        });
    }
    /**
     * Perfrom IP port scan to find an IP with port 55443 open rather than using SSDP discovery method
     * @requires {Promise<IDevice[]>} promise of list of device found
     */
    public async scanByIp(): Promise<IDevice[]> {
        const localIp = address();
        const count = 0;
        const availabledIps = Utils.getListIpAddress(localIp);
        const promises = availabledIps.map((x) => this.detectLightIP(x));
        await Promise.all(promises);
 
        return this.devices;
    }
    /**
     * The class to discover yeelight device on wifi network using UDP package
     * You need to turn on "LAN Control" on phone app to get SSDP discover function work
     * @returns {Promise<IDevice[]>} a promise that could resolve to 1 or many devices on the network
     */
    public start(): Promise<IDevice[]> {
 
        return new Promise((resolve, reject) => {
            try {
                Eif (!this.clientBound) {
                    this.clientBound = true;
                    this.client.bind(this.options.port, null, resolve as any);
                } else {
                    // Already bound to a port
                    resolve(null);
                }
            } catch (e) {
                reject(e);
            }
        })
            .then(() => {
 
                return new Promise<IDevice[]>((resolve, reject) => {
                    this.logger.debug("discover options: ", this.options);
 
                    this.client.setBroadcast(true);
                    this.client.send(this.getMessage(), this.options.port, this.options.multicastHost, (err) => {
                        Iif (err) {
                            this.logger.log("ERROR", err);
                            reject(err);
                        } else {
                            let ts = 0;
                            const interval = this.options.scanInterval || 200;
 
                            let timer: NodeJS.Timer | null = null;
                            const callback = (error: any, result?: IDevice[]) => {
                                Eif (timer) {
                                    clearInterval(timer);
                                    timer = null;
                                }
                                Iif (error) {
                                    reject(error);
                                } else {
                                    resolve(result);
                                }
                            };
                            timer = setInterval(() => {
                                Iif (this.isDestroyed) {
                                    callback("Discover got destroyed");
                                    return;
                                }
                                ts += interval;
                                Eif (this.options.limit && this.devices.length >= this.options.limit) {
                                    clearInterval(this.timer);
                                    callback(null, this.devices);
                                    return;
                                }
                                if (this.options.timeout > 0 && this.options.timeout < ts) {
                                    if (this.devices.length > 0) {
                                        clearInterval(this.timer);
                                        callback(null, this.devices);
                                        return;
                                    } else {
                                        clearInterval(this.timer);
                                        if (!this.options.fallback) {
                                            callback("No device found after timeout exceeded : " + ts);
                                            return;
                                        }
                                    }
                                }
                                this.client.send(this.getMessage(), this.options.port, this.options.multicastHost);
                                if (ts > this.options.timeout && this.options.fallback) {
                                    this.scanByIp()
                                        .catch((error) => {
                                            callback(error);
                                        });
                                }
                            }, interval);
                        }
                    });
                });
            });
    }
    /**
     * Clean up resource and close all open connection,
     * call this function after you finish your action to avoid memory leak
     * @returns {Promise} return a promise, fullfil will call after internal socket connection dropped
     */
    public destroy(): Promise<void> {
        this.isDestroyed = true;
        Iif (!this.client) {
            return Promise.resolve();
        }
        return new Promise((resolve) => {
            this.removeAllListeners();
            Eif (this.client) {
                this.client.close(resolve);
            }
        });
    }
    /**
     * Internal function to handle socket error
     * @param error Error details
     */
    private onError(error: Error) {
        this.logger.error("Internal Error ", error);
        this.emit("error", error);
    }
    /**
     * Generate the UDP message to discover device on local network.
     */
    private getMessage(): Buffer {
        // tslint:disable-next-line:max-line-length
        const message = `M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1982\r\nMAN: "ssdp:discover"\r\nST: wifi_bulb\r\n`;
        return Buffer.from(message);
    }
    /**
     * The event run when recieved the message devices
     * @param {Buffer} buffer the buffer revieved from the socket
     * @param {AddressInfo} address the TCP info of the devices who send the message
     */
    private onSocketMessage(buffer: Buffer, addressInfo: AddressInfo) {
        const message = buffer.toString();
        Eif (this.options.debug && this.logger) {
            this.logger.info(message);
        }
        const device = Utils.parseDeviceInfo(message);
        Eif (device) {
            this.addDevice(device);
        }
    }
    /**
     * Add the new discovered device into the internal list
     * @param {IDevice} device - the new device found from network
     * @returns {0 |1 } return 0 if device already existing, 1 if new device added to the list
     */
    private addDevice(device: IDevice): void {
        Eif (
            !this.options.filter ||
            this.options.filter(device)
        ) {
            const existDevice = this.devices.findIndex((x) => {
                return (
                    x.host && device.host && x.host === device.host &&
                    x.port && device.port && x.port === device.port
                );
            });
            Eif (existDevice === -1) {
                this.devices.push(device);
                this.emit("deviceAdded", device);
            }
            this.devices[existDevice] = device;
        }
    }
}