[IoT ๐ /Homebridge] - [JS] Homebridge Smartthings ์ฐ๋ ํ๋ฌ๊ทธ์ธ ์ ์, OAuth ์ธ์ฆ - 1
์ ๋ฒ์ Homebridge๋ก Smartthings ์ฐ๋ํ๋ ํ๋ฌ๊ทธ์ธ์ ์ฌ์ฉํด,
Smartthings ๊ธฐ๊ธฐ๋ค์ Home ์ฑ์ ์ถ๊ฐํ๋ ์์ ์ ํ์๋ค.
ํ์ง๋ง, Smartthings ์ ์ฑ ์ด ๋ฐ๋๋ฉด์
๊ธฐ์กด PAT(๊ฐ์ธ ์ก์ธ์ค ํ ํฐ)์ ์ ํจ๊ธฐ๊ฐ์ด 24์๊ฐ์ผ๋ก ๋ฐ๋์ด๋ฒ๋ ธ๋ค.
๋งค๋ฒ ํ ํฐ์ ์ฌ๋ฐ๊ธ ๋ฐ๊ณ , ์๋ฒ์์ ์์ ํ๋ฉด์ ์ฐ๋๊ฑด ๋นํจ์จ์ ์ด๊ณ
์ด๋ค ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํ ํด๋ณผ์ง, ์ ๋ณด๋ฅผ ์ฐพ์๋ณด๋ค๊ฐ
ํ ํด์ธ ์ ์ ๊ฐ, OAuth ๋ฐฉ์์ ์ฌ์ฉํ์ฌ ์ฐ๋ํ๋ ๊ฒ์ ๋ณด์๋ค.
https://github.com/aziz66/homebridge-smartthings
GitHub - aziz66/homebridge-smartthings: homebridge smartthings plugin
homebridge smartthings plugin. Contribute to aziz66/homebridge-smartthings development by creating an account on GitHub.
github.com
ํ ์คํธ ๊ฒธ ์ฌ์ฉํด๋ณธ ๊ฒฐ๊ณผ, ์ค๋ฅ๋ ์์ง ๋ง๊ณ
๋ด๊ฐ ์ํ๋ ๊ธฐ๊ธฐ๋ฅผ ์ ๋ง๋๋ก ๋งคํํ๊ธฐ๊ฐ ์ด๋ ค์์
์ง์ ๋ด๊ฐ ํ๋ฌ๊ทธ์ธ์ ์ ์ํ์ฌ ์ฐ๋ํด ๋ณด๋ ค๊ณ ํ๋ค.
OAuth ํ๋ก์ ์๋ฒ ๊ตฌ์ถ
๋จผ์ , Smartthings์ ์ ์ ์ฑ ์ ๋์ํ๊ธฐ ์ํด, ์์ ๋ฐฉ์์ ๋ฒค์น๋งํน ํด๋ณด์
ngrok๋ฅผ ์ด์ฉํ์ฌ https ํ๋ก์ ์๋ฒ๋ฅผ ๋์ด ์ ๋ฒํ ์ธ์ฆ ์๋ฒ๋ฅผ ๋๋ ๊ฒ์ธ๋ฐ,
ํ์์ iptime ์๋ฒ๋ https๋ฅผ ์ง์ํ์ง ์์ผ๋ฏ๋ก ์ด ๋ฐฉ์์ ์ ์ฉํด ๋ณด๋ ค ํ๋ค.
์ผ๋จ ngrok ๊ณต์ ํํ์ด์ง์ ๋์์๋ ์ค๋ช ๋๋ก
ubuntu ํ๊ฒฝ์ ngrok ์ค์น์ ํ ํฐ ๋ฐ๊ธ์ ํด์ผํ๋ค.
https://dashboard.ngrok.com/get-started/setup/linux
ngrok — Log in
dashboard.ngrok.com
๋ฌธ์์ ๊ธฐ์ฌ๋ ๋ด์ฉ๊ณผ ๊ฐ์ด,
homebridge ์๋ฒ๊ฐ ๊ตฌ์ถ๋ ubuntu ํ๊ฒฝ์ ngrok๋ฅผ ์ค์นํด์ค๋ค.
curl -sSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc \
| sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null \
&& echo "deb https://ngrok-agent.s3.amazonaws.com bookworm main" \
| sudo tee /etc/apt/sources.list.d/ngrok.list \
&& sudo apt update \
&& sudo apt install ngrok
๋ค์์ผ๋ก ngrok ์ฌ์ดํธ์์ ๋ฐ๊ธํ ํ ํฐ๋ ์ ์ฉ
๊ทธ ํ, 8000๋ฒ ํฌํธ๋ก ์คํํ๋ค. (์ ์ ์ฌ๋)
ngrok config add-authtoken "token"
ngrok http 8000
๊ฐ๋จํ ํ๋ก์ ์๋ฒ๋ ๊ตฌ์ถ ๋.
๊ตณ์ด ngrok์ด ์๋๋๋ผ๋, CloudFlare๋ Nginx + ํฌํธํฌ์๋ฉ ๋ฐฉ๋ฒ ๋ฑ ์ ๋ง์ ๋ง๊ฒ ํ๋ฉด ๋ ๊ฒ ๊ฐ๋ค.
Smartthing CLI ์์ฑ
์๋ฒ๊ฐ ๊ตฌ์ถ๋์ผ๋ฉด, ์ด์ ๊ฐ์ ์ฑ์ด ํ์ํ๋ค.
Smartthings๊ฐ ์ ๊ณตํ๋, ๋ช ๋ น์ค์์ ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฅํ CLI ์ฑ์ ์์ฑํด๋ณด๋๋ก ํ์.
https://developer.smartthings.com/docs/sdks/cli
Get Started With the SmartThings CLI | Developer Documentation | SmartThings
Quickstart guide for the SmartThings CLI
developer.smartthings.com
์ด๊ณณ์์ ๊ธฐ๋ณธ์ ์ผ๋ก CLI๋ฅผ ์์ฑํ ํ ํฐ์ ๋ฐ๊ธ ๋ฐ๊ณ
์ด์ ์ PAT ํ ํฐ์ ๋ฐ๊ธ๋ฐ๋ ๋ฐฉ๋ฒ๊ณผ ๊ฐ๋ค.
https://account.smartthings.com/tokens
SmartThings. Add a little smartness to your things.
account.smartthings.com
Smartthings CLI๋ฅผ ์ค์น ํ, ์ฑ ์์ฑ์ ์งํ
smartthings apps:create --token "token"
์ฑ ์ด๋ฆ๊ณผ, ์ค๋ช ์ ๋ฃ๊ณ
Target URL์๋ ngrok์์ ๋ฐ๊ธ๋ฐ์ ๋ด ํ๋ก์ ์๋ฒ ์ฃผ์๋ฅผ ๋ฃ๊ณ ,
๊ถํ์ ๊ธฐ๋ณธ์ ์ธ ์ฝ๊ธฐ,์์ ,์คํ(r/w/x:device:*)
๋ง์ง๋ง์ผ๋ก Redirect URL์๋ ํ๋ก์์๋ฒ ์ฃผ์์ /oauth/callback์ ๊ผญ ๋ถ์ฌ์ ๋ฃ์ด์ค์ผํ๋ค.

๊ทธ๋ผ ์ ์์ ์ผ๋ก, Client ID/Secret์ด ์์ฑ๋๋ค.

ํ๋ฌ๊ทธ์ธ ์ ์
๋จผ์ ํ๋ฌ๊ทธ์ธ์์ OAuth ์ธ์ฆ ์ฒ๋ฆฌ๋ฅผ ํ ์ฝ๋๋ถํฐ ๊ตฌํํด๋ณด์
OAuthServer ํด๋์ค๋ฅผ ์์ฑ ํ, ์ธ์ฆ URL์ ์์ฑํ๋ ๋ฉ์๋ ๋ถ๋ถ์ ๋ง๋ค์ด์ฃผ์๋ค.
๊ถํ์ ๊ธฐ๊ธฐ ์ฝ๊ธฐ, ์คํ ๋ฑ์ ๋ฃ์ด์ฃผ๊ณ (API ๋ฌธ์๋ฅผ ํ์ธํด์ ๋ ๋ง์ ๊ถํ์ ๋ฃ์ ์ ์๋ค.)
state๋ก CSRF ๊ณต๊ฒฉ ๋ฐฉ์ง ๊ธฐ๋ฅ๋ ์ถ๊ฐ ํด ์ฃผ์๋ค.
getAuthUrl() {
const scope = 'r:devices:* x:devices:*';
const state = 'st_state_' + Date.now();
const authUrl = new URL('https://api.smartthings.com/oauth/authorize');
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('client_id', this.config.clientId);
authUrl.searchParams.append('scope', scope);
authUrl.searchParams.append('state', state);
authUrl.searchParams.append('redirect_uri', this.redirectUri);
return authUrl.toString();
}
์ด์ ๊ถํ ๋งํฌ์์ ๋ก๊ทธ์ธ์ ๋ง์น๋ฉด,
์ค๋งํธ์ฑ์ค ์๋ฒ๊ฐ ์ํ๋ ์ฃผ์๋ก ๋ฆฌ๋ค์ด๋ ํธ ํด์ค ์ ์๊ฒ ์ฝ๋ฐฑ์ฒ๋ฆฌ๋ฅผ ํด์ฃผ๊ณ
async handleOAuthCallback(req, res) {
const parsedUrl = url.parse(req.url, true);
const code = parsedUrl.query.code;
if (!code) {
res.writeHead(400);
return res.end('OAuth Error: No code received.');
}
try {
const tokenData = await this.exchangeCodeForToken(code);
this.platform.accessToken = tokenData.access_token;
this.platform.refreshToken = tokenData.refresh_token;
this.platform.persistTokens();
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>์ธ์ฆ ์๋ฃ!</h1><p>Homebridge ๋ก๊ทธ๋ฅผ ํ์ธํ๊ณ ํ๋ฌ๊ทธ์ธ์ ์ฌ์์ํ์ธ์.</p>');
this.platform.discoverDevices();
} catch (error) {
res.writeHead(500);
res.end('<h1>Token Exchange Failed.</h1>');
}
}
๋ค์์ผ๋ก๋ ํต์ฌ ๋จ๊ณ๋ก,
์ถ์ถํ ์ผํ์ฉ ์ฝ๋๋ฅผ ์ค์ ์ฌ์ฉ๋ accessToken์ผ๋ก ๋ฐ๊พธ๋ ๋ฉ์๋์ด๋ค.
์๊น CLI๋ก ๋ฐ๊ธ๋ฐ์ clientId, clientSecret์ ๊ฒฐํฉํด base64๋ก ์ธ์ฝ๋ฉ์ ํ๊ณ
ํค๋์ ๋ด์ POST ์์ฒญ์ ๋ณด๋ด๋ฉด, accessToken๊ณผ refreshToken์ด ๋ฆฌํด๋๋ค.
async exchangeCodeForToken(code) {
const tokenUrl = 'https://api.smartthings.com/oauth/token';
const authHeader = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
const params = new URLSearchParams();
params.append('grant_type', 'authorization_code');
params.append('code', code);
params.append('redirect_uri', this.redirectUri);
try {
const response = await axios.post(tokenUrl, params.toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${authHeader}`,
'Accept': 'application/json'
}
});
return response.data;
} catch (error) {
const status = error.response ? error.response.status : 'N/A';
const errorData = error.response ? JSON.stringify(error.response.data) : 'No response data';
throw new Error(`SmartThings API Error: ${status}`);
}
}
๋ง์ง๋ง์ผ๋ก, ํ ํฐ์ด ์๋ค๋ฉด OAuth ์๋ฒ๋ฅผ ์คํ์์ผ,
์ธ์ฆ ํ์ด์ง ์ฃผ์๋ฅผ ๋ก๊ทธ์ ๋จ๊ฒจ์ ๊ถํ ์ค์ ์ ์ ๋ํ๊ฒ ๋ง๋ค์ด๋์๋ค.
start() {
if (this.platform.accessToken) return;
const callbackPort = this.config.callbackPort || 8000;
this.httpServer = http.createServer(this.handleRequest.bind(this));
this.httpServer.listen(callbackPort, () => {
const authUrl = this.getAuthUrl();
this.log.warn('SmartThings OAuth ์ธ์ฆ์ด ํ์ํฉ๋๋ค. ์๋ ์ฃผ์๋ฅผ ๋ธ๋ผ์ฐ์ ์ ์
๋ ฅํ์ธ์:');
this.log.warn(`${authUrl}`);
});
}
์ด์ , ๋ฉ์ธ์ธ index.js์ ์ธ์ฆ ์ํ์ ๋ฐ๋ผ ๋ถ๊ธฐ์ฒ๋ฆฌ๋ฅผ ๊ตฌํํด์ฃผ๊ณ
(token์ด ์์๋๋ง, OAuth ์ธ์ฆ์๋ฒ ์คํ)
initAuthentication() {
if (this.accessToken) {
this.discoverDevices();
} else {
const server = new OAuthServer(this);
server.start();
}
}
OAuthServer ํด๋์ค์์ ์ฑ๊ณต์ ์ผ๋ก ํ ํฐ์ ๋ฐ์์ค๋ฉด,
fs ๋ชจ๋์ ์ด์ฉํด ์ ํ ํฐ์ config.json์ ์๋์ผ๋ก ์ถ๊ฐํ๊ฒ ๋ง๋ค์๋ค.
persistTokens() {
const configPath = this.api.user.configPath();
try {
if (!fs.existsSync(configPath)) throw new Error(`์ค์ ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค: ${configPath}`);
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const platformConfig = config.platforms.find(p => p.platform === PLATFORM_NAME);
if (platformConfig) {
platformConfig.accessToken = this.accessToken;
platformConfig.refreshToken = this.refreshToken;
fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'utf8');
}
} catch (err) {
this.log.error('์๋ ์ ์ฅ ์คํจ:', err.message);
}
this.discoverDevices();
}
์ด ๋, ํ๋ฌ๊ทธ์ธ์ด homebridge ์๋ฒ์ config.json ํ์ผ์ ์ ๊ทผ ํด์ผํ๋ฏ๋ก
ubuntu ์์ ๊ถํ ๋ถ์ฌ ๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํด ์ฃผ์ด์ผ ํ๋ค.
sudo chmod 664 /var/lib/homebridge/config.json
๊ทธ๋ฆฌ๊ณ ๋ง์ง๋ง์ผ๋ก ํ ํฐ์ด ๋ง๋ฃ ๋์์, (24์๊ฐ)
์ ์ฅ๋ refreshToken์ผ๋ก ์ ํจ๊ธฐ๊ฐ์ด ๋๋ accessToken์ ๊ฐฑ์ ํ๋ ํจ์๋ ๊ตฌํํ๋ค.
async refreshAccessToken() {
const tokenUrl = 'https://api.smartthings.com/oauth/token';
const authHeader = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
const params = new URLSearchParams();
params.append('grant_type', 'refresh_token');
params.append('refresh_token', this.refreshToken);
try {
const response = await axios.post(tokenUrl, params.toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${authHeader}`
}
});
this.accessToken = response.data.access_token;
this.refreshToken = response.data.refresh_token;
this.persistTokens();
return this.accessToken;
} catch (error) {
throw error;
}
}
์ฝ๋ ์์ ์๋ฃ. ์ด์ ํ๋ฌ๊ทธ์ธ ์ค์น ํ ํ ์คํธ ํด๋ณด์
ํ ์คํธ
์ ๋ฒ ๊ฒ์๊ธ์์ ์ค๋ช ํ๋ ๊ฒ ์ฒ๋ผ ์ง์ ํ๋ฌ๊ทธ์ธ์ ์ค์น ํ
ํ๋ฌ๊ทธ์ธ ์ค์ ์์, config ํ์ผ์ ํ์ํ ๊ฐ๋ค์ ๋ฃ์ด์ฃผ์๋ค.
์๊น ๋ฐ๊ธํ, CLI ID์ Secret, ngrok์ผ๋ก ๋ง๋ ํ๋ก์ ์๋ฒ ์ฃผ์(callbackIp)

๊ทธ๋ฌ๋ฉด ์๊น OAuthServer ํด๋์ค์์ ๊ตฌํํ๋๋ก,
๋ก๊ทธ์ ๊ถํ ์ค์ ์ฃผ์๊ฐ ๋์ค๊ฒ ๋๋ค.

ํด๋น ์ฃผ์๋ก ์ ์ ํด์, ๊ถํ ์ค์ ์ ๋ง์น๋ฉด
์๊น handleOAuthCallback ๋ฉ์๋์์ ๊ตฌํํ ํ๋ฉด์ด ์ ์์ ์ผ๋ก ์ถ๋ ฅ๋๋ค.


์ด์ ํ๋ฌ๊ทธ์ธ์ ์ฌ์์ ํ๋ฉด configํ์ผ์
accessToken๊ณผ refreshToken์ด ์ ์์ ์ผ๋ก ์ถ๊ฐ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.

์ด๋ ๊ฒ https ํ๋ก์ ์๋ฒ๋ฅผ ์ด์ฉํด, OAuth ๋ฐฉ์์ผ๋ก ํ ํฐ์ ๋ฐ๊ธ ๋ฐ๊ณ ,
24์๊ฐ ๋ง๋ค ์๋์ผ๋ก ๋ง๋ฃ๋ ํ ํฐ์ ๊ฐฑ์ ํ๊ฒ ํ์ฌ,
์์ ์ ์๊ตฌ์ ์ผ๋ก ์ฌ์ฉ๊ฐ๋ฅํ PAT ๋ฐฉ์์ฒ๋ผ ์ผ์ถ ๊ตฌํํด๋ณด์๋ค.
์ด์ ์ ์์ ์ผ๋ก Smartthings ๋๋ฐ์ด์ค๋ค์ ๋ถ๋ฌ์ฌ ์ ์๊ฒ ๋์ผ๋
๋ค์ ๊ฒ์๊ธ์์๋ ์ค์ ์ฌ์ฉ์ค์ธ ๊ธฐ๊ธฐ๋ค์ ํ๋ธ๋ฆฟ์ง ์ ์ธ์๋ฆฌ์ ๋งคํํด๋ณด๋๋ก ํ๊ฒ ๋ค.
์๋ณธ ๊ฒ์๊ธ
https://blog.naver.com/101artspace/224122443835
Homebridge Smartthings ์ฐ๋ ํ๋ฌ๊ทธ์ธ ์ ์, OAuth ์ธ์ฆ - 1
์ ๋ฒ์ Homebridge๋ก Smartthings ์ฐ๋ํ๋ ํ๋ฌ๊ทธ์ธ์ ์ฌ์ฉํด, Smartthings ๊ธฐ๊ธฐ๋ค์ Home ์ฑ์ ์ถ๊ฐ...
blog.naver.com