Refactor internal APIs
This commit is contained in:
parent
4fad222b82
commit
479fbe5727
11 changed files with 51 additions and 75 deletions
|
@ -1,14 +1,9 @@
|
||||||
import type { Device } from '../../boundaries/hap'
|
import type { Config, Device } from '../../boundaries'
|
||||||
import type { Logger } from 'homebridge'
|
import type { Logger } from 'homebridge'
|
||||||
|
|
||||||
type Pin = string
|
export interface HapDiscoveryConfig {
|
||||||
|
config: Pick<Config, 'debug' | 'pin' | 'refresh_interval' | 'discovery_timeout' | 'request_timeout'>
|
||||||
export interface HapConfig {
|
log: Logger
|
||||||
pin: Pin
|
|
||||||
refreshInterval: number
|
|
||||||
discoveryTimeout: number
|
|
||||||
requestTimeout: number
|
|
||||||
logger: Logger
|
|
||||||
debug: boolean
|
|
||||||
}
|
}
|
||||||
export type HapDiscover = (config: HapConfig) => Promise<Device[]>
|
|
||||||
|
export type HapDiscover = (config: HapDiscoveryConfig) => Promise<Device[]>
|
||||||
|
|
|
@ -1,25 +1,18 @@
|
||||||
import type { HapDiscover } from './api'
|
import type { HapDiscover } from './api'
|
||||||
import { HAPNodeJSClient } from 'hap-node-client'
|
import { HAPNodeJSClient, type HAPNodeJSClientConfig } from 'hap-node-client'
|
||||||
import { type Device, DeviceBoundary, checkBoundary } from '../../boundaries'
|
import { type Device, DeviceBoundary, checkBoundary } from '../../boundaries'
|
||||||
import type { Logger } from 'homebridge'
|
import type { Logger } from 'homebridge'
|
||||||
import z from 'zod'
|
import z from 'zod'
|
||||||
|
|
||||||
const MaybeDevices = z.array(z.unknown())
|
const MaybeDevices = z.array(z.unknown())
|
||||||
|
|
||||||
interface HapConfig {
|
|
||||||
debug: boolean
|
|
||||||
refresh: number
|
|
||||||
timeout: number
|
|
||||||
reqTimeout: number
|
|
||||||
pin: string
|
|
||||||
}
|
|
||||||
type ResolveFunc = (devices: Device[]) => void
|
type ResolveFunc = (devices: Device[]) => void
|
||||||
type RejectFunc = (error: unknown) => void
|
type RejectFunc = (error: unknown) => void
|
||||||
|
|
||||||
const clientMap: Record<string, HAPNodeJSClient> = {}
|
const clientMap: Record<string, HAPNodeJSClient> = {}
|
||||||
const promiseMap: Record<string, [ResolveFunc, RejectFunc]> = {}
|
const promiseMap: Record<string, [ResolveFunc, RejectFunc]> = {}
|
||||||
|
|
||||||
function startDiscovery(logger: Logger, config: HapConfig, resolve: ResolveFunc, reject: RejectFunc) {
|
function startDiscovery(logger: Logger, config: HAPNodeJSClientConfig, resolve: ResolveFunc, reject: RejectFunc) {
|
||||||
const key = JSON.stringify(config)
|
const key = JSON.stringify(config)
|
||||||
|
|
||||||
if (!clientMap[key]) {
|
if (!clientMap[key]) {
|
||||||
|
@ -49,16 +42,16 @@ function startDiscovery(logger: Logger, config: HapConfig, resolve: ResolveFunc,
|
||||||
promiseMap[key] = [resolve, reject]
|
promiseMap[key] = [resolve, reject]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const discover: HapDiscover = ({ pin, refreshInterval, discoveryTimeout, requestTimeout, logger, debug }) => {
|
export const hapNodeJsClientDiscover: HapDiscover = ({ config, log }) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
startDiscovery(
|
startDiscovery(
|
||||||
logger,
|
log,
|
||||||
{
|
{
|
||||||
debug: debug,
|
debug: config.debug,
|
||||||
refresh: refreshInterval,
|
refresh: config.refresh_interval,
|
||||||
timeout: discoveryTimeout,
|
timeout: config.discovery_timeout,
|
||||||
reqTimeout: requestTimeout,
|
reqTimeout: config.request_timeout,
|
||||||
pin,
|
pin: config.pin,
|
||||||
},
|
},
|
||||||
resolve,
|
resolve,
|
||||||
reject,
|
reject,
|
||||||
|
|
2
src/adapters/discovery/index.ts
Normal file
2
src/adapters/discovery/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './api'
|
||||||
|
export * from './hap_node_js_client'
|
|
@ -1,4 +1,7 @@
|
||||||
import type { HttpServer } from '../../http'
|
import type { Logger } from 'homebridge'
|
||||||
|
import type { RequestListener, Server } from 'http'
|
||||||
|
import type { Config } from '../../boundaries'
|
||||||
|
import type { Metric } from '../../metrics'
|
||||||
|
|
||||||
export interface HttpResponse {
|
export interface HttpResponse {
|
||||||
statusCode?: number
|
statusCode?: number
|
||||||
|
@ -11,3 +14,16 @@ export interface HttpServerController {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HttpAdapter = (config: HttpServer) => Promise<HttpServerController>
|
export type HttpAdapter = (config: HttpServer) => Promise<HttpServerController>
|
||||||
|
|
||||||
|
export type HttpConfig = Pick<Config, 'debug' | 'port' | 'prefix'>
|
||||||
|
|
||||||
|
export interface HttpServer {
|
||||||
|
log?: Logger
|
||||||
|
config: HttpConfig
|
||||||
|
serverFactory?: (requestListener: RequestListener) => Server
|
||||||
|
onRequest(): HttpResponse | undefined
|
||||||
|
onMetrics(): HttpResponse
|
||||||
|
onNotFound(): HttpResponse
|
||||||
|
onError(error: unknown): HttpResponse
|
||||||
|
updateMetrics(metrics: Metric[]): void
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import Fastify, { type FastifyReply, type FastifyRequest, type HookHandlerDoneFunction } from 'fastify'
|
import Fastify, { type FastifyReply, type FastifyRequest, type HookHandlerDoneFunction } from 'fastify'
|
||||||
import type { HttpAdapter, HttpResponse } from './api'
|
import type { HttpAdapter, HttpResponse, HttpServer } from './api'
|
||||||
import type { HttpServer } from '../../http'
|
|
||||||
|
|
||||||
function adaptResponseToReply(response: HttpResponse, reply: FastifyReply): void {
|
function adaptResponseToReply(response: HttpResponse, reply: FastifyReply): void {
|
||||||
if (response.statusCode) {
|
if (response.statusCode) {
|
||||||
|
@ -23,7 +22,7 @@ function formatCombinedLog(request: FastifyRequest, reply: FastifyReply): string
|
||||||
return `${remoteAddress} - "${request.method} ${request.url} HTTP/${request.raw.httpVersion}" ${reply.statusCode} "${request.protocol}://${request.hostname}" "${userAgent}" "${contentType}"`
|
return `${remoteAddress} - "${request.method} ${request.url} HTTP/${request.raw.httpVersion}" ${reply.statusCode} "${request.protocol}://${request.hostname}" "${userAgent}" "${contentType}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const serve: HttpAdapter = async (server: HttpServer) => {
|
export const fastifyServe: HttpAdapter = async (server: HttpServer) => {
|
||||||
const fastify = Fastify({
|
const fastify = Fastify({
|
||||||
logger: false,
|
logger: false,
|
||||||
serverFactory: server.serverFactory,
|
serverFactory: server.serverFactory,
|
||||||
|
@ -32,7 +31,7 @@ export const serve: HttpAdapter = async (server: HttpServer) => {
|
||||||
fastify.addHook('onResponse', (request: FastifyRequest, reply: FastifyReply) => {
|
fastify.addHook('onResponse', (request: FastifyRequest, reply: FastifyReply) => {
|
||||||
if (reply.statusCode >= 400) {
|
if (reply.statusCode >= 400) {
|
||||||
server.log?.warn(formatCombinedLog(request, reply))
|
server.log?.warn(formatCombinedLog(request, reply))
|
||||||
} else if (server.debug) {
|
} else if (server.config.debug) {
|
||||||
server.log?.debug(formatCombinedLog(request, reply))
|
server.log?.debug(formatCombinedLog(request, reply))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -59,7 +58,7 @@ export const serve: HttpAdapter = async (server: HttpServer) => {
|
||||||
adaptResponseToReply(server.onMetrics(), reply)
|
adaptResponseToReply(server.onMetrics(), reply)
|
||||||
})
|
})
|
||||||
|
|
||||||
await fastify.listen({ port: server.port, host: '::' })
|
await fastify.listen({ port: server.config.port, host: '::' })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shutdown() {
|
shutdown() {
|
||||||
|
|
2
src/adapters/http/index.ts
Normal file
2
src/adapters/http/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './api'
|
||||||
|
export * from './fastify'
|
16
src/http.ts
16
src/http.ts
|
@ -1,16 +0,0 @@
|
||||||
import type { HttpResponse } from './adapters/http/api'
|
|
||||||
import type { Metric } from './metrics'
|
|
||||||
import type { Logger } from 'homebridge'
|
|
||||||
import type { RequestListener, Server } from 'http'
|
|
||||||
|
|
||||||
export interface HttpServer {
|
|
||||||
port: number
|
|
||||||
debug: boolean
|
|
||||||
log?: Logger
|
|
||||||
serverFactory?: (requestListener: RequestListener) => Server
|
|
||||||
onRequest(): HttpResponse | undefined
|
|
||||||
onMetrics(): HttpResponse
|
|
||||||
onNotFound(): HttpResponse
|
|
||||||
onError(error: unknown): HttpResponse
|
|
||||||
updateMetrics(metrics: Metric[]): void
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Accessory, Device, Service } from './boundaries/hap'
|
import type { Accessory, Device, Service } from './boundaries'
|
||||||
import { Uuids } from './generated/services'
|
import { Uuids } from './generated/services'
|
||||||
import { assertTypeExhausted, isType } from './std'
|
import { assertTypeExhausted, isType } from './std'
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import type { API, IndependentPlatformPlugin, Logger, PlatformConfig } from 'homebridge'
|
import type { API, IndependentPlatformPlugin, Logger, PlatformConfig } from 'homebridge'
|
||||||
|
|
||||||
import { aggregate } from './metrics'
|
import { aggregate } from './metrics'
|
||||||
import { discover } from './adapters/discovery/hap_node_js_client'
|
import { hapNodeJsClientDiscover as discover } from './adapters/discovery'
|
||||||
import { serve } from './adapters/http/fastify'
|
import { type HttpServerController, fastifyServe as serve } from './adapters/http'
|
||||||
import type { HttpServerController } from './adapters/http/api'
|
|
||||||
import { PrometheusServer } from './prometheus'
|
import { PrometheusServer } from './prometheus'
|
||||||
import { type Config, ConfigBoundary, checkBoundary } from './boundaries'
|
import { type Config, ConfigBoundary, checkBoundary } from './boundaries'
|
||||||
|
|
||||||
|
@ -28,7 +27,7 @@ export class PrometheusExporterPlatform implements IndependentPlatformPlugin {
|
||||||
|
|
||||||
this.log.debug('Starting Prometheus HTTP server on port %d', this.config.port)
|
this.log.debug('Starting Prometheus HTTP server on port %d', this.config.port)
|
||||||
|
|
||||||
this.httpServer = new PrometheusServer(this.config.port, this.log, this.config.debug, this.config.prefix)
|
this.httpServer = new PrometheusServer(this.config, this.log)
|
||||||
serve(this.httpServer)
|
serve(this.httpServer)
|
||||||
.then((httpServerController) => {
|
.then((httpServerController) => {
|
||||||
this.log.debug('HTTP server started on port %d', this.config.port)
|
this.log.debug('HTTP server started on port %d', this.config.port)
|
||||||
|
@ -46,14 +45,7 @@ export class PrometheusExporterPlatform implements IndependentPlatformPlugin {
|
||||||
|
|
||||||
private startHapDiscovery(): void {
|
private startHapDiscovery(): void {
|
||||||
this.log.debug('Starting HAP discovery')
|
this.log.debug('Starting HAP discovery')
|
||||||
discover({
|
discover({ log: this.log, config: this.config })
|
||||||
logger: this.log,
|
|
||||||
refreshInterval: this.config.refresh_interval,
|
|
||||||
discoveryTimeout: this.config.discovery_timeout,
|
|
||||||
requestTimeout: this.config.request_timeout,
|
|
||||||
pin: this.config.pin,
|
|
||||||
debug: this.config.debug,
|
|
||||||
})
|
|
||||||
.then((devices) => {
|
.then((devices) => {
|
||||||
const metrics = aggregate(devices, new Date())
|
const metrics = aggregate(devices, new Date())
|
||||||
this.httpServer.updateMetrics(metrics)
|
this.httpServer.updateMetrics(metrics)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { Metric } from './metrics'
|
import type { Metric } from './metrics'
|
||||||
import type { Logger } from 'homebridge'
|
import type { Logger } from 'homebridge'
|
||||||
import type { HttpResponse } from './adapters/http/api'
|
import type { HttpConfig, HttpResponse, HttpServer } from './adapters/http'
|
||||||
import type { HttpServer } from './http'
|
|
||||||
|
|
||||||
export class MetricsRenderer {
|
export class MetricsRenderer {
|
||||||
constructor(private readonly prefix: string) {}
|
constructor(private readonly prefix: string) {}
|
||||||
|
@ -44,12 +43,7 @@ export class PrometheusServer implements HttpServer {
|
||||||
private metricsInitialized = false
|
private metricsInitialized = false
|
||||||
private metrics: Metric[] = []
|
private metrics: Metric[] = []
|
||||||
|
|
||||||
constructor(
|
constructor(public readonly config: HttpConfig, public readonly log: Logger | undefined = undefined) {}
|
||||||
public readonly port: number,
|
|
||||||
public readonly log: Logger | undefined,
|
|
||||||
public readonly debug: boolean,
|
|
||||||
private readonly prefix: string,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
onRequest(): HttpResponse | undefined {
|
onRequest(): HttpResponse | undefined {
|
||||||
if (!this.metricsInitialized) {
|
if (!this.metricsInitialized) {
|
||||||
|
@ -62,7 +56,7 @@ export class PrometheusServer implements HttpServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMetrics(): HttpResponse {
|
onMetrics(): HttpResponse {
|
||||||
const renderer = new MetricsRenderer(this.prefix)
|
const renderer = new MetricsRenderer(this.config.prefix)
|
||||||
const metrics = this.metrics.map((metric) => renderer.render(metric)).join('\n')
|
const metrics = this.metrics.map((metric) => renderer.render(metric)).join('\n')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { describe, test } from '@jest/globals'
|
import { describe, test } from '@jest/globals'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import { PrometheusServer } from '../../../src/prometheus'
|
import { PrometheusServer } from '../../../src/prometheus'
|
||||||
import { serve } from '../../../src/adapters/http/fastify'
|
import { type HttpServer, fastifyServe } from '../../../src/adapters/http'
|
||||||
import { type Server, createServer } from 'http'
|
import { type Server, createServer } from 'http'
|
||||||
import type { HttpServer } from '../../../src/http'
|
|
||||||
import { Metric } from '../../../src/metrics'
|
import { Metric } from '../../../src/metrics'
|
||||||
|
|
||||||
class TestablePrometheusServer extends PrometheusServer {
|
class TestablePrometheusServer extends PrometheusServer {
|
||||||
|
@ -12,9 +11,9 @@ class TestablePrometheusServer extends PrometheusServer {
|
||||||
|
|
||||||
function createTestServer(): { http: Server; prometheus: HttpServer } {
|
function createTestServer(): { http: Server; prometheus: HttpServer } {
|
||||||
const http = createServer()
|
const http = createServer()
|
||||||
const prometheus = new TestablePrometheusServer(0, undefined, false, 'homebridge')
|
const prometheus = new TestablePrometheusServer({ port: 0, debug: false, prefix: 'homebridge' })
|
||||||
prometheus.serverFactory = (handler) => http.on('request', handler)
|
prometheus.serverFactory = (handler) => http.on('request', handler)
|
||||||
serve(prometheus).catch((err: Error) => {
|
fastifyServe(prometheus).catch((err: Error) => {
|
||||||
if (!('code' in err) || (err as unknown as { code: unknown }).code !== 'ERR_SERVER_ALREADY_LISTEN') {
|
if (!('code' in err) || (err as unknown as { code: unknown }).code !== 'ERR_SERVER_ALREADY_LISTEN') {
|
||||||
console.debug(err)
|
console.debug(err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue