Compare commits
4
Commits
dev
...
unifi-ssl-error
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c3b584bd2 | ||
|
|
e3cf680cbd | ||
|
|
24989ddd16 | ||
|
|
8cd9e8c826 |
No files matched your search
@@ -1,40 +1,48 @@
|
|||||||
---
|
---
|
||||||
title: 'Unifi Controller'
|
title: "Unifi Controller"
|
||||||
description: "Unifi Controller is a network management platform for Unifi devices."
|
description: "Unifi Controller is a network management platform for Unifi devices."
|
||||||
hide_title: true
|
hide_title: true
|
||||||
---
|
---
|
||||||
|
|
||||||
import { IntegrationHeader } from '@site/src/components/integrations/header';
|
import { IntegrationHeader } from "@site/src/components/integrations/header";
|
||||||
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
|
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
|
||||||
import { AddingIntegration } from '@site/src/components/integrations/adding';
|
import { AddingIntegration } from "@site/src/components/integrations/adding";
|
||||||
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
|
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
|
||||||
import { IconTopologyFull } from "@tabler/icons-react";
|
import { IconTopologyFull } from "@tabler/icons-react";
|
||||||
import { unifiControllerIntegration } from '.';
|
import { unifiControllerIntegration } from ".";
|
||||||
import { networkControllerSummaryWidget } from '@site/docs/widgets/network-controller-summary';
|
import { networkControllerSummaryWidget } from "@site/docs/widgets/network-controller-summary";
|
||||||
import { networkControllerStatusWidget } from '@site/docs/widgets/network-controller-status';
|
import { networkControllerStatusWidget } from "@site/docs/widgets/network-controller-status";
|
||||||
|
|
||||||
|
<IntegrationHeader integration={unifiControllerIntegration} categories={["Network controller"]} />
|
||||||
<IntegrationHeader
|
|
||||||
integration={unifiControllerIntegration}
|
|
||||||
categories={['Network controller']}
|
|
||||||
/>
|
|
||||||
|
|
||||||
This integration is using the [node-unifi](https://www.npmjs.com/package/node-unifi) library under the hood to connect to the Unifi Controller.
|
This integration is using the [node-unifi](https://www.npmjs.com/package/node-unifi) library under the hood to connect to the Unifi Controller.
|
||||||
Therfore it should support CloudKey Gen1, CloudKey Gen2, UnifiOS-based UDM-Pro controllers as well as self-hosted UniFi controllers.
|
Therfore it should support CloudKey Gen1, CloudKey Gen2, UnifiOS-based UDM-Pro controllers as well as self-hosted UniFi controllers.
|
||||||
|
|
||||||
### Widgets & Capabilities
|
### Widgets & Capabilities
|
||||||
|
|
||||||
<IntegrationCapabilites
|
<IntegrationCapabilites
|
||||||
items={[
|
items={[{ widget: networkControllerSummaryWidget }, { widget: networkControllerStatusWidget }]}
|
||||||
{ widget: networkControllerSummaryWidget },
|
|
||||||
{ widget: networkControllerStatusWidget}
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
### Adding the integration
|
### Adding the integration
|
||||||
|
|
||||||
<AddingIntegration />
|
<AddingIntegration />
|
||||||
|
|
||||||
|
Use the local HTTPS address of your controller:
|
||||||
|
|
||||||
|
- UniFi OS consoles such as Dream Router, Dream Machine, Cloud Gateway, and CloudKey use `https://<host>` (port 443).
|
||||||
|
- Self-hosted UniFi Network Servers normally use `https://<host>:8443`.
|
||||||
|
- If you omit the port, Homarr tries port 443 first and then port 8443 when the first port cannot be reached.
|
||||||
|
|
||||||
|
Use a local administrator account. Cloud-only accounts and accounts with multi-factor authentication are not supported by the underlying client.
|
||||||
|
|
||||||
### Secrets
|
### Secrets
|
||||||
<IntegrationSecrets secrets={[{
|
|
||||||
credentials: ['username', 'password'],
|
<IntegrationSecrets
|
||||||
steps: []
|
secrets={[
|
||||||
}]} />
|
{
|
||||||
|
credentials: ["username", "password"],
|
||||||
|
steps: [],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
@@ -60,8 +60,8 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/tags/connections"
|
| "/docs/tags/connections"
|
||||||
| "/docs/tags/custom-widgets"
|
| "/docs/tags/custom-widgets"
|
||||||
| "/docs/tags/customization"
|
| "/docs/tags/customization"
|
||||||
| "/docs/tags/data-sources"
|
|
||||||
| "/docs/tags/data"
|
| "/docs/tags/data"
|
||||||
|
| "/docs/tags/data-sources"
|
||||||
| "/docs/tags/database"
|
| "/docs/tags/database"
|
||||||
| "/docs/tags/developer"
|
| "/docs/tags/developer"
|
||||||
| "/docs/tags/development"
|
| "/docs/tags/development"
|
||||||
@@ -99,8 +99,8 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/tags/redis"
|
| "/docs/tags/redis"
|
||||||
| "/docs/tags/responsive"
|
| "/docs/tags/responsive"
|
||||||
| "/docs/tags/roles"
|
| "/docs/tags/roles"
|
||||||
| "/docs/tags/search-engines"
|
|
||||||
| "/docs/tags/search"
|
| "/docs/tags/search"
|
||||||
|
| "/docs/tags/search-engines"
|
||||||
| "/docs/tags/secrets"
|
| "/docs/tags/secrets"
|
||||||
| "/docs/tags/security"
|
| "/docs/tags/security"
|
||||||
| "/docs/tags/self-signed"
|
| "/docs/tags/self-signed"
|
||||||
@@ -132,15 +132,6 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/advanced/running-as-different-user"
|
| "/docs/advanced/running-as-different-user"
|
||||||
| "/docs/advanced/single-sign-on"
|
| "/docs/advanced/single-sign-on"
|
||||||
| "/docs/advanced/styling"
|
| "/docs/advanced/styling"
|
||||||
| "/docs/category/advanced"
|
|
||||||
| "/docs/category/community"
|
|
||||||
| "/docs/category/developer-guides"
|
|
||||||
| "/docs/category/getting-started"
|
|
||||||
| "/docs/category/installation-1"
|
|
||||||
| "/docs/category/installation"
|
|
||||||
| "/docs/category/integrations"
|
|
||||||
| "/docs/category/management"
|
|
||||||
| "/docs/category/widgets"
|
|
||||||
| "/docs/community/donate"
|
| "/docs/community/donate"
|
||||||
| "/docs/community/faq"
|
| "/docs/community/faq"
|
||||||
| "/docs/community/get-in-touch"
|
| "/docs/community/get-in-touch"
|
||||||
@@ -168,6 +159,7 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/integrations/archiveteam-warrior"
|
| "/docs/integrations/archiveteam-warrior"
|
||||||
| "/docs/integrations/aria2"
|
| "/docs/integrations/aria2"
|
||||||
| "/docs/integrations/audiobookshelf"
|
| "/docs/integrations/audiobookshelf"
|
||||||
|
| "/docs/integrations/bazarr"
|
||||||
| "/docs/integrations/beszel"
|
| "/docs/integrations/beszel"
|
||||||
| "/docs/integrations/coolify"
|
| "/docs/integrations/coolify"
|
||||||
| "/docs/integrations/dash-dot"
|
| "/docs/integrations/dash-dot"
|
||||||
@@ -192,6 +184,7 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/integrations/opnsense"
|
| "/docs/integrations/opnsense"
|
||||||
| "/docs/integrations/overseerr"
|
| "/docs/integrations/overseerr"
|
||||||
| "/docs/integrations/paperless-ngx"
|
| "/docs/integrations/paperless-ngx"
|
||||||
|
| "/docs/integrations/patchmon"
|
||||||
| "/docs/integrations/peanut"
|
| "/docs/integrations/peanut"
|
||||||
| "/docs/integrations/pi-hole"
|
| "/docs/integrations/pi-hole"
|
||||||
| "/docs/integrations/plex"
|
| "/docs/integrations/plex"
|
||||||
@@ -205,9 +198,11 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/integrations/slskd"
|
| "/docs/integrations/slskd"
|
||||||
| "/docs/integrations/sonarr"
|
| "/docs/integrations/sonarr"
|
||||||
| "/docs/integrations/speedtest-tracker"
|
| "/docs/integrations/speedtest-tracker"
|
||||||
|
| "/docs/integrations/synology"
|
||||||
| "/docs/integrations/tdarr"
|
| "/docs/integrations/tdarr"
|
||||||
| "/docs/integrations/technitium-dns"
|
| "/docs/integrations/technitium-dns"
|
||||||
| "/docs/integrations/tracearr"
|
| "/docs/integrations/tracearr"
|
||||||
|
| "/docs/integrations/traefik"
|
||||||
| "/docs/integrations/transmission"
|
| "/docs/integrations/transmission"
|
||||||
| "/docs/integrations/truenas"
|
| "/docs/integrations/truenas"
|
||||||
| "/docs/integrations/umami"
|
| "/docs/integrations/umami"
|
||||||
@@ -230,6 +225,7 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/widgets/anchor-note"
|
| "/docs/widgets/anchor-note"
|
||||||
| "/docs/widgets/app"
|
| "/docs/widgets/app"
|
||||||
| "/docs/widgets/audio-stats"
|
| "/docs/widgets/audio-stats"
|
||||||
|
| "/docs/widgets/bazarr"
|
||||||
| "/docs/widgets/beszel-alerts"
|
| "/docs/widgets/beszel-alerts"
|
||||||
| "/docs/widgets/beszel-system-grid"
|
| "/docs/widgets/beszel-system-grid"
|
||||||
| "/docs/widgets/beszel-system-stats"
|
| "/docs/widgets/beszel-system-stats"
|
||||||
@@ -249,6 +245,7 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/widgets/immich-album-carousel"
|
| "/docs/widgets/immich-album-carousel"
|
||||||
| "/docs/widgets/immich-server-stats"
|
| "/docs/widgets/immich-server-stats"
|
||||||
| "/docs/widgets/indexer-manager"
|
| "/docs/widgets/indexer-manager"
|
||||||
|
| "/docs/widgets/media-missing"
|
||||||
| "/docs/widgets/media-releases"
|
| "/docs/widgets/media-releases"
|
||||||
| "/docs/widgets/media-request-list"
|
| "/docs/widgets/media-request-list"
|
||||||
| "/docs/widgets/media-request-stats"
|
| "/docs/widgets/media-request-stats"
|
||||||
@@ -260,15 +257,18 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/widgets/notebook"
|
| "/docs/widgets/notebook"
|
||||||
| "/docs/widgets/notifications"
|
| "/docs/widgets/notifications"
|
||||||
| "/docs/widgets/paperless-ngx"
|
| "/docs/widgets/paperless-ngx"
|
||||||
|
| "/docs/widgets/patchmon"
|
||||||
| "/docs/widgets/releases"
|
| "/docs/widgets/releases"
|
||||||
| "/docs/widgets/rss-feed"
|
| "/docs/widgets/rss-feed"
|
||||||
| "/docs/widgets/smart-home-entity-state"
|
| "/docs/widgets/smart-home-entity-state"
|
||||||
| "/docs/widgets/smart-home-execute-automation"
|
| "/docs/widgets/smart-home-execute-automation"
|
||||||
| "/docs/widgets/speedtest-tracker"
|
| "/docs/widgets/speedtest-tracker"
|
||||||
| "/docs/widgets/stock-price"
|
| "/docs/widgets/stock-price"
|
||||||
|
| "/docs/widgets/system-disks"
|
||||||
| "/docs/widgets/system-resources"
|
| "/docs/widgets/system-resources"
|
||||||
| "/docs/widgets/timetable"
|
| "/docs/widgets/timetable"
|
||||||
| "/docs/widgets/tracearr"
|
| "/docs/widgets/tracearr"
|
||||||
|
| "/docs/widgets/traefik"
|
||||||
| "/docs/widgets/umami"
|
| "/docs/widgets/umami"
|
||||||
| "/docs/widgets/ups"
|
| "/docs/widgets/ups"
|
||||||
| "/docs/widgets/uptime-kuma"
|
| "/docs/widgets/uptime-kuma"
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.hoisted(() => {
|
||||||
|
process.env.SKIP_ENV_VALIDATION = "true";
|
||||||
|
process.env.SECRET_ENCRYPTION_KEY = "ff3f4f7ce30e870c9630de9e5d244ffa81101a24ed0dfe5f064beb53a7e684f1";
|
||||||
|
process.env.ENABLE_DNS_CACHING = "false";
|
||||||
|
});
|
||||||
|
|
||||||
|
const controllerCtor = vi.fn();
|
||||||
|
const controllerLogin = vi.fn().mockResolvedValue(true);
|
||||||
|
|
||||||
|
vi.mock("@homarr/redis", () => ({
|
||||||
|
createGetSetChannel: () => ({
|
||||||
|
getAsync: vi.fn().mockResolvedValue(null),
|
||||||
|
setAsync: vi.fn().mockResolvedValue(undefined),
|
||||||
|
removeAsync: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@homarr/core/infrastructure/logs", () => ({
|
||||||
|
createLogger: () => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }),
|
||||||
|
ErrorWithMetadata: class extends Error {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@homarr/core/infrastructure/http", () => ({
|
||||||
|
createCustomCheckServerIdentity: () => (() => undefined) as never,
|
||||||
|
fetchWithTrustedCertificatesAsync: vi.fn(),
|
||||||
|
createAxiosCertificateInstanceAsync: vi.fn().mockResolvedValue({}),
|
||||||
|
createCertificateAgentAsync: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@homarr/core/infrastructure/certificates", () => ({
|
||||||
|
getTrustedCertificateHostnamesAsync: vi.fn().mockResolvedValue([]),
|
||||||
|
getAllTrustedCertificatesAsync: vi.fn().mockResolvedValue([]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("axios", () => ({
|
||||||
|
AxiosError: Error,
|
||||||
|
default: {
|
||||||
|
create: vi.fn().mockReturnValue({}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("http-cookie-agent/http", () => ({
|
||||||
|
HttpCookieAgent: class {},
|
||||||
|
HttpsCookieAgent: class {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@homarr/node-unifi", () => {
|
||||||
|
class FakeController {
|
||||||
|
public options: Record<string, unknown>;
|
||||||
|
constructor(options: Record<string, unknown>) {
|
||||||
|
this.options = options;
|
||||||
|
controllerCtor(options);
|
||||||
|
}
|
||||||
|
public async login(): Promise<true> {
|
||||||
|
return await controllerLogin(this.options.port);
|
||||||
|
}
|
||||||
|
public async getSitesStats(): Promise<unknown[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { default: { Controller: FakeController } };
|
||||||
|
});
|
||||||
|
|
||||||
|
import { UnifiControllerIntegration } from "../unifi-controller-integration";
|
||||||
|
|
||||||
|
const createIntegration = (url: string) =>
|
||||||
|
new UnifiControllerIntegration({
|
||||||
|
id: "test-unifi",
|
||||||
|
name: "Test Unifi",
|
||||||
|
url,
|
||||||
|
externalUrl: null,
|
||||||
|
decryptedSecrets: [
|
||||||
|
{ kind: "username", value: "admin" },
|
||||||
|
{ kind: "password", value: "secret" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("UnifiControllerIntegration port resolution", () => {
|
||||||
|
test("a URL without a port uses the UniFi OS port 443", async () => {
|
||||||
|
controllerCtor.mockClear();
|
||||||
|
controllerLogin.mockClear().mockResolvedValue(true);
|
||||||
|
await createIntegration("http://192.168.1.1").getNetworkSummaryAsync();
|
||||||
|
|
||||||
|
expect(controllerCtor).toHaveBeenCalledTimes(1);
|
||||||
|
expect(controllerCtor).toHaveBeenCalledWith(expect.objectContaining({ host: "192.168.1.1", port: 443 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a connection failure on port 443 falls back to the self-hosted controller port 8443", async () => {
|
||||||
|
controllerCtor.mockClear();
|
||||||
|
controllerLogin
|
||||||
|
.mockClear()
|
||||||
|
.mockRejectedValueOnce(Object.assign(new Error("connect ECONNREFUSED"), { isAxiosError: true }))
|
||||||
|
.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
|
await createIntegration("http://controller.lan").getNetworkSummaryAsync();
|
||||||
|
|
||||||
|
expect(controllerCtor).toHaveBeenNthCalledWith(1, expect.objectContaining({ port: 443 }));
|
||||||
|
expect(controllerCtor).toHaveBeenNthCalledWith(2, expect.objectContaining({ port: 8443 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("https://controller.lan:8443 keeps the user-specified port", async () => {
|
||||||
|
controllerCtor.mockClear();
|
||||||
|
controllerLogin.mockClear().mockResolvedValue(true);
|
||||||
|
await createIntegration("https://controller.lan:8443").getNetworkSummaryAsync();
|
||||||
|
|
||||||
|
expect(controllerCtor).toHaveBeenCalledWith(expect.objectContaining({ host: "controller.lan", port: 8443 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("https://192.168.1.1:8443 keeps the user-specified port", async () => {
|
||||||
|
controllerCtor.mockClear();
|
||||||
|
controllerLogin.mockClear().mockResolvedValue(true);
|
||||||
|
await createIntegration("https://192.168.1.1:8443").getNetworkSummaryAsync();
|
||||||
|
|
||||||
|
expect(controllerCtor).toHaveBeenCalledWith(expect.objectContaining({ host: "192.168.1.1", port: 8443 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an unusual port like 10443 is honored verbatim", async () => {
|
||||||
|
controllerCtor.mockClear();
|
||||||
|
controllerLogin.mockClear().mockResolvedValue(true);
|
||||||
|
await createIntegration("https://192.168.1.1:10443").getNetworkSummaryAsync();
|
||||||
|
|
||||||
|
expect(controllerCtor).toHaveBeenCalledWith(expect.objectContaining({ host: "192.168.1.1", port: 10443 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an authentication failure on port 443 does not try another port", async () => {
|
||||||
|
controllerCtor.mockClear();
|
||||||
|
controllerLogin.mockClear().mockRejectedValueOnce(
|
||||||
|
Object.assign(new Error("Request failed with status code 401"), {
|
||||||
|
isAxiosError: true,
|
||||||
|
response: { status: 401 },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(createIntegration("https://controller.lan").getNetworkSummaryAsync()).rejects.toThrow();
|
||||||
|
|
||||||
|
expect(controllerCtor).toHaveBeenCalledTimes(1);
|
||||||
|
expect(controllerCtor).toHaveBeenCalledWith(expect.objectContaining({ port: 443 }));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,6 @@ import type tls from "node:tls";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { HttpCookieAgent, HttpsCookieAgent } from "http-cookie-agent/http";
|
import { HttpCookieAgent, HttpsCookieAgent } from "http-cookie-agent/http";
|
||||||
|
|
||||||
import { getPortFromUrl } from "@homarr/common";
|
|
||||||
import {
|
import {
|
||||||
getAllTrustedCertificatesAsync,
|
getAllTrustedCertificatesAsync,
|
||||||
getTrustedCertificateHostnamesAsync,
|
getTrustedCertificateHostnamesAsync,
|
||||||
@@ -67,26 +66,53 @@ export class UnifiControllerIntegration extends Integration implements NetworkCo
|
|||||||
checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()),
|
checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()),
|
||||||
};
|
};
|
||||||
|
|
||||||
const client = new Unifi.Controller({
|
// ponytail: node-unifi always connects over HTTPS, regardless of the protocol in the
|
||||||
host: url.hostname,
|
// integration URL. UniFi OS consoles use port 443 while self-hosted controllers normally
|
||||||
port: getPortFromUrl(url),
|
// use 8443. If no port was provided, try both without hiding authentication errors.
|
||||||
username: this.getSecretValue("username"),
|
const ports = url.port ? [Number(url.port)] : [443, 8443];
|
||||||
password: this.getSecretValue("password"),
|
const createClientForPortAsync = async (port: number) => {
|
||||||
createAxiosInstance({ cookies }) {
|
const client = new Unifi.Controller({
|
||||||
return axios.create({
|
host: url.hostname,
|
||||||
adapter: "http",
|
port,
|
||||||
httpAgent: new HttpCookieAgent({ cookies }),
|
username: this.getSecretValue("username"),
|
||||||
httpsAgent: new HttpsCookieAgent({
|
password: this.getSecretValue("password"),
|
||||||
cookies,
|
createAxiosInstance({ cookies }) {
|
||||||
requestCert: true,
|
return axios.create({
|
||||||
...certificateOptions,
|
adapter: "http",
|
||||||
}),
|
httpAgent: new HttpCookieAgent({ cookies }),
|
||||||
});
|
httpsAgent: new HttpsCookieAgent({
|
||||||
},
|
cookies,
|
||||||
});
|
requestCert: true,
|
||||||
|
...certificateOptions,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await client.login(this.getSecretValue("username"), this.getSecretValue("password"), null);
|
await client.login(this.getSecretValue("username"), this.getSecretValue("password"), null);
|
||||||
return client;
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [index, port] of ports.entries()) {
|
||||||
|
try {
|
||||||
|
return await createClientForPortAsync(port);
|
||||||
|
} catch (error) {
|
||||||
|
const isLastPort = index === ports.length - 1;
|
||||||
|
if (isLastPort || !this.isPortFallbackError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unable to connect to the UniFi controller");
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPortFallbackError(error: unknown) {
|
||||||
|
if (typeof error === "object" && error !== null && "isAxiosError" in error && error.isAxiosError === true) {
|
||||||
|
return !("response" in error) || error.response === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error instanceof Error && error.message === "failed to detect UniFiOS status";
|
||||||
}
|
}
|
||||||
|
|
||||||
private getStatusValueOverAllSites<S extends HealthSubsystem>(
|
private getStatusValueOverAllSites<S extends HealthSubsystem>(
|
||||||
|
|||||||
Reference in new issue
Block a user