@coffeerunhobby/mcp-ha-connect 2.2.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/.env.example +1 -1
  2. package/README.md +24 -2
  3. package/dist/omadaClient/accessControl.d.ts +290 -0
  4. package/dist/omadaClient/accessControl.js +509 -0
  5. package/dist/omadaClient/accessControl.js.map +1 -0
  6. package/dist/omadaClient/client.d.ts +6 -1
  7. package/dist/omadaClient/client.js +12 -1
  8. package/dist/omadaClient/client.js.map +1 -1
  9. package/dist/omadaClient/diagnose.d.ts +68 -0
  10. package/dist/omadaClient/diagnose.js +164 -0
  11. package/dist/omadaClient/diagnose.js.map +1 -0
  12. package/dist/omadaClient/index.d.ts +9 -0
  13. package/dist/omadaClient/index.js +19 -0
  14. package/dist/omadaClient/index.js.map +1 -1
  15. package/dist/omadaClient/macGroups.d.ts +58 -0
  16. package/dist/omadaClient/macGroups.js +110 -0
  17. package/dist/omadaClient/macGroups.js.map +1 -0
  18. package/dist/omadaClient/serialize.d.ts +13 -0
  19. package/dist/omadaClient/serialize.js +29 -0
  20. package/dist/omadaClient/serialize.js.map +1 -0
  21. package/dist/omadaClient/timeRange.d.ts +41 -0
  22. package/dist/omadaClient/timeRange.js +109 -0
  23. package/dist/omadaClient/timeRange.js.map +1 -0
  24. package/dist/tools/infra/actions.d.ts +1 -1
  25. package/dist/tools/infra/actions.js +1 -1
  26. package/dist/tools/omada/accessControl.d.ts +151 -0
  27. package/dist/tools/omada/accessControl.js +158 -0
  28. package/dist/tools/omada/accessControl.js.map +1 -0
  29. package/dist/tools/omada/index.js +9 -0
  30. package/dist/tools/omada/index.js.map +1 -1
  31. package/dist/tools/omada/macGroups.d.ts +19 -0
  32. package/dist/tools/omada/macGroups.js +77 -0
  33. package/dist/tools/omada/macGroups.js.map +1 -0
  34. package/dist/tools/omada/namespace.js +16 -0
  35. package/dist/tools/omada/namespace.js.map +1 -1
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/package.json +5 -1
  39. package/.codex/review-rules.md +0 -16
  40. package/.dockerignore +0 -8
  41. package/.gitattributes +0 -30
  42. package/.github/workflows/ci.yml +0 -193
  43. package/AGENTS.md +0 -389
  44. package/Dockerfile +0 -50
  45. package/docker-compose.yml +0 -23
  46. package/docs/CHANGELOG.md +0 -341
  47. package/docs/COVERAGE.md +0 -219
  48. package/docs/QUICK_START.md +0 -240
  49. package/eslint.config.js +0 -33
  50. package/n8n/ai-agent-safety-monitor.json +0 -355
  51. package/openapi.json +0 -1259
  52. package/scripts/check-version-sync.mjs +0 -69
  53. package/scripts/generate-jwt.ts +0 -121
  54. package/src/config.ts +0 -440
  55. package/src/haClient/automations.ts +0 -261
  56. package/src/haClient/calendars.ts +0 -94
  57. package/src/haClient/config.ts +0 -52
  58. package/src/haClient/devices.ts +0 -653
  59. package/src/haClient/entities.ts +0 -128
  60. package/src/haClient/events.ts +0 -689
  61. package/src/haClient/history.ts +0 -73
  62. package/src/haClient/index.ts +0 -336
  63. package/src/haClient/request.ts +0 -142
  64. package/src/haClient/services.ts +0 -45
  65. package/src/haClient/states.ts +0 -60
  66. package/src/haClient/updates.ts +0 -58
  67. package/src/haClient/wsCommand.ts +0 -44
  68. package/src/index.ts +0 -164
  69. package/src/localAI/index.ts +0 -88
  70. package/src/localAI/prompts.ts +0 -50
  71. package/src/localAI/providers/base.ts +0 -120
  72. package/src/localAI/providers/ollama.ts +0 -78
  73. package/src/localAI/providers/openai.ts +0 -86
  74. package/src/localAI/types.ts +0 -67
  75. package/src/omadaClient/auth.ts +0 -178
  76. package/src/omadaClient/client.ts +0 -337
  77. package/src/omadaClient/device.ts +0 -118
  78. package/src/omadaClient/index.ts +0 -310
  79. package/src/omadaClient/network.ts +0 -284
  80. package/src/omadaClient/request.ts +0 -365
  81. package/src/omadaClient/security.ts +0 -51
  82. package/src/omadaClient/site.ts +0 -344
  83. package/src/ownerQuestions/service.ts +0 -536
  84. package/src/permissions/index.ts +0 -143
  85. package/src/resources/index.ts +0 -213
  86. package/src/server/auth.ts +0 -175
  87. package/src/server/chatFace.ts +0 -789
  88. package/src/server/chatSlice.ts +0 -57
  89. package/src/server/common.ts +0 -170
  90. package/src/server/eventSubscription.ts +0 -343
  91. package/src/server/http.ts +0 -626
  92. package/src/server/instructions.ts +0 -97
  93. package/src/server/rateLimiter.ts +0 -227
  94. package/src/server/stdio.ts +0 -41
  95. package/src/server/stream.ts +0 -205
  96. package/src/tools/ai/analyzeSensors.ts +0 -33
  97. package/src/tools/ai/index.ts +0 -20
  98. package/src/tools/common.ts +0 -442
  99. package/src/tools/homeassistant/askOwner.ts +0 -133
  100. package/src/tools/homeassistant/automationControls.ts +0 -66
  101. package/src/tools/homeassistant/calendar.ts +0 -97
  102. package/src/tools/homeassistant/callService.ts +0 -25
  103. package/src/tools/homeassistant/controlClimate.ts +0 -68
  104. package/src/tools/homeassistant/controlCover.ts +0 -48
  105. package/src/tools/homeassistant/controlFan.ts +0 -44
  106. package/src/tools/homeassistant/controlLight.ts +0 -38
  107. package/src/tools/homeassistant/controlMediaPlayer.ts +0 -50
  108. package/src/tools/homeassistant/createAutomation.ts +0 -35
  109. package/src/tools/homeassistant/deleteAutomation.ts +0 -24
  110. package/src/tools/homeassistant/entityAction.ts +0 -35
  111. package/src/tools/homeassistant/entityGuard.ts +0 -28
  112. package/src/tools/homeassistant/getAllSensors.ts +0 -46
  113. package/src/tools/homeassistant/getAutomationTrace.ts +0 -30
  114. package/src/tools/homeassistant/getDomainSummary.ts +0 -29
  115. package/src/tools/homeassistant/getEntitiesByDomain.ts +0 -56
  116. package/src/tools/homeassistant/getHistory.ts +0 -24
  117. package/src/tools/homeassistant/getState.ts +0 -24
  118. package/src/tools/homeassistant/getStates.ts +0 -46
  119. package/src/tools/homeassistant/getVersion.ts +0 -21
  120. package/src/tools/homeassistant/index.ts +0 -121
  121. package/src/tools/homeassistant/listAutomations.ts +0 -35
  122. package/src/tools/homeassistant/listEntities.ts +0 -44
  123. package/src/tools/homeassistant/listPersons.ts +0 -46
  124. package/src/tools/homeassistant/sceneAndScript.ts +0 -53
  125. package/src/tools/homeassistant/searchEntities.ts +0 -58
  126. package/src/tools/homeassistant/sendNotification.ts +0 -179
  127. package/src/tools/homeassistant/systemTools.ts +0 -52
  128. package/src/tools/homeassistant/triggerAutomation.ts +0 -24
  129. package/src/tools/index.ts +0 -78
  130. package/src/tools/infra/actions.ts +0 -95
  131. package/src/tools/infra/index.ts +0 -138
  132. package/src/tools/omada/blockClient.ts +0 -28
  133. package/src/tools/omada/cyclePoePort.ts +0 -32
  134. package/src/tools/omada/deleteClient.ts +0 -27
  135. package/src/tools/omada/disableClientRateLimit.ts +0 -23
  136. package/src/tools/omada/getClient.ts +0 -14
  137. package/src/tools/omada/getDevice.ts +0 -14
  138. package/src/tools/omada/getFirewallSetting.ts +0 -14
  139. package/src/tools/omada/getInternetInfo.ts +0 -14
  140. package/src/tools/omada/getLanNetworkList.ts +0 -14
  141. package/src/tools/omada/getLanProfileList.ts +0 -14
  142. package/src/tools/omada/getPortForwardingStatus.ts +0 -26
  143. package/src/tools/omada/getRateLimitProfiles.ts +0 -14
  144. package/src/tools/omada/getSiteNtpStatus.ts +0 -14
  145. package/src/tools/omada/getSsidDetail.ts +0 -22
  146. package/src/tools/omada/getSsidList.ts +0 -21
  147. package/src/tools/omada/getSwitchStackDetail.ts +0 -14
  148. package/src/tools/omada/getThreatList.ts +0 -42
  149. package/src/tools/omada/getWlanGroupList.ts +0 -14
  150. package/src/tools/omada/graph.ts +0 -187
  151. package/src/tools/omada/index.ts +0 -153
  152. package/src/tools/omada/listBlockedClients.ts +0 -17
  153. package/src/tools/omada/listClients.ts +0 -14
  154. package/src/tools/omada/listClientsActivity.ts +0 -25
  155. package/src/tools/omada/listClientsPastConnections.ts +0 -44
  156. package/src/tools/omada/listDevices.ts +0 -14
  157. package/src/tools/omada/listDevicesStats.ts +0 -41
  158. package/src/tools/omada/listMostActiveClients.ts +0 -14
  159. package/src/tools/omada/listSites.ts +0 -18
  160. package/src/tools/omada/namespace.ts +0 -1164
  161. package/src/tools/omada/searchDevices.ts +0 -20
  162. package/src/tools/omada/setClientRateLimit.ts +0 -26
  163. package/src/tools/omada/setClientRateLimitProfile.ts +0 -25
  164. package/src/tools/omada/setSiteNtpServers.ts +0 -36
  165. package/src/tools/omada/setSsidEnabled.ts +0 -38
  166. package/src/tools/omada/unblockClient.ts +0 -26
  167. package/src/tools/types.ts +0 -11
  168. package/src/tools/utils/pagination-schema.ts +0 -27
  169. package/src/types/ai/index.ts +0 -11
  170. package/src/types/ai/types.ts +0 -67
  171. package/src/types/homeassistant/automations.ts +0 -43
  172. package/src/types/homeassistant/calendars.ts +0 -26
  173. package/src/types/homeassistant/config.ts +0 -14
  174. package/src/types/homeassistant/entities.ts +0 -14
  175. package/src/types/homeassistant/errors.ts +0 -33
  176. package/src/types/homeassistant/haConfig.ts +0 -7
  177. package/src/types/homeassistant/history.ts +0 -13
  178. package/src/types/homeassistant/index.ts +0 -46
  179. package/src/types/homeassistant/request.ts +0 -6
  180. package/src/types/homeassistant/serviceCall.ts +0 -18
  181. package/src/types/homeassistant/states.ts +0 -58
  182. package/src/types/homeassistant/updates.ts +0 -28
  183. package/src/types/index.ts +0 -12
  184. package/src/types/omada/activeClientInfo.ts +0 -17
  185. package/src/types/omada/clientActivity.ts +0 -32
  186. package/src/types/omada/clientBlockStatus.ts +0 -11
  187. package/src/types/omada/clientPastConnection.ts +0 -82
  188. package/src/types/omada/index.ts +0 -14
  189. package/src/types/omada/omadaApiResponse.ts +0 -5
  190. package/src/types/omada/omadaClientInfo.ts +0 -11
  191. package/src/types/omada/omadaDeviceInfo.ts +0 -11
  192. package/src/types/omada/omadaDeviceStats.ts +0 -31
  193. package/src/types/omada/omadaSiteSummary.ts +0 -5
  194. package/src/types/omada/oswStackDetail.ts +0 -11
  195. package/src/types/omada/paginatedResult.ts +0 -6
  196. package/src/types/omada/rateLimitProfile.ts +0 -58
  197. package/src/types/omada/threatInfo.ts +0 -38
  198. package/src/types/omada/tokenResult.ts +0 -6
  199. package/src/types.ts +0 -2
  200. package/src/utils/config-validations.ts +0 -68
  201. package/src/utils/jwt.ts +0 -112
  202. package/src/utils/logger.ts +0 -121
  203. package/src/utils/plainText.ts +0 -48
  204. package/src/utils/redact.ts +0 -59
  205. package/src/utils/sanitizeError.ts +0 -18
  206. package/src/utils/tlsDispatcher.ts +0 -69
  207. package/src/version.ts +0 -5
  208. package/tests/bdd/automation-creation.spec.ts +0 -214
  209. package/tests/bdd/automation-management.spec.ts +0 -267
  210. package/tests/bdd/calendar.spec.ts +0 -190
  211. package/tests/bdd/configuration.spec.ts +0 -283
  212. package/tests/bdd/device-control.spec.ts +0 -344
  213. package/tests/bdd/entity-listing.spec.ts +0 -208
  214. package/tests/bdd/entity-search.spec.ts +0 -183
  215. package/tests/bdd/history-retrieval.spec.ts +0 -195
  216. package/tests/bdd/notifications.spec.ts +0 -238
  217. package/tests/bdd/service-calls.spec.ts +0 -144
  218. package/tests/bdd/state-retrieval.spec.ts +0 -204
  219. package/tests/bench/hotpaths.bench.ts +0 -68
  220. package/tests/config.test.ts +0 -355
  221. package/tests/features/automation-creation.feature +0 -36
  222. package/tests/features/automation-management.feature +0 -54
  223. package/tests/features/calendar.feature +0 -37
  224. package/tests/features/configuration.feature +0 -70
  225. package/tests/features/device-control.feature +0 -100
  226. package/tests/features/entity-listing.feature +0 -51
  227. package/tests/features/entity-search.feature +0 -44
  228. package/tests/features/history-retrieval.feature +0 -38
  229. package/tests/features/notifications.feature +0 -37
  230. package/tests/features/service-calls.feature +0 -35
  231. package/tests/features/state-retrieval.feature +0 -37
  232. package/tests/fixtures/openapi-default-slice.json +0 -290
  233. package/tests/haClient/automationTrace.test.ts +0 -59
  234. package/tests/haClient/automations.test.ts +0 -149
  235. package/tests/haClient/config.test.ts +0 -117
  236. package/tests/haClient/entities.test.ts +0 -188
  237. package/tests/haClient/events.test.ts +0 -455
  238. package/tests/haClient/eventsTls.test.ts +0 -52
  239. package/tests/haClient/history.test.ts +0 -228
  240. package/tests/haClient/index.test.ts +0 -243
  241. package/tests/haClient/request.test.ts +0 -217
  242. package/tests/haClient/services.test.ts +0 -120
  243. package/tests/haClient/states.test.ts +0 -119
  244. package/tests/haClient/updates.test.ts +0 -193
  245. package/tests/index.test.ts +0 -160
  246. package/tests/integration/homeassistant.test.ts +0 -144
  247. package/tests/integration/omada.test.ts +0 -167
  248. package/tests/integration/server.test.ts +0 -151
  249. package/tests/localAI/index.test.ts +0 -177
  250. package/tests/localAI/ollama.test.ts +0 -187
  251. package/tests/localAI/openai.test.ts +0 -257
  252. package/tests/omadaClient/actions.test.ts +0 -161
  253. package/tests/omadaClient/clientBlocks.test.ts +0 -170
  254. package/tests/omadaClient/ntp.test.ts +0 -381
  255. package/tests/omadaClient/site.test.ts +0 -37
  256. package/tests/ownerQuestions/service.test.ts +0 -499
  257. package/tests/ownerQuestions/tools.test.ts +0 -186
  258. package/tests/permissions/index.test.ts +0 -217
  259. package/tests/resources/index.test.ts +0 -288
  260. package/tests/scripts/generateJwt.test.ts +0 -44
  261. package/tests/security/accessControl.test.ts +0 -223
  262. package/tests/security/authentication.test.ts +0 -117
  263. package/tests/security/configuration.test.ts +0 -170
  264. package/tests/security/cryptoTransport.test.ts +0 -172
  265. package/tests/security/dependencies.test.ts +0 -152
  266. package/tests/security/injection.test.ts +0 -117
  267. package/tests/security/insecureDesign.test.ts +0 -121
  268. package/tests/security/logging.test.ts +0 -131
  269. package/tests/security/resourceConsumption.test.ts +0 -142
  270. package/tests/server/auth.test.ts +0 -249
  271. package/tests/server/chatFace.test.ts +0 -320
  272. package/tests/server/common.test.ts +0 -84
  273. package/tests/server/http.test.ts +0 -561
  274. package/tests/server/instructions.test.ts +0 -152
  275. package/tests/server/memoizeJsonSchema.test.ts +0 -71
  276. package/tests/server/stdio.test.ts +0 -99
  277. package/tests/server/stream.test.ts +0 -285
  278. package/tests/server/toolVisibility.http.test.ts +0 -111
  279. package/tests/server/toolVisibility.test.ts +0 -129
  280. package/tests/tools/ai/analyzeSensors.test.ts +0 -227
  281. package/tests/tools/common.test.ts +0 -123
  282. package/tests/tools/homeassistant/handlers.test.ts +0 -872
  283. package/tests/tools/infra/invokeAction.test.ts +0 -280
  284. package/tests/tools/omada/graph.test.ts +0 -385
  285. package/tests/tools/omada/handlers.test.ts +0 -897
  286. package/tests/tools/omada/namespace.test.ts +0 -483
  287. package/tests/tools/registry.test.ts +0 -168
  288. package/tests/utils/config-validations.test.ts +0 -352
  289. package/tests/utils/jwt.test.ts +0 -115
  290. package/tests/utils/logger.test.ts +0 -327
  291. package/tests/utils/redact.test.ts +0 -105
  292. package/tsconfig.json +0 -24
  293. package/vitest.config.ts +0 -38
@@ -1,344 +0,0 @@
1
- import type { OmadaApiResponse, OmadaSiteSummary } from '../types/index.js';
2
-
3
- import type { RequestHandler } from './request.js';
4
-
5
- /** Omada caps a site's NTP server list at 5 entries. */
6
- export const MAX_NTP_SERVERS = 5;
7
-
8
- /**
9
- * The controller's own pattern for an NTP server address (`ntp server` schema in
10
- * the Open API spec): 3-64 chars, dot-separated labels. It accepts hostnames and
11
- * dotted IPv4 alike.
12
- */
13
- const NTP_ADDRESS_PATTERN = /^(?=.{3,64}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$/;
14
-
15
- const IPV4_LIKE = /^[0-9.]+$/;
16
-
17
- /**
18
- * Validate one NTP server address before it reaches the controller. Anything that
19
- * looks numeric must be a real IPv4 address (the controller's pattern alone would
20
- * accept `999.1.1.1`).
21
- * @returns an error message, or null if the address is acceptable
22
- */
23
- export function ntpAddressError(address: string): string | null {
24
- if (!NTP_ADDRESS_PATTERN.test(address)) {
25
- return `'${address}' is not a valid NTP server address (expected a hostname or IPv4 address)`;
26
- }
27
- if (IPV4_LIKE.test(address)) {
28
- const octets = address.split('.');
29
- if (octets.length !== 4 || octets.some((o) => o === '' || Number(o) > 255 || (o.length > 1 && o.startsWith('0')))) {
30
- return `'${address}' is not a valid IPv4 address`;
31
- }
32
- }
33
- return null;
34
- }
35
-
36
- /** Daylight-saving start/end point, identical in the site GET and PUT shapes. */
37
- export interface DstTime {
38
- month: number;
39
- serial: number;
40
- day: number;
41
- hour: number;
42
- minute: number;
43
- }
44
-
45
- /** `GET /sites/{siteId}` result (OperationId: getSiteEntity). */
46
- export interface OmadaSiteInfo {
47
- siteId?: string;
48
- name?: string;
49
- type?: number;
50
- tagIds?: string[];
51
- region: string;
52
- timeZone: string;
53
- scenario: string;
54
- ntpEnable?: boolean;
55
- ntpServers?: string[];
56
- dst?: {
57
- enable?: boolean;
58
- mode?: number;
59
- start?: DstTime;
60
- end?: DstTime;
61
- offset?: number;
62
- // Read-only fields the PUT does not accept: status, startTime, endTime, nextStart, ...
63
- [readOnly: string]: unknown;
64
- };
65
- longitude?: number;
66
- latitude?: number;
67
- address?: string;
68
- supportES?: boolean;
69
- supportL2?: boolean;
70
- }
71
-
72
- /** `PUT /sites/{siteId}` body (UpdateSiteEntity). */
73
- export interface SiteUpdateBody {
74
- name?: string;
75
- region: string;
76
- timeZone: string;
77
- scenario: string;
78
- tagIds?: string[];
79
- ntpEnable: boolean;
80
- ntpServers: Array<{ address: string }>;
81
- dst?: { enable?: boolean; mode?: number; start?: DstTime; end?: DstTime; offset?: number };
82
- longitude?: number;
83
- latitude?: number;
84
- address?: string;
85
- supportES?: boolean;
86
- supportL2?: boolean;
87
- }
88
-
89
- export interface SiteNtpChangeResult {
90
- siteId: string;
91
- siteName?: string;
92
- applied: boolean;
93
- before: { ntpEnable?: boolean; ntpServers: string[] };
94
- after: { ntpEnable: boolean; ntpServers: string[] };
95
- /** The exact body sent (or, on a dry run, that would be sent) to PUT /sites/{siteId}. */
96
- request: SiteUpdateBody;
97
- }
98
-
99
- /**
100
- * Build the full-replacement `PUT /sites/{siteId}` body from the site's current
101
- * settings, changing only the NTP fields.
102
- *
103
- * The PUT replaces the site's settings as a whole (region, timeZone and scenario
104
- * are required), so every writable field is carried over from the GET. The GET
105
- * and PUT shapes differ: NTP servers are plain strings on read but
106
- * `{ address }` objects on write, and the read-only DST fields (status,
107
- * timestamps) must be dropped.
108
- */
109
- export function buildSiteUpdateBody(site: OmadaSiteInfo, ntpServers: string[], ntpEnable: boolean): SiteUpdateBody {
110
- for (const field of ['region', 'timeZone', 'scenario'] as const) {
111
- if (typeof site[field] !== 'string' || site[field] === '') {
112
- throw new Error(`Site info is missing '${field}', which the controller requires to update the site; refusing to write`);
113
- }
114
- }
115
-
116
- const body: SiteUpdateBody = {
117
- region: site.region,
118
- timeZone: site.timeZone,
119
- scenario: site.scenario,
120
- ntpEnable,
121
- ntpServers: ntpServers.map((address) => ({ address })),
122
- };
123
- if (site.name !== undefined) body.name = site.name;
124
- if (site.tagIds !== undefined) body.tagIds = site.tagIds;
125
- if (site.longitude !== undefined) body.longitude = site.longitude;
126
- if (site.latitude !== undefined) body.latitude = site.latitude;
127
- if (site.address !== undefined) body.address = site.address;
128
- if (site.supportES !== undefined) body.supportES = site.supportES;
129
- if (site.supportL2 !== undefined) body.supportL2 = site.supportL2;
130
- if (site.dst) {
131
- const { enable, mode, start, end, offset } = site.dst;
132
- const dst: NonNullable<SiteUpdateBody['dst']> = {};
133
- if (enable !== undefined) dst.enable = enable;
134
- if (mode !== undefined) dst.mode = mode;
135
- // With DST off the controller returns start/end as `{}` but rejects them on
136
- // write ("should not be null"), so only complete points are sent back.
137
- if (isCompleteDstTime(start)) dst.start = start;
138
- if (isCompleteDstTime(end)) dst.end = end;
139
- if (offset !== undefined) dst.offset = offset;
140
- if (enable === true && (dst.start === undefined || dst.end === undefined)) {
141
- throw new Error('Site has daylight saving enabled but an incomplete start/end; refusing to write');
142
- }
143
- body.dst = dst;
144
- }
145
- return body;
146
- }
147
-
148
- const DST_TIME_FIELDS = ['month', 'serial', 'day', 'hour', 'minute'] as const;
149
-
150
- function isCompleteDstTime(value: unknown): value is DstTime {
151
- if (typeof value !== 'object' || value === null) {
152
- return false;
153
- }
154
- const point = value as Record<string, unknown>;
155
- return DST_TIME_FIELDS.every((field) => typeof point[field] === 'number');
156
- }
157
-
158
- /**
159
- * Validate a configured default site (`OMADA_SITE_ID`) against the controller's
160
- * live site list.
161
- *
162
- * The Omada controller is the source of truth; `OMADA_SITE_ID` is only a
163
- * selector/filter against it. A value that isn't on the controller is a
164
- * deployment/configuration bug (e.g. the site id drifted after a controller
165
- * migration, exactly like `OMADA_OMADAC_ID` does) — not a transient outage. If
166
- * left unvalidated it silently scopes every site-scoped read to a dead site and
167
- * surfaces later as a mysterious "user does not have permissions to access this
168
- * site" error.
169
- *
170
- * @returns an actionable error message if `OMADA_SITE_ID` is set but absent from
171
- * `sites`; `null` if it is unset (no default) or valid.
172
- */
173
- export function checkConfiguredSite(
174
- configuredSiteId: string | undefined,
175
- sites: OmadaSiteSummary[]
176
- ): string | null {
177
- if (!configuredSiteId) {
178
- return null;
179
- }
180
- if (sites.some((s) => s.siteId === configuredSiteId)) {
181
- return null;
182
- }
183
- const available = sites.map((s) => `${s.name} (${s.siteId})`).join(', ') || '(controller returned no sites)';
184
- return `OMADA_SITE_ID="${configuredSiteId}" was not found on the controller. Set OMADA_SITE_ID to one of: ${available}.`;
185
- }
186
-
187
- /**
188
- * Site-related operations for the Omada API.
189
- */
190
- export class SiteOperations {
191
- /** Tail of the queued site-settings writes, per site (see withSiteLock). */
192
- private readonly siteWriteQueues = new Map<string, Promise<void>>();
193
-
194
- constructor(
195
- private readonly request: RequestHandler,
196
- private readonly buildPath: (path: string) => string,
197
- private readonly defaultSiteId?: string
198
- ) {}
199
-
200
- /**
201
- * List all sites accessible to the authenticated user.
202
- */
203
- public async listSites(): Promise<OmadaSiteSummary[]> {
204
- return await this.request.fetchPaginated<OmadaSiteSummary>(this.buildPath('/sites'));
205
- }
206
-
207
- /**
208
- * Resolve a site ID from the parameter or default configuration.
209
- * @throws {Error} If no site ID is available
210
- */
211
- public resolveSiteId(siteId?: string): string {
212
- if (siteId) {
213
- return siteId;
214
- }
215
-
216
- if (this.defaultSiteId) {
217
- return this.defaultSiteId;
218
- }
219
-
220
- throw new Error('A site id must be provided either in the environment or as a parameter. Use omada_browse at path / to discover available sites and their IDs.');
221
- }
222
-
223
- /**
224
- * Get a site's full settings.
225
- * OperationId: getSiteEntity
226
- */
227
- public async getSiteInfo(siteId?: string): Promise<OmadaSiteInfo> {
228
- const resolvedSiteId = this.resolveSiteId(siteId);
229
- const response = await this.request.get<OmadaApiResponse<OmadaSiteInfo>>(
230
- this.buildPath(`/sites/${encodeURIComponent(resolvedSiteId)}`)
231
- );
232
- return this.request.ensureSuccess(response);
233
- }
234
-
235
- /**
236
- * Get a site's NTP server status and configuration.
237
- * OperationId: getNtpStatus
238
- */
239
- public async getSiteNtpStatus(siteId?: string): Promise<unknown> {
240
- const resolvedSiteId = this.resolveSiteId(siteId);
241
- const response = await this.request.get<OmadaApiResponse<unknown>>(
242
- this.buildPath(`/sites/${encodeURIComponent(resolvedSiteId)}/setting/ntp`)
243
- );
244
- return this.request.ensureSuccess(response);
245
- }
246
-
247
- /**
248
- * Run site-settings writes for one site one at a time, so two read-modify-write
249
- * calls from this server never interleave (the second reads after the first
250
- * has written).
251
- */
252
- private async withSiteLock<T>(siteId: string, fn: () => Promise<T>): Promise<T> {
253
- const previous = this.siteWriteQueues.get(siteId) ?? Promise.resolve();
254
- let release!: () => void;
255
- const current = new Promise<void>((resolve) => {
256
- release = resolve;
257
- });
258
- const tail = previous.then(() => current);
259
- this.siteWriteQueues.set(siteId, tail);
260
- await previous;
261
- try {
262
- return await fn();
263
- } finally {
264
- release();
265
- if (this.siteWriteQueues.get(siteId) === tail) {
266
- this.siteWriteQueues.delete(siteId);
267
- }
268
- }
269
- }
270
-
271
- /**
272
- * Set a site's NTP servers (and NTP on/off) by read-modify-write of the site
273
- * settings: Omada has no NTP-only write, NTP lives on `PUT /sites/{siteId}`.
274
- * With `dryRun`, returns the exact body without writing.
275
- *
276
- * The controller offers no conditional update (no version field or If-Match),
277
- * so a change made elsewhere between our read and our write would be undone.
278
- * Writes are serialized per site within this server, and the site is re-read
279
- * just before the PUT: if anything other than NTP changed since the first
280
- * read, the write is refused. Only the moment between that re-read and the
281
- * PUT remains unprotected.
282
- * OperationId: modifySite
283
- */
284
- public async setSiteNtpServers(
285
- servers: string[],
286
- options: { enabled?: boolean; siteId?: string; dryRun?: boolean } = {}
287
- ): Promise<SiteNtpChangeResult> {
288
- const enabled = options.enabled ?? true;
289
- if (enabled && servers.length === 0) {
290
- throw new Error('At least one NTP server is required to enable NTP');
291
- }
292
- if (servers.length > MAX_NTP_SERVERS) {
293
- throw new Error(`Omada allows at most ${MAX_NTP_SERVERS} NTP servers per site (got ${servers.length})`);
294
- }
295
- const errors = servers.map(ntpAddressError).filter((e): e is string => e !== null);
296
- if (errors.length > 0) {
297
- throw new Error(errors.join('; '));
298
- }
299
- if (new Set(servers).size !== servers.length) {
300
- throw new Error('NTP server list contains duplicates');
301
- }
302
-
303
- const resolvedSiteId = this.resolveSiteId(options.siteId);
304
- if (options.dryRun) {
305
- return this.planNtpChange(resolvedSiteId, await this.getSiteInfo(resolvedSiteId), servers, enabled);
306
- }
307
-
308
- return await this.withSiteLock(resolvedSiteId, async () => {
309
- const site = await this.getSiteInfo(resolvedSiteId);
310
- const result = this.planNtpChange(resolvedSiteId, site, servers, enabled);
311
-
312
- // Re-read right before writing; refuse if a non-NTP setting moved.
313
- const latest = await this.getSiteInfo(resolvedSiteId);
314
- if (JSON.stringify(buildSiteUpdateBody(latest, servers, enabled)) !== JSON.stringify(result.request)) {
315
- throw new Error(
316
- 'Site settings changed while preparing the NTP update (another change is in progress); nothing was written. Try again.'
317
- );
318
- }
319
-
320
- const response = await this.request.put<OmadaApiResponse<unknown>>(
321
- this.buildPath(`/sites/${encodeURIComponent(resolvedSiteId)}`),
322
- result.request
323
- );
324
- this.request.ensureSuccess(response);
325
-
326
- // Report what the controller now holds, not what we asked for.
327
- const updated = await this.getSiteInfo(resolvedSiteId);
328
- result.applied = true;
329
- result.after = { ntpEnable: updated.ntpEnable ?? enabled, ntpServers: updated.ntpServers ?? [] };
330
- return result;
331
- });
332
- }
333
-
334
- private planNtpChange(siteId: string, site: OmadaSiteInfo, servers: string[], enabled: boolean): SiteNtpChangeResult {
335
- return {
336
- siteId,
337
- siteName: site.name,
338
- applied: false,
339
- before: { ntpEnable: site.ntpEnable, ntpServers: site.ntpServers ?? [] },
340
- after: { ntpEnable: enabled, ntpServers: servers },
341
- request: buildSiteUpdateBody(site, servers, enabled),
342
- };
343
- }
344
- }