@coffeerunhobby/mcp-ha-connect 2.2.1 → 2.3.1

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 (281) hide show
  1. package/.env.example +1 -1
  2. package/README.md +11 -2
  3. package/dist/omadaClient/client.d.ts +6 -1
  4. package/dist/omadaClient/client.js +12 -1
  5. package/dist/omadaClient/client.js.map +1 -1
  6. package/dist/omadaClient/diagnose.d.ts +68 -0
  7. package/dist/omadaClient/diagnose.js +164 -0
  8. package/dist/omadaClient/diagnose.js.map +1 -0
  9. package/dist/omadaClient/index.d.ts +6 -0
  10. package/dist/omadaClient/index.js +15 -0
  11. package/dist/omadaClient/index.js.map +1 -1
  12. package/dist/omadaClient/macGroups.d.ts +58 -0
  13. package/dist/omadaClient/macGroups.js +124 -0
  14. package/dist/omadaClient/macGroups.js.map +1 -0
  15. package/dist/tools/infra/actions.d.ts +1 -1
  16. package/dist/tools/infra/actions.js +1 -1
  17. package/dist/tools/omada/index.js +6 -0
  18. package/dist/tools/omada/index.js.map +1 -1
  19. package/dist/tools/omada/macGroups.d.ts +19 -0
  20. package/dist/tools/omada/macGroups.js +77 -0
  21. package/dist/tools/omada/macGroups.js.map +1 -0
  22. package/dist/tools/omada/namespace.js +8 -0
  23. package/dist/tools/omada/namespace.js.map +1 -1
  24. package/dist/version.d.ts +1 -1
  25. package/dist/version.js +1 -1
  26. package/package.json +5 -1
  27. package/.codex/review-rules.md +0 -16
  28. package/.dockerignore +0 -8
  29. package/.gitattributes +0 -30
  30. package/.github/workflows/ci.yml +0 -193
  31. package/AGENTS.md +0 -389
  32. package/Dockerfile +0 -50
  33. package/docker-compose.yml +0 -23
  34. package/docs/CHANGELOG.md +0 -341
  35. package/docs/COVERAGE.md +0 -219
  36. package/docs/QUICK_START.md +0 -240
  37. package/eslint.config.js +0 -33
  38. package/n8n/ai-agent-safety-monitor.json +0 -355
  39. package/openapi.json +0 -1259
  40. package/scripts/check-version-sync.mjs +0 -69
  41. package/scripts/generate-jwt.ts +0 -121
  42. package/src/config.ts +0 -440
  43. package/src/haClient/automations.ts +0 -261
  44. package/src/haClient/calendars.ts +0 -94
  45. package/src/haClient/config.ts +0 -52
  46. package/src/haClient/devices.ts +0 -653
  47. package/src/haClient/entities.ts +0 -128
  48. package/src/haClient/events.ts +0 -689
  49. package/src/haClient/history.ts +0 -73
  50. package/src/haClient/index.ts +0 -336
  51. package/src/haClient/request.ts +0 -142
  52. package/src/haClient/services.ts +0 -45
  53. package/src/haClient/states.ts +0 -60
  54. package/src/haClient/updates.ts +0 -58
  55. package/src/haClient/wsCommand.ts +0 -44
  56. package/src/index.ts +0 -164
  57. package/src/localAI/index.ts +0 -88
  58. package/src/localAI/prompts.ts +0 -50
  59. package/src/localAI/providers/base.ts +0 -120
  60. package/src/localAI/providers/ollama.ts +0 -78
  61. package/src/localAI/providers/openai.ts +0 -86
  62. package/src/localAI/types.ts +0 -67
  63. package/src/omadaClient/auth.ts +0 -178
  64. package/src/omadaClient/client.ts +0 -337
  65. package/src/omadaClient/device.ts +0 -118
  66. package/src/omadaClient/index.ts +0 -310
  67. package/src/omadaClient/network.ts +0 -284
  68. package/src/omadaClient/request.ts +0 -365
  69. package/src/omadaClient/security.ts +0 -51
  70. package/src/omadaClient/site.ts +0 -344
  71. package/src/ownerQuestions/service.ts +0 -536
  72. package/src/permissions/index.ts +0 -143
  73. package/src/resources/index.ts +0 -213
  74. package/src/server/auth.ts +0 -175
  75. package/src/server/chatFace.ts +0 -789
  76. package/src/server/chatSlice.ts +0 -57
  77. package/src/server/common.ts +0 -170
  78. package/src/server/eventSubscription.ts +0 -343
  79. package/src/server/http.ts +0 -626
  80. package/src/server/instructions.ts +0 -97
  81. package/src/server/rateLimiter.ts +0 -227
  82. package/src/server/stdio.ts +0 -41
  83. package/src/server/stream.ts +0 -205
  84. package/src/tools/ai/analyzeSensors.ts +0 -33
  85. package/src/tools/ai/index.ts +0 -20
  86. package/src/tools/common.ts +0 -442
  87. package/src/tools/homeassistant/askOwner.ts +0 -133
  88. package/src/tools/homeassistant/automationControls.ts +0 -66
  89. package/src/tools/homeassistant/calendar.ts +0 -97
  90. package/src/tools/homeassistant/callService.ts +0 -25
  91. package/src/tools/homeassistant/controlClimate.ts +0 -68
  92. package/src/tools/homeassistant/controlCover.ts +0 -48
  93. package/src/tools/homeassistant/controlFan.ts +0 -44
  94. package/src/tools/homeassistant/controlLight.ts +0 -38
  95. package/src/tools/homeassistant/controlMediaPlayer.ts +0 -50
  96. package/src/tools/homeassistant/createAutomation.ts +0 -35
  97. package/src/tools/homeassistant/deleteAutomation.ts +0 -24
  98. package/src/tools/homeassistant/entityAction.ts +0 -35
  99. package/src/tools/homeassistant/entityGuard.ts +0 -28
  100. package/src/tools/homeassistant/getAllSensors.ts +0 -46
  101. package/src/tools/homeassistant/getAutomationTrace.ts +0 -30
  102. package/src/tools/homeassistant/getDomainSummary.ts +0 -29
  103. package/src/tools/homeassistant/getEntitiesByDomain.ts +0 -56
  104. package/src/tools/homeassistant/getHistory.ts +0 -24
  105. package/src/tools/homeassistant/getState.ts +0 -24
  106. package/src/tools/homeassistant/getStates.ts +0 -46
  107. package/src/tools/homeassistant/getVersion.ts +0 -21
  108. package/src/tools/homeassistant/index.ts +0 -121
  109. package/src/tools/homeassistant/listAutomations.ts +0 -35
  110. package/src/tools/homeassistant/listEntities.ts +0 -44
  111. package/src/tools/homeassistant/listPersons.ts +0 -46
  112. package/src/tools/homeassistant/sceneAndScript.ts +0 -53
  113. package/src/tools/homeassistant/searchEntities.ts +0 -58
  114. package/src/tools/homeassistant/sendNotification.ts +0 -179
  115. package/src/tools/homeassistant/systemTools.ts +0 -52
  116. package/src/tools/homeassistant/triggerAutomation.ts +0 -24
  117. package/src/tools/index.ts +0 -78
  118. package/src/tools/infra/actions.ts +0 -95
  119. package/src/tools/infra/index.ts +0 -138
  120. package/src/tools/omada/blockClient.ts +0 -28
  121. package/src/tools/omada/cyclePoePort.ts +0 -32
  122. package/src/tools/omada/deleteClient.ts +0 -27
  123. package/src/tools/omada/disableClientRateLimit.ts +0 -23
  124. package/src/tools/omada/getClient.ts +0 -14
  125. package/src/tools/omada/getDevice.ts +0 -14
  126. package/src/tools/omada/getFirewallSetting.ts +0 -14
  127. package/src/tools/omada/getInternetInfo.ts +0 -14
  128. package/src/tools/omada/getLanNetworkList.ts +0 -14
  129. package/src/tools/omada/getLanProfileList.ts +0 -14
  130. package/src/tools/omada/getPortForwardingStatus.ts +0 -26
  131. package/src/tools/omada/getRateLimitProfiles.ts +0 -14
  132. package/src/tools/omada/getSiteNtpStatus.ts +0 -14
  133. package/src/tools/omada/getSsidDetail.ts +0 -22
  134. package/src/tools/omada/getSsidList.ts +0 -21
  135. package/src/tools/omada/getSwitchStackDetail.ts +0 -14
  136. package/src/tools/omada/getThreatList.ts +0 -42
  137. package/src/tools/omada/getWlanGroupList.ts +0 -14
  138. package/src/tools/omada/graph.ts +0 -187
  139. package/src/tools/omada/index.ts +0 -153
  140. package/src/tools/omada/listBlockedClients.ts +0 -17
  141. package/src/tools/omada/listClients.ts +0 -14
  142. package/src/tools/omada/listClientsActivity.ts +0 -25
  143. package/src/tools/omada/listClientsPastConnections.ts +0 -44
  144. package/src/tools/omada/listDevices.ts +0 -14
  145. package/src/tools/omada/listDevicesStats.ts +0 -41
  146. package/src/tools/omada/listMostActiveClients.ts +0 -14
  147. package/src/tools/omada/listSites.ts +0 -18
  148. package/src/tools/omada/namespace.ts +0 -1164
  149. package/src/tools/omada/searchDevices.ts +0 -20
  150. package/src/tools/omada/setClientRateLimit.ts +0 -26
  151. package/src/tools/omada/setClientRateLimitProfile.ts +0 -25
  152. package/src/tools/omada/setSiteNtpServers.ts +0 -36
  153. package/src/tools/omada/setSsidEnabled.ts +0 -38
  154. package/src/tools/omada/unblockClient.ts +0 -26
  155. package/src/tools/types.ts +0 -11
  156. package/src/tools/utils/pagination-schema.ts +0 -27
  157. package/src/types/ai/index.ts +0 -11
  158. package/src/types/ai/types.ts +0 -67
  159. package/src/types/homeassistant/automations.ts +0 -43
  160. package/src/types/homeassistant/calendars.ts +0 -26
  161. package/src/types/homeassistant/config.ts +0 -14
  162. package/src/types/homeassistant/entities.ts +0 -14
  163. package/src/types/homeassistant/errors.ts +0 -33
  164. package/src/types/homeassistant/haConfig.ts +0 -7
  165. package/src/types/homeassistant/history.ts +0 -13
  166. package/src/types/homeassistant/index.ts +0 -46
  167. package/src/types/homeassistant/request.ts +0 -6
  168. package/src/types/homeassistant/serviceCall.ts +0 -18
  169. package/src/types/homeassistant/states.ts +0 -58
  170. package/src/types/homeassistant/updates.ts +0 -28
  171. package/src/types/index.ts +0 -12
  172. package/src/types/omada/activeClientInfo.ts +0 -17
  173. package/src/types/omada/clientActivity.ts +0 -32
  174. package/src/types/omada/clientBlockStatus.ts +0 -11
  175. package/src/types/omada/clientPastConnection.ts +0 -82
  176. package/src/types/omada/index.ts +0 -14
  177. package/src/types/omada/omadaApiResponse.ts +0 -5
  178. package/src/types/omada/omadaClientInfo.ts +0 -11
  179. package/src/types/omada/omadaDeviceInfo.ts +0 -11
  180. package/src/types/omada/omadaDeviceStats.ts +0 -31
  181. package/src/types/omada/omadaSiteSummary.ts +0 -5
  182. package/src/types/omada/oswStackDetail.ts +0 -11
  183. package/src/types/omada/paginatedResult.ts +0 -6
  184. package/src/types/omada/rateLimitProfile.ts +0 -58
  185. package/src/types/omada/threatInfo.ts +0 -38
  186. package/src/types/omada/tokenResult.ts +0 -6
  187. package/src/types.ts +0 -2
  188. package/src/utils/config-validations.ts +0 -68
  189. package/src/utils/jwt.ts +0 -112
  190. package/src/utils/logger.ts +0 -121
  191. package/src/utils/plainText.ts +0 -48
  192. package/src/utils/redact.ts +0 -59
  193. package/src/utils/sanitizeError.ts +0 -18
  194. package/src/utils/tlsDispatcher.ts +0 -69
  195. package/src/version.ts +0 -5
  196. package/tests/bdd/automation-creation.spec.ts +0 -214
  197. package/tests/bdd/automation-management.spec.ts +0 -267
  198. package/tests/bdd/calendar.spec.ts +0 -190
  199. package/tests/bdd/configuration.spec.ts +0 -283
  200. package/tests/bdd/device-control.spec.ts +0 -344
  201. package/tests/bdd/entity-listing.spec.ts +0 -208
  202. package/tests/bdd/entity-search.spec.ts +0 -183
  203. package/tests/bdd/history-retrieval.spec.ts +0 -195
  204. package/tests/bdd/notifications.spec.ts +0 -238
  205. package/tests/bdd/service-calls.spec.ts +0 -144
  206. package/tests/bdd/state-retrieval.spec.ts +0 -204
  207. package/tests/bench/hotpaths.bench.ts +0 -68
  208. package/tests/config.test.ts +0 -355
  209. package/tests/features/automation-creation.feature +0 -36
  210. package/tests/features/automation-management.feature +0 -54
  211. package/tests/features/calendar.feature +0 -37
  212. package/tests/features/configuration.feature +0 -70
  213. package/tests/features/device-control.feature +0 -100
  214. package/tests/features/entity-listing.feature +0 -51
  215. package/tests/features/entity-search.feature +0 -44
  216. package/tests/features/history-retrieval.feature +0 -38
  217. package/tests/features/notifications.feature +0 -37
  218. package/tests/features/service-calls.feature +0 -35
  219. package/tests/features/state-retrieval.feature +0 -37
  220. package/tests/fixtures/openapi-default-slice.json +0 -290
  221. package/tests/haClient/automationTrace.test.ts +0 -59
  222. package/tests/haClient/automations.test.ts +0 -149
  223. package/tests/haClient/config.test.ts +0 -117
  224. package/tests/haClient/entities.test.ts +0 -188
  225. package/tests/haClient/events.test.ts +0 -455
  226. package/tests/haClient/eventsTls.test.ts +0 -52
  227. package/tests/haClient/history.test.ts +0 -228
  228. package/tests/haClient/index.test.ts +0 -243
  229. package/tests/haClient/request.test.ts +0 -217
  230. package/tests/haClient/services.test.ts +0 -120
  231. package/tests/haClient/states.test.ts +0 -119
  232. package/tests/haClient/updates.test.ts +0 -193
  233. package/tests/index.test.ts +0 -160
  234. package/tests/integration/homeassistant.test.ts +0 -144
  235. package/tests/integration/omada.test.ts +0 -167
  236. package/tests/integration/server.test.ts +0 -151
  237. package/tests/localAI/index.test.ts +0 -177
  238. package/tests/localAI/ollama.test.ts +0 -187
  239. package/tests/localAI/openai.test.ts +0 -257
  240. package/tests/omadaClient/actions.test.ts +0 -161
  241. package/tests/omadaClient/clientBlocks.test.ts +0 -170
  242. package/tests/omadaClient/ntp.test.ts +0 -381
  243. package/tests/omadaClient/site.test.ts +0 -37
  244. package/tests/ownerQuestions/service.test.ts +0 -499
  245. package/tests/ownerQuestions/tools.test.ts +0 -186
  246. package/tests/permissions/index.test.ts +0 -217
  247. package/tests/resources/index.test.ts +0 -288
  248. package/tests/scripts/generateJwt.test.ts +0 -44
  249. package/tests/security/accessControl.test.ts +0 -223
  250. package/tests/security/authentication.test.ts +0 -117
  251. package/tests/security/configuration.test.ts +0 -170
  252. package/tests/security/cryptoTransport.test.ts +0 -172
  253. package/tests/security/dependencies.test.ts +0 -152
  254. package/tests/security/injection.test.ts +0 -117
  255. package/tests/security/insecureDesign.test.ts +0 -121
  256. package/tests/security/logging.test.ts +0 -131
  257. package/tests/security/resourceConsumption.test.ts +0 -142
  258. package/tests/server/auth.test.ts +0 -249
  259. package/tests/server/chatFace.test.ts +0 -320
  260. package/tests/server/common.test.ts +0 -84
  261. package/tests/server/http.test.ts +0 -561
  262. package/tests/server/instructions.test.ts +0 -152
  263. package/tests/server/memoizeJsonSchema.test.ts +0 -71
  264. package/tests/server/stdio.test.ts +0 -99
  265. package/tests/server/stream.test.ts +0 -285
  266. package/tests/server/toolVisibility.http.test.ts +0 -111
  267. package/tests/server/toolVisibility.test.ts +0 -129
  268. package/tests/tools/ai/analyzeSensors.test.ts +0 -227
  269. package/tests/tools/common.test.ts +0 -123
  270. package/tests/tools/homeassistant/handlers.test.ts +0 -872
  271. package/tests/tools/infra/invokeAction.test.ts +0 -280
  272. package/tests/tools/omada/graph.test.ts +0 -385
  273. package/tests/tools/omada/handlers.test.ts +0 -897
  274. package/tests/tools/omada/namespace.test.ts +0 -483
  275. package/tests/tools/registry.test.ts +0 -168
  276. package/tests/utils/config-validations.test.ts +0 -352
  277. package/tests/utils/jwt.test.ts +0 -115
  278. package/tests/utils/logger.test.ts +0 -327
  279. package/tests/utils/redact.test.ts +0 -105
  280. package/tsconfig.json +0 -24
  281. 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
- }