-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #247 from Drenso/rework-doorbell
Unify camera and doorbell implementation
- Loading branch information
Showing
15 changed files
with
1,234 additions
and
323 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,179 +1,5 @@ | ||
import * as TuyaOAuth2Util from '../../lib/TuyaOAuth2Util'; | ||
import { constIncludes, getFromMap } from '../../lib/TuyaOAuth2Util'; | ||
import { SettingsEvent, TuyaStatus } from '../../types/TuyaTypes'; | ||
import { | ||
CAMERA_ALARM_EVENT_CAPABILITIES, | ||
CAMERA_SETTING_LABELS, | ||
HomeyCameraSettings, | ||
SIMPLE_CAMERA_CAPABILITIES, | ||
TuyaCameraSettings, | ||
} from './TuyaCameraConstants'; | ||
import TuyaTimeOutAlarmDevice from '../../lib/TuyaTimeOutAlarmDevice'; | ||
import TuyaDeviceWithCamera from '../../lib/camera/device'; | ||
|
||
module.exports = class TuyaOAuth2DeviceCamera extends TuyaTimeOutAlarmDevice { | ||
async onOAuth2Init(): Promise<void> { | ||
await super.onOAuth2Init(); | ||
|
||
for (const capability of this.getCapabilities()) { | ||
// Basic capabilities | ||
if (constIncludes(SIMPLE_CAMERA_CAPABILITIES.read_write, capability)) { | ||
this.registerCapabilityListener(capability, value => | ||
this.sendCommand({ | ||
code: capability, | ||
value: value, | ||
}), | ||
); | ||
} | ||
|
||
// PTZ control | ||
if (capability === 'ptz_control_vertical') { | ||
this.registerCapabilityListener(capability, value => this.ptzCapabilityListener(value, '0', '4')); | ||
} | ||
if (capability === 'ptz_control_horizontal') { | ||
this.registerCapabilityListener(capability, value => this.ptzCapabilityListener(value, '6', '2')); | ||
} | ||
|
||
if (capability === 'ptz_control_zoom') { | ||
this.registerCapabilityListener(capability, value => this.zoomCapabilityListener(value)); | ||
} | ||
|
||
// Other capabilities | ||
if (capability === 'onoff') { | ||
this.registerCapabilityListener(capability, value => | ||
this.sendCommand({ | ||
code: 'basic_private', | ||
value: !value, | ||
}), | ||
); | ||
} | ||
} | ||
|
||
// Reset alarms in case a timeout was interrupted | ||
for (const tuyaCapability in CAMERA_ALARM_EVENT_CAPABILITIES) { | ||
const capability = getFromMap(CAMERA_ALARM_EVENT_CAPABILITIES, tuyaCapability); | ||
if (capability && this.hasCapability(capability)) { | ||
await this.setCapabilityValue(capability, false); | ||
} | ||
} | ||
} | ||
|
||
async onTuyaStatus(status: TuyaStatus, changed: string[]): Promise<void> { | ||
await super.onTuyaStatus(status, changed); | ||
|
||
for (const statusKey in status) { | ||
const value = status[statusKey]; | ||
|
||
// Basic capabilities | ||
if ( | ||
constIncludes(SIMPLE_CAMERA_CAPABILITIES.read_write, statusKey) || | ||
constIncludes(SIMPLE_CAMERA_CAPABILITIES.read_only, statusKey) | ||
) { | ||
await this.setCapabilityValue(statusKey, value).catch(this.error); | ||
} | ||
|
||
if (constIncludes(SIMPLE_CAMERA_CAPABILITIES.setting, statusKey)) { | ||
await this.setSettings({ | ||
[statusKey]: value, | ||
}).catch(this.error); | ||
} | ||
|
||
// PTZ control | ||
if ( | ||
(statusKey === 'ptz_stop' && value && changed.includes('ptz_stop')) || | ||
(statusKey === 'ptz_control' && value === '8' && changed.includes('ptz_control')) | ||
) { | ||
await this.setCapabilityValue('ptz_control_horizontal', 'idle').catch(this.error); | ||
await this.setCapabilityValue('ptz_control_vertical', 'idle').catch(this.error); | ||
} | ||
|
||
if (statusKey === 'zoom_stop' && value && changed.includes('zoom_stop')) { | ||
await this.setCapabilityValue('ptz_control_zoom', 'idle').catch(this.error); | ||
} | ||
|
||
// Other capabilities | ||
if (statusKey === 'basic_private') { | ||
await this.setCapabilityValue('onoff', !value).catch(this.error); | ||
} | ||
|
||
if (statusKey === 'wireless_electricity') { | ||
await this.setCapabilityValue('measure_battery', value).catch(this.error); | ||
} | ||
|
||
// Event messages | ||
if (statusKey === 'initiative_message' && changed.includes('initiative_message')) { | ||
// Event messages are base64 encoded JSON | ||
const encoded = status[statusKey] as string; | ||
const decoded = Buffer.from(encoded, 'base64'); | ||
const data = JSON.parse(decoded.toString()); | ||
const notificationType = data.cmd; | ||
const dataType = data.type; | ||
this.log('Notification:', notificationType, dataType); | ||
|
||
// Check if the event is for a known alarm | ||
if (notificationType in CAMERA_ALARM_EVENT_CAPABILITIES) { | ||
const alarmCapability = getFromMap(CAMERA_ALARM_EVENT_CAPABILITIES, notificationType); | ||
if (!alarmCapability) { | ||
continue; | ||
} | ||
|
||
if (!this.hasCapability(alarmCapability)) { | ||
await this.addCapability(alarmCapability).catch(this.error); | ||
} | ||
await this.setAlarm(alarmCapability); | ||
} | ||
} | ||
} | ||
} | ||
|
||
async setAlarm(capability: string): Promise<void> { | ||
await super.setAlarm( | ||
capability, | ||
async () => { | ||
const deviceTriggerCard = this.homey.flow.getDeviceTriggerCard(`camera_${capability}_true`); | ||
await deviceTriggerCard.trigger(this).catch(this.error); | ||
await this.setCapabilityValue(capability, true).catch(this.error); | ||
}, | ||
async () => { | ||
const deviceTriggerCard = this.homey.flow.getDeviceTriggerCard(`camera_${capability}_false`); | ||
await deviceTriggerCard.trigger(this).catch(this.error); | ||
await this.setCapabilityValue(capability, false).catch(this.error); | ||
}, | ||
); | ||
} | ||
|
||
// Map from up/idle/down to commands so the ternary UI shows arrows | ||
async ptzCapabilityListener(value: 'up' | 'idle' | 'down', up: string, down: string): Promise<void> { | ||
if (value === 'idle') { | ||
await this.sendCommand({ code: 'ptz_stop', value: true }); | ||
} else { | ||
await this.sendCommand({ code: 'ptz_control', value: value === 'up' ? up : down }); | ||
} | ||
} | ||
|
||
async zoomCapabilityListener(value: 'up' | 'idle' | 'down'): Promise<void> { | ||
if (value === 'idle') { | ||
await this.sendCommand({ code: 'zoom_stop', value: true }); | ||
} else { | ||
await this.sendCommand({ code: 'zoom_control', value: value === 'up' ? '1' : '0' }); | ||
} | ||
} | ||
|
||
async onSettings(event: SettingsEvent<HomeyCameraSettings>): Promise<string | void> { | ||
const tuyaSettingsEvent = TuyaOAuth2Util.filterTuyaSettings<HomeyCameraSettings, TuyaCameraSettings>(event, [ | ||
'motion_switch', | ||
'motion_tracking', | ||
'decibel_switch', | ||
'cry_detection_switch', | ||
'pet_detection', | ||
'motion_sensitivity', | ||
'decibel_sensitivity', | ||
'basic_nightvision', | ||
'basic_device_volume', | ||
'basic_anti_flicker', | ||
'basic_osd', | ||
'basic_flip', | ||
'basic_indicator', | ||
]); | ||
return await TuyaOAuth2Util.onSettings<TuyaCameraSettings>(this, tuyaSettingsEvent, CAMERA_SETTING_LABELS); | ||
} | ||
module.exports = class TuyaOAuth2DeviceCamera extends TuyaDeviceWithCamera { | ||
DOORBELL_TRIGGER_FLOW = 'camera_doorbell_rang'; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,94 +1,3 @@ | ||
import { DEVICE_CATEGORIES } from '../../lib/TuyaOAuth2Constants'; | ||
import TuyaOAuth2Driver, { ListDeviceProperties } from '../../lib/TuyaOAuth2Driver'; | ||
import { constIncludes, getFromMap } from '../../lib/TuyaOAuth2Util'; | ||
import { | ||
type TuyaDeviceDataPointResponse, | ||
TuyaDeviceResponse, | ||
TuyaDeviceSpecificationResponse, | ||
} from '../../types/TuyaApiTypes'; | ||
import type { StandardFlowArgs } from '../../types/TuyaTypes'; | ||
import { | ||
CAMERA_ALARM_CAPABILITIES, | ||
CAMERA_SETTING_LABELS, | ||
COMPLEX_CAMERA_CAPABILITIES, | ||
SIMPLE_CAMERA_CAPABILITIES, | ||
SIMPLE_CAMERA_FLOWS, | ||
} from './TuyaCameraConstants'; | ||
import TuyaDriverWithCamera from '../../lib/camera/driver'; | ||
|
||
module.exports = class TuyaOAuth2DriverCamera extends TuyaOAuth2Driver { | ||
TUYA_DEVICE_CATEGORIES = [DEVICE_CATEGORIES.SECURITY_VIDEO_SURV.SMART_CAMERA] as const; | ||
|
||
async onInit(): Promise<void> { | ||
await super.onInit(); | ||
|
||
for (const capability of SIMPLE_CAMERA_FLOWS.read_write) { | ||
this.homey.flow.getActionCard(`camera_${capability}`).registerRunListener(async (args: StandardFlowArgs) => { | ||
await args.device.triggerCapabilityListener(capability, args.value); | ||
}); | ||
} | ||
|
||
// Apply the same way as in onSettings, but for an individual value | ||
for (const setting of SIMPLE_CAMERA_FLOWS.setting) { | ||
this.addSettingFlowHandler(setting, CAMERA_SETTING_LABELS); | ||
} | ||
} | ||
|
||
onTuyaPairListDeviceProperties( | ||
device: TuyaDeviceResponse, | ||
specifications?: TuyaDeviceSpecificationResponse, | ||
dataPoints?: TuyaDeviceDataPointResponse, | ||
): ListDeviceProperties { | ||
const props = super.onTuyaPairListDeviceProperties(device, specifications, dataPoints); | ||
|
||
for (const status of device.status) { | ||
const capability = status.code; | ||
|
||
// Basic capabilities | ||
if ( | ||
constIncludes(SIMPLE_CAMERA_CAPABILITIES.read_write, capability) || | ||
constIncludes(SIMPLE_CAMERA_CAPABILITIES.read_only, capability) | ||
) { | ||
props.store.tuya_capabilities.push(capability); | ||
props.capabilities.push(capability); | ||
} | ||
|
||
// More complicated capabilities | ||
if (constIncludes(COMPLEX_CAMERA_CAPABILITIES, capability)) { | ||
props.store.tuya_capabilities.push(capability); | ||
} | ||
} | ||
|
||
// Add battery capacity if supported | ||
if (props.store.tuya_capabilities.includes('wireless_electricity')) { | ||
props.capabilities.push('measure_battery'); | ||
} | ||
|
||
// Add privacy mode control if supported | ||
if (props.store.tuya_capabilities.includes('basic_private')) { | ||
props.capabilities.push('onoff'); | ||
} | ||
|
||
// Add camera movement control capabilities if supported | ||
if (props.store.tuya_capabilities.includes('ptz_control') && props.store.tuya_capabilities.includes('ptz_stop')) { | ||
props.capabilities.push('ptz_control_horizontal', 'ptz_control_vertical'); | ||
} | ||
|
||
if (props.store.tuya_capabilities.includes('zoom_control') && props.store.tuya_capabilities.includes('zoom_stop')) { | ||
props.capabilities.push('ptz_control_zoom'); | ||
} | ||
|
||
// Add alarm event capabilities if supported, based on the toggles that are available | ||
// e.g. motion_switch means alarm_motion gets added | ||
if (props.store.tuya_capabilities.includes('initiative_message')) { | ||
// Add the alarm capabilities based on the toggles that are available | ||
for (const capability of props.store.tuya_capabilities) { | ||
const alarmCapability = getFromMap(CAMERA_ALARM_CAPABILITIES, capability); | ||
if (alarmCapability) { | ||
props.capabilities.push(alarmCapability); | ||
} | ||
} | ||
} | ||
|
||
return props; | ||
} | ||
}; | ||
module.exports = class TuyaOAuth2DriverCamera extends TuyaDriverWithCamera {}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,5 @@ | ||
import TuyaOAuth2Device from '../../lib/TuyaOAuth2Device'; | ||
import { TuyaStatus } from '../../types/TuyaTypes'; | ||
import TuyaDeviceWithCamera from '../../lib/camera/device'; | ||
|
||
module.exports = class TuyaOAuth2DeviceDoorbell extends TuyaOAuth2Device { | ||
async onTuyaStatus(status: TuyaStatus, changedStatusCodes: string[]): Promise<void> { | ||
await super.onTuyaStatus(status, changedStatusCodes); | ||
|
||
if (changedStatusCodes.includes('alarm_message')) { | ||
try { | ||
const decoded = JSON.parse(Buffer.from(status['alarm_message'] as string, 'base64').toString('utf8')); | ||
this.log(`Decoded Message: ${JSON.stringify(decoded)}`); | ||
|
||
if (decoded.cmd === 'ipc_doorbell') { | ||
this.log('Doorbell Rang!', decoded); | ||
|
||
this.homey.flow | ||
.getDeviceTriggerCard('doorbell_rang') | ||
.trigger(this) | ||
.catch((err: Error) => this.error(`Error Triggering Doorbell Rang: ${err.message}`)); | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
} catch (err: any) { | ||
this.error(`Error Parsing Message: ${err.message}`); | ||
} | ||
} | ||
} | ||
module.exports = class TuyaOAuth2DeviceDoorbell extends TuyaDeviceWithCamera { | ||
DOORBELL_TRIGGER_FLOW = 'doorbell_rang'; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.