Skip to content

Commit

Permalink
Support turning on/off ringer
Browse files Browse the repository at this point in the history
Support turning on/off answering machine
  • Loading branch information
Marc Vanbrabant committed Nov 22, 2023
1 parent 7e65605 commit 125f539
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 26 deletions.
6 changes: 3 additions & 3 deletions plugins/bticino/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion plugins/bticino/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
61 changes: 61 additions & 0 deletions plugins/bticino/src/bticino-aswm-switch.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
this.on = false
return this.camera.turnOffAswm()
}

turnOn(): Promise<void> {
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<void> {
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,
});
}
}
}
69 changes: 66 additions & 3 deletions plugins/bticino/src/bticino-camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<void> {
return new Promise<void>( (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<boolean> {
return new Promise<boolean>( (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<VideoClip[]> {
return new Promise<VideoClip[]>( (resolve,reject ) => {
let c300x = SipHelper.getIntercomIp(this)
Expand Down Expand Up @@ -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(); ;
});
}

Expand Down Expand Up @@ -132,6 +177,18 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
} )
}

turnOnAswm() : Promise<void> {
return this.persistentSipManager.enable().then( (sipCall) => {
sipCall.message( "*8*91##" )
} )
}

turnOffAswm() : Promise<void> {
return this.persistentSipManager.enable().then( (sipCall) => {
sipCall.message( "*8*92##" )
} )
}

async takePicture(option?: PictureOptions): Promise<MediaObject> {
throw new Error("The SIP doorbell camera does not provide snapshots. Install the Snapshot Plugin if snapshots are available via an URL.");
}
Expand Down Expand Up @@ -378,11 +435,17 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
]
}

async getDevice(nativeId: string) : Promise<BticinoSipLock> {
async getDevice(nativeId: string) : Promise<any> {
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<void> {
this.stopIntercom()
this.voicemailHandler.cancelTimer()
this.persistentSipManager.cancelTimer()
this.controllerApi.cancelTimer()
Expand Down
34 changes: 19 additions & 15 deletions plugins/bticino/src/bticino-voicemailHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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() {
Expand All @@ -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
Expand All @@ -53,17 +53,21 @@ 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)
}
}
}
}
}

isEnabled() : boolean {
return this.sipCamera?.storage?.getItem('notifyVoicemail')?.toLocaleLowerCase() === 'true' || false
}

isAswmEnabled() : boolean {
return this.aswmIsEnabled
}
}
4 changes: 2 additions & 2 deletions plugins/bticino/src/c300x-controller-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
Expand All @@ -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() {
Expand Down
32 changes: 30 additions & 2 deletions plugins/bticino/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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})`,
Expand All @@ -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)
Expand Down

0 comments on commit 125f539

Please sign in to comment.