commit 18e4b39ade9fdda206f4f8e6a1ceea47c50884e9 Author: abearxiong Date: Fri Dec 5 00:33:03 2025 +0800 update diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2e04e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules + +.env \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fb95376 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,33 @@ +{ + "name": "ha-api", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ha-api", + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "@types/node": "^24.10.1" + } + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cf49176 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "ha-api", + "version": "0.0.1", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "abearxiong (https://www.xiongxiao.me)", + "license": "MIT", + "packageManager": "pnpm@10.24.0", + "type": "module", + "devDependencies": { + "@types/bun": "^1.3.3", + "@types/node": "^24.10.1" + }, + "dependencies": { + "dotenv": "^17.2.3" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..2c15371 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,56 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + dotenv: + specifier: ^17.2.3 + version: 17.2.3 + devDependencies: + '@types/bun': + specifier: ^1.3.3 + version: 1.3.3 + '@types/node': + specifier: ^24.10.1 + version: 24.10.1 + +packages: + + '@types/bun@1.3.3': + resolution: {integrity: sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g==} + + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + + bun-types@1.3.3: + resolution: {integrity: sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ==} + + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + +snapshots: + + '@types/bun@1.3.3': + dependencies: + bun-types: 1.3.3 + + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + + bun-types@1.3.3: + dependencies: + '@types/node': 24.10.1 + + dotenv@17.2.3: {} + + undici-types@7.16.0: {} diff --git a/src/core.ts b/src/core.ts new file mode 100644 index 0000000..9a5ea2f --- /dev/null +++ b/src/core.ts @@ -0,0 +1,61 @@ +const homeassistantURL = 'http://home.mz.zxj.im:8123'; + +const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmMzk3ZDUwMWU3ODE0MTA4Yjk4ZjYwNDFjMzI3NzVkZSIsImlhdCI6MTc2NDg2NDUyMywiZXhwIjoyMDgwMjI0NTIzfQ.S2zO3DNzeVYgd1c_N9IkRc13zmtj2HGVq-n6IUmttRQ' + +type HACoreOptions = { + token: string; + homeassistantURL?: string; +} +export class HACore { + token: string; + homeassistantURL?: string; + constructor(options: HACoreOptions) { + this.token = options.token; + this.homeassistantURL = options.homeassistantURL || 'http://localhost:8123'; + } + async get(opts: { url: string, params?: Record }): Promise { + const _u = new URL(opts.url, this.homeassistantURL); + if (opts.params) { + Object.entries(opts.params).forEach(([key, value]) => { + _u.searchParams.append(key, value); + }); + } + return fetch(_u.toString(), { + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json', + }, + }).then(response => { + if (!response.ok) { + throw new Error(`Failed to fetch ${opts.url}: ${response.statusText}`); + } + return response.json(); + }); + } + async post(opts: { url: string, body?: any }): Promise { + return fetch(new URL(opts.url, this.homeassistantURL).toString(), { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(opts.body || {}), + }).then(response => { + if (!response.ok) { + throw new Error(`Failed to post to ${opts.url}: ${response.statusText}`); + } + return response.json(); + }); + } +} + +export const getEntities = async (): Promise => { + const hacore = new HACore({ token, homeassistantURL }); + return hacore.get({ url: '/api/states' }); +} + +export const getLights = async (): Promise => { + const hacore = new HACore({ token, homeassistantURL }); + const entities = await hacore.get({ url: '/api/states' }); + return entities.filter((entity: any) => entity.entity_id.startsWith('light.')); +} \ No newline at end of file diff --git a/test/common.ts b/test/common.ts new file mode 100644 index 0000000..678ef20 --- /dev/null +++ b/test/common.ts @@ -0,0 +1,113 @@ +import { HACore } from "../src/core"; + +import util from 'node:util'; +import dotenv from 'dotenv'; +dotenv.config(); + + +const hacore = new HACore({ token: process.env.HAAS_TOKEN || '', homeassistantURL: process.env.HAAS_URL }); +const enti = await hacore.get({ url: '/api/states' }); +// console.log(util.inspect(enti, { depth: null })); + +// const lightEntities = enti.filter((entity: any) => { +// const hasLight = entity.entity_id.startsWith('light.'); +// const name = entity.attributes?.friendly_name || ''; + +// return hasLight && name.includes('次卧'); +// }); +// console.log(util.inspect(lightEntities, { depth: null })); + +const lights = [ + { + entity_id: 'light.lemesh_wy0c14_f18d_light', + state: 'off', + attributes: { + min_color_temp_kelvin: 2700, + max_color_temp_kelvin: 6500, + min_mireds: 153, + max_mireds: 370, + effect_list: [ + 'WY', 'Day', + 'Night', 'Warmth', + 'Tv', 'Reading', + 'Computer', 'Hospitality', + 'Entertainment', 'Wakeup', + 'Dusk', 'Sleep' + ], + supported_color_modes: ['color_temp'], + effect: null, + color_mode: null, + brightness: null, + color_temp_kelvin: null, + color_temp: null, + hs_color: null, + rgb_color: null, + xy_color: null, + 'light.mode': 0, + 'light.on': false, + 'light.color_temperature': 6500, + 'light.brightness': 1, + friendly_name: '次卧灯 灯光', + supported_features: 4 + }, + last_changed: '2025-12-02T16:35:00.229216+00:00', + last_reported: '2025-12-02T16:36:00.314622+00:00', + last_updated: '2025-12-02T16:35:00.229216+00:00', + context: { + id: '01KBFYNN05ZYWZHTGC1W7N66Z5', + parent_id: null, + user_id: null + } + }, + { + entity_id: 'light.lemesh_cn_1099991426_wy0c14_s_2_light', + state: 'off', + attributes: { + min_color_temp_kelvin: 2700, + max_color_temp_kelvin: 6500, + min_mireds: 153, + max_mireds: 370, + effect_list: [ + '色温模式', + '日光', + '月光(夜间)模式', + '温馨', + '电视模式(影院模式)', + '阅读模式', + '电脑模式', + '会客模式', + '娱乐模式', + '清晨唤醒', + '黄昏明亮', + '夜晚助眠' + ], + supported_color_modes: ['color_temp'], + effect: null, + color_mode: null, + brightness: null, + color_temp_kelvin: null, + color_temp: null, + hs_color: null, + rgb_color: null, + xy_color: null, + friendly_name: '次卧灯 灯光', + supported_features: 4 + }, + last_changed: '2025-12-04T13:28:05.284635+00:00', + last_reported: '2025-12-04T13:28:09.331178+00:00', + last_updated: '2025-12-04T13:28:05.284635+00:00', + context: { + id: '01KBMRRTX4ZE10MGHTVVNC1K7Y', + parent_id: null, + user_id: null + } + } +] + +const res = await hacore.post({ + url: '/api/services/light/turn_off', + body: { + entity_id: 'light.lemesh_cn_1099991426_wy0c14_s_2_light' // 或第二个次卧灯的ID + } +}); +console.log(util.inspect(res, { depth: null })); \ No newline at end of file