UNPKG

@homebridge-plugins/homebridge-noip

Version:

The No-IP plugin allows you to update your No-IP hostname(s) for your homebridge instance.

222 lines 10.6 kB
import { Buffer } from 'node:buffer'; import { interval, throwError } from 'rxjs'; import { skipWhile, timeout } from 'rxjs/operators'; import { request } from 'undici'; import { noip } from '../settings.js'; import { deviceBase } from './device.js'; /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ export class ContactSensor extends deviceBase { platform; // Service ContactSensor; // Others interval; renewalInterval; // Updates SensorUpdateInProgress; RenewalInProgress; // Renewal settings autoRenewal; renewalIntervalDays; constructor(platform, accessory, device) { super(platform, accessory, device); this.platform = platform; // Contact Sensor Service this.debugLog('Configure Contact Sensor Service'); this.ContactSensor = { Service: this.accessory.getService(this.hap.Service.ContactSensor) ?? this.accessory.addService(this.hap.Service.ContactSensor), ContactSensorState: this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED, }; // Add Contact Sensor Service's Characteristics this.ContactSensor.Service .setCharacteristic(this.hap.Characteristic.Name, device.hostname.split('.')[0]); // this is subject we use to track when we need to POST changes to the NoIP API this.SensorUpdateInProgress = false; this.RenewalInProgress = false; // Set up renewal settings this.autoRenewal = device.autoRenewal ?? this.platform.platformAutoRenewal ?? false; this.renewalIntervalDays = device.renewalInterval ?? this.platform.platformRenewalInterval ?? 25; // Default to 25 days for free accounts // Retrieve initial values and updateHomekit this.refreshStatus(); this.updateHomeKitCharacteristics(); // Start an update interval interval(this.deviceRefreshRate * 1000) .pipe(skipWhile(() => this.SensorUpdateInProgress)) .subscribe(async () => { await this.refreshStatus(); }); // Start renewal interval if auto-renewal is enabled if (this.autoRenewal) { const renewalIntervalMs = this.renewalIntervalDays * 24 * 60 * 60 * 1000; // Convert days to milliseconds interval(renewalIntervalMs) .pipe(skipWhile(() => this.RenewalInProgress)) .subscribe(async () => { await this.renewDomain(); }); // Log renewal setup (called asynchronously to avoid blocking constructor) this.infoLog(`Auto-renewal enabled for ${device.hostname} every ${this.renewalIntervalDays} days`); } } /** * Renews the No-IP domain to prevent expiration by making an update request * This works by confirming the hostname is still in use, which extends its validity period */ async renewDomain() { if (this.RenewalInProgress) { await this.debugLog('Renewal already in progress, skipping'); return; } this.RenewalInProgress = true; try { await this.infoLog(`Starting domain renewal for ${this.device.hostname}`); // Get current IP to make an update request (this serves as a renewal) const currentIP = this.device.ipv4or6 === 'ipv6' ? await this.platform.publicIPv6(this.device) : await this.platform.publicIPv4(this.device); if (!currentIP) { await this.errorLog('Could not retrieve current IP for renewal'); return; } const { body, statusCode } = await request(noip, { method: 'GET', headers: { 'Authorization': `Basic ${Buffer.from(`${this.device.username}:${this.device.password}`).toString('base64')}`, 'User-Agent': `Homebridge-NoIP/v${this.device.firmware}`, }, // Use update endpoint with current IP to confirm hostname usage query: { hostname: this.device.hostname, myip: currentIP, }, }); const response = await body.text(); await this.debugWarnLog(`Renewal statusCode: ${JSON.stringify(statusCode)}`); await this.debugLog(`Renewal response: ${JSON.stringify(response)}`); if (statusCode === 200) { if (response.includes('good') || response.includes('nochg')) { await this.successLog(`Domain ${this.device.hostname} renewed successfully (IP confirmed: ${currentIP})`); } else if (response.includes('nohost') || response.includes('badauth')) { await this.errorLog(`Domain renewal failed - authentication or hostname error: ${response}`); } else { await this.warnLog(`Domain renewal completed but response unclear: ${response}`); } } else { await this.errorLog(`Domain renewal failed with status ${statusCode}: ${response}`); } } catch (e) { await this.errorLog(`Failed to renew domain ${this.device.hostname}, Error: ${JSON.stringify(e.message ?? e)}`); } finally { this.RenewalInProgress = false; } } /** * Parse the device status from the noip api */ async parseStatus(response) { if (response.includes('nochg')) { this.ContactSensor.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED; } else { this.ContactSensor.ContactSensorState = this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; } await this.debugLog(`ContactSensorState: ${this.ContactSensor.ContactSensorState}`); } /** * Asks the NoIP API for the latest device information */ async refreshStatus() { try { const ip = this.device.ipv4or6 === 'ipv6' ? await this.platform.publicIPv6(this.device) : await this.platform.publicIPv4(this.device); const ipv4or6 = this.device.ipv4or6 === 'ipv6' ? 'IPv6' : 'IPv4'; const ipProvider = this.device.ipProvider === 'ipify' ? 'ipify.org' : this.device.ipProvider === 'getmyip' ? 'getmyip.dev' : this.device.ipProvider === 'ipapi' ? 'ipapi.co' : this.device.ipProvider === 'myip' ? 'my-ip.io' : 'ipinfo.io'; const { body, statusCode } = await request(noip, { method: 'GET', query: { hostname: this.device.hostname, myip: ip, }, headers: { 'Authorization': `Basic ${Buffer.from(`${this.device.username}:${this.device.password}`).toString('base64')}`, 'User-Agent': `Homebridge-NoIP/v${this.device.firmware}`, }, }); const response = await body.text(); await this.debugWarnLog(`statusCode: ${JSON.stringify(statusCode)}`); await this.debugLog(`${ipProvider} ${ipv4or6} respsonse: ${JSON.stringify(response)}`); const data = response.trim(); const f = data.match(/good|nochg/g); if (f) { await this.debugLog(`data: ${f[0]}`); this.status(f, data); } else { await this.errorLog(`error: ${data}`); } await this.parseStatus(response); await this.updateHomeKitCharacteristics(); } catch (e) { await this.errorLog(`failed to update status, Error: ${JSON.stringify(e.message ?? e)}`); await this.apiError(e); } } async status(f, data) { switch (f[0]) { case 'nochg': await this.debugLog(`IP Address has not updated, IP Address: ${data.split(' ')[1]}`); break; case 'good': await this.warnLog(`IP Address has been updated, IP Address: ${data.split(' ')[1]}`); break; case 'nohost': await this.errorLog('Hostname supplied does not exist under specified account, client exit and require user to enter new login credentials before performing an additional request.'); await this.timeout(); break; case 'badauth': await this.errorLog('Invalid username password combination.'); await this.timeout(); break; case 'badagent': await this.errorLog('Client disabled. Client should exit and not perform any more updates without user intervention. '); await this.timeout(); break; case '!donator': await this.errorLog('An update request was sent, ' + 'including a feature that is not available to that particular user such as offline options.'); await this.timeout(); break; case 'abuse': await this.errorLog('Username is blocked due to abuse. Either for not following our update specifications or disabled due to violation of the No-IP terms of service. Our terms of service can be viewed [here](https://www.noip.com/legal/tos). Client should stop sending updates.'); await this.timeout(); break; case '911': await this.errorLog('A fatal error on our side such as a database outage. Retry the update no sooner than 30 minutes. '); await this.timeout(); break; default: await this.debugLog(data); } } async timeout() { this.interval.pipe(timeout({ each: 1000, with: () => throwError(() => new Error('nohost')) })).subscribe({ error: this.errorLog }); } /** * Updates the status for each of the HomeKit Characteristics */ async updateHomeKitCharacteristics() { // ContactSensorState await this.updateCharacteristic(this.ContactSensor.Service, this.hap.Characteristic.ContactSensorState, this.ContactSensor.ContactSensorState, 'ContactSensorState'); } async apiError(e) { this.ContactSensor.Service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, e); } } //# sourceMappingURL=contactsensor.js.map