๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
IoT ๐Ÿ /Homebridge

[JS] Homebridge ์ „๊ธฐ๋งคํŠธ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ œ์ž‘, ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ตฌํ˜„ - 2

by GeekSean 2025. 12. 7.
๋ฐ˜์‘ํ˜•

[IoT ๐Ÿ /Homebridge] - Homebridge ์ „๊ธฐ๋งคํŠธ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ œ์ž‘, ๋ธ”๋ฃจํˆฌ์Šค ํŒจํ‚ท ๋ถ„์„ - 1

[IoT ๐Ÿ /Homebridge] - [JS] Homebridge ์ „๊ธฐ๋งคํŠธ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ œ์ž‘, ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ตฌํ˜„ - 2

 

์ €๋ฒˆ ๊ฒŒ์‹œ๊ธ€์—์„œ, ์ „๊ธฐ๋งคํŠธ์˜ ๋ธ”๋ฃจํˆฌ์Šค ํ†ต์‹  ํŒจํ‚ท์„ ๋ถ„์„ํ•ด ๋ณด์•˜๋‹ค.

์ด์   ๊ทธ ํŒจํ‚ท๋“ค์„ ํ† ๋Œ€๋กœ Homebridge ํ”Œ๋Ÿฌ๊ทธ์ธ์— ์ ์šฉ์‹œ์ผœ ๋ณด๋„๋ก ํ•˜๊ฒ ๋‹ค.

 

ํ”Œ๋Ÿฌ๊ทธ์ธ ์ œ์ž‘์€ ์ด๋ฏธ ํ•œ ๋ฒˆ ํ•ด๋ดค์œผ๋ฏ€๋กœ

Homebridge API ์„ค๋ช…์€ ์ƒ๋žตํ•˜๋„๋ก ํ•˜๊ฒ ๋‹ค.

 

 

ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ฐœ๋ฐœ (์•…์„ธ์„œ๋ฆฌ ๊ตฌํ˜„)

 

๋จผ์ € node.js ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ ํ›„,

linux ํ™˜๊ฒฝ์—์„œ ๋Œ์•„๊ฐˆ node-ble ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•ด ์ฃผ์—ˆ๋‹ค.

npm install node-ble
 

 

๊ธฐ๋ณธ์ ์ธ ํ™ˆ๋ธŒ๋ฆฟ์ง€ ์•…์„ธ์„œ๋ฆฌ ๋ถ€๋ถ„์„ ๊ตฌํ˜„ํ•ด๋ณด์ž

์˜จ๋„์กฐ์ ˆ ํŒŒํŠธ๋Š” Thermostat ์•…์„ธ์„œ๋ฆฌ ํƒ€์ผ์„,

ํƒ€์ด๋จธ๋Š” ๋งˆ๋•…ํ•œ ์•…์„ธ์„œ๋ฆฌ๊ฐ€ ์—†๊ธฐ์— Lightburb ์•…์„ธ์„œ๋ฆฌ์— ๋„ฃ์„ ๊ฒƒ์ด๋‹ค.

https://developers.homebridge.io/#/service/Thermostat

 

Homebridge Plugin Developer Documentation

Homebridge plugin developer documentation and API reference.

developers.homebridge.io

https://developers.homebridge.io/#/service/Lightbulb

 

Homebridge Plugin Developer Documentation

Homebridge plugin developer documentation and API reference.

developers.homebridge.io

 

 

๋จผ์ €, ์˜จ๋„ ์กฐ์ ˆ ์„œ๋น„์Šค ํƒ€์ผ ๊ตฌํ˜„ ๋ถ€๋ถ„์ด๋‹ค.

Thermostat ์„œ๋น„์Šค๋กœ ์˜จ๋„์กฐ์ ˆ ํ˜•์‹์˜ ํƒ€์ผ์„ ์„ ์–ธํ•˜๊ณ ,

TargetTemperature ๋กœ ์˜จ๋„์˜ 15~50๋„ ๋ฒ”์œ„๋ฅผ 5๋„ ๋‹จ์œ„๋กœ ๊ฐ๊ฐ ๋งคํŠธ์˜ ๋‹จ๊ณ„์™€ ๋งคํ•‘ํ–ˆ๋‹ค.

๊ทธ ์™ธ์— On/Off ํ˜„์žฌ ์˜จ๋„ ์ƒํƒœ ํ‘œ์‹œ ๋ถ€๋ถ„๋„ ๊ตฌํ˜„ํ–ˆ๋‹ค.

initServices() {
    ...
    this.thermostatService = new this.Service.Thermostat(this.name + ' ์˜จ๋„');

    this.thermostatService.getCharacteristic(this.Characteristic.TargetTemperature)
        .setProps({ minValue: MIN_TEMP, maxValue: MAX_TEMP, minStep: 5 })
        .onSet(this.handleSetTargetTemperature.bind(this))
        .onGet(() => this.currentState.targetTemp);

    this.thermostatService.getCharacteristic(this.Characteristic.CurrentTemperature)
        .setProps({ minValue: MIN_TEMP, maxValue: MAX_TEMP, minStep: 1 })
        .onGet(() => this.currentState.currentTemp);

    const targetHeatingCoolingStateCharacteristic = this.thermostatService.getCharacteristic(this.Characteristic.TargetHeatingCoolingState);
    targetHeatingCoolingStateCharacteristic.setProps({
        validValues: [this.Characteristic.TargetHeatingCoolingState.OFF, this.Characteristic.TargetHeatingCoolingState.HEAT]
    });
    targetHeatingCoolingStateCharacteristic
        .onSet(this.handleSetTargetHeatingCoolingState.bind(this))
        .onGet(() => {
            return this.currentState.currentHeatingCoolingState === this.Characteristic.CurrentHeatingCoolingState.OFF
                ? this.Characteristic.TargetHeatingCoolingState.OFF
                : this.Characteristic.TargetHeatingCoolingState.HEAT;
        });

    this.thermostatService.getCharacteristic(this.Characteristic.CurrentHeatingCoolingState)
        .onGet(() => this.currentState.currentHeatingCoolingState);

    this.thermostatService.setCharacteristic(this.Characteristic.TemperatureDisplayUnits, this.Characteristic.TemperatureDisplayUnits.CELSIUS);
    ...
}
 

 

๋‹ค์Œ์œผ๋กœ ์˜จ๋„ ์„ค์ • ๋กœ์ง ๋ถ€๋ถ„,

๋งคํŠธ์˜ ์˜จ๋„ ๋‹จ๊ณ„์™€, ์•…์„ธ์„œ๋ฆฌ์˜ ์˜จ๋„๋ถ€๋ถ„์„ ์„ค์ •ํ•ด์ฃผ๋Š” ๋ถ€๋ถ„๊ณผ,

ํ†ต์‹  ์•ˆ์ •ํ™”๋ฅผ ์œ„ํ•œ ๋””๋ฐ”์šด์‹ฑ, ์ค‘๋ณต ๋ช…๋ น ๋ฐฉ์ง€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค.

handleSetTargetTemperature(value) {
    let level = TEMP_LEVEL_MAP[Math.round(value / 5) * 5] || 0;
    if (value < MIN_TEMP) level = 0;
    if (value >= MAX_TEMP) level = 7;

    if (level === this.lastSentLevel && this.currentState.targetTemp === value) {
        // ์ด๋ฏธ ์ „์†ก๋œ ๊ฐ’ ํŒจ์Šค
        return;
    }

    if (this.setTempTimeout) {
        clearTimeout(this.setTempTimeout);
    }

    this.setTempTimeout = setTimeout(async () => {
        try {
            await this.sendTemperatureCommand(value, level);
        } catch (e) {
            // ์˜จ๋„ ์„ค์ • ์ค‘ ble ํ†ต์‹  ์˜ค๋ฅ˜
        }
    }, 350);
}
 

 

๋งˆ์ง€๋ง‰์œผ๋กœ ์˜จ๋„ ์ œ์–ด ํŒจํ‚ท์˜ BLE ํ†ต์‹  ๋ถ€๋ถ„์ด๋‹ค.

์ด ๋ฉ”์„œ๋“œ์—์„œ๋Š” ํŒจํ‚ท ์ƒ์„ฑ, BLE ํ†ต์‹  ์ „์†ก/์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ

Homekit ์•…์„ธ์„œ๋ฆฌ์™€์˜ ์ƒํƒœ ๋™๊ธฐํ™” ๋ถ€๋ถ„์„ ๊ตฌํ˜„ํ–ˆ๋‹ค.

async sendTemperatureCommand(value, level) {
    this.setTempTimeout = null;
    const packet = this.createControlPacket(level);

    if (this.tempCharacteristic && this.isConnected) {
        try {
            await this.safeWriteValue(this.tempCharacteristic, packet);
            this.lastSentLevel = level;

            this.currentState.targetTemp = value;
            this.currentState.currentTemp = LEVEL_TEMP_MAP[level];
            this.currentState.currentHeatingCoolingState =
                level > 0 ? this.Characteristic.CurrentHeatingCoolingState.HEAT : this.Characteristic.CurrentHeatingCoolingState.OFF;

            if (level > 0) {
                this.currentState.lastHeatTemp = value;
            }

            this.thermostatService.updateCharacteristic(this.Characteristic.CurrentTemperature, this.currentState.currentTemp);
            this.thermostatService.updateCharacteristic(this.Characteristic.CurrentHeatingCoolingState, this.currentState.currentHeatingCoolingState);
            this.thermostatService.updateCharacteristic(this.Characteristic.TargetHeatingCoolingState, this.currentState.currentHeatingCoolingState === this.Characteristic.CurrentHeatingCoolingState.OFF
                ? this.Characteristic.TargetHeatingCoolingState.OFF
                : this.Characteristic.TargetHeatingCoolingState.HEAT);
        } catch (error) {
            // BLE ์“ฐ๊ธฐ ์˜ค๋ฅ˜
            throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
        }
    } else {
        if (level === 0) {
            return;
        } else {
            // BLE ์—ฐ๊ฒฐ ์—†์Œ, ๋ช…๋ น ์ „์†ก ๋ถˆ๊ฐ€ 
            throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
        }
    }
}
 

 

ํƒ€์ด๋จธ ๋ถ€๋ถ„๋„ ๋™์ผํ•˜๋‹ค.

์„œ๋น„์Šค ์ดˆ๊ธฐํ™” ๋ฉ”์„œ๋“œ์— Lightbulb ์„œ๋น„์Šค ํƒ€์ž…์œผ๋กœ ์„ ์–ธํ•œ ํ›„,

ํƒ€์ด๋จธ(1~15์‹œ๊ฐ„)์„ 100/15 == ์•ฝ 6.67%์— ํ•œ ์‹œ๊ฐ„์œผ๋กœ ์„ค์ •ํ–ˆ๋‹ค.

initServices() {
    ...
    this.timerService = new this.Service.Lightbulb(this.name + ' ํƒ€์ด๋จธ ์„ค์ •');

    this.timerService.getCharacteristic(this.Characteristic.On)
        .onSet(this.handleTimerSwitch.bind(this))
        .onGet(() => this.currentState.timerOn);

    this.timerService.getCharacteristic(this.Characteristic.Brightness)
        .setProps({ minValue: 0, maxValue: 100, minStep: BRIGHTNESS_PER_HOUR })
        .onSet(this.handleSetTimerHours.bind(this))
        .onGet(() => this.currentState.timerHours * BRIGHTNESS_PER_HOUR);

    this.timerService.setCharacteristic(this.Characteristic.Brightness, this.currentState.timerHours * BRIGHTNESS_PER_HOUR);
    this.timerService.setCharacteristic(this.Characteristic.On, this.currentState.timerOn);
    ...
}
 

 

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ํƒ€์ด๋จธ ๋กœ์ง ๋ถ€๋ถ„,

์œ„์— ๋งํ–ˆ๋“ฏ ์•…์„ธ์„œ๋ฆฌ์˜ 6.67%๋ฅผ ํ•œ ์‹œ๊ฐ„์œผ๋กœ ๊ณ„์‚ฐํ•ด์ฃผ๋Š” ๋กœ์ง๊ณผ,

BLE ์ „์†ก ๋ฐ ์—…๋ฐ์ดํŠธ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค.

async handleSetTimerHours(value) {
    let hours = Math.round(value / BRIGHTNESS_PER_HOUR);

    if (value > 0 && hours === 0) {
        hours = 1;
    }

    if (hours > MAX_TIMER_HOURS) {
        hours = MAX_TIMER_HOURS;
    }

    if (hours === 0) {
        // 0์‹œ๊ฐ„ > OFF
        this.handleSetTargetTemperature(MIN_TEMP);
    }

    await this.sendTimerCommand(hours);

    this.currentState.timerHours = hours;
    this.currentState.timerOn = hours > 0;

    const brightnessToSet = hours * BRIGHTNESS_PER_HOUR;

    this.timerService.updateCharacteristic(this.Characteristic.On, this.currentState.timerOn);
    this.timerService.updateCharacteristic(this.Characteristic.Brightness, brightnessToSet);
}
 

 

์—ฌ๊ธฐ๋„ BLE ํ†ต์‹ ๊ณผ, Homekit๊ณผ ์ƒํƒœ ๋™๊ธฐํ™”,

ํƒ€์ด๋จธ ์ œ์–ด ํŒจํ‚ท ์ƒ์„ฑ ๋ถ€๋ถ„์„ ๊ตฌํ˜„ํ•ด ์ฃผ์—ˆ๋‹ค.

async sendTimerCommand(hours) {
    const packet = this.createControlPacket(hours); // ํŒจํ‚ท ์ „์†ก

    if (this.timeCharacteristic && this.isConnected) {
        try {
            await this.safeWriteValue(this.timeCharacteristic, packet);
        } catch (error) {
            // BLE ์˜ค๋ฅ˜
            throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
        }
    } else {
        if (hours === 0) {
            return;
        } else {
            // BLE ์—ฐ๊ฒฐ ์—†์Œ, ๋ช…๋ น ์ „์†ก ๋ถˆ๊ฐ€
            throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
        }
    }
}
 

 

 

ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ฐœ๋ฐœ (BLE ์—ฐ๊ฒฐ ๊ตฌํ˜„)

 

์ด์ œ ์‹ค์ œ ๋งคํŠธ์™€ ์„œ๋ฒ„๊ฐ€ ๋ธ”๋ฃจํˆฌ์Šค๋กœ ํ†ต์‹  ํ•  ๋ถ€๋ถ„์„ ๊ตฌํ˜„ํ•ด ์ฃผ์–ด์•ผ ํ•œ๋‹ค.

 

node-ble ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด ble ์„œ๋น„์Šค๋ฅผ ์ดˆ๊ธฐํ™” ํ•˜๊ณ ,

์žฅ์น˜๊ฐ€ ์˜คํ”„๋ผ์ธ ์ผ ๊ฒฝ์šฐ, ์žฌ์—ฐ๊ฒฐ ๋ฃจํ”„๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๋ถ€๋ถ„์„ ๊ตฌํ˜„ํ–ˆ๋‹ค.

async initializeBleAdapter() {
    try {
        // ble ์ดˆ๊ธฐํ™”
        const { bluetooth } = NodeBle.createBluetooth();
        this.startScanningLoop(); // ์Šค์บ” ๋ฃจํ”„ ์‹œ์ž‘
    } catch (error) {
        // ์ดˆ๊ธฐํ™” ์‹คํŒจ 
    }
}

async startScanningLoop() {
    if (!this.adapter || this.isScanningLoopActive) {
        // ์‹คํ–‰ ์ค‘ ํ˜น์€ ์–ด๋Œ‘ํ„ฐ ์—†์Œ
        return;
    }
    this.isScanningLoopActive = true; // ์—ฐ๊ฒฐ ๋ฃจํ”„ ์‹œ์ž‘

    while (this.isScanningLoopActive) {
        if (!this.isConnected) {
            // ์Šค์บ” ์‹œ์ž‘
            try {
                await this.adapter.startDiscovery();
                const targetAddress = this.macAddress.toUpperCase();

                await this.adapter.stopDiscovery();
                const deviceAddresses = await this.adapter.devices();

                let targetDevice = null;
                let foundAddress = null;

                for (const address of deviceAddresses) {
                    const normalizedAddress = address.toUpperCase().replace(/:/g, '');

                    if (normalizedAddress === targetAddress) {
                        targetDevice = await this.adapter.getDevice(address);
                        foundAddress = address;
                        break;
                    }
                }

                if (targetDevice) {
                    this.device = targetDevice;  // ์žฅ์น˜ ๋ฐœ๊ฒฌ
                    await this.connectDevice();
                } else {
                    if (deviceAddresses.length > 0) {
                        // ์žฅ์น˜ ๋ฐœ๊ฒฌ ์‹คํŒจ
                    } else {
                        // ์žฅ์น˜ ๋ฐœ๊ฒฌ ์‹คํŒจ(all)
                    }
                }
            } catch (error) {
                // BLE ์Šค์บ” ์˜ค๋ฅ˜
            }
        } else {
            // ์—ฐ๊ฒฐ ์œ ์ง€
        }
        await sleep(this.scanInterval);
    }
}
 

 

connectDevice ๋ฉ”์„œ๋“œ์—์„œ๋Š”, ์Šค์บ” ๋ฃจํ”„์—์„œ ๋ฐœ๊ฒฌ๋œ ์žฅ์น˜์™€ ์—ฐ๊ฒฐ์„ ์ง„ํ–‰ํ•œ๋‹ค.

discoverCharacteristics ๋ฉ”์„œ๋“œ๋Š” ์ œ์–ด UUID๋ฅผ ์ ์šฉํ•˜์—ฌ,

์˜จ๋„ ๋ฐ ํƒ€์ด๋จธ์˜ ์‹ค์ œ ์ œ์–ด๋ฅผ ๋‹ด๋‹นํ•œ๋‹ค.

๋งˆ์ง€๋ง‰์œผ๋กœ ์—ฐ๊ฒฐ์ด ๋Š์–ด์กŒ์„๋•Œ ์ดˆ๊ธฐํ™” ํ•˜๋Š” ๋ถ€๋ถ„๋„ ๊ตฌํ˜„ํ–ˆ๋‹ค.

async connectDevice() {
    if (!this.device || this.isConnected) {
        return;
    }

    try {
        await this.device.connect();  // ์—ฐ๊ฒฐ ์‹œ๋„
        this.isConnected = true;  // ์—ฐ๊ฒฐ ์„ฑ๊ณต

        this.device.on('disconnect', () => {
            this.disconnectDevice(); // ํ•ด์ œ๋จ, ์žฌ์—ฐ๊ฒฐ ๋ฃจํ”„ ์‹คํ–‰
        });

        // GATT ํƒ์ƒ‰ ์ „ le-connection-abort-by-local ๋ฐฉ์ง€
        await sleep(500);
        await this.discoverCharacteristics();
    } catch (error) {
        this.disconnectDevice(true);
    }
}

async discoverCharacteristics() {
    if (!this.device) return;

    try {
        // ํŠน์„ฑ ํƒ์ƒ‰ ๋Œ€์ƒ
        const gatt = await this.device.gatt();
        const service = await gatt.getPrimaryService(this.serviceUuid);
        // ๋ฐœ๊ฒฌ ์„ฑ๊ณต 
        if (this.charSetUuid) {
            this.setCharacteristic = await service.getCharacteristic(this.charSetUuid);
        }
        this.tempCharacteristic = await service.getCharacteristic(this.charTempUuid);
        this.timeCharacteristic = await service.getCharacteristic(this.charTimeUuid);

        if (this.tempCharacteristic && this.timeCharacteristic) {
            // ์ œ์–ด์ค€๋น„ ์™„๋ฃŒ
            if (this.setCharacteristic) {
                // ์—ฐ๊ฒฐ ํ›„ ์ดˆ๊ธฐํ™” ํŒจํ‚ท ์ „์†ก
                await this.sendInitializationPacket();
            }
            // Keep-Alive ํŒจํ‚ท
            this.startKeepAlive();

        } else {
            // ์˜จ๋„ or ํƒ€์ด๋จธ ํƒ์ƒ‰ ์‹คํŒจ
            this.disconnectDevice(true);
        }
    } catch (error) {
        // ํƒ์ƒ‰ ์˜ค๋ฅ˜ UUID ํ™•์ธ
        this.disconnectDevice(true);
    }
}

disconnectDevice(resetDevice = false) {
    this.stopKeepAlive();

    const deviceToDisconnect = this.device;

    this.isConnected = false;
    this.tempCharacteristic = null;
    this.timeCharacteristic = null;
    this.setCharacteristic = null;

    if (resetDevice) {
        this.device = null;
    }

    if (deviceToDisconnect) {
        deviceToDisconnect.disconnect().catch(e => {
            if (!e.message.includes('not connected') && !e.message.includes('does not exist')) {
                // ์•ˆ์ „ ์—ฐ๊ฒฐ ํ•ด์ œ ์‹คํŒจ
            }
        });
    }
}
 

 

๋‹ค์Œ์œผ๋กœ๋Š” ์—ฐ๊ฒฐ ์•ˆ์ •ํ™” ๋ถ€๋ถ„์ด๋‹ค.

 

์—ฌ๊ธฐ์„œ๋Š”, ์—ฐ๊ฒฐ ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด keep-alive ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์ ์šฉํ–ˆ๋‹ค.

Linux BLE ์Šคํƒ(BlueZ)์˜ 110์ดˆ ํƒ€์ž„์•„์›ƒ์„ ํšŒํ”ผํ•˜๊ณ ,

์ดˆ๊ธฐ ์ง€์—ฐ ๋ฐ, ์ฃผ๊ธฐ์ ์ธ keep-alive ํŒจํ‚ท ์ „์†ก์œผ๋กœ ์•ˆ์ •์„ฑ์„ ํ™•๋ณดํ–ˆ๋‹ค.

async sendInitialKeepAlivePacket() {
    try {
        await this.sendInitializationPacket(true);
        // Keep-Alive ํŒจํ‚ท ์ „์†ก
    } catch (e) {
        // ์ „์†ก ์‹คํŒจ
    }
}

startKeepAlive() {
    this.stopKeepAlive();
    
    this.keepAliveTimer = setTimeout(() => {
        if (!this.isConnected) return;
        this.sendInitialKeepAlivePacket();

        this.keepAliveInterval = setInterval(async () => {
            if (this.isConnected) {
                try {
                    await this.sendInitializationPacket(true);
                    // keep-alive ํŒจํ‚ท ์žฌ์ „์†ก
                } catch (e) {
                    // keep-alive ํŒจํ‚ท ์ „์†ก ์‹คํŒจ 
                }
            }
        }, KEEP_ALIVE_INTERVAL_MS);
    }, KEEP_ALIVE_INITIAL_DELAY_MS);
}

stopKeepAlive() {
    if (this.keepAliveTimer) {
        clearTimeout(this.keepAliveTimer);
        this.keepAliveTimer = null;
    }
    if (this.keepAliveInterval) {
        clearInterval(this.keepAliveInterval);
        this.keepAliveInterval = null;
    }
}

async sendInitializationPacket(isKeepAlive = false) {
    if (!this.setCharacteristic || !this.isConnected || !this.initPacketHex) {
        return;
    }

    try {
        const initPacket = Buffer.from(this.initPacketHex, 'hex');
        if (!isKeepAlive) {
            // keep-alive ํŒจํ‚ท ์ „์†ก ์‹œ๋„
        }
        await this.setCharacteristic.writeValue(initPacket, { type: 'command' });

        if (!isKeepAlive) {
            // ํŒจํ‚ท ์ „์†ก ์„ฑ๊ณต
            await sleep(500); 
        }
    } catch (error) {
        if (!isKeepAlive) {
            // ํŒจํ‚ท ์ „์†ก ์˜ค๋ฅ˜
            this.disconnectDevice(true); 
            throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
        }
        throw error; 
    }
}
 

 

 

ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ฐœ๋ฐœ (์œ ํ‹ธ๋ฆฌํ‹ฐ)

 

๋งˆ์ง€๋ง‰์œผ๋กœ ํ”Œ๋Ÿฌ๊ทธ์ธ์— ์‚ฌ์šฉํ•  ์œ ํ‹ธ ๋ถ€๋ถ„์ด๋‹ค.

 

์—ฌ๊ธฐ์„œ๋Š” ์ง€๋‚œ ๊ฒŒ์‹œ๊ธ€์—์„œ ๋ถ„์„ํ•œ ์ œ์–ด ํŒจํ‚ท์ธ,

[date, cheksum, data, checksum] ๊ตฌ์กฐ์˜ ํŒจํ‚ท์„ ๋งŒ๋“œ๋Š” ๋ฉ”์„œ๋“œ์™€

 

์˜จ๋„ ํƒ€์ด๋จธ ์ œ์–ด UUID์— ํŒจํ‚ท ์ „์†ก์‹œ, ์žฌ์‹œ๋„ ๋ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ๋ฉ”์„œ๋“œ์ด๋‹ค.

๋ช…๋ น ์ถฉ๋Œ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์ง€์—ฐ ์ฒ˜๋ฆฌ์™€, ์žฌ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ 3ํšŒ๋กœ ๋‘์–ด ์•ˆ์ •์„ฑ์„ ๋†’์˜€๋‹ค.

createControlPacket(value) {
    const dataByte = value;
    const checkSum = (0xFF - dataByte) & 0xFF;

    const buffer = Buffer.alloc(4);

    buffer.writeUInt8(dataByte, 0);
    buffer.writeUInt8(checkSum, 1);
    buffer.writeUInt8(dataByte, 2);
    buffer.writeUInt8(checkSum, 3);

    return buffer;
}

async safeWriteValue(characteristic, packet, maxRetries = 3, delayMs = WRITE_DELAY_MS) {
    if (!this.isConnected) {
        throw new Error("Device not connected.");
    }
    const writeOptions = { type: 'command' };
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            await characteristic.writeValue(packet, writeOptions); // ์“ฐ๊ธฐ ์‹œ๋„
            await sleep(delayMs);
            return true;
        } catch (error) {
            // ์“ฐ๊ธฐ ์˜ค๋ฅ˜ ๋ฐœ์ƒ
        }
    }
}
 

 

์ด๋ ‡๊ฒŒ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ œ์ž‘์ด ๋๋‚ฌ๋‹ค.

์ด์ œ ์‹ค์ œ Homebridge ์„œ๋ฒ„์— ๋“ฑ๋ก ํ›„ ์„ค์ •์„ ์ง„ํ–‰ํ•ด๋ณด์ž

 

 

ํ”Œ๋Ÿฌ๊ทธ์ธ ๋“ฑ๋ก

 

์ €๋ฒˆ ๋ฐ์Šคํฌํƒ‘ ์ œ์–ด ํ”Œ๋Ÿฌ๊ทธ์ธ ์„ค์น˜ ๋•Œ์™€ ๊ฐ™์€ ๋ฐฉ๋ฒ•์„ ์ง„ํ–‰ํ•  ๊ฒƒ์ด๋ฏ€๋กœ

์„ค๋ช…์€ ์ƒ๋žต ํ•˜๊ณ  ๋ฐ”๋กœ ์„ค์น˜๋ฅผ ์ง„ํ–‰ํ•ด ์ฃผ์—ˆ๋‹ค.

 

์„ค์น˜ ํ›„, ํ”Œ๋Ÿฌ๊ทธ์ธ ๋ชฉ๋ก์— ์ •์ƒ์ ์œผ๋กœ ๋œจ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

์ด์ œ configํŒŒ์ผ์— ์ €๋ฒˆ ๊ฒŒ์‹œ๊ธ€์˜ ํŒจํ‚ท ๋ถ„์„์—์„œ ์ฐพ์€ UUID๊ฐ’๊ณผ

๋งคํŠธ์˜ MAC ์ฃผ์†Œ, ๊ทธ ์™ธ ์„ค์ •๋“ค์„ ์ž‘์„ฑํ•ด ์ฃผ์—ˆ๋‹ค.

 

์ €์žฅ ํ›„, Home ์•ฑ์— ๋“ฑ๋ก์‹œ, ๋ธŒ๋ฆฟ์ง€๊ฐ€ ์ •์ƒ์œผ๋กœ ๋œจ๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

์•…์„ธ์„œ๋ฆฌ ํƒ€์ผ์€ ์˜จ๋„์กฐ์ ˆ ํƒ€์ผ๋กœ ์ž˜ ๋œจ๋Š” ๋ชจ์Šต

 

์ด์ œ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด ๋ณด์•˜๋‹ค.

์˜จ๋„ ์กฐ์ ˆ๊ณผ ํƒ€์ด๋จธ ์กฐ์ ˆ ๋ชจ๋‘ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 

์ด๋กœ์จ ๊ตฌํ˜• ์ „๊ธฐ๋งคํŠธ๋„ ์• ํ”Œ ํ™ˆ ์ƒํƒœ๊ณ„์— ๋“ฑ๋กํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

๋•๋ถ„์— ๋ธ”๋ฃจํˆฌ์Šค ํ†ต์‹  ๊ตฌ์กฐ์— ๋Œ€ํ•œ ์ดํ•ด์™€ ํ†ต์‹  ๋งค์ปค๋‹ˆ์ฆ˜๋„ ์ดํ•ดํ•˜๋Š” ๊ณ„๊ธฐ๊ฐ€ ๋˜์—ˆ๋‹ค.

์ด๋ฒˆ ๊ฒจ์šธ์—๋Š” Siri๋ฅผ ์ด์šฉํ•ด ์ „๊ธฐ ๋งคํŠธ๋„ ์ œ์–ด ํ•  ์ˆ˜ ์žˆ์–ด์„œ ๋งŒ์กฑ์Šค๋Ÿฝ๋‹ค

 


์›๋ณธ ๊ฒŒ์‹œ๊ธ€

https://blog.naver.com/101artspace/224061230387

 

Homebridge ์ „๊ธฐ๋งคํŠธ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ œ์ž‘, ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ตฌํ˜„ - 2

์ €๋ฒˆ ๊ฒŒ์‹œ๊ธ€์—์„œ, ์ „๊ธฐ๋งคํŠธ์˜ ๋ธ”๋ฃจํˆฌ์Šค ํ†ต์‹  ํŒจํ‚ท์„ ๋ถ„์„ํ•ด ๋ณด์•˜๋‹ค. ์ด์   ๊ทธ ํŒจํ‚ท๋“ค์„ ํ† ๋Œ€๋กœ Homebridge ...

blog.naver.com

๋ฐ˜์‘ํ˜•