Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .homeycompose/capabilities/vividstorm_lock_down.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"type": "button",
"title": {
"en": "Set Lower Limit"
},
"getable": false,
"setable": true,
"uiComponent": "button",
"icon": "/assets/capabilities/lock.svg"
Comment thread
Simon-CR marked this conversation as resolved.
}
10 changes: 10 additions & 0 deletions .homeycompose/capabilities/vividstorm_lock_up.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"type": "button",
"title": {
"en": "Set Upper Limit"
},
"getable": false,
"setable": true,
"uiComponent": "button",
"icon": "/assets/capabilities/lock.svg"
Comment thread
Simon-CR marked this conversation as resolved.
}
24 changes: 23 additions & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -6181,7 +6181,9 @@
{
"capabilities": [
"windowcoverings_state",
"windowcoverings_set"
"windowcoverings_set",
"vividstorm_lock_up",
"vividstorm_lock_down"
],
"connectivity": [
"cloud"
Expand Down Expand Up @@ -6586,6 +6588,26 @@
"setable": true,
"uiComponent": "button",
"icon": "assets/capabilities/alarm_siren.svg"
},
"vividstorm_lock_up": {
"type": "button",
"title": {
"en": "Set Upper Limit"
},
"getable": false,
"setable": true,
"uiComponent": "button",
"icon": "/assets/capabilities/lock.svg"
Comment thread
Simon-CR marked this conversation as resolved.
},
"vividstorm_lock_down": {
"type": "button",
"title": {
"en": "Set Lower Limit"
},
"getable": false,
"setable": true,
"uiComponent": "button",
"icon": "/assets/capabilities/lock.svg"
Comment thread
Simon-CR marked this conversation as resolved.
Comment thread
Simon-CR marked this conversation as resolved.
}
}
}
1 change: 1 addition & 0 deletions assets/capabilities/lock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 12 additions & 1 deletion drivers/window_coverings/TuyaWindowCoveringsConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,21 @@ export const WINDOW_COVERINGS_CAPABILITY_MAPPING = {
position: 'windowcoverings_set',
percent_control: 'windowcoverings_set',
percent_state: 'windowcoverings_set',
// Vividstorm / Curtain Settings
// Mapping 'border' so it passes allowlists. Logic handled in device.ts sends 'up'/'down' values for 'border' code.
border: 'vividstorm_lock_up',
} as const;

export const VIVIDSTORM_PRODUCT_IDS = ['lfkr93x0ukp5gaia']; // Vividstorm Motorised Screens

export const WINDOW_COVERINGS_CAPABILITIES = {
read_write: ['control', 'position', 'mach_operate', 'percent_control'],
read_write: [
'control',
'position',
'mach_operate',
'percent_control',
'border',
],
setting: ['opposite', 'control_back'],
} as const;

Expand Down
14 changes: 14 additions & 0 deletions drivers/window_coverings/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
WINDOW_COVERINGS_SETTING_LABELS,
HomeyWindowCoveringsSettings,
TuyaWindowCoveringsSettings,
VIVIDSTORM_PRODUCT_IDS,
} from './TuyaWindowCoveringsConstants';

module.exports = class TuyaOAuth2DeviceWindowCoverings extends TuyaOAuth2Device {
Expand Down Expand Up @@ -43,6 +44,19 @@ module.exports = class TuyaOAuth2DeviceWindowCoverings extends TuyaOAuth2Device
this.sendCommand({ code: code, value: Math.round(value * 100) }),
);
}
// Vividstorm Lock Listeners
if (VIVIDSTORM_PRODUCT_IDS.includes(this.getData().productId)) {
if (this.hasCapability('vividstorm_lock_up')) {
this.registerCapabilityListener('vividstorm_lock_up', async () => {
return await this.sendCommand({ code: 'border', value: 'up' });
});
}
if (this.hasCapability('vividstorm_lock_down')) {
this.registerCapabilityListener('vividstorm_lock_down', async () => {
return await this.sendCommand({ code: 'border', value: 'down' });
});
}
}
}

async onTuyaStatus(status: TuyaStatus, changed: string[]): Promise<void> {
Expand Down
21 changes: 20 additions & 1 deletion drivers/window_coverings/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,20 @@ import {
TuyaDeviceResponse,
TuyaDeviceSpecificationResponse,
} from '../../types/TuyaApiTypes';
import { WINDOW_COVERINGS_CAPABILITIES, WINDOW_COVERINGS_CAPABILITY_MAPPING } from './TuyaWindowCoveringsConstants';
import {
WINDOW_COVERINGS_CAPABILITIES,
WINDOW_COVERINGS_CAPABILITY_MAPPING,
VIVIDSTORM_PRODUCT_IDS,
} from './TuyaWindowCoveringsConstants';

module.exports = class TuyaOAuth2DriverWindowCoverings extends TuyaOAuth2Driver {
TUYA_DEVICE_CATEGORIES = [DEVICE_CATEGORIES.SMALL_HOME_APPLIANCES.CURTAIN] as const;
Comment thread
Simon-CR marked this conversation as resolved.

onTuyaPairListDeviceFilter(device: TuyaDeviceResponse): boolean {
if (VIVIDSTORM_PRODUCT_IDS.includes(device.product_id)) return true;
return super.onTuyaPairListDeviceFilter(device);
}
Comment thread
Simon-CR marked this conversation as resolved.

onTuyaPairListDeviceProperties(
device: TuyaDeviceResponse,
specifications?: TuyaDeviceSpecificationResponse,
Expand All @@ -32,6 +41,16 @@ module.exports = class TuyaOAuth2DriverWindowCoverings extends TuyaOAuth2Driver
}
}

// Vividstorm Specific: Explicitly add lock capabilities if 'border' is present
if (VIVIDSTORM_PRODUCT_IDS.includes(device.product_id) && props.store.tuya_capabilities.includes('border')) {
const vividstormCapabilities = ['vividstorm_lock_up', 'vividstorm_lock_down'];
const uniqueCapabilities = new Set(props.capabilities);
for (const capability of vividstormCapabilities) {
uniqueCapabilities.add(capability);
}
props.capabilities = Array.from(uniqueCapabilities);
}

return props;
}
};
3 changes: 2 additions & 1 deletion lib/TuyaHaClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,8 @@ export default class TuyaHaClient extends OAuth2Client<TuyaHaToken> {
}

private refreshApiToken(): void {
if (Date.now() - this.lastTokenSave < (this.tokenExpireTime - 100) * 1000) {
const token = this.getToken();
if (token && Date.now() - this.lastTokenSave < (this.tokenExpireTime - 100) * 1000) {
// No need to refresh
return;
}
Expand Down
24 changes: 20 additions & 4 deletions lib/TuyaOAuth2Driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,15 +222,22 @@ export default class TuyaOAuth2Driver extends OAuth2Driver<TuyaHaClient> {
}

async onPairListDevices({ oAuth2Client }: { oAuth2Client: TuyaHaClient }): Promise<OAuth2DeviceResult[]> {
const devices = await oAuth2Client.getDevices();
let devices: TuyaDeviceResponse[] = [];
try {
devices = await oAuth2Client.getDevices();
} catch (err) {
this.error('Failed to get devices from cloud. Automatic discovery unavailable; proceed with manual pairing.', err instanceof Error ? err.message : err);
}

const filteredDevices = devices.filter(device => {
return !oAuth2Client.isRegistered(device.product_id, device.id) && this.onTuyaPairListDeviceFilter(device);
});

const listDevices: OAuth2DeviceResult[] = [];

this.log('Listing devices to pair:');

for (const device of filteredDevices) {
const devicePromises = filteredDevices.map(async device => {
this.log('Device:', JSON.stringify(TuyaOAuth2Util.redactFields(device)));
const deviceSpecs =
(await oAuth2Client
Expand All @@ -249,14 +256,23 @@ export default class TuyaOAuth2Driver extends OAuth2Driver<TuyaHaClient> {

const deviceProperties = this.onTuyaPairListDeviceProperties({ ...device }, deviceSpecs, dataPoints);

listDevices.push({
return {
...deviceProperties,
name: device.name,
data: {
deviceId: device.id,
productId: device.product_id,
},
});
};
});

const results = await Promise.allSettled(devicePromises);
for (const result of results) {
if (result.status === 'fulfilled') {
listDevices.push(result.value);
} else {
this.error('Failed to prepare device for pairing', result.reason);
}
}
return listDevices;
}
Expand Down
Loading