diff --git a/plugins/bticino/package-lock.json b/plugins/bticino/package-lock.json index a65f9fbe3..9ebb4b9aa 100644 --- a/plugins/bticino/package-lock.json +++ b/plugins/bticino/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/bticino", - "version": "0.0.11", + "version": "0.0.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/bticino", - "version": "0.0.11", + "version": "0.0.12", "dependencies": { "@slyoldfox/sip": "^0.0.6-1", "sdp": "^3.0.3", @@ -40,7 +40,7 @@ }, "../../sdk": { "name": "@scrypted/sdk", - "version": "0.2.103", + "version": "0.2.105", "dev": true, "license": "ISC", "dependencies": { diff --git a/plugins/bticino/package.json b/plugins/bticino/package.json index 80f8f8640..a9230424c 100644 --- a/plugins/bticino/package.json +++ b/plugins/bticino/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/bticino", - "version": "0.0.11", + "version": "0.0.12", "scripts": { "scrypted-setup-project": "scrypted-setup-project", "prescrypted-setup-project": "scrypted-package-json", diff --git a/plugins/bticino/src/bticino-aswm-switch.ts b/plugins/bticino/src/bticino-aswm-switch.ts new file mode 100644 index 000000000..39363997e --- /dev/null +++ b/plugins/bticino/src/bticino-aswm-switch.ts @@ -0,0 +1,61 @@ +import { ScryptedDeviceBase, HttpRequest, HttpResponse, HttpRequestHandler, OnOff } from "@scrypted/sdk"; +import { BticinoSipCamera } from "./bticino-camera"; +import { VoicemailHandler } from "./bticino-voicemailHandler"; + +export class BticinoAswmSwitch extends ScryptedDeviceBase implements OnOff, HttpRequestHandler { + private timeout : NodeJS.Timeout + + constructor(private camera: BticinoSipCamera, private voicemailHandler : VoicemailHandler) { + super( camera.nativeId + "-aswm-switch") + this.timeout = setTimeout( () => this.syncStatus() , 5000 ) + } + + turnOff(): Promise { + this.on = false + return this.camera.turnOffAswm() + } + + turnOn(): Promise { + this.on = true + return this.camera.turnOnAswm() + } + + syncStatus() { + this.on = this.voicemailHandler.isAswmEnabled() + this.timeout = setTimeout( () => this.syncStatus() , 5000 ) + } + + cancelTimer() { + if( this.timeout ) { + clearTimeout(this.timeout) + } + } + + public async onRequest(request: HttpRequest, response: HttpResponse): Promise { + if (request.url.endsWith('/disabled')) { + this.on = false + response.send('Success', { + code: 200, + }); + } else if( request.url.endsWith('/enabled') ) { + this.on = true + response.send('Success', { + code: 200, + }); + } else if( request.url.endsWith('/enable') ) { + this.turnOn() + response.send('Success', { + code: 200, + }); + } else if( request.url.endsWith('/disable') ) { + this.turnOff() + response.send('Success', { + code: 200, + }); + } else { + response.send('Unsupported operation', { + code: 400, + }); + } + } +} \ No newline at end of file diff --git a/plugins/bticino/src/bticino-camera.ts b/plugins/bticino/src/bticino-camera.ts index 6eb46ede2..e89876c81 100644 --- a/plugins/bticino/src/bticino-camera.ts +++ b/plugins/bticino/src/bticino-camera.ts @@ -20,6 +20,8 @@ import { SipRequest } from '../../sip/src/sip-manager'; import { get } from 'http' import { ControllerApi } from './c300x-controller-api'; +import { BticinoAswmSwitch } from './bticino-aswm-switch'; +import { BticinoMuteSwitch } from './bticino-mute-switch'; const STREAM_TIMEOUT = 65000; const { mediaManager } = sdk; @@ -63,10 +65,50 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid get(`http://${c300x}:8080/reboot?now`, (res) => { console.log("Reboot API result: " + res.statusCode) - }); + }).on('error', (error) => { + this.console.error(error) + reject(error) + } ).end(); }) } + muteRinger(mute : boolean): Promise { + return new Promise( (resolve,reject ) => { + let c300x = SipHelper.getIntercomIp(this) + + get(`http://${c300x}:8080/mute?raw=true&enable=` + mute, (res) => { + console.log("Mute API result: " + res.statusCode) + }).on('error', (error) => { + this.console.error(error) + reject(error) + } ).end(); + }) + } + + muteStatus(): Promise { + return new Promise( (resolve,reject ) => { + let c300x = SipHelper.getIntercomIp(this) + + get(`http://${c300x}:8080/mute?status=true&raw=true`, (res) => { + let rawData = ''; + res.on('data', (chunk) => { rawData += chunk; }) + res.on('error', (error) => this.console.log(error)) + res.on('end', () => { + try { + return resolve(JSON.parse(rawData)) + } catch (e) { + console.error(e.message); + reject(e.message) + + } + }) + }).on('error', (error) => { + this.console.error(error) + reject(error) + } ).end(); + }) + } + getVideoClips(options?: VideoClipOptions): Promise { return new Promise( (resolve,reject ) => { let c300x = SipHelper.getIntercomIp(this) @@ -95,7 +137,10 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid console.error(e.message); } }) - }); + }).on('error', (error) => { + this.console.error(error) + reject(error) + } ).end(); ; }); } @@ -132,6 +177,18 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid } ) } + turnOnAswm() : Promise { + return this.persistentSipManager.enable().then( (sipCall) => { + sipCall.message( "*8*91##" ) + } ) + } + + turnOffAswm() : Promise { + return this.persistentSipManager.enable().then( (sipCall) => { + sipCall.message( "*8*92##" ) + } ) + } + async takePicture(option?: PictureOptions): Promise { throw new Error("The SIP doorbell camera does not provide snapshots. Install the Snapshot Plugin if snapshots are available via an URL."); } @@ -378,11 +435,17 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid ] } - async getDevice(nativeId: string) : Promise { + async getDevice(nativeId: string) : Promise { + if( nativeId && nativeId.endsWith('-aswm-switch')) { + return new BticinoAswmSwitch(this, this.voicemailHandler) + } else if( nativeId && nativeId.endsWith('-mute-switch') ) { + return new BticinoMuteSwitch(this) + } return new BticinoSipLock(this) } async releaseDevice(id: string, nativeId: string): Promise { + this.stopIntercom() this.voicemailHandler.cancelTimer() this.persistentSipManager.cancelTimer() this.controllerApi.cancelTimer() diff --git a/plugins/bticino/src/bticino-voicemailHandler.ts b/plugins/bticino/src/bticino-voicemailHandler.ts index 45fc4cead..9f121cf13 100644 --- a/plugins/bticino/src/bticino-voicemailHandler.ts +++ b/plugins/bticino/src/bticino-voicemailHandler.ts @@ -3,6 +3,7 @@ import { BticinoSipCamera } from "./bticino-camera" export class VoicemailHandler extends SipRequestHandler { private timeout : NodeJS.Timeout + private aswmIsEnabled: boolean constructor( private sipCamera : BticinoSipCamera ) { super() @@ -15,14 +16,12 @@ export class VoicemailHandler extends SipRequestHandler { checkVoicemail() { if( !this.sipCamera ) return - if( this.isEnabled() ) { - this.sipCamera.console.debug("Checking answering machine, cameraId: " + this.sipCamera.id ) - this.sipCamera.getAswmStatus().catch( e => this.sipCamera.console.error(e) ) - } else { - this.sipCamera.console.debug("Answering machine check not enabled, cameraId: " + this.sipCamera.id ) - } - //TODO: make interval customizable, now every 5 minutes - this.timeout = setTimeout( () => this.checkVoicemail() , 5 * 60 * 1000 ) + + this.sipCamera.console.debug("Checking answering machine, cameraId: " + this.sipCamera.id ) + this.sipCamera.getAswmStatus().catch( e => this.sipCamera.console.error(e) ) + + //TODO: make interval customizable, now every minute + this.timeout = setTimeout( () => this.checkVoicemail() , 1 * 60 * 1000 ) } cancelTimer() { @@ -32,10 +31,11 @@ export class VoicemailHandler extends SipRequestHandler { } handle(request: SipRequest) { - if( this.isEnabled() ) { - const lastVoicemailMessageTimestamp : number = Number.parseInt( this.sipCamera.storage.getItem('lastVoicemailMessageTimestamp') ) || -1 - const message : string = request.content.toString() - if( message.startsWith('*#8**40*0*0*1176*0*2##') ) { + const lastVoicemailMessageTimestamp : number = Number.parseInt( this.sipCamera.storage.getItem('lastVoicemailMessageTimestamp') ) || -1 + const message : string = request.content.toString() + if( message.startsWith('*#8**40*0*0*') || message.startsWith('*#8**40*1*0*') ) { + this.aswmIsEnabled = message.startsWith('*#8**40*1*0*'); + if( this.isEnabled() ) { this.sipCamera.console.debug("Handling incoming answering machine reply") const messages : string[] = message.split(';') let lastMessageTimestamp : number = 0 @@ -53,12 +53,12 @@ export class VoicemailHandler extends SipRequestHandler { } } ) if( (lastVoicemailMessageTimestamp == null && lastMessageTimestamp > 0) || - ( lastVoicemailMessageTimestamp != null && lastMessageTimestamp > lastVoicemailMessageTimestamp ) ) { + ( lastVoicemailMessageTimestamp != null && lastMessageTimestamp > lastVoicemailMessageTimestamp ) ) { this.sipCamera.log.a(`You have ${countNewMessages} new voicemail messages.`) this.sipCamera.storage.setItem('lastVoicemailMessageTimestamp', lastMessageTimestamp.toString()) - } else { + } else { this.sipCamera.console.debug("No new messages since: " + lastVoicemailMessageTimestamp + " lastMessage: " + lastMessageTimestamp) - } + } } } } @@ -66,4 +66,8 @@ export class VoicemailHandler extends SipRequestHandler { isEnabled() : boolean { return this.sipCamera?.storage?.getItem('notifyVoicemail')?.toLocaleLowerCase() === 'true' || false } + + isAswmEnabled() : boolean { + return this.aswmIsEnabled + } } \ No newline at end of file diff --git a/plugins/bticino/src/c300x-controller-api.ts b/plugins/bticino/src/c300x-controller-api.ts index c0f18667a..b4ef87485 100644 --- a/plugins/bticino/src/c300x-controller-api.ts +++ b/plugins/bticino/src/c300x-controller-api.ts @@ -99,7 +99,7 @@ export class ControllerApi { }) } console.log("Endpoint registration status: " + res.statusCode) - }); + }).on('error', (e) => this.sipCamera.console.error(e) ); // The default evict time on the c300x-controller is 5 minutes, so this will certainly be within bounds this.timeout = setTimeout( () => this.registerEndpoints( false ) , 2 * 60 * 1000 ) @@ -114,7 +114,7 @@ export class ControllerApi { return new Promise( (resolve, reject) => get(`http://${ipAddress}:8080/register-endpoint?raw=true&updateStreamEndpoint=${sipFrom}`, (res) => { if( res.statusCode != 200 ) reject( "ERROR: Could not update streaming endpoint, call returned: " + res.statusCode ) else resolve() - } ) ); + } ).on('error', (error) => this.sipCamera.console.error(error) ).end() ); } public cancelTimer() { diff --git a/plugins/bticino/src/main.ts b/plugins/bticino/src/main.ts index 2b7119c9a..b5dd24a91 100644 --- a/plugins/bticino/src/main.ts +++ b/plugins/bticino/src/main.ts @@ -36,7 +36,7 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid const name = settings.newCamera?.toString() === undefined ? "Doorbell" : settings.newCamera?.toString() await this.updateDevice(nativeId, name) - const device: Device = { + const lockDevice: Device = { providerNativeId: nativeId, info: { //model: `${camera.model} (${camera.data.kind})`, @@ -49,10 +49,38 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid type: ScryptedDeviceType.Lock, interfaces: [ScryptedInterface.Lock, ScryptedInterface.HttpRequestHandler], } + + const aswmSwitchDevice: Device = { + providerNativeId: nativeId, + info: { + //model: `${camera.model} (${camera.data.kind})`, + manufacturer: 'BticinoPlugin', + //firmware: camera.data.firmware_version, + //serialNumber: camera.data.device_id + }, + nativeId: nativeId + '-aswm-switch', + name: name + ' Voicemail', + type: ScryptedDeviceType.Switch, + interfaces: [ScryptedInterface.OnOff, ScryptedInterface.HttpRequestHandler], + } + + const muteSwitchDevice: Device = { + providerNativeId: nativeId, + info: { + //model: `${camera.model} (${camera.data.kind})`, + manufacturer: 'BticinoPlugin', + //firmware: camera.data.firmware_version, + //serialNumber: camera.data.device_id + }, + nativeId: nativeId + '-mute-switch', + name: name + ' Muted', + type: ScryptedDeviceType.Switch, + interfaces: [ScryptedInterface.OnOff, ScryptedInterface.HttpRequestHandler], + } await deviceManager.onDevicesChanged({ providerNativeId: nativeId, - devices: [device], + devices: [lockDevice, aswmSwitchDevice, muteSwitchDevice], }) let sipCamera : BticinoSipCamera = await this.getDevice(nativeId)