๐งฉ ์ ๊ธฐ์ฅํ์ ์ ํ ํ์ ์ฐ๊ฒฐํด๋ณด์โโโ2ํธ ํ๋ฌ๊ทธ์ธ ๊ฐ๋ฐ
Homebridge ํ๋ฌ๊ทธ์ธ์ ๊ฐ๋ฐํด๋ณด์!
๐งฉ ์ ๊ธฐ์ฅํ์ ์ ํ ํ์ ์ฐ๊ฒฐํด๋ณด์ โ 2ํธ ํ๋ฌ๊ทธ์ธ ๊ฐ๋ฐ
Homebridge ํ๋ฌ๊ทธ์ธ์ ๊ฐ๋ฐํด๋ณด์!

์ด์ ํธโ ๋ฆฌ๋ฒ์ฑํธ
์๋ ํ์ธ์, ์ค๋งํธ ํ ์๋ํ์ ์ง์ฌ์ธ ๊ฐ๋ฐ์ ์ ์ฑ์ฐ์ ๋๋ค.
์ด์ ํธ์์ ์ค๋งํธํฐ ์ฑ์ ๋ถ์ํด์ ์ ๊ธฐ์ฅํ์ ์ฝ๋๋ก ์ ์ดํ๋ ๋ฐฉ๋ฒ์ ์์๋์ต๋๋ค. ์ด๋ฒ ํธ์์ ์์๋ธ ์ฝ๋๋ฅผ ์ด์ฉํด์ ์ ํ ํ์ด ์ ๊ธฐ์ฅํ์ ์ ์ดํ ์ ์๋๋ก ์ฐ๊ฒฐํด์ฃผ๋ ํ๋ธ๋ฆฟ์ง ํ๋ฌ๊ทธ์ธ์ ๊ฐ๋ฐํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
ํ๋ธ๋ฆฟ์ง๋?
์ด์ ํธ์์๋ ์ค๋ช ํ์ง๋ง ๋ค์ ๊ฐ๋จํ ์ค๋ช ๋๋ฆฌ์๋ฉด, ํ๋ธ๋ฆฟ์ง๋ node.js ๊ธฐ๋ฐ์ผ๋ก Apple HomeKit๊ณผ ์ฝ๊ฒ ์ฐ๋ํ ์ ์๊ฒ ํด์ฃผ๋ ํ๋ก๊ทธ๋จ์ ๋๋ค.

ํ๋ธ๋ฆฟ์ง๋ฅผ ์ค์นํ๊ณ ๋๋ฉด ์์ ๊ฐ์ด UI์์ ์ํ๋ ํ๋ฌ๊ทธ์ธ์ ๊ฒ์ํ๊ณ ์ค์นํ ์ ์์ต๋๋ค. ์ด ํ๋ฌ๊ทธ์ธ๋ค์ ๊ธฐ์ /๋จ์ฒด์์ ๊ณต์์ผ๋ก ๋ฐฐํฌํ๋ ๊ฒ์ด ์๋๊ณ ๊ฐ์ธ ๊ฐ๋ฐ์๋ค์ด ์ง์ ๊ฐ๋ฐํ๊ณ ๋ฐฐํฌํ๋ ๊ฒ์ ๋๋ค.
๊ทธ๋์ ์ ๋ ๊ฐ๋ฐ์๋ก์, ํ๋ฌ๊ทธ์ธ์ ๊ฐ๋ฐํด๋ณด์ ์๊ฐ์ด ๋ค์์ต๋๋ค.
ํ๋ฌ๊ทธ์ธ ๊ฐ๋ฐ์ ํ๊ธฐ ์ํด์ ๊ฒ์ํด๋ณด๋ ์ ๊ณต์ ๊ฐ๋ฐ ๋ฌธ์๋ฅผ ์ฐพ์์ต๋๋ค. ํ๋ฌ๊ทธ์ธ ํ ํ๋ฆฟ ๋ ํฌ์งํ ๋ฆฌ๋ฅผ ํฌํฌํด์ ํ๋ธ๋ฆฟ์ง API์ ๋ง๊ฒ ์ฝ๋๋ฅผ ์ง์ฃผ๋ฉด ๋๋ค๊ณ ํฉ๋๋ค. ์ด์ ๊ฐ๋ฐํ๊ฒฝ์ ๊ตฌ์ถํด๋ด ์๋ค.
์ค๋น 1๋จ๊ณ โ ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ถํ๊ธฐ
๊ฐ๋ฐ ํ๊ฒฝ์ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ํด์ ์ ํ ํ๋ฆฟ ๋ ํฌ์งํ ๋ฆฌ์ README์ ์์ธํ ์ ํ ์์ต๋๋ค. ๊ทธ๋ฐ๋ฐ ๊ธ์ ์ฝ์ด๋ณด๋ ๊ฐ๋ฐํ๊ธฐ ๋ฒ๊ฑฐ๋ก์ธ ๊ฒ ๊ฐ์ ๋ถ๋ถ์ด ์์ต๋๋ค.
Link To Homebridge ์ ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ด ํ๋ผ๊ณ ์ ํ ์์ต๋๋ค.
npm link๋ฅผ ํตํด ๊ฐ๋ฐ์ค์ธ ํ๋ฌ๊ทธ์ธ์ ๊ธ๋ก๋ฒ๋ก ์ค์นํ๋ค.homebridge์ธ์คํด์ค๋ฅผ ์คํํ๋ค.
์ฌ๊ธฐ์ ๋ฌธ์ ๋ ํ๋ธ๋ฆฟ์ง๊ฐ ์ค์น๋ ๊ธฐ๊ธฐ์ ํ๋ฌ๊ทธ์ธ๋ฅผ ๊ฐ๋ฐํ๋ ๊ธฐ๊ธฐ๊ฐ ๊ฐ์์ผ ํ๋ค๋ ์ ์ ๋๋ค. ์ ๋ ํ๋ธ๋ฆฟ์ง๋ ์๋๋ก์ง ๋์ค์ ์ค์นํ๊ณ , ํ๋ฌ๊ทธ์ธ ๊ฐ๋ฐ์ ์ ๋งฅ๋ถ์์ ํ๋ ค๊ณ ํ๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ๋ง์ถฐ์ค ์ ์์ต๋๋ค. ๐ฅ
๊ฐ์ฅ ์ฌํํ ํด๊ฒฐ์ฑ
์ ์คํํ ๋ ๋ง๋ค scp๋ฅผ ํ๊ฑฐ๋ git push & pull์ ํด์ ๋ฐฐํฌํ๋ ๋ฐฉ๋ฒ์ด ์๊ธด ํฉ๋๋ค๋ง, ๋งค์ฐ ๋ฒ๊ฑฐ๋ก์ธ ๊ฒ์ด ๋ปํ์ต๋๋ค.
๊ทธ๋์ ์๊ฐํ ๋ฐฉ๋ฒ์ ์ค์ ์๋๋ก์ง ๋์ค๋ ๋ฉ๋๊ณ , ์ ๋งฅ๋ถ์๋ค๊ฐ ๋ณ๋์ ํ๋ธ๋ฆฟ์ง๋ฅผ ์ค์นํด ๊ฐ๋ฐํ๋ ๋์ ์ ๋งฅ๋ถ์ ํ๋ธ๋ฆฟ์ง ์๋ฒ๋ก์จ ๋์์ํค๋ ๊ฒ์ ๋๋ค.
$ npm install -g homebridge homebridge-config-ui-x
ํ๋ธ๋ฆฟ์ง ์ค์น๋ ๊ฐ๋จํฉ๋๋ค. npm ํจํค์ง๋ฅผ ๊ธ๋ก๋ฒ๋ก ์ค์นํด์ฃผ๊ธฐ๋ง ํ๋ฉด ๋ฉ๋๋ค. (์ฐธ๊ณ ๋ก homebridge-config-ui-x ํจํค์ง๋ ํ๋ธ๋ฆฟ์ง UI์ ๋ํ ํจํค์ง๋ก ํ๋ธ๋ฆฟ์ง๊ฐ ์คํ ์ค์ localhost:8581๋ก ์ ๊ทผํ๋ฉด ์น UI๋ฅผ ์ ๊ณตํฉ๋๋ค.)
์ด์ ์ ๋งฅ๋ถ์์ ํ๋ฌ๊ทธ์ธ๋ฅผ ์คํํ ์ ์๊ฒ ๋์์ต๋๋ค.๐ฅณ
๊ทธ๋ฐ๋ฐ ์ฌ๊ธฐ์ ๋์ด ์๋๊ณ ์ด์ ๋๋ฒ๊น ํ๊ฒฝ์ ๊ฐ์ถฐ์ผํฉ๋๋ค. ์ ๋ ๊ฐ๋ฐ์ ํ ๋, IDE(์ฃผ๋ก VS Code)์์ ๋๋ฒ๊น ๋ชจ๋๋ก ์คํ(ํน์ Attach)ํ์ฌ ๋ธ๋ ์ดํน ํฌ์ธํธ๋ฅผ ์ก๊ฑฐ๋, ๋ณ์์ ๊ฐ์ ํ์ธํ๋ ๋ฑ์ ๋๋ฒ๊น ํ๊ฒฝ์ ๊ฐ์ถ๋ ๊ฒ์ด ๋งค์ฐ ์ค์ํ๋ค๊ณ ์๊ฐํฉ๋๋ค.
์ผ๋ฐ์ ์ธ ๊ฐ๋ฐ(ํ๋ก ๋์๋ or ๋ฐฑ์๋)์์์ ๋๋ฒ๊น ํ๊ฒฝ์ ๊ฐ์ถ๋ ๊ฒ์ ๋งค์ฐ ๊ฐ๋จํ์ง๋ง ์ด๋ ๊ฒ ํน์ํ ์ํฉ์์๋ ๊ฐ์ถ๊ธฐ๊ฐ ๋ณต์กํฉ๋๋ค. ๋คํํ๋ ๊ด๋ จํด์ ์ฐพ์๋ณด๋ ๋น๊ต์ ๊ฐ๋จํ ๋๋ฒ๊น ํ๊ฒฝ์ ๊ฐ์ถ ์ ์์์ต๋๋ค.
์๋ ๋ ๊ธ์ ์ฐธ๊ณ ํด์ VS Code์์ ๋๋ฒ๊น ํ๊ฒฝ์ ๋ง๋ค์์ต๋๋ค
- Reddit โ Live debugging using VSCode
- YouTube โ Getting Started with Homebridge Plugin Development
**lanch.json**
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"name": "Debug Homebridge",
"program": "${workspaceFolder}/node_modules/.bin/homebridge",
"preLaunchTask": "tsc: build - tsconfig.json",
"internalConsoleOptions": "neverOpen",
"args": ["-I", "-D"],
"request": "launch",
"skipFiles": [
"<node_internals>/**"
],
}
]
}
program์ ์ค์นํ ํ๋ธ๋ฆฟ์ง ํ๋ก๊ทธ๋จ์ ์ฃผ์๋ฅผ ์ ์ด์ค๋๋ค. ($ which homebridge)
์ฐธ๊ณ ๋ก ์ ๋ ๊ธ๋ก๋ฒ๋ก ์ค์นํ์ง ์๊ณ devDependencies์ homebridge์ homebridge-config-ui-x๋ฅผ ์ถ๊ฐํ๊ณ node_modules์ ์๋ ๋ฐ์ด๋๋ฆฌ ์ฃผ์๋ฅผ ๋ฃ์ด์คฌ์ต๋๋ค.
**tasks.json**
{
"version": "2.0.0",
"tasks": [
{
"label": "tsc: build - tsconfig.json",
"type": "typescript",
"tsconfig": "tsconfig.json",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$tsc"
]
}
]
}
ํ์ ์คํฌ๋ฆฝํธ ํ๋ก์ ํธ์ด๊ธฐ ๋๋ฌธ์, ์คํํ๊ธฐ ์ ๋ฌด์กฐ๊ฑด ๋น๋ํ๊ณ ์คํํ ์ ์๋๋ก ๋น๋ ํ์คํฌ๋ ์ถ๊ฐํด์คฌ์ต๋๋ค.
์ด๋ ๊ฒ ํด์ VS Code์์ Ctrl + F5 ๋ก ํ๋ธ๋ฆฟ์ง ์๋ฒ์ ํ๋ฌ๊ทธ์ธ์ ์คํ์ํค๊ณ , ๋ธ๋ ์ดํฌ ํฌ์ธํธ ์ก์ ์ ์๋ ๋ฑ ๊ฐ๋ฐ + ๋๋ฒ๊น ํ๊ฒฝ์ ๋ชจ๋ ๊ฐ์ถ์์ต๋๋ค.
(์ฐธ๊ณ ๋ก ๋ณ๋์ ํ๋ธ๋ฆฟ์ง ์๋ฒ๊ธฐ ๋๋ฌธ์, ์ต์ด ํ ๋ฒ localhost:8581์ ์ผ๊ณ ์ ํ ํ์์ ์ถ๊ฐํด์ค์ผ ํฉ๋๋ค.)
์ค๋น 2๋จ๊ณ โ ํ๋ธ๋ฆฟ์ง ํ๋ฌ๊ทธ์ธ ๊ตฌ์กฐ ํ์ ํ๊ธฐ
๋ญ๋ ์ง ๊ฐ๋ฐ์ ํ๊ธฐ ์์ ๋ณธ์ธ์ด ๊ฐ๋ฐํ๋ ค๋ ํ๋ ์์ํฌ์ ๋ํด ์ ์ดํดํด์ผ ํฉ๋๋ค. ํ๋ธ๋ฆฟ์ง ํ๋ฌ๊ทธ์ธ์ ๋ํ ์ค๋ช ์ ์ ์ฌ์ดํธ์์ ์์ ์ฝ๋ ๋ฑ์ผ๋ก ๋ณผ ์ ์์ง๋ง ์ฌ์ค ์ด๊ฒ๋ง ๋ณด๊ณ ์ดํดํ๊ธฐ๊ฐ ์ฝ์ง ์์ต๋๋ค. ์ ๊ฐ ๊ฐ๋ฐํ๋ฉด์ ๋๋ ๊ตฌ์กฐ๋ฅผ ๋ณด๊ธฐ ์ฝ๊ฒ ๋ค์ด์ด๊ทธ๋จ์ผ๋ก ์ ๋ฆฌํด๋ณด์์ต๋๋ค.
[embed]
- ํ๋ซํผ ํ๋ฌ๊ทธ์ธ (Platform Plugin) ํ๋ฌ๊ทธ์ธ์ด ์คํ๋๋ ์ต์๋จ ์ํธ๋ฆฌ ํฌ์ธํธ๋ก์จ, ์บ์๋ ์์ธ์๋ฆฌ๋ฅผ ๋ถ๋ฌ์ค๊ฑฐ๋ ๋์ (API ํธ์ถ ๋ฑ)์ผ๋ก ๊ธฐ๊ธฐ๋ฅผ ๊ฒ์/๋ธ๋ผ์ฐ์ง ํ ํ ์ด๋ฅผ ํ๋ซํผ ์์ธ์๋ฆฌ ๊ฐ์ฒด๋ก ๋ง๋ญ๋๋ค.
- ์ก์ธ์๋ฆฌ (Accessory) ๊ธฐ๊ธฐ ํ๋์ ๋์๋๋ ๊ฐ์ฒด์ ๋๋ค. ๋ง์ฝ ์ค๋งํธ ์ ๊ตฌ ํ๋ฌ๊ทธ์ธ์ด๋ผ๋ฉด ์ง ์์ ์ฌ๋ฌ ์ ๊ตฌ ํ๋ํ๋ ๋ง๋ค ์ก์ธ์๋ฆฌ ๊ฐ์ฒด๋ฅผ ๊ฐ์ง๋๋ค. (๊ธฐ๊ธฐ๋ง๋ค ๊ตฌ๋ณํ ์ ์๋ ๊ณ ์ ์์ด๋ ๊ฐ์ ๊ฐ์ง๊ณ ์์ด์ผ ํฉ๋๋ค.)
- ์๋น์ค (Service)
๊ด๋ จ๋ ์ฌ๋ฌ ํน์ฑ/์ ๋ณด(์บ๋ฆญํฐ๋ฆฌ์คํฑ)์ ๋ํ ๊ทธ๋ฃน์ผ๋ก์จ, ์์ธ์๋ฆฌ ํ๋์ ์ฌ๋ฌ ๊ฐ์ ์๋น์ค๊ฐ ์์ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด
๋ถ๋น์ด ๋๋ ์ ํ๊ธฐ๋ ์์ธ์๋ฆฌ๋LightBulb์๋น์ค๋ก ๋ถ๋น ์ ์ด,Fan์๋น์ค๋ก ํํฅ, ํ์ ์ ์ด ๋ฑ์ ํ ์ ์๋ค๊ณ ์๊ฐํ๋ฉด ๋ฉ๋๋ค. - ์บ๋ฆญํฐ๋ฆฌ์คํฑ (Characteristic)
์๋น์ค๊ฐ ๊ฐ์ง๊ณ ์๋ ํน์ฑ/์ ๋ณด ๋ค์ ์๋ฏธํฉ๋๋ค. ์๋ฅผ ๋ค์ด
Fan์๋น์ค์๋[On](https://developers.homebridge.io/#/characteristic/On) (boolean),[Name](https://developers.homebridge.io/#/characteristic/Name) (string),[Rotation Direction](https://developers.homebridge.io/#/characteristic/RotationDirection) (enum),[Rotation Speed](https://developers.homebridge.io/#/characteristic/RotationSpeed) (float)์ ์บ๋ฆญํฐ๋ฆฌ์คํฑ์ด ์์ต๋๋ค.
์บ๋ฆญํฐ๋ฆฌ์คํฑ์ ๊ฐ๊ฐ์ ์บ๋ฆญํฐ๋ฆฌ์คํฑ๋ง๋ค onGet, onSet ๋ผ๋ ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ์๋๋ฐ, onGet์ ์ ํ ํ ์ฑ์ด ์ผ์ง ๋์ฒ๋ผ ์ ๋ณด๋ฅผ ์์ฒญํ ๋ ๋ถ๋ฆฌ๊ณ , onSet์ ์ ํ ํ์์ ์กฐ์์ ํ ๋ ๊ฐ์ด ๊ธฐ๊ธฐ๋ฅผ ์ ์ดํ ๋ ๋ถ๋ฆฝ๋๋ค.
์ฐ๋ฆฌ๋ onGet ์์ ๊ธฐ๊ธฐ์ ์ํ๋ฅผ ์ฝ์ด์ ๋ฆฌํดํ๊ณ , onSet์์ API๋ฅผ ํธ์ถํ๊ธฐ๋ง ํ๋๋ก ๊ตฌํํ๋ฉด ๋ฉ๋๋ค.
๊ฐ๋ฐ 1๋จ๊ณ โ ๊ฐ์ง๋ก ๋์ํ๋ ํ๋ฌ๊ทธ์ธ ๋ง๋ค๊ธฐ

์ค์ API์ ์ํตํ์ง ์๊ณ ๊ฐ์ง ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ฐํ๋ ํ๋ฌ๊ทธ์ธ ๊ตฌ์กฐ
์ค์ ๋๋น์ API์ ์ฐ๋ํ๋ ๋น์ฆ๋์ค ๋ก์ง์ ๊ตฌํํ๊ธฐ ์ ์ ํ๋ฌ๊ทธ์ธ ๋ถ๋ถ์ ๋จผ์ ๊ตฌํํ๊ณ ์ ํฉ๋๋ค. ์ค์ ๊ธฐ๊ธฐ ์ํ๋ฅผ ์ฝ์ง ์๊ณ , ๋ฉ๋ชจ๋ฆฌ์ ๊ฐ์ง ๋ฐ์ดํฐ๋ฅผ ๋๊ฒจ์ฃผ๊ณ ์ด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋ ๊ฒ์ ๋จผ์ ํ์ธํ ๊ฒ์ ๋๋ค.
์ด๋ ๊ฒ ๋ ์ด์ด๋ฅผ ๋๋ ์ผ ํ ์คํธํ๊ธฐ๋ ์ฝ๊ณ , ๊ฐ๋ฐ ์ค ๋ฌธ์ ๊ฐ ์๊ฒผ์ ๋ ํ๋ฌ๊ทธ์ธ ๋ถ๋ถ์ด ๋ฌธ์ ์ธ์ง, ๋น์ฆ๋์ค ๋ก์ง ๋ถ๋ถ์ด ๋ฌธ์ ์ธ์ง ์๊ธฐ ์ฝ์ต๋๋ค. ํ ๋ฒ์ ๊ฐ๋ฐํ๋ค๋ฉด ์ด๋์ ๋ฌธ์ ์ธ์ง ์๊ธฐ๊ฐ ์ฝ์ง ์์ฃ . ๋ง์ง๋ง์ ๋น์ฆ๋์ค ๋ก์ง์ ๊ตฌํํ๊ณ ๋๋ฉด ๊ฐ์ง ๋ถ๋ถ์ ์ค์ ์๋น์ค๋ก ๊ฐ์์น์ฐ๊ธฐ๋ง ํ๋ฉด ๋ฉ๋๋ค.
์ด์ PlatformPlugin๋ฅผ ๊ตฌํํด๋ด
์๋ค.
export class NavienHomebridgePlatform implements DynamicPlatformPlugin {
constructor(
public readonly log: Logging,
config: PlatformConfig,
public readonly api: API,
) {
// ...
this.api.on('didFinishLaunching', this.onLaunched.bind(this));
}
async onLaunched() {
this.log.info('onLaunched called');
// run the method to discover / register your devices as accessories
const devices = this.service.getDevices();
for (const device of devices) {
this._registerDeviceAsAccessory(device);
}
}
private _registerDeviceAsAccessory(device: NavienDevice) {
this.log.info('Adding new accessory:', device.name);
const uuid = this.api.hap.uuid.generate(device.id);
const accessory = new this.api.platformAccessory<NavienDeviceContext>(device.name, uuid);
accessory.context.device = device;
new ElectricMat(this, accessory);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
}
(๋๋ถ๋ถ ํ ํ๋ฆฟ์์ ํฌํฌํ ์ฝ๋์ด๊ธฐ ๋๋ฌธ์ ์ค์ํ ๋ถ๋ถ๋ง ๊ฐ์ ธ์๋ดค์ต๋๋ค.)
ํ๋ฌ๊ทธ์ธ์ ์คํ๋์์ ๋, ๊ธฐ๊ธฐ๋ฅผ ๊ฒ์ํ๊ณ ์ด๋ฅผ ์ก์ธ์๋ฆฌ๋ก ๋ฑ๋กํฉ๋๋ค. ์ฐ๋ฆฌ๋ ์ฐ์ API ํธ์ถ์ ํ์ง ์๊ณ service.getDevices()์์ ํ๋ ์ฝ๋ฉ๋ ๊ธฐ๊ธฐ ๋ชฉ๋ก์ ๋ฆฌํดํ๋๋ก ํฉ์๋ค.
์ด์ ์์ฑ๋ Accessory๋ฅผ ๊ด๋ฆฌํ๋ ElectricMat ํด๋์ค๋ฅผ ๋ง๋ค์ด๋ด
์๋ค.
export default class ElectricMat {
constructor(
private readonly platform: NavienHomebridgePlatform,
private readonly accessory: NavienPlatformAccessory,
) {
const { device } = accessory.context;
// set accessory information
this.accessory.getService(Service.AccessoryInformation)!
.setCharacteristic(Characteristic.Manufacturer, 'Navien')
.setCharacteristic(Characteristic.Model, device.modelName)
.setCharacteristic(Characteristic.SerialNumber, device.id);
// TODO: implement
}
}
๊ธฐ๋ณธ์ ์ผ๋ก ํ์ํ [AccessoryInformation](https://developers.homebridge.io/#/service/AccessoryInformation) ์๋น์ค๋ฅผ ํตํด ๊ธฐ๊ธฐ์ ์ ๋ณด๋ค์ ๋ฃ์ด์ค๋๋ค. ๋ฃ์ด์ค ์ ๋ณด๋ ๋ค์๊ณผ ๊ฐ์ด ์ ํ ํ์์ ๊ธฐ๊ธฐ์ ์์ธ ์ ๋ณด ํ๋ฉด์ ๋ํ๋ฉ๋๋ค.

๊ฐ๋ฐ 1โ2๋จ๊ณ โ ๊ธฐ๊ธฐ์ ์นดํ ๊ณ ๋ฆฌ ์ค์ ํ๊ธฐ
์ ํ ํ์์ ๋ค์ํ ๊ธฐ๊ธฐ์ ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ ๊ตฌ๐ก๋ ์ค์์น๐๋ถํฐ ์์ํด์ ๋์ด๋ฒจ๐, ๋์ด๋ฝ๐์ด๋ผ๋ ์ง, ์ฌ์ง์ด ์ค์ ํค๋๐ฟ (โ๏ธ)ํ์ ๋ ์์ต๋๋ค! ๐ซข
์ ๊ธฐ ์ฅํ์ ์ด๋ค ์นดํ
๊ณ ๋ฆฌ๊ฐ ์ ํฉํ ์ง ์ฐพ์๋ณด๋, ์จ๋ ์กฐ์ ๊ด๋ จํ ๊ธฐ๊ธฐ ์นดํ
๊ณ ๋ฆฌ๋ [HeaterCooler](https://developers.homebridge.io/#/service/HeaterCooler)์ [Thermostat](https://developers.homebridge.io/#/service/Thermostat)์ด ์์์ต๋๋ค.
๋ฌธ์๋ฅผ ์ฝ์ด๋ณด๋ HeaterCooler๋ ์์ด์ปจ์ธ๋ฐ, ํน์ ์จ๋๋ก ์ค์ ํ์ง ๋ชปํ๊ณ ๋/๋๋ฐฉ๋ง ์ค์ ํ ์ ์๋ ๊ธฐ๊ธฐ, Thermostat์ ๋ณด์ผ๋ฌ๋ ์ง์ ์จ๋์กฐ์ ๊ธฐ์ฒ๋ผ ํน์ ์จ๋๋ฅผ ์ค์ ํ๋ฉด ๊ทธ์ ๋ฐ๋ผ ๋/๋๋ฐฉ์ด ๋์ํ๋ ๊ธฐ๊ธฐ๋ก ์ดํดํ์ต๋๋ค. ๊ทธ๋์ Thermostat์ด ๋ ์ ํฉํ๋ค๊ณ ์๊ฐํ์ต๋๋ค.
// create thermostat service
const thermostat = this.accessory.getService(Service.Thermostat)
|| this.accessory.addService(Service.Thermostat);
์์ ๊ฐ์ด Thermostat ์๋น์ค๋ฅผ ์์ฑํฉ๋๋ค. ์์ธ์๋ฆฌ๊ฐ ์ฒ์ ์์ฑ๋ ๋ getService()๊ฐ undefined๋ฅผ ๋ฆฌํดํ๊ณ , addService()๋ฅผ ํตํด์ ์๋น์ค๊ฐ ์ถ๊ฐ๋ฉ๋๋ค. ์บ์ฑ์ด ๋ ์ดํ๋ก๋ถํด getService()๋ฅผ ํตํด ์๋น์ค์ ์ ๊ทผํฉ๋๋ค.
Thermostat์๋ ๋ค์๊ณผ ๊ฐ์ 5๊ฐ์ ์บ๋ฆญํฐ๋ฆฌ์คํฑ์ด ์์ต๋๋ค.
- Current Heating Cooling State
- Target Heating Cooling State
- Current Temperature
- Target Temperature
- Temperature Display Units
์ด๋ฅผ ๋ชจ๋ ๊ตฌํํด์ค์ผ ํ๋๋ฐ, ๊ทธ ์ ์ ์ ๊น ๊ฐ๊ฐ ์บ๋ฆญํฐ๋ฆฌ์คํฑ์ onGet, onSet ์์ ๋ถ๋ฆด ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ๋ชจ๋ ์ ์ํด์ค์๋ค.
export default class ElectricMat {
private async getHeatingState(): Promise<CharacteristicValue> {
const { isActive } = this.device;
const state = isActive ? Characteristic.CurrentHeatingCoolingState.HEAT : Characteristic.CurrentHeatingCoolingState.OFF;
return state;
}
private async setHeatingState(value: CharacteristicValue) {
const state = value as number;
const isActive = !!state;
this.device.isActive = isActive;
}
private async getTemperature(): Promise<CharacteristicValue> {
const { temperature } = this.device;
return temperature;
}
private async setTemperature(value: CharacteristicValue) {
const temperature = value as number;
this.device.temperature = temperature;
}
}
ํ์ฌ๋ ๋จ์ํ device์ ํ๋ ๊ฐ์ ์ฝ๊ณ ์ฐ๊ธฐ๋ง ํ๋ ํธ๋ค๋ฌ๋ฅผ ์์ฑํฉ์๋ค. ์ฐธ๊ณ ๋ก ์ถํ์ ์ด๋ฒคํธ ํธ๋ค๋ฌ์์ API ์์ฒญ์ ๋ณด๋ผ ๊ฒ์ด๋ฏ๋ก ๋ชจ๋ async ํจ์๋ก ๋ง๋ค์ด ๋์ต๋๋ค.
์ด์ ๊ฐ๊ฐ์ ์บ๋ฆญํฐ๋ฆฌ์คํฑ์๋ค๊ฐ ์์ฑํ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ๋ฃ์ด์ค์๋ค.
// current state
thermostat.getCharacteristic(Characteristic.CurrentHeatingCoolingState)
.setProps({
validValues: [
Characteristic.CurrentHeatingCoolingState.OFF,
Characteristic.CurrentHeatingCoolingState.HEAT,
],
})
.onGet(this.getHeatingState.bind(this));
// target state
thermostat.getCharacteristic(Characteristic.TargetHeatingCoolingState)
.setProps({
validValues: [
Characteristic.TargetHeatingCoolingState.OFF,
Characteristic.TargetHeatingCoolingState.HEAT,
],
})
.onGet(this.getHeatingState.bind(this))
.onSet(this.setHeatingState.bind(this));
์ฒซ๋ฒ์งธ HeatingCoolingState๋ ํ์ฌ ์ค์ ๋ ๋/๋๋ฐฉ ์ํ ์ ๋ณด๋ฅผ ๋ํ๋
๋๋ค. ์ ๊ธฐ์ฅํ์ (๋น์ฐํ๊ฒ๋) ๋๋ฐฉ์ ์ง์ํ์ง ์์ผ๋๊น validValues์ OFF์ HEAT ๋ง์ ์ฃผ์ด์ ๋๊ธฐ/๋๋ฐฉ๋ง ์ค์ ํ ์ ์๋๋ก ํฉ๋๋ค.
์ฐธ๊ณ ๋ก Current์ Target ์ ์ฐจ์ด๋ ์๋ ๋/๋๋ฐฉ์ ๋ชจ๋ ์ง์ํ๋ ๊ธฐ๊ธฐ๋ฅผ ์ํ ์ต์
์
๋๋ค. ์๋ฅผ ๋ค์ด Target์ ์๋(TargetHeatingCoolingState.AUTO)์ผ๋ก ์ค์ ํ๋ฉด ๊ธฐ๊ธฐ๋ ์ค์ ๋ ์จ๋์ ๋ฐ๋ผ ์์์ ๋/๋๋ฐฉ์ ํฉ๋๋ค. ๊ทธ ๋ ํ์ฌ ๋์ํ๊ณ ์๋ ์ํ๋ฅผ ๋ํ๋ด๋ ํน์ฑ์ด Current์
๋๋ค. (๋น์ฐํ๊ฒ๋CurrentHeatingCoolingState๋ AUTO๊ฐ ์์ต๋๋ค.)
์ฐ๋ฆฌ๋ ํด๋น ์ฌํญ์ด ์๋๋ฏ๋ก ๋ ํน์ฑ์ ๋๊ฐ์ด ์ฒ๋ฆฌํฉ์๋ค.
// current temperature
thermostat.getCharacteristic(Characteristic.CurrentTemperature)
.onGet(this.getTemperature.bind(this));
๋๋ฒ์งธ CurrentTemperature๋ ํ์ฌ ์ธก์ ๋๋ ์จ๋(์ค๋ด ์จ๋)๋ฅผ ๋ํ๋
๋๋ค. ์ค๋ด์จ๋ ์ ๋ณด๋ ์๋ ์์ด์ปจ๊ณผ ๋ฌ๋ฆฌ ์ ๊ธฐ์ฅํ์๋ ๊ทธ๋ฐ ๊ธฐ๋ฅ์ด ์์ผ๋ฏ๋ก ๊ทธ๋ฅ ํ์ฌ ์ค์ ๋ ์จ๋๋ฅผ ๋ฆฌํดํ๋๋ก ํฉ์๋ค. (์ด๋ก ์ธํด ์ ํ ํ ์์ฝ์์ ์ง์ ์จ๋๊ฐ ์ ๊ธฐ์ฅํ ์ค์ ์จ๋๋ก ๋์ค๊ฒ ๋ฉ๋๋ค. ์์ฝ์์ ์ ์ธ๋ฅผ ๋๋ฌ์ ์ ์ธ์์ผ ์ค์๋ค.)
// target temperature
const { heatRange } = device.functions;
thermostat.getCharacteristic(Characteristic.TargetTemperature)
.setProps({
minValue: heatRange.min,
maxValue: heatRange.max,
minStep: heatRange.step,
})
.onGet(this.getTemperature.bind(this))
.onSet(this.setTemperature.bind(this));
์ธ๋ฒ์งธ TargetTemperature๋ ํ์ฌ ์ค์ ๋ ์จ๋๋ฅผ ๋ํ๋
๋๋ค. minValue, maxValue, minStep ๋ฑ์ ์ฃผ์ด์ ๊ธฐ๊ธฐ๊ฐ ์ค์ ๊ฐ๋ฅํ ์จ๋ ๋ฒ์๋ฅผ ์ ํด์ค๋๋ค.
// temp unit
thermostat.getCharacteristic(Characteristic.TemperatureDisplayUnits)
.setProps({
validValues: [Characteristic.TemperatureDisplayUnits.CELSIUS],
})
.setValue(Characteristic.TemperatureDisplayUnits.CELSIUS);
๋ง์ง๋ง TemperatureDisplayUnits๋ ์จ๋ ๋จ์์
๋๋ค. ์ ๊ธฐ์ฅํ์ด ์ญ์จ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ ํ์จ๋ก ์ค์ ์ ๋ฐ๊ฟ๋ ๋ฌด์ํฉ์๋ค. ๊ธฐ์กด๊ณผ ๋ค๋ฅด๊ฒ onGet, onSet์ ์ฃผ์ง ์๊ณ setValue๋ก ๊ณ ์ ๋ ๊ฐ์ ์ค๋๋ค.


์ง์~๐คญ ์์ง API ์ฐ๋์ ํ์ง ์์ ์ค์ ๊ธฐ๊ธฐ ์กฐ์์ ์ ๋์ง๋ง, ์ ์์ดํฐ์ ์ปจํธ๋กค ์ผํฐ์์ ์ ํ๋ฉด๊ณผ ๊ฐ์ด ์ ์ดํ ์ ์๋ UI๊ฐ ์๊ฒผ์ต๋๋ค.
์จ๋ ๋ฒ์์ OFF/HEAT ์ต์
๋ฑ์ด ์ฝ๋์์ ์ค์ ํ๋๋ก ์ ๋ฐ์๋์์ต๋๋ค. ์์์ ๋ง๋ ์ด๋ฒคํธ ํธ๋ค๋ฌ์ ๋ก๊ทธ๋ฅผ ์ถ๊ฐํด๋ณด๋ฉด UI๋ฅผ ์กฐ์ํ ๋, ์์ฒญ์ด ์ ์ค๋ ๊ฒ๋ ๋ณผ ์ ์์ต๋๋ค.
์ด์ ์ค์ API์ ์ฐ๋์ ํด๋ณผ ์ฐจ๋ก์ ๋๋ค.
๊ฐ๋ฐ 1โ3๋จ๊ณ โ ์ค์ UI ๋ง๋ค๊ธฐ
๋น์ฐํ๊ฒ ์ง๋ง ์ค์ API์ ์์ฒญ์ ๋ณด๋ด๊ธฐ ์ํด์ ๋ก๊ทธ์ธ์ ํด์ผ ํฉ๋๋ค. ์ฆ ์ฑ์ ๊ณ์ ์ ๋ณด๐๊ฐ ํ์ํ๋ฐ ์ด ํ์ํ ์ ๋ณด๋ฅผ ์ ์ ๊ฐ ์ค์ UI์์ ์ ๋ ฅํด์ ๋ฐ์ ์ ์๋๋ก ํด์ค์๋ค.
ํ๋ธ๋ฆฟ์ง ํ๋ฌ๊ทธ์ธ์ config.schema.json๋ฅผ ์คํ์ ๋งก๊ฒ ๊ตฌ์ฑํ๋ฉด ์ด์ ๋ง๊ฒ ์์์ ์ค์ UI๋ฅผ ๊ตฌ์ฑํด์ค๋๋ค. (์ฐธ๊ณ ๋ก ์ด JSON ๊ท๊ฒฉ์ *Angular JSON Schema Form*๋ผ๊ณ ํ๋ค๊ณ ํฉ๋๋ค. demonstration playground๋ฅผ ํตํด์ ๋ผ์ด๋ธ๋ก ํ
์คํธ๋ฅผ ํด๋ณผ ์ ์์ต๋๋ค.)
**config.schema.json**
{
"pluginAlias": "NavienSmart",
"pluginType": "platform",
"singular": true,
"schema": {
"type": "object",
"properties": {
"username": {
"title": "Username",
"type": "string"
},
"password": {
"title": "Password",
"type": "string"
}
}
}
}
์ฐ๋ฆฌ ํ๋ฌ๊ทธ์ธ์๋ ๋จ์ํ ์์ด๋, ๋น๋ฐ๋ฒํธ๋ง ํ์ํ๋ฏ๋ก ์์ ๊ฐ์ด ๊ตฌ์ฑํด์ค์๋ค.

config.schema.json ์ ๋ฐ๋ผ ์์ฑ๋ ์ค์ UI
ํ๋ธ๋ฆฟ์ง ์ค์ ํ์ด์ง์์ ํ์ธํด๋ณด๋ฉด ์์ ๊ฐ์ UI๊ฐ ์๊ธฐ๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
์ด์ ์ด ์ค์ ์ ๋ฐํ์์์ config๋ ํ๋ผ๋ฏธํฐ๋ก์จ ํ๋ฌ๊ทธ์ธ ์์ฑ์์์ ์ฃผ์
๋์ด {"username": "<your-username>", "password": "<your-password>"}์ ๊ฐ์ด JSON์ผ๋ก ๋์ด์ค๊ฒ ๋ฉ๋๋ค. ์ด์ ์ด config์์ ๊ฐ์ ์ฝ์ด ๋ก๊ทธ์ธ ์ฒ๋ฆฌ๋ฅผ ํด์ค์๋ค.
๊ฐ๋ฐ 2๋จ๊ณ โ ๋ก๊ทธ์ธ ์ฒ๋ฆฌํ๊ธฐ

๊ธฐ์กด ๊ฐ์ง ํ๋ฌ๊ทธ์ธ ๊ตฌ์กฐ์์ ๋ก๊ทธ์ธ๋ง ์ฐ๋ํด๋ด ์๋ค.
๋ก๊ทธ์ธ์ ์ด์ ํฌ์คํธ์์ ์๊ธฐํ๋ฏ์ด ๋ค์๊ณผ ๊ฐ์ด ๋์ํฉ๋๋ค.
๋ ๊ฐ์ง์ ์ธ์ฆ ๊ณผ์ ์ด ์๋๋ฐ 1. ์น ๋ก๊ทธ์ธ๊ณผ 2. ํ ํฐ ๋ก๊ทธ์ธ ์ผ๋ก ๋๋์ด ์์์ต๋๋ค. ์น์์ UI๋ฅผ ํตํด ์ ์ ์์ ์์ด๋์ ํจ์ค์๋๋ฅผ ๋ฐ์ผ๋ฉด Form ํ๊ทธ๋ก ๋ก๊ทธ์ธ ์์ฒญ์ ๋ณด๋ด๊ณ , ๋ก๊ทธ์ธ ์ฑ๊ณต ์ ์ธ์ฆ ์ ๋ณด(ํ ํฐ ๋ฑ)์ ์๋๋ก์ด๋/iOS ์น๋ทฐ์๊ฒ ์ฝ๋ฐฑ์ผ๋ก ๋ณด๋ ๋๋ค. ์ฑ์์๋ ์ด ์ ๋ณด๋ฅผ ๋ฐ์์ ํ ํฐ ๋ก๊ทธ์ธ API ์์ฒญ์ ๋ณด๋ด์ ์ต์ข ์ ์ผ๋ก ๋ก๊ทธ์ธ์ ์๋ฃํฉ๋๋ค.
์ฐ์ ์น๋ก๊ทธ์ธ์ ๋ฐ๋ผํด์ผ ํฉ๋๋ค. ์น ๋ก๊ทธ์ธ์ HTML์ Form ๊ธฐ๋ฐ์ผ๋ก ํ๊ณ ์๋๋ฐ ์ด๋ฅผ Postman์์ ๋ฐ๋ผ์ ์์ฒญํด๋ณด๋ ๋คํํ ๋์ํฉ๋๋ค. ๊ทธ๋์ ์ด๊ฑธ ๊ธฐ๋ฐ์ผ๋ก ์ฝ๋๋ก ์ฎ๊ฒจ์ ํ
์คํธํด๋ณด๋ node.js ๋จ์์ 500 ์๋ต์ด ๋์ด์ต๋๋ค. ๐ฑ
node-fetch๋ฅผ axios ๋ก ๋ฐ๊ฟ๋ ๋ณด๊ณ , HTTP 1.1/2.0 ๋ฌธ์ ์ธ๊ฐ ์ถ์ด์ ๋ฐ๊ฟ๋ณด๊ณ , ์ฌ๋ฌ๊ฐ์ง ๋ณ๊ฒฝํด๋ด๋ ๋์ผํ์ต๋๋ค. ๋์ ํ ์์ธ์ ๋ชจ๋ฅด๊ฒ ๋โฆ ๋ฏธ๊ถ์ ๋น ์ก์ต๋๋ค.
๊ทธ๋ฌ๋ค๊ฐ curl๋ก ์์ฒญ์ ํด๋ ๋ง์ฐฌ๊ฐ์ง๋ก ๋์์ ํ์ง ์๋ ๊ฒ์ ๋ฐ๊ฒฌํ์ต๋๋ค. ์์ธํ ํ์ธ์ ์ํด curl -v๋ก ์์ฒญ์ ๋ณด๋ด๋ณด๋ POST /member/login ์ ๋ก๊ทธ์ธ ์ฑ๊ณต์ผ๋ก ์ฒ๋ฆฌํด์ /member/loginOk๋ก ๋ฆฌ๋ค์ด๋ ํธ๋ฅผ ํ๋๋ฐ, ์ด ์์ฒญ์ ์๋ต์ด 500์ด ๋์ค๊ณ ์๋ค๋ ๊ฒ์ ๋ฐ๊ฒฌ, ์ด๊ฒ node.js ์์ ๋ฐ๋ ์๋ต๊ณผ ๋์ผํ๋ค๋ ๊ฒ์ ์์์ต๋๋ค.
๋ฆฌ๋ค์ด๋ ํธํ ๋ ์ด๋ค ์ฟผ๋ฆฌ ์คํธ๋ง์ด๋ ํค๋ ๊ฐ์ ๊ฒ์ด ์์์ผ๋ฏ๋ก, ์ธ์
/์ฟ ํค ๋ฑ์ ํตํด ์ธ์ฆ ์ ๋ณด๋ฅผ ์ฃผ๋ ๊ฒ ๊ฐ๋ค๊ณ ์ถ์ธก๐ง, Postman์ ์์ฒด ์ฟ ํค ์ปจํ
์ด๋๊ฐ ์์ผ๋ ๋๊ณ , curl๊ณผ node-fetch๋ ๊ธฐ๋ณธ์ ์ผ๋ก๋ ์ฟ ํค ์ปจํ
์ด๋๊ฐ ์์ด ๋์ํ๋ 500 ์ด์๊ฐ ์๊ธด ๊ฒ์ผ๋ก ๋ณด์์ต๋๋ค.
fetch๊ฐ ์ฟ ํค ์ปจํ
์ด๋๋ ํจ๊ป ๋์ํ๋ ํจํค์ง๋ฅผ ์ฐพ์์ ์ด๋ฅผ ์ ์ฉํด๋ดค๋๋ฐ, ๋น๊ณ ~! ์ด์ ๋ 500์์ด Postman๊ณผ ๋์ผํ ์๋ต์ด ๋์ด์ต๋๋ค. ๐คฉ
์ด ํ์ ์๋ต HTML ๋ด javascript ํ๊ทธ์์ ์น๋ทฐ๋ก ๋ณด๋ผ JSON์ ๊ทธ๋๋ก ํ์ฑํด์ ๊ฐ์ ธ์ค๊ณ , ์ด๋ฅผ ํตํด ํ ํฐ ๋ก๊ทธ์ธ API๋ฅผ ํธ์ถํด๋ณด๋ ์ ๋์ํฉ๋๋ค. ๋ก๊ทธ์ธ ์๋ฃ!
๊ฐ๋ฐ 3๋จ๊ณ โ API ํธ์ถํ๊ธฐ

์ฐ๋ํ ๋ก๊ทธ์ธ์ ๊ธฐ๋ฐ์ผ๋ก API๋ ์ฐ๋ํด๋ด ์๋ค.
์ด์ ๋ก๊ทธ์ธ๋ ์ฐ๋ํ๊ณ , ์ป์ credentials์ ํตํด API๋ฅผ ํธ์ถํ๋ฉด ๋ฉ๋๋ค.
ํ์ํ ๊ณณ์ ์ด์ ์ ๋ฆฌ๋ฒ์ฑํด์ ์ป์ ๊ธฐ๊ธฐ ๋ชฉ๋ก API, ๊ธฐ๊ธฐ ์ ์ด API๋ฅผ ์ฌ์ฉํด๋ด ์๋ค.
- ๊ธฐ๊ธฐ ๋ชฉ๋ก API
PlatformPlugin์ด ๊ธฐ๊ธฐ๋ฅผ ์ฐพ๋ ๋ถ๋ถ์์ API๋ฅผ ํธ์ถํ๋๋ก ์์ ํฉ๋๋ค.
async onLaunched() {
this.log.info('onLaunched called');
// run the method to discover / register your devices as accessories
const devices = await this.service.getDevices()
for (const device of devices) {
this._registerDeviceAsAccessory(device);
}
}
- ๊ธฐ๊ธฐ ์ ์ด API
๊ฐ๊ฐ์ ์บ๋ฆญํฐ๋ฆฌ์คํฑ
onSet์์ API ํธ์ถ์ ํ๋๋ก ์์ ํฉ๋๋ค.
private async setHeatingState(value: CharacteristicValue) {
const state = value as number;
const isActive = !!state;
// api calll
await this.service.activate(this.device, isActive);
}
private async setTemperature(value: CharacteristicValue) {
const temperature = value as number;
// api call
await this.service.setTemperature(this.device, temperature);
}
(์์์ ์๊ธฐํ๋ฏ์ด, ์ฌ๊ธฐ์ service๋ฅผ ๊ธฐ์กด Fake๋ก ๋์ํ๋ ์๋น์ค์์ ์ค์ API๋ฅผ ํธ์ถํ๋ ์๋น์ค๋ก ๊ฐ์๋ผ์ ์ฃผ๊ธฐ๋ง ํ๋ฉด ๋ฉ๋๋ค.)
์ด์ ํ๋ฌ๊ทธ์ธ์ด ์คํ๋ ๋, ์์์ ๋ด ๊ณ์ ์ ๋ฑ๋ก๋ ์ ๊ธฐ์ฅํ์ ์ฐพ์์ ์ ํ ํ์ ๋ณด์ฌ์ฃผ๊ณ , ์ ํ ํ UI์์ ์กฐ์ ์ ์ค์ ๋ก ๊ธฐ๊ธฐ๊ฐ ์กฐ์์ด ๋ฉ๋๋ค. ์ด์ ๊ฑฐ์ ๋ค ์๋ค!!!
๊ฐ๋ฐ 4๋จ๊ณ โ AWS PubSub ์ฐ๋ํ๊ธฐ
[embed]๋ง์ง๋ง์ผ๋ก ์ค์ ๊ธฐ๊ธฐ ์ํ๋ฅผ ์๋ ค์ฃผ๋ AWS PubSub ๋ฅผ ์ฐ๋ํด๋ด ์๋ค.
์ด์ ๋ง์ง๋ง์ผ๋ก AWS PubSub ์ฐ๋๋ง์ด ๋จ์์ต๋๋ค. ํ์ฌ๊น์ง ๊ธฐ๊ธฐ์ ์ํ๋ฅผ ๋ด๋ถ ๋ฉ๋ชจ๋ฆฌ ๋ณ์์ ์ ์ฅํ ๊ฒ์ ๋ณด์ฌ์ฃผ๊ณ ์๊ธฐ ๋๋ฌธ์ ๋ง์ฝ ์ ํ ํ์ด ์๋๋ผ ์ง์ ๊ธฐ๊ธฐ๋ฅผ ์กฐ์ํ๋ค๋ฉด ์ค์ ๊ธฐ๊ธฐ์ ์ํ์ ๋ฌ๋ผ์ง๊ฒ ๋ฉ๋๋ค. ์ค์ ๊ธฐ๊ธฐ์ ์ํ์ ์ฑํฌ๐๋ฅผ ํด๋ด ์๋ค.
PubSub์ ๊ด๋ฆฌํ๋ ํด๋์ค๋ฅผ ๋ง๋ค์ด ๋ด ์๋ค. ์ด์ ํฌ์คํธ์์ ์ค๋ช ํ ๊ฒ๊ณผ ๊ฐ์ด ์ธ์ฆ๋ ํ ์ ์๋๋ก ํ๊ณ , ์ฐ๋ฆฌ๊ฐ ๊ถ๊ธํ ์ด๋ฒคํธ๋ฅผ ์๋ ค์ฃผ๋ ๋ฉ์๋๋ ๋ง๋ค์ด๋ด ์๋ค.
import { PubSub } from '@aws-amplify/pubsub';
import { filter, map, Observable } from 'rxjs';
export class AwsPubSub {
private readonly _pubsub: PubSub;
constructor(...) {
// create pubsub instance
this._pubsub = new PubSub({
region: AWS_IOT_REGION,
endpoint: `wss://${AWS_IOT_ENDPOINT}/mqtt`,
clientId: "<client-id>",
});
}
public deviceStatusChanges(deviceId: string): Observable<DeviceEvent> {
return this._pubsub.subscribe({
topics: [
`$aws/things/${deviceId}/shadow/name/.../get/accepted`,
`$aws/things/${deviceId}/shadow/name/.../update/accepted`,
],
}).pipe(
// event is Record<string, unknown> type, so we double cast it
map((event) => event as unknown as DeviceEvent),
// ignore if it doesn't have state.reported
filter((event) => !!event.state.reported),
);
}
}
๊ธฐ๊ธฐ์ ์ด๋ฒคํธ๋ฅผ ๊ตฌ๋
ํ๊ณ ๊ทธ ์ค ํ์ํ ์ด๋ฒคํธ๋ง ํํฐํ๋ Observable๋ฅผ ๋ง๋ค์ด์ค๋๋ค. (์ฐธ๊ณ ๋ก PubSub ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ rxjs ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๊ธธ๋ ๋์ผํ๊ฒ ๋ง์ถฐ์ฃผ์์ต๋๋ค.)
import { BehaviorSubject, Subscription } from 'rxjs';
export interface NavienDevice extends Device { }
export class NavienDevice {
private _isActive: boolean | null = null;
private _temperature: number | null = null;
private readonly isActiveSubject = new BehaviorSubject<boolean | null>(null);
private readonly temperatureSubject = new BehaviorSubject<number | null>(null);
constructor(
private readonly pubsub: AwsPubSub,
json: Device,
) {
Object.assign(this, json);
this.pubsub.deviceStatusChanges(this.id).subscribe((event) => {
// parse status
const state = event.payload.state.reported!;
const isActive = ...;
const temperature = ...;
this.isActive = isActive;
this.temperature = temperature;
});
}
get isActive() {
return this._isActive;
}
set isActive(value: boolean | null) {
if (this._isActive === value) {
return;
}
this._isActive = value;
this.isActiveSubject.next(value);
}
get temperature() {
return this._temperature;
}
set temperature(value: number | null) {
if (this._temperature === value) {
return;
}
this._temperature = value;
this.temperatureSubject.next(value);
}
get activeChanges() {
return this.isActiveSubject.asObservable();
}
get temperatureChanges() {
return this.temperatureSubject.asObservable();
}
}
Device ํด๋์ค์์ BehaviorSubject๋ฅผ ํตํด ์ํ, ์ค์ ์จ๋๊ฐ ๋ฌ๋ผ์ก์ ๋ ์๋ ค์ฃผ๋ activeChanges, temperatureChanges๋ฅผ ๋ง๋ค์ด์ฃผ๊ณ , ์ PubSub ๊ฐ์ฒด๋ก๋ถํฐ ๊ธฐ๊ธฐ ์ํ๋ฅผ ๋ฐ์ ํ์ฑํด์, ์ํ๋ ๋ฐ์ดํฐ๋ง ๊ณจ๋ผ ์ ์ฅํด์ฃผ์ด ์ํ๊ฐ ๋ฌ๋ผ์ก์ ๋ ์ด๋ฒคํธ๊ฐ ๋ถ๋ฆฌ๋๋ก ํด์ค๋๋ค.
// subscribe to device events
this.device.activeChanges.subscribe((isActive) => {
thermostat.updateCharacteristic(
CurrentHeatingCoolingState,
isActive ? CurrentHeatingCoolingState.HEAT : CurrentHeatingCoolingState.OFF,
);
thermostat.updateCharacteristic(
TargetHeatingCoolingState,
isActive ? TargetHeatingCoolingState.HEAT : TargetHeatingCoolingState.OFF,
);
});
this.device.temperatureChanges.subscribe((temperature) => {
thermostat.updateCharacteristic(TargetTemperature, temperature);
thermostat.updateCharacteristic(CurrentTemperature, temperature);
});
์์ธ์๋ฆฌ์์ ์ Device๋ก๋ถํฐ ์ํ, ์ค์ ์จ๋๊ฐ ๋ฌ๋ผ์ง ๋ ๋ง๋ค ์ด๋ฒคํธ๋ฅผ ๋ฐ์updateCharacteristic()๋ฅผ ํธ์ถํด, ์ ํ ํ์ ์
๋ฐ์ดํธ ์์ฒญ์ ๋ณด๋ด๋๋ก ํฉ๋๋ค.
์ด๋ ๊ฒ ํด์ PubSub โ Device โ Accessory โ Apple Home ์์ผ๋ก ์ด๋ฒคํธ๋ฅผ ํตํด ๊ธฐ๊ธฐ์ ์ํ๊ฐ ์ ๋ฌ๋์ด ์ ๋ฐ์ดํธ๊ฐ ์ด๋ค์ง๊ฒ ๋ฉ๋๋ค.
์ด๋ก์จ ์ํ ์ฑํฌ๋ ์๋ฃํ์ต๋๋ค! ๐ฅณ ์ด์ ์ค์ ๊ธฐ๊ธฐ๋ฅผ ๋ฌผ๋ฆฌ์ ์ผ๋ก ์กฐ์ํด๋ ํ๋ฌ๊ทธ์ธ์ด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ ์ ํ ํ์์ ๋ฐ์์ด ๋๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
์๋ฃ โ ๊ฒฐ๊ณผ๋ฌผ
[embed]Siri๋ก ์ ์ดํ๋ ๋ชจ์ต
[embed]์ปจํธ๋กค ์ผํฐ์์ ์ ์ดํ๋ ๋ชจ์ต
๊ฒฐ๊ณผ ์์์ ๋๋ค. ์ ํ ํ์ ์ฐ๋ํ ๋งํผ, ๋ณ๋์ ์์ ์์ด๋ Siri์๋ ์ฐ๋๋์ด ๋์ํ๋ ๋ชจ์ต์ ๋ณผ ์ ์์ต๋๋ค.
๋ธ๋ก๊ทธ์ ๊ธ์ ์ ๋ฆฌํ๋ฉด์ ์ฝ๋์ ๋ง์ ๋ถ๋ถ์ ๊ฐ๋จํ ์ค์ฌ์ ์ฌ๋ฆฌ๊ธฐ๋ ํ๊ณ , ๊ตฌ์กฐ ๋ณ๊ฒฝ์ด๋ ์ฝ๋๊ฐ ์ ๋ฐ์ดํธ๋ ๋ถ๋ถ๋ค๋ ์๋๋ฐ, ์ต๋ํ ๊ฐ๋ฐ ์์ ์ ๊ธฐ์ค์ผ๋ก ์ฝ๋๋ฅผ ์์ฑํ์ต๋๋ค. ์์ธํ ์ฝ๋๊ฐ ๊ถ๊ธํ๋ค๋ฉด ์ ๋ ํฌ์งํ ๋ฆฌ๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์.
๋ง๋ฌด๋ฆฌ
์ด๋ ๊ฒ ์ด์ฌํ ๋ง๋ ํ๋ฌ๊ทธ์ธ์ ์ ํผ์๋ง ์ฐ๊ธฐ์๋ ๋งค์ฐ ์๊น์ฃ . ์ ๋ง๊ณ ๋ ๋ค๋ฅธ ์ฌ๋๋ค๋ ์ธ ์ ์๋๋ก ํ๋ ค๋ฉด ํ๋ฌ๊ทธ์ธ์ ๋ฐฐํฌ๋ฅผ ํด์ผ ํฉ๋๋ค.
๋ฐฐํฌ๋ฅผ ํ๋ ๊ณผ์ ๊ณผ ๊ทธ ์ธ ์ฌ์ํ ๋ด์ฉ๋ค์ ๋ค์ ๋ง์ง๋ง ํธ์์ ์ฝ์ด์ฃผ์ธ์!
์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค~~ ๐
๋ฉํ๋ฐ์ดํฐ
- post_id
- d5f7fa0b0aef
- slug
- ์ ๊ธฐ์ฅํ์-์ ํ-ํ์-์ฐ๊ฒฐํด๋ณด์-2ํธ-ํ๋ฌ๊ทธ์ธ-๊ฐ๋ฐ-d5f7fa0b0aef
- url
- https://medium.com/@kyle_seongwoo_jun/%EC%A0%84%EA%B8%B0%EC%9E%A5%ED%8C%90%EC%9D%84-%EC%95%A0%ED%94%8C-%ED%99%88%EC%97%90-%EC%97%B0%EA%B2%B0%ED%95%B4%EB%B3%B4%EC%9E%90-2%ED%8E%B8-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-%EA%B0%9C%EB%B0%9C-d5f7fa0b0aef
- canonical_url
- https://medium.com/@kyle_seongwoo_jun/%EC%A0%84%EA%B8%B0%EC%9E%A5%ED%8C%90%EC%9D%84-%EC%95%A0%ED%94%8C-%ED%99%88%EC%97%90-%EC%97%B0%EA%B2%B0%ED%95%B4%EB%B3%B4%EC%9E%90-2%ED%8E%B8-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-%EA%B0%9C%EB%B0%9C-d5f7fa0b0aef
- author_url
- https://medium.com/@kyle_seongwoo_jun
- status
- ok
- fetched_at
- 2026-09-13 04:12:31