@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.
- package/.env.example +1 -1
- package/README.md +11 -2
- package/dist/omadaClient/client.d.ts +6 -1
- package/dist/omadaClient/client.js +12 -1
- package/dist/omadaClient/client.js.map +1 -1
- package/dist/omadaClient/diagnose.d.ts +68 -0
- package/dist/omadaClient/diagnose.js +164 -0
- package/dist/omadaClient/diagnose.js.map +1 -0
- package/dist/omadaClient/index.d.ts +6 -0
- package/dist/omadaClient/index.js +15 -0
- package/dist/omadaClient/index.js.map +1 -1
- package/dist/omadaClient/macGroups.d.ts +58 -0
- package/dist/omadaClient/macGroups.js +124 -0
- package/dist/omadaClient/macGroups.js.map +1 -0
- package/dist/tools/infra/actions.d.ts +1 -1
- package/dist/tools/infra/actions.js +1 -1
- package/dist/tools/omada/index.js +6 -0
- package/dist/tools/omada/index.js.map +1 -1
- package/dist/tools/omada/macGroups.d.ts +19 -0
- package/dist/tools/omada/macGroups.js +77 -0
- package/dist/tools/omada/macGroups.js.map +1 -0
- package/dist/tools/omada/namespace.js +8 -0
- package/dist/tools/omada/namespace.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
- package/.codex/review-rules.md +0 -16
- package/.dockerignore +0 -8
- package/.gitattributes +0 -30
- package/.github/workflows/ci.yml +0 -193
- package/AGENTS.md +0 -389
- package/Dockerfile +0 -50
- package/docker-compose.yml +0 -23
- package/docs/CHANGELOG.md +0 -341
- package/docs/COVERAGE.md +0 -219
- package/docs/QUICK_START.md +0 -240
- package/eslint.config.js +0 -33
- package/n8n/ai-agent-safety-monitor.json +0 -355
- package/openapi.json +0 -1259
- package/scripts/check-version-sync.mjs +0 -69
- package/scripts/generate-jwt.ts +0 -121
- package/src/config.ts +0 -440
- package/src/haClient/automations.ts +0 -261
- package/src/haClient/calendars.ts +0 -94
- package/src/haClient/config.ts +0 -52
- package/src/haClient/devices.ts +0 -653
- package/src/haClient/entities.ts +0 -128
- package/src/haClient/events.ts +0 -689
- package/src/haClient/history.ts +0 -73
- package/src/haClient/index.ts +0 -336
- package/src/haClient/request.ts +0 -142
- package/src/haClient/services.ts +0 -45
- package/src/haClient/states.ts +0 -60
- package/src/haClient/updates.ts +0 -58
- package/src/haClient/wsCommand.ts +0 -44
- package/src/index.ts +0 -164
- package/src/localAI/index.ts +0 -88
- package/src/localAI/prompts.ts +0 -50
- package/src/localAI/providers/base.ts +0 -120
- package/src/localAI/providers/ollama.ts +0 -78
- package/src/localAI/providers/openai.ts +0 -86
- package/src/localAI/types.ts +0 -67
- package/src/omadaClient/auth.ts +0 -178
- package/src/omadaClient/client.ts +0 -337
- package/src/omadaClient/device.ts +0 -118
- package/src/omadaClient/index.ts +0 -310
- package/src/omadaClient/network.ts +0 -284
- package/src/omadaClient/request.ts +0 -365
- package/src/omadaClient/security.ts +0 -51
- package/src/omadaClient/site.ts +0 -344
- package/src/ownerQuestions/service.ts +0 -536
- package/src/permissions/index.ts +0 -143
- package/src/resources/index.ts +0 -213
- package/src/server/auth.ts +0 -175
- package/src/server/chatFace.ts +0 -789
- package/src/server/chatSlice.ts +0 -57
- package/src/server/common.ts +0 -170
- package/src/server/eventSubscription.ts +0 -343
- package/src/server/http.ts +0 -626
- package/src/server/instructions.ts +0 -97
- package/src/server/rateLimiter.ts +0 -227
- package/src/server/stdio.ts +0 -41
- package/src/server/stream.ts +0 -205
- package/src/tools/ai/analyzeSensors.ts +0 -33
- package/src/tools/ai/index.ts +0 -20
- package/src/tools/common.ts +0 -442
- package/src/tools/homeassistant/askOwner.ts +0 -133
- package/src/tools/homeassistant/automationControls.ts +0 -66
- package/src/tools/homeassistant/calendar.ts +0 -97
- package/src/tools/homeassistant/callService.ts +0 -25
- package/src/tools/homeassistant/controlClimate.ts +0 -68
- package/src/tools/homeassistant/controlCover.ts +0 -48
- package/src/tools/homeassistant/controlFan.ts +0 -44
- package/src/tools/homeassistant/controlLight.ts +0 -38
- package/src/tools/homeassistant/controlMediaPlayer.ts +0 -50
- package/src/tools/homeassistant/createAutomation.ts +0 -35
- package/src/tools/homeassistant/deleteAutomation.ts +0 -24
- package/src/tools/homeassistant/entityAction.ts +0 -35
- package/src/tools/homeassistant/entityGuard.ts +0 -28
- package/src/tools/homeassistant/getAllSensors.ts +0 -46
- package/src/tools/homeassistant/getAutomationTrace.ts +0 -30
- package/src/tools/homeassistant/getDomainSummary.ts +0 -29
- package/src/tools/homeassistant/getEntitiesByDomain.ts +0 -56
- package/src/tools/homeassistant/getHistory.ts +0 -24
- package/src/tools/homeassistant/getState.ts +0 -24
- package/src/tools/homeassistant/getStates.ts +0 -46
- package/src/tools/homeassistant/getVersion.ts +0 -21
- package/src/tools/homeassistant/index.ts +0 -121
- package/src/tools/homeassistant/listAutomations.ts +0 -35
- package/src/tools/homeassistant/listEntities.ts +0 -44
- package/src/tools/homeassistant/listPersons.ts +0 -46
- package/src/tools/homeassistant/sceneAndScript.ts +0 -53
- package/src/tools/homeassistant/searchEntities.ts +0 -58
- package/src/tools/homeassistant/sendNotification.ts +0 -179
- package/src/tools/homeassistant/systemTools.ts +0 -52
- package/src/tools/homeassistant/triggerAutomation.ts +0 -24
- package/src/tools/index.ts +0 -78
- package/src/tools/infra/actions.ts +0 -95
- package/src/tools/infra/index.ts +0 -138
- package/src/tools/omada/blockClient.ts +0 -28
- package/src/tools/omada/cyclePoePort.ts +0 -32
- package/src/tools/omada/deleteClient.ts +0 -27
- package/src/tools/omada/disableClientRateLimit.ts +0 -23
- package/src/tools/omada/getClient.ts +0 -14
- package/src/tools/omada/getDevice.ts +0 -14
- package/src/tools/omada/getFirewallSetting.ts +0 -14
- package/src/tools/omada/getInternetInfo.ts +0 -14
- package/src/tools/omada/getLanNetworkList.ts +0 -14
- package/src/tools/omada/getLanProfileList.ts +0 -14
- package/src/tools/omada/getPortForwardingStatus.ts +0 -26
- package/src/tools/omada/getRateLimitProfiles.ts +0 -14
- package/src/tools/omada/getSiteNtpStatus.ts +0 -14
- package/src/tools/omada/getSsidDetail.ts +0 -22
- package/src/tools/omada/getSsidList.ts +0 -21
- package/src/tools/omada/getSwitchStackDetail.ts +0 -14
- package/src/tools/omada/getThreatList.ts +0 -42
- package/src/tools/omada/getWlanGroupList.ts +0 -14
- package/src/tools/omada/graph.ts +0 -187
- package/src/tools/omada/index.ts +0 -153
- package/src/tools/omada/listBlockedClients.ts +0 -17
- package/src/tools/omada/listClients.ts +0 -14
- package/src/tools/omada/listClientsActivity.ts +0 -25
- package/src/tools/omada/listClientsPastConnections.ts +0 -44
- package/src/tools/omada/listDevices.ts +0 -14
- package/src/tools/omada/listDevicesStats.ts +0 -41
- package/src/tools/omada/listMostActiveClients.ts +0 -14
- package/src/tools/omada/listSites.ts +0 -18
- package/src/tools/omada/namespace.ts +0 -1164
- package/src/tools/omada/searchDevices.ts +0 -20
- package/src/tools/omada/setClientRateLimit.ts +0 -26
- package/src/tools/omada/setClientRateLimitProfile.ts +0 -25
- package/src/tools/omada/setSiteNtpServers.ts +0 -36
- package/src/tools/omada/setSsidEnabled.ts +0 -38
- package/src/tools/omada/unblockClient.ts +0 -26
- package/src/tools/types.ts +0 -11
- package/src/tools/utils/pagination-schema.ts +0 -27
- package/src/types/ai/index.ts +0 -11
- package/src/types/ai/types.ts +0 -67
- package/src/types/homeassistant/automations.ts +0 -43
- package/src/types/homeassistant/calendars.ts +0 -26
- package/src/types/homeassistant/config.ts +0 -14
- package/src/types/homeassistant/entities.ts +0 -14
- package/src/types/homeassistant/errors.ts +0 -33
- package/src/types/homeassistant/haConfig.ts +0 -7
- package/src/types/homeassistant/history.ts +0 -13
- package/src/types/homeassistant/index.ts +0 -46
- package/src/types/homeassistant/request.ts +0 -6
- package/src/types/homeassistant/serviceCall.ts +0 -18
- package/src/types/homeassistant/states.ts +0 -58
- package/src/types/homeassistant/updates.ts +0 -28
- package/src/types/index.ts +0 -12
- package/src/types/omada/activeClientInfo.ts +0 -17
- package/src/types/omada/clientActivity.ts +0 -32
- package/src/types/omada/clientBlockStatus.ts +0 -11
- package/src/types/omada/clientPastConnection.ts +0 -82
- package/src/types/omada/index.ts +0 -14
- package/src/types/omada/omadaApiResponse.ts +0 -5
- package/src/types/omada/omadaClientInfo.ts +0 -11
- package/src/types/omada/omadaDeviceInfo.ts +0 -11
- package/src/types/omada/omadaDeviceStats.ts +0 -31
- package/src/types/omada/omadaSiteSummary.ts +0 -5
- package/src/types/omada/oswStackDetail.ts +0 -11
- package/src/types/omada/paginatedResult.ts +0 -6
- package/src/types/omada/rateLimitProfile.ts +0 -58
- package/src/types/omada/threatInfo.ts +0 -38
- package/src/types/omada/tokenResult.ts +0 -6
- package/src/types.ts +0 -2
- package/src/utils/config-validations.ts +0 -68
- package/src/utils/jwt.ts +0 -112
- package/src/utils/logger.ts +0 -121
- package/src/utils/plainText.ts +0 -48
- package/src/utils/redact.ts +0 -59
- package/src/utils/sanitizeError.ts +0 -18
- package/src/utils/tlsDispatcher.ts +0 -69
- package/src/version.ts +0 -5
- package/tests/bdd/automation-creation.spec.ts +0 -214
- package/tests/bdd/automation-management.spec.ts +0 -267
- package/tests/bdd/calendar.spec.ts +0 -190
- package/tests/bdd/configuration.spec.ts +0 -283
- package/tests/bdd/device-control.spec.ts +0 -344
- package/tests/bdd/entity-listing.spec.ts +0 -208
- package/tests/bdd/entity-search.spec.ts +0 -183
- package/tests/bdd/history-retrieval.spec.ts +0 -195
- package/tests/bdd/notifications.spec.ts +0 -238
- package/tests/bdd/service-calls.spec.ts +0 -144
- package/tests/bdd/state-retrieval.spec.ts +0 -204
- package/tests/bench/hotpaths.bench.ts +0 -68
- package/tests/config.test.ts +0 -355
- package/tests/features/automation-creation.feature +0 -36
- package/tests/features/automation-management.feature +0 -54
- package/tests/features/calendar.feature +0 -37
- package/tests/features/configuration.feature +0 -70
- package/tests/features/device-control.feature +0 -100
- package/tests/features/entity-listing.feature +0 -51
- package/tests/features/entity-search.feature +0 -44
- package/tests/features/history-retrieval.feature +0 -38
- package/tests/features/notifications.feature +0 -37
- package/tests/features/service-calls.feature +0 -35
- package/tests/features/state-retrieval.feature +0 -37
- package/tests/fixtures/openapi-default-slice.json +0 -290
- package/tests/haClient/automationTrace.test.ts +0 -59
- package/tests/haClient/automations.test.ts +0 -149
- package/tests/haClient/config.test.ts +0 -117
- package/tests/haClient/entities.test.ts +0 -188
- package/tests/haClient/events.test.ts +0 -455
- package/tests/haClient/eventsTls.test.ts +0 -52
- package/tests/haClient/history.test.ts +0 -228
- package/tests/haClient/index.test.ts +0 -243
- package/tests/haClient/request.test.ts +0 -217
- package/tests/haClient/services.test.ts +0 -120
- package/tests/haClient/states.test.ts +0 -119
- package/tests/haClient/updates.test.ts +0 -193
- package/tests/index.test.ts +0 -160
- package/tests/integration/homeassistant.test.ts +0 -144
- package/tests/integration/omada.test.ts +0 -167
- package/tests/integration/server.test.ts +0 -151
- package/tests/localAI/index.test.ts +0 -177
- package/tests/localAI/ollama.test.ts +0 -187
- package/tests/localAI/openai.test.ts +0 -257
- package/tests/omadaClient/actions.test.ts +0 -161
- package/tests/omadaClient/clientBlocks.test.ts +0 -170
- package/tests/omadaClient/ntp.test.ts +0 -381
- package/tests/omadaClient/site.test.ts +0 -37
- package/tests/ownerQuestions/service.test.ts +0 -499
- package/tests/ownerQuestions/tools.test.ts +0 -186
- package/tests/permissions/index.test.ts +0 -217
- package/tests/resources/index.test.ts +0 -288
- package/tests/scripts/generateJwt.test.ts +0 -44
- package/tests/security/accessControl.test.ts +0 -223
- package/tests/security/authentication.test.ts +0 -117
- package/tests/security/configuration.test.ts +0 -170
- package/tests/security/cryptoTransport.test.ts +0 -172
- package/tests/security/dependencies.test.ts +0 -152
- package/tests/security/injection.test.ts +0 -117
- package/tests/security/insecureDesign.test.ts +0 -121
- package/tests/security/logging.test.ts +0 -131
- package/tests/security/resourceConsumption.test.ts +0 -142
- package/tests/server/auth.test.ts +0 -249
- package/tests/server/chatFace.test.ts +0 -320
- package/tests/server/common.test.ts +0 -84
- package/tests/server/http.test.ts +0 -561
- package/tests/server/instructions.test.ts +0 -152
- package/tests/server/memoizeJsonSchema.test.ts +0 -71
- package/tests/server/stdio.test.ts +0 -99
- package/tests/server/stream.test.ts +0 -285
- package/tests/server/toolVisibility.http.test.ts +0 -111
- package/tests/server/toolVisibility.test.ts +0 -129
- package/tests/tools/ai/analyzeSensors.test.ts +0 -227
- package/tests/tools/common.test.ts +0 -123
- package/tests/tools/homeassistant/handlers.test.ts +0 -872
- package/tests/tools/infra/invokeAction.test.ts +0 -280
- package/tests/tools/omada/graph.test.ts +0 -385
- package/tests/tools/omada/handlers.test.ts +0 -897
- package/tests/tools/omada/namespace.test.ts +0 -483
- package/tests/tools/registry.test.ts +0 -168
- package/tests/utils/config-validations.test.ts +0 -352
- package/tests/utils/jwt.test.ts +0 -115
- package/tests/utils/logger.test.ts +0 -327
- package/tests/utils/redact.test.ts +0 -105
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -38
package/src/omadaClient/site.ts
DELETED
|
@@ -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
|
-
}
|