import { getCurrentLanguage } from './i18n' function safeJsonParse(value, fallback = null) { if (typeof value !== 'string') { return value } const text = value.trim() if (!text) { return fallback } const looksLikeJson = ( (text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']')) ) if (!looksLikeJson) { return fallback } try { return JSON.parse(text) } catch (error) { return fallback } } function pickFirstFilledValue(...values) { for (let index = 0; index < values.length; index += 1) { const value = values[index] if (value === undefined || value === null) { continue } const text = String(value).trim() if (text) { return text } } return '' } function getSmartDeviceText(key = '', params = {}) { const isEn = getCurrentLanguage() === 'en' const map = { commonSwitch: isEn ? 'General Switch' : '通用开关', slideControl: isEn ? 'Slider Control' : '滑动控制', curtainButton: isEn ? 'Curtain Button' : '窗帘按钮', unknownFunction: isEn ? 'Unknown Function' : '未知功能', fullyOpen: isEn ? 'Fully Open' : '已全开', fullyClosed: isEn ? 'Fully Closed' : '已全关', paused: isEn ? 'Paused' : '已暂停', openness: isEn ? `Open Level ${params.value}%` : `开合度 ${params.value}%`, controllable: isEn ? 'Ready to Control' : '可控制', currentValue: isEn ? `Current Value ${params.value}%` : `当前值 ${params.value}%`, adjustable: isEn ? 'Adjustable' : '可调节', enabled: isEn ? 'Enabled' : '已开启', disabled: isEn ? 'Disabled' : '已关闭', unnamedDevice: isEn ? 'Unnamed Device' : '未命名设备' } return map[key] || '' } function collectObjectArray(value, depth = 0) { if (!value || depth > 6) { return [] } const normalizedValue = typeof value === 'string' ? (safeJsonParse(value, value)) : value if (Array.isArray(normalizedValue)) { if (normalizedValue.some(item => item && typeof item === 'object' && !Array.isArray(item))) { return normalizedValue } for (let index = 0; index < normalizedValue.length; index += 1) { const nestedList = collectObjectArray(normalizedValue[index], depth + 1) if (nestedList.length) { return nestedList } } return [] } if (!normalizedValue || typeof normalizedValue !== 'object') { return [] } const candidateKeys = ['data', 'list', 'rows', 'items', 'records', 'result', 'payload', '$values'] for (let index = 0; index < candidateKeys.length; index += 1) { const key = candidateKeys[index] if (!Object.prototype.hasOwnProperty.call(normalizedValue, key)) { continue } const nestedList = collectObjectArray(normalizedValue[key], depth + 1) if (nestedList.length) { return nestedList } } return [] } function isSmartDeviceObject(value = {}) { if (!value || typeof value !== 'object' || Array.isArray(value)) { return false } if (Array.isArray(value.functions)) { return true } const deviceKeys = [ 'deviceId', 'deviceNo', 'deviceName', 'platformDeviceId', 'deviceType', 'facilityCode', 'facilityType', 'currentStatus' ] return deviceKeys.some(key => Object.prototype.hasOwnProperty.call(value, key)) } function unwrapSmartDevicePayload(value, depth = 0) { if (!value || depth > 6) { return {} } const normalizedValue = typeof value === 'string' ? (safeJsonParse(value, value)) : value if (!normalizedValue || typeof normalizedValue !== 'object') { return {} } if (Array.isArray(normalizedValue)) { for (let index = 0; index < normalizedValue.length; index += 1) { const nestedDevice = unwrapSmartDevicePayload(normalizedValue[index], depth + 1) if (Object.keys(nestedDevice).length) { return nestedDevice } } return {} } if (isSmartDeviceObject(normalizedValue)) { return normalizedValue } const candidateKeys = ['data', 'result', 'payload', 'item', 'device', 'record'] for (let index = 0; index < candidateKeys.length; index += 1) { const key = candidateKeys[index] if (!Object.prototype.hasOwnProperty.call(normalizedValue, key)) { continue } const nestedDevice = unwrapSmartDevicePayload(normalizedValue[key], depth + 1) if (Object.keys(nestedDevice).length) { return nestedDevice } } return {} } function isCurtainDeviceLike(device = {}) { const numericDeviceType = Number(device.deviceType) if (numericDeviceType === 2) { return true } const name = pickFirstFilledValue(device.deviceName, device.deviceNo).toLowerCase() return name.includes('窗帘') || name.includes('curtain') } function normalizeFunctionCode(functionCode = '', device = {}) { const normalizedCode = String(functionCode || '').trim().toLowerCase() if (!normalizedCode) { return '' } if (['light', 'switch', 'common:switch'].includes(normalizedCode)) { return 'common:switch' } if (['slide', 'common:slide'].includes(normalizedCode)) { return 'common:slide' } if (['curtain', 'curtain:button'].includes(normalizedCode)) { return 'curtain:button' } if (normalizedCode === 'button') { return isCurtainDeviceLike(device) ? 'curtain:button' : 'button' } return normalizedCode } function normalizeFunctionItem(item = {}, device = {}) { const schema = item.commandSchema === undefined ? null : (typeof item.commandSchema === 'string' ? (safeJsonParse(item.commandSchema, item.commandSchema)) : item.commandSchema) const rawFunctionCode = pickFirstFilledValue(item.functionCode, item.controlType) const normalizedCode = normalizeFunctionCode(rawFunctionCode, device) return { ...item, functionCode: pickFirstFilledValue(item.functionCode, item.controlType), functionDes: pickFirstFilledValue(item.functionDes, item.description), controlType: normalizedCode, commandSchema: schema } } function normalizeSchemaOption(option, index = 0) { if (option && typeof option === 'object' && !Array.isArray(option)) { const value = Object.prototype.hasOwnProperty.call(option, 'value') ? option.value : ( Object.prototype.hasOwnProperty.call(option, 'code') ? option.code : (Object.prototype.hasOwnProperty.call(option, 'key') ? option.key : index) ) return { label: pickFirstFilledValue(option.label, option.text, option.name, option.title, value), value } } return { label: String(option), value: option } } function normalizeSchemaOptions(rawOptions) { if (!Array.isArray(rawOptions)) { return [] } return rawOptions.map((item, index) => normalizeSchemaOption(item, index)) } function isSchemaMetaKey(key = '') { return [ 'type', 'title', 'description', 'required', '$schema', 'properties', 'fields', 'params', 'definitions' ].includes(String(key || '').trim()) } function inferSchemaFieldType(fieldName = '', rawField) { if (Array.isArray(rawField)) { return 'enum' } if (typeof rawField === 'string') { const normalizedType = rawField.trim().toLowerCase() if (['boolean', 'bool'].includes(normalizedType)) { return 'boolean' } if (['number', 'integer', 'int', 'float', 'double'].includes(normalizedType)) { return 'number' } } if (!rawField || typeof rawField !== 'object') { return typeof rawField === 'boolean' ? 'boolean' : (typeof rawField === 'number' ? 'number' : 'string') } const explicitType = String( rawField.type || rawField.valueType || rawField.inputType || rawField.dataType || '' ).trim().toLowerCase() if (rawField.enum || rawField.options || rawField.oneOf || rawField.values) { return 'enum' } if (['boolean', 'bool'].includes(explicitType)) { return 'boolean' } if (['number', 'integer', 'int', 'float', 'double'].includes(explicitType)) { return 'number' } if (['string', 'text'].includes(explicitType)) { return 'string' } if (typeof rawField.default === 'boolean') { return 'boolean' } if (typeof rawField.default === 'number') { return 'number' } if (fieldName === 'value' && typeof rawField === 'object' && !Array.isArray(rawField)) { return 'number' } return 'string' } function normalizeSchemaField(fieldName = '', rawField = {}) { const normalizedType = inferSchemaFieldType(fieldName, rawField) const fieldObject = rawField && typeof rawField === 'object' && !Array.isArray(rawField) ? rawField : {} const optionCandidates = fieldObject.enum || fieldObject.options || fieldObject.oneOf || fieldObject.values || rawField return { name: String(fieldName || '').trim(), label: pickFirstFilledValue( fieldObject.label, fieldObject.title, fieldObject.name, fieldObject.description, fieldName ), type: normalizedType, required: !!fieldObject.required, min: Number.isFinite(Number(fieldObject.min)) ? Number(fieldObject.min) : (Number.isFinite(Number(fieldObject.minimum)) ? Number(fieldObject.minimum) : undefined), max: Number.isFinite(Number(fieldObject.max)) ? Number(fieldObject.max) : (Number.isFinite(Number(fieldObject.maximum)) ? Number(fieldObject.maximum) : undefined), step: Number.isFinite(Number(fieldObject.step)) ? Number(fieldObject.step) : (Number.isFinite(Number(fieldObject.multipleOf)) ? Number(fieldObject.multipleOf) : 1), placeholder: pickFirstFilledValue(fieldObject.placeholder, fieldObject.hint, fieldObject.description), defaultValue: Object.prototype.hasOwnProperty.call(fieldObject, 'default') ? fieldObject.default : undefined, options: normalizeSchemaOptions(optionCandidates) } } function parseCurrentStatus(value) { if (value === undefined || value === null) { return null } if (typeof value === 'object') { return value } const parsedJson = safeJsonParse(value, null) if (parsedJson !== null) { return parsedJson } const text = String(value).trim() if (!text) { return null } if (text === 'true' || text === 'false') { return { value: text === 'true' } } const numericValue = Number(text) if (Number.isFinite(numericValue) && String(numericValue) === text) { return { value: numericValue } } return { text } } function resolveFunctionCurrentStatus(device = {}, preferredControlType = '') { const functionList = Array.isArray(device.functions) ? device.functions : [] if (!functionList.length) { return null } const preferredCategory = getControlTypeCategory(preferredControlType) const preferredFunction = functionList.find((item) => { const itemControlType = pickFirstFilledValue(item && item.controlType, item && item.functionCode) return getControlTypeCategory(itemControlType) === preferredCategory && parseCurrentStatus(item && item.currentStatus) }) if (preferredFunction) { return preferredFunction.currentStatus } const fallbackFunction = functionList.find(item => parseCurrentStatus(item && item.currentStatus)) return fallbackFunction ? fallbackFunction.currentStatus : null } function resolveDeviceCurrentStatus(device = {}, preferredControlType = '') { const deviceStatus = parseCurrentStatus(device.currentStatus) if (deviceStatus) { return device.currentStatus } return resolveFunctionCurrentStatus(device, preferredControlType) } const CONTROL_TYPE_CATEGORY_MAP = { light: 'switch', switch: 'switch', 'common:switch': 'switch', slide: 'slide', 'common:slide': 'slide', curtain: 'curtain', 'curtain:button': 'curtain' } export const SUPPORTED_DEVICE_CONTROL_TYPES = ['common:switch', 'common:slide', 'curtain:button', 'light', 'switch', 'curtain'] export function getControlTypeCategory(controlType = '') { const normalizedType = String(controlType || '').trim().toLowerCase() return CONTROL_TYPE_CATEGORY_MAP[normalizedType] || normalizedType } export function isSupportedDeviceControlType(controlType = '') { const normalizedType = String(controlType || '').trim().toLowerCase() return SUPPORTED_DEVICE_CONTROL_TYPES.includes(normalizedType) || ['switch', 'slide', 'curtain'].includes(getControlTypeCategory(normalizedType)) } export function getDeviceControlLabel(controlType = '') { const category = getControlTypeCategory(controlType) if (category === 'switch') { return getSmartDeviceText('commonSwitch') } if (category === 'slide') { return getSmartDeviceText('slideControl') } if (category === 'curtain') { return getSmartDeviceText('curtainButton') } return String(controlType || '').trim() || getSmartDeviceText('unknownFunction') } export function inferDeviceControlType(device = {}) { const numericDeviceType = Number(device.deviceType) if (numericDeviceType === 2) { return 'curtain:button' } const functionList = Array.isArray(device.functions) ? device.functions : [] const normalizedFunctionTypes = functionList .map(item => normalizeFunctionCode( pickFirstFilledValue(item && item.functionCode, item && item.controlType), device )) .filter(Boolean) if (normalizedFunctionTypes.some(item => getControlTypeCategory(item) === 'switch')) { return 'common:switch' } if (normalizedFunctionTypes.some(item => getControlTypeCategory(item) === 'slide')) { return 'common:slide' } if (normalizedFunctionTypes.some(item => getControlTypeCategory(item) === 'curtain')) { return 'curtain:button' } if (numericDeviceType === 1 || numericDeviceType === 0) { return 'common:switch' } const name = pickFirstFilledValue(device.deviceName, device.deviceNo).toLowerCase() if (name.includes('窗帘') || name.includes('curtain')) { return 'curtain:button' } if (name.includes('开关') || name.includes('switch')) { return 'common:switch' } if (name.includes('亮度') || name.includes('滑块') || name.includes('slider') || name.includes('slide')) { return 'common:slide' } return 'common:switch' } export function getDeviceIconClass(controlType = '') { const category = getControlTypeCategory(controlType) if (category === 'curtain') { return 'tn-icon-hardware-fill' } if (category === 'slide') { return 'tn-icon-more-horizontal' } if (category === 'switch') { return 'tn-icon-power' } return 'tn-icon-light-fill' } export function buildDeviceStateText(device = {}) { const controlType = inferDeviceControlType(device) const category = getControlTypeCategory(controlType) const status = parseCurrentStatus(resolveDeviceCurrentStatus(device, controlType)) if (category === 'curtain' || Number(device.deviceType) === 2) { const action = pickFirstFilledValue( status && status.command, status && status.action, status && status.state, status && status.text ).toLowerCase() const percentValue = Number( status && ( status.value !== undefined ? status.value : (status.percent !== undefined ? status.percent : status.position) ) ) if (action === 'open') { return getSmartDeviceText('fullyOpen') } if (action === 'close') { return getSmartDeviceText('fullyClosed') } if (action === 'stop') { return getSmartDeviceText('paused') } if (Number.isFinite(percentValue)) { return getSmartDeviceText('openness', { value: percentValue }) } return getSmartDeviceText('controllable') } if (category === 'slide') { const slideValue = Number( status && ( status.value !== undefined ? status.value : (status.percent !== undefined ? status.percent : status.position) ) ) if (Number.isFinite(slideValue)) { return getSmartDeviceText('currentValue', { value: slideValue }) } return getSmartDeviceText('adjustable') } const switchValue = status && status.value if (typeof switchValue === 'boolean') { return switchValue ? getSmartDeviceText('enabled') : getSmartDeviceText('disabled') } const switchCommand = String( (status && ( status.command !== undefined ? status.command : (status.action !== undefined ? status.action : status.text) )) || '' ).trim().toLowerCase() if (switchCommand === 'on') { return getSmartDeviceText('enabled') } if (switchCommand === 'off') { return getSmartDeviceText('disabled') } if (status && status.text) { return String(status.text) } return getSmartDeviceText('controllable') } export function isDeviceActive(device = {}) { const controlType = inferDeviceControlType(device) const category = getControlTypeCategory(controlType) const status = parseCurrentStatus(resolveDeviceCurrentStatus(device, controlType)) if (!status) { return false } if (category === 'curtain' || Number(device.deviceType) === 2) { const action = pickFirstFilledValue( status.command, status.action, status.state, status.text ).toLowerCase() const value = Number( status.value !== undefined ? status.value : (status.percent !== undefined ? status.percent : status.position) ) if (action === 'open') { return true } if (action === 'close') { return false } if (Number.isFinite(value)) { return value > 0 } return false } if (category === 'slide') { const value = Number( status.value !== undefined ? status.value : (status.percent !== undefined ? status.percent : status.position) ) return Number.isFinite(value) ? value > 0 : false } const switchCommand = String( ( status.command !== undefined ? status.command : (status.action !== undefined ? status.action : status.text) ) || '' ).trim().toLowerCase() if (switchCommand === 'on') { return true } if (switchCommand === 'off') { return false } return !!status.value } export function normalizeSmartDevice(item = {}) { const sourceDevice = unwrapSmartDevicePayload(item) const functionList = Array.isArray(sourceDevice.functions) ? sourceDevice.functions.filter(entry => entry && typeof entry === 'object').map(entry => normalizeFunctionItem(entry, sourceDevice)) : [] const normalizedDevice = { ...sourceDevice, functions: functionList } const controlType = inferDeviceControlType(normalizedDevice) const currentStatus = resolveDeviceCurrentStatus(normalizedDevice, controlType) return { ...normalizedDevice, currentStatus, controlType, iconClass: getDeviceIconClass(controlType), active: isDeviceActive(normalizedDevice), stateText: buildDeviceStateText(normalizedDevice), roomName: pickFirstFilledValue(sourceDevice.roomName), deviceName: pickFirstFilledValue(sourceDevice.deviceName, sourceDevice.deviceNo, sourceDevice.platformDeviceId, getSmartDeviceText('unnamedDevice')) } } export function getDeviceFunctions(device = {}, includeFallback = true) { const functionList = Array.isArray(device.functions) ? device.functions.filter(item => item && typeof item === 'object').map(item => normalizeFunctionItem(item, device)) : [] if (functionList.length) { return functionList } if (!includeFallback) { return [] } const fallbackType = inferDeviceControlType(device) return fallbackType ? [{ functionCode: fallbackType, functionDes: getDeviceControlLabel(fallbackType), controlType: fallbackType, commandSchema: null, isFallback: true }] : [] } export function buildCommandSchemaFields(commandSchema = null) { if (!commandSchema) { return [] } if (Array.isArray(commandSchema)) { return commandSchema .map((item, index) => normalizeSchemaField( pickFirstFilledValue(item && item.name, item && item.key, `field${index + 1}`), item )) .filter(item => item.name) } if (typeof commandSchema !== 'object') { return [] } let schemaSource = null if (commandSchema.properties && typeof commandSchema.properties === 'object') { schemaSource = commandSchema.properties } else if (commandSchema.fields && typeof commandSchema.fields === 'object') { schemaSource = commandSchema.fields } else if (commandSchema.params && typeof commandSchema.params === 'object') { schemaSource = commandSchema.params } else { const candidateEntries = Object.keys(commandSchema) .filter(key => !isSchemaMetaKey(key)) .filter(key => { const value = commandSchema[key] return value !== undefined && value !== null && typeof value !== 'function' }) if (candidateEntries.length) { schemaSource = candidateEntries.reduce((result, key) => { result[key] = commandSchema[key] return result }, {}) } } if (!schemaSource || typeof schemaSource !== 'object') { return [] } return Object.keys(schemaSource) .map(key => normalizeSchemaField(key, schemaSource[key])) .filter(item => item.name) } export function normalizeSmartDeviceList(response) { const payload = response && response.data !== undefined ? response.data : response const deviceList = collectObjectArray(payload) if (deviceList.length) { return deviceList .filter(item => item && typeof item === 'object' && !Array.isArray(item)) .map(item => normalizeSmartDevice(item)) } const singleDevice = unwrapSmartDevicePayload(payload) return Object.keys(singleDevice).length ? [normalizeSmartDevice(singleDevice)] : [] } export function patchDeviceStatus(device = {}, command = {}, forcedControlType = '') { const controlType = String(forcedControlType || '').trim().toLowerCase() || inferDeviceControlType(device) const category = getControlTypeCategory(controlType) if (category === 'curtain') { const action = String(command.command || command.action || command.value || '').trim().toLowerCase() if (!action) { return normalizeSmartDevice(device) } return normalizeSmartDevice({ ...device, currentStatus: { command: action } }) } if (category === 'slide') { const value = Number( command.value !== undefined ? command.value : (command.percent !== undefined ? command.percent : command.position) ) return normalizeSmartDevice({ ...device, currentStatus: { value: Number.isFinite(value) ? value : 0 } }) } const switchCommand = String( ( command.command !== undefined ? command.command : ( command.action !== undefined ? command.action : ( command.value === true ? 'on' : (command.value === false ? 'off' : command.value) ) ) ) || '' ).trim().toLowerCase() if (switchCommand) { return normalizeSmartDevice({ ...device, currentStatus: { command: switchCommand } }) } return normalizeSmartDevice(device) }