homebridge-smartone
Version:
Homebridge plugin for SmartOne devices
204 lines (173 loc) • 6.19 kB
JavaScript
const express = require("express");
const axios = require("axios");
const fs = require("fs");
const path = require("path");
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
next();
});
app.use((err, req, res, next) => {
console.error('Error:', err.message);
res.status(500).json({ error: 'Internal server error' });
});
const TOKEN_URL = "https://auth-us-east-1.smartone-solutions.com/connect/token";
const CLIENT_ID = "bdf1af15-da1f-4389-b3eb-111e2feb00b6";
const CLIENT_SECRET = ""; // add if required
const REDIRECT_URI = "smartone://login";
const TOKENS_FILE = path.join(__dirname, "tokens.json");
function loadTokens() {
if (fs.existsSync(TOKENS_FILE)) {
return JSON.parse(fs.readFileSync(TOKENS_FILE, "utf-8"));
}
return {
access_token: null,
refresh_token: "", // initial refresh token
expires_at: 0
};
}
function saveTokens(tokens) {
fs.writeFileSync(TOKENS_FILE, JSON.stringify(tokens, null, 2));
}
let tokens = loadTokens();
async function refreshAccessToken() {
try {
const params = new URLSearchParams();
params.append("client_id", CLIENT_ID);
if (CLIENT_SECRET) params.append("client_secret", CLIENT_SECRET);
params.append("refresh_token", tokens.refresh_token);
params.append("grant_type", "refresh_token");
params.append("redirect_uri", REDIRECT_URI);
params.append("scope", "offline_access openid profile email ApiGateway");
const response = await axios.post(TOKEN_URL, params.toString(), {
headers: { "Content-Type": "application/x-www-form-urlencoded" }
});
const data = response.data;
tokens.access_token = data.access_token;
tokens.refresh_token = data.refresh_token || tokens.refresh_token;
tokens.expires_at = Math.floor(Date.now() / 1000) + data.expires_in;
saveTokens(tokens);
return tokens.access_token;
} catch (err) {
throw new Error(`Failed to refresh token: ${err.response?.data || err.message}`);
}
}
async function getAccessToken() {
const now = Math.floor(Date.now() / 1000);
if (!tokens.access_token || now >= tokens.expires_at) {
return await refreshAccessToken();
}
return tokens.access_token;
}
app.get('/api/devices', async (req, res) => {
try {
const token = await getAccessToken();
const response = await axios.get("https://api-us-east-1.smartone-solutions.com/SmartDeviceService/api/devices/list", {
headers: { Authorization: `Bearer ${token}` }
});
res.json(response.data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/user', async (req, res) => {
try {
const token = await getAccessToken();
const response = await axios.post(
"https://api-us-east-1.smartone-solutions.com/UserManagementService/api/v2/User/GetBy/AccessToken",
{},
{
headers: {
Authorization: `Bearer ${token}`,
Host: "api-us-east-1.smartone-solutions.com",
Accept: "application/json, text/plain, */*",
"app-version": "mobile-app:1.10.4",
"Content-Length": "0",
"User-Agent": "ONE!/5358 CFNetwork/3860.100.1 Darwin/25.0.0",
Connection: "keep-alive",
},
}
);
res.json(response.data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/token/refresh', async (req, res) => {
try {
const token = await refreshAccessToken();
res.json({ access_token: token, expires_at: tokens.expires_at });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/devices/doorlock/set', async (req, res) => {
try {
const { state } = req.query;
if (!state) {
return res.status(400).json({ error: 'state is required' });
}
const token = await getAccessToken();
// Get devices list to find the door lock
const devicesResponse = await axios.get("https://api-us-east-1.smartone-solutions.com/SmartDeviceService/api/devices/list", {
headers: { Authorization: `Bearer ${token}` }
});
const doorLock = devicesResponse.data.devices.find(device => device.type === 'DoorLock');
if (!doorLock) {
return res.status(404).json({ error: 'No door lock found' });
}
const response = await axios.post(
"https://api-us-east-1.smartone-solutions.com/SmartDeviceService/api/devices/doorLock/set",
{ state, deviceId: doorLock.id },
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
res.json(response.data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/devices/thermostat/set', async (req, res) => {
try {
const { coolingSetPoint } = req.query;
if (coolingSetPoint === undefined) {
return res.status(400).json({ error: 'coolingSetPoint are required' });
}
const token = await getAccessToken();
// Get devices list to find the thermostat
const devicesResponse = await axios.get("https://api-us-east-1.smartone-solutions.com/SmartDeviceService/api/devices/list", {
headers: { Authorization: `Bearer ${token}` }
});
const thermostat = devicesResponse.data.devices.find(device => device.type === 'Thermostat');
if (!thermostat) {
return res.status(404).json({ error: 'No thermostat found' });
}
const resourceId = "qLnLY1x6aYQg1If3YoucaV5F3I3MH7YrHLdE1RUB05CBcXVKH1";
const response = await axios.post(
"https://api-us-east-1.smartone-solutions.com/SmartDeviceService/api/Devices/thermostat/set",
{
resourceId,
coolingSetPoint,
deviceId: thermostat.id
},
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
res.json(response.data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});