├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── stale.yml └── workflows │ ├── main.yml │ └── publish.yml ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── config.schema.json ├── package-lock.json ├── package.json ├── sample-config.json ├── src ├── index.ts ├── piholeClient.ts └── types.ts ├── test-configuration ├── .gitignore ├── config.json └── test.sh └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | es6: true, 5 | }, 6 | globals: { 7 | Atomics: "readonly", 8 | SharedArrayBuffer: "readonly", 9 | }, 10 | plugins: ["@typescript-eslint"], 11 | extends: ["eslint:recommended"], 12 | parserOptions: { 13 | sourceType: "module", 14 | ecmaVersion: 2018, 15 | }, 16 | overrides: [ 17 | { 18 | files: ["**/*.ts", "**/*.tsx"], 19 | parser: "@typescript-eslint/parser", 20 | extends: [ 21 | "eslint:recommended", 22 | "plugin:@typescript-eslint/eslint-recommended", 23 | "plugin:@typescript-eslint/recommended", 24 | ], 25 | }, 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: arendruni 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | daysUntilStale: 20 2 | daysUntilClose: 7 3 | exemptLabels: 4 | - pinned 5 | - security 6 | - enhancement 7 | staleLabel: wontfix 8 | markComment: > 9 | This issue has been automatically marked as stale because it has not had 10 | recent activity. It will be closed if no further activity occurs. Thank you 11 | for your contributions. 12 | closeComment: false 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | matrix: 7 | node-version: [18.x, 20.x, 22.x] 8 | homebridge-version: [latest, ^2.0.0-beta] 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: ${{ matrix.node-version }} 15 | - run: npm install 16 | - run: npm run build 17 | - run: npm test 18 | - name: test script homebridge 19 | run: ./test-configuration/test.sh 20 | shell: bash 21 | env: 22 | HOMEBRIDGE_VERSION: ${{ matrix.homebridge-version }} 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version: "20.x" 13 | registry-url: "https://registry.npmjs.org" 14 | - run: npm install 15 | - run: npm run build 16 | - run: npm publish 17 | env: 18 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | dist 64 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | npm run husky:precommit 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "useTabs": true, 4 | "semi": true, 5 | "quoteProps": "consistent", 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Homebridge Pi-hole [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) 2 | 3 | [![npm](https://img.shields.io/npm/v/homebridge-pihole.svg)](https://www.npmjs.com/package/homebridge-pihole) 4 | [![npm](https://img.shields.io/npm/dt/homebridge-pihole.svg)](https://www.npmjs.com/package/homebridge-pihole) 5 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/8bf5a87dc8a84df6a15deb699d43ee2b)](https://www.codacy.com/manual/arendruni/homebridge-pihole) 6 | [![Build Status](https://github.com/arendruni/homebridge-pihole/workflows/Main/badge.svg?branch=master)](https://github.com/arendruni/homebridge-pihole/actions?query=workflow%3AMain) 7 | [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 8 | 9 | [Pi-hole](https://github.com/pi-hole/pi-hole) plugin for Homebridge. 10 | This plugin publishes a virtual switch that disables Pi-hole, making it easier to temporarily turn off the ad-blocker. Supports SSL connections and can be configured with a timer to turn Pi-hole back on. 11 | 12 | ## Requirements 13 | 14 | - [Homebridge](https://github.com/nfarina/homebridge) - _HomeKit support for the impatient_ 15 | - [Pi-hole](https://github.com/pi-hole/pi-hole) - _A black hole for Internet advertisements_ 16 | 17 | ## Installation 18 | 19 | 1. Install this plugin `npm install -g homebridge-pihole` 20 | 2. Update your configuration file. See sample-config.json in this repository for a sample. 21 | 22 | See the Pi-hole [installation section](https://github.com/pi-hole/pi-hole#one-step-automated-install) for more details. 23 | 24 | ## Configuration 25 | 26 | There are the following options: 27 | 28 | - `name` Required. Accessory name, default is _Pi-hole_. 29 | 30 | ### Pi-hole Configuration 31 | 32 | - `auth` Pi-hole password or app password when using 2FA, see the section on [how to get an app password](#how-to-get-a-pi-hole-app-password). 33 | - `baseUrl` Pi-hole base URL, default is `http://localhost`. 34 | - `path` The directory where the Pi-hole dashboard is located. Default is `/api`. 35 | - `rejectUnauthorized` If the HTTPS agent should check the validity of SSL cert, set it to `false` to allow self-signed certs to work. Default is `true`. 36 | - `time` How long Pi-hole will be disabled, in seconds, default is 0 that means permanently disabled. 37 | - `reversed` When set to `true` reverse the status of Pi-hole. When Pi-hole is _off_ the plugin will be set to _on_ and when Pi-hole is _on_ the plugin will be set to _off_. Default is `false`. 38 | - `logLevel` Logging level, three different levels: 0: logging disabled, 1: logs only HTTP errors, 2: logs each HTTP response. Default is set to 1. 39 | 40 | ### Device Information 41 | 42 | - `manufacturer` Custom manufacturer, default is _Raspberry Pi_. 43 | - `model` Custom model, default is _Pi-hole_. 44 | - `serial-number` Should be a 9 digit number in the string format _123-456-789_. 45 | 46 | See the [sample-config.json](sample-config.json) file to see an example of how to configure the accessory. In the example the configured accessory will disable Pi-hole for a time interval of two minutes (120 seconds). 47 | 48 | ## How to get a Pi-hole app password 49 | 50 | 1. Login into your Pi-hole Admin Console. 51 | 2. Navigate to the _Settings_ page and then to the _Web interface / API_ tab, and enable the _Expert_ settings. 52 | 3. In the _Advanced Settings_ panel, click on the _Configure app password_ button, a popup window will ask for confirmation, go ahead and copy the app password then click on _Enable new app password_. 53 | 4. Paste your App password in the homebridge-pihole configuration file. 54 | -------------------------------------------------------------------------------- /config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "Pihole", 3 | "pluginType": "accessory", 4 | "schema": { 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "title": "Name", 9 | "type": "string", 10 | "required": true, 11 | "default": "Pi-hole", 12 | "description": "A unique name for the accessory. It will be used as the accessory name in HomeKit." 13 | }, 14 | "auth": { 15 | "title": "Auth Token", 16 | "type": "string", 17 | "required": false, 18 | "description": "Pi-hole auth token." 19 | }, 20 | "rejectUnauthorized": { 21 | "title": "Reject Unauthorized", 22 | "type": "boolean", 23 | "default": true, 24 | "required": false, 25 | "description": "If the HTTPS agent should check the validity of SSL cert." 26 | }, 27 | "baseUrl": { 28 | "title": "Base URL", 29 | "type": "string", 30 | "placeholder": "http://localhost", 31 | "default": "http://localhost", 32 | "required": false, 33 | "description": "Pi-hole base URL, default is http://localhost" 34 | }, 35 | "path": { 36 | "title": "Path", 37 | "type": "string", 38 | "placeholder": "/api", 39 | "default": "/api", 40 | "required": false, 41 | "description": "The directory where the Pi-hole dashboard is located. Typically /api." 42 | }, 43 | "time": { 44 | "title": "Time", 45 | "type": "integer", 46 | "placeholder": "0", 47 | "default": 0, 48 | "required": false, 49 | "description": "How long Pi-hole will be disabled, in seconds, default is 0 that means permanently disabled." 50 | }, 51 | "reversed": { 52 | "title": "Reversed", 53 | "type": "boolean", 54 | "default": false, 55 | "required": false, 56 | "description": "When set to true reverse the status of Pi-hole. When Pi-hole is Off the plugin will be set to On and when Pi-hole is On the plugin will be set to Off." 57 | }, 58 | "logLevel": { 59 | "title": "Loging level", 60 | "type": "integer", 61 | "placeholder": "1", 62 | "default": 1, 63 | "oneOf": [ 64 | { 65 | "title": "Off", 66 | "enum": [0] 67 | }, 68 | { 69 | "title": "Error", 70 | "enum": [1] 71 | }, 72 | { 73 | "title": "Info", 74 | "enum": [2] 75 | } 76 | ], 77 | "required": false, 78 | "description": "Three different levels: 0: logging disabled, 1: logs only HTTP errors (Default), 2: logs each HTTP response." 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-pihole", 3 | "displayName": "Pi-hole", 4 | "version": "1.0.0", 5 | "description": "Pi-hole Switch for Homebridge: https://github.com/nfarina/homebridge", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "build": "npm run clean && tsc", 9 | "clean": "rm -rf ./dist", 10 | "postpublish": "npm run clean", 11 | "prepublishOnly": "npm run build", 12 | "test": "node ./dist/index.js && echo \"No syntax errors! (node $(node -v))\"", 13 | "prettify": "prettier --write 'src/*.{js,ts}' '*.json'", 14 | "lint": "eslint 'src/*.{js,ts}'", 15 | "husky:precommit": "lint-staged", 16 | "watch": "tsc --watch", 17 | "prepare": "husky" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/arendruni/homebridge-pihole" 22 | }, 23 | "keywords": [ 24 | "homebridge-plugin" 25 | ], 26 | "author": "arendruni", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/arendruni/homebridge-pihole/issues" 30 | }, 31 | "homepage": "https://github.com/arendruni/homebridge-pihole#readme", 32 | "dependencies": { 33 | "undici": "^7.3.0" 34 | }, 35 | "engines": { 36 | "homebridge": "^1.6.0 || ^2.0.0-beta.0", 37 | "node": "^18.20.4 || ^20.15.1" 38 | }, 39 | "devDependencies": { 40 | "@types/node": "^20", 41 | "@typescript-eslint/eslint-plugin": "^8.24.1", 42 | "@typescript-eslint/parser": "^8.24.1", 43 | "eslint": "^8.57.1", 44 | "homebridge": "^1.6.0", 45 | "husky": "^9.1.7", 46 | "lint-staged": "^15.4.1", 47 | "prettier": "^3.5.2", 48 | "typescript": "^5" 49 | }, 50 | "lint-staged": { 51 | "src/*.{js,ts}": [ 52 | "prettier --write", 53 | "eslint" 54 | ], 55 | "./*.json": [ 56 | "prettier --write" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /sample-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "HomebridgePI", 4 | "username": "CD:22:3D:E3:CE:30", 5 | "port": 51826, 6 | "pin": "031-45-156" 7 | }, 8 | "description": "This is an example configuration file", 9 | "platforms": [], 10 | "accessories": [ 11 | { 12 | "accessory": "Pihole", 13 | "name": "Homelab pi-hole - 2 minutes", 14 | "auth": "3c469e9d6c5875d37a43f353d4f88e61fcf812c66eee3457465a40b0da4153e0", 15 | "ssl": false, 16 | "host": "192.168.1.2", 17 | "baseDirectory": "/myPiHoleConfig/", 18 | "time": 120 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { AccessoryPlugin, API, HAP, Logging, Service } from "homebridge"; 2 | import { BlockingResponse, PiholeClient, PiholeResponse } from "./piholeClient"; 3 | import { LogLevel, PiHoleAccessoryConfig, PiholeConfig } from "./types"; 4 | 5 | export default (api: API): void => { 6 | api.registerAccessory("homebridge-pihole", "Pihole", PiholeSwitch); 7 | }; 8 | 9 | const DEFAULT_CONFIG = { 10 | "manufacturer": "Raspberry Pi", 11 | "model": "Pi-hole", 12 | "serial-number": "123-456-789", 13 | "path": "/api", 14 | "baseUrl": "http://localhost", 15 | "logLevel": LogLevel.INFO, 16 | "rejectUnauthorized": true, 17 | "reversed": false, 18 | } as const satisfies PiholeConfig; 19 | 20 | class PiholeSwitch implements AccessoryPlugin { 21 | private readonly logLevel: LogLevel; 22 | 23 | private readonly informationService: Service; 24 | private readonly switchService: Service; 25 | 26 | private readonly piholeClient: PiholeClient; 27 | 28 | private readonly hap: HAP; 29 | 30 | constructor( 31 | private log: Logging, 32 | _config: PiHoleAccessoryConfig, 33 | api: API, 34 | ) { 35 | ({ hap: this.hap } = api); 36 | const { auth, reversed, ...config } = { ...DEFAULT_CONFIG, ..._config }; 37 | 38 | this.logLevel = config.logLevel; 39 | 40 | this.piholeClient = new PiholeClient( 41 | { 42 | auth, 43 | baseUrl: config.baseUrl, 44 | path: config.path, 45 | rejectUnauthorized: config.rejectUnauthorized, 46 | logLevel: config.logLevel, 47 | }, 48 | log, 49 | ); 50 | 51 | this.informationService = new this.hap.Service.AccessoryInformation() 52 | .setCharacteristic(this.hap.Characteristic.Manufacturer, config.manufacturer) 53 | .setCharacteristic(this.hap.Characteristic.Model, config.model) 54 | .setCharacteristic(this.hap.Characteristic.SerialNumber, config["serial-number"]); 55 | 56 | this.switchService = new this.hap.Service.Switch(config.name); 57 | this.switchService 58 | .getCharacteristic(this.hap.Characteristic.On) 59 | .onGet(async () => { 60 | try { 61 | const response = await this.piholeClient.getBlocking(); 62 | 63 | return this.postRequest(response, reversed); 64 | } catch (e) { 65 | if (this.logLevel >= LogLevel.ERROR) { 66 | this.log.error("Error", e); 67 | } 68 | 69 | throw e; 70 | } 71 | }) 72 | .onSet(async (value) => { 73 | const newValue = value as boolean; 74 | const switchState = reversed ? !newValue : newValue; 75 | 76 | try { 77 | const response = await this.piholeClient.setBlocking( 78 | switchState, 79 | switchState === false ? config.time : undefined, 80 | ); 81 | 82 | this.postRequest(response, reversed); 83 | } catch (e) { 84 | if (this.logLevel >= LogLevel.ERROR) { 85 | this.log.error("Error", e); 86 | } 87 | 88 | throw e; 89 | } 90 | }); 91 | } 92 | 93 | getServices(): Service[] { 94 | return [this.informationService, this.switchService]; 95 | } 96 | 97 | private postRequest(response: PiholeResponse, reversed: boolean) { 98 | const { 99 | body: { blocking }, 100 | response: { statusCode }, 101 | } = response; 102 | 103 | if (statusCode >= 400) { 104 | throw new Error("Api Error", { cause: response }); 105 | } 106 | 107 | if (this.logLevel >= LogLevel.INFO) { 108 | this.log.info(JSON.stringify({ ...response, response: { ...response.response, body: {} } })); 109 | } 110 | 111 | if (blocking === "disabled" || blocking === "enabled") { 112 | return reversed ? blocking === "disabled" : blocking === "enabled"; 113 | } else { 114 | throw new Error("Invalid status", { cause: { blocking } }); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/piholeClient.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "homebridge"; 2 | import { Agent, Dispatcher, request } from "undici"; 3 | import { LogLevel } from "./types"; 4 | 5 | interface PiholeClientOptions { 6 | auth?: string; 7 | path?: string; 8 | baseUrl: string; 9 | rejectUnauthorized: boolean; 10 | logLevel: LogLevel; 11 | } 12 | 13 | export type BlockingResponse = { 14 | timer?: number | null; 15 | blocking: "enabled" | "disabled" | "failed" | "unknown"; 16 | took?: number; 17 | }; 18 | 19 | export type PiholeResponse = { 20 | body: T; 21 | response: Dispatcher.ResponseData; 22 | }; 23 | 24 | type Session = { 25 | valid: boolean; 26 | sid: string | null; 27 | validity: number; 28 | totp: boolean; 29 | csrf: string | null; 30 | message: string | null; 31 | }; 32 | 33 | type SessionResponse = { 34 | session: Session; 35 | took?: number; 36 | }; 37 | 38 | export class PiholeClient { 39 | private dispatcher?: Dispatcher; 40 | private session?: Session; 41 | 42 | private readonly baseUrl: string; 43 | 44 | constructor( 45 | private readonly options: PiholeClientOptions, 46 | readonly logger: Logger, 47 | ) { 48 | const url = new URL(this.options.path ?? "/api", this.options.baseUrl); 49 | this.baseUrl = url.toString(); 50 | 51 | if (this.baseUrl.endsWith("/")) { 52 | this.baseUrl = this.baseUrl.slice(0, -1); 53 | } 54 | 55 | if (url.protocol === "https:" && options.rejectUnauthorized === false) { 56 | this.dispatcher = new Agent({ connect: { rejectUnauthorized: options.rejectUnauthorized } }); 57 | } 58 | } 59 | 60 | private async makeRequest( 61 | method: "GET" | "POST", 62 | path: `/${string}`, 63 | body?: unknown, 64 | ): Promise> { 65 | const url = `${this.baseUrl}${path}`; 66 | 67 | if (this.options.logLevel >= LogLevel.INFO) { 68 | this.logger.info("Request", { 69 | method, 70 | body, 71 | url, 72 | }); 73 | } 74 | 75 | const response = await request(url, { 76 | method, 77 | dispatcher: this.dispatcher, 78 | body: body ? JSON.stringify(body) : undefined, 79 | headers: { 80 | "Content-Type": "application/json", 81 | "User-Agent": "PiholeClient", 82 | ...(this.session?.sid ? { "X-FTL-SID": this.session.sid } : {}), 83 | }, 84 | }); 85 | 86 | const responseBody = (await response.body.json()) as Res; 87 | 88 | if (this.options.logLevel >= LogLevel.INFO) { 89 | this.logger.info("Response", { 90 | method, 91 | body, 92 | responseBody, 93 | response: { ...response, body: {} }, 94 | url, 95 | }); 96 | } 97 | 98 | return { body: responseBody, response }; 99 | } 100 | 101 | private async setupSession() { 102 | const { body } = await this.makeRequest("GET", "/auth", undefined); 103 | 104 | if (!body.session.valid) { 105 | if (!this.options.auth) { 106 | throw new Error("Auth is required"); 107 | } 108 | 109 | const { body, response } = await this.makeRequest("POST", "/auth", { 110 | password: this.options.auth, 111 | }); 112 | 113 | if (!body.session.valid) { 114 | throw new Error("Auth not valid", { cause: JSON.stringify({ body, response }) }); 115 | } 116 | 117 | this.session = body.session; 118 | } else { 119 | this.session = body.session; 120 | } 121 | } 122 | 123 | async setBlocking(blocking: boolean, timer?: number) { 124 | await this.setupSession(); 125 | 126 | return this.makeRequest("POST", "/dns/blocking", { blocking, timer }); 127 | } 128 | 129 | async getBlocking() { 130 | await this.setupSession(); 131 | 132 | return this.makeRequest("GET", "/dns/blocking"); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { AccessoryConfig } from "homebridge"; 2 | 3 | export enum LogLevel { 4 | DISABLED = 0, 5 | ERROR = 1, 6 | INFO = 2, 7 | } 8 | 9 | export type PiholeConfig = { 10 | "auth"?: string; 11 | "path"?: string; 12 | "baseUrl"?: string; 13 | "rejectUnauthorized"?: boolean; 14 | "manufacturer"?: string; 15 | "model"?: string; 16 | "serial-number"?: string; 17 | "reversed"?: boolean; 18 | "time"?: number; 19 | "logLevel"?: LogLevel; 20 | }; 21 | 22 | export type PiHoleAccessoryConfig = PiholeConfig & AccessoryConfig; 23 | -------------------------------------------------------------------------------- /test-configuration/.gitignore: -------------------------------------------------------------------------------- 1 | persist 2 | accessories -------------------------------------------------------------------------------- /test-configuration/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "HomebridgePI-test", 4 | "username": "CD:22:3D:E3:CE:30", 5 | "port": 51826, 6 | "pin": "031-45-156" 7 | }, 8 | "description": "This is a test configuration file", 9 | "platforms": [], 10 | "accessories": [ 11 | { 12 | "accessory": "Pihole", 13 | "name": "pi-hole - test", 14 | "baseUrl": "https://pihole.raspi.io", 15 | "rejectUnauthorized": false, 16 | "auth": "998ed4d621742d0c2d85ed84173db569afa194d4597686cae947324aa58ab4bb", 17 | "reversed": true, 18 | "time": 120 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /test-configuration/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # install homebridge 4 | npm install -g --unsafe-perm homebridge@${HOMEBRIDGE_VERSION-latest} 5 | 6 | # test configuration 7 | DEBUG=* timeout --preserve-status --kill-after 30s --signal SIGINT 20s homebridge --debug --no-qrcode --user-storage-path ./test-configuration --plugin-path ./ 8 | 9 | if [ $? -eq 130 ]; then 10 | exit 0 11 | fi 12 | 13 | exit $? 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2023", 4 | "module": "commonjs", 5 | "lib": ["ES2022"], 6 | "sourceMap": true, 7 | "rootDir": "src", 8 | "noImplicitAny": true, 9 | "outDir": "dist", 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "include": ["src"] 15 | } 16 | --------------------------------------------------------------------------------