diff --git a/lib/server.js b/lib/server.js index 47c1c0d..68de723 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1114,6 +1114,7 @@ const NAMESPACE_ChannelController = "Alexa.ChannelController"; const NAMESPACE_InputController = "Alexa.InputController"; const NAMESPACE_PlaybackController = "Alexa.PlaybackController"; const NAMESPACE_RangeController = "Alexa.RangeController"; +const NAMESPACE_ModeController = "Alexa.ModeController"; const NAMESPACE_TemperatureSensor = "Alexa.TemperatureSensor"; const NAMESPACE_ContactSensor = "Alexa.ContactSensor"; @@ -2371,6 +2372,10 @@ var handler = function(event, callback) { response = handleRangeController.bind(this)(event); break; + case NAMESPACE_ModeController: + response = handleModeController.bind(this)(event); + break; + default: log.error("Unsupported namespace: " + requestedNamespace); @@ -2809,6 +2814,21 @@ function propertiesFromDevice(device, informId) { } } + if( mapping = device.mappings.ModeController ) { + if( informId === undefined || informId === mapping.informId ) { + //var current = device.fhem.reading2homekit(mapping, device.fhem.cached(mapping.informId)); + var current = device.fhem.cached(mapping.informId); + properties.push( { + "namespace": NAMESPACE_ModeController, + "instance": instanceForMode(device, mapping), + "name": "mode", + "value": valueForMode(device, mapping, current), + "timeOfSample": new Date(Date.now()).toISOString(), + "uncertaintyInMilliseconds": 500 + } ); + } + } + if( mapping = device.mappings.TargetTemperature ) { if( informId === undefined || informId === mapping.informId ) { var current = device.fhem.reading2homekit(mapping, device.fhem.cached(mapping.informId)); @@ -2988,7 +3008,26 @@ var handleReportState = function(event) { return { "context": context, "event": { "header": header, "endpoint": endpoint , "payload": {} } }; } //handleReportState +function instanceForMode(device, mapping) { + var instance = 'fhem.'; + instance += device.name; + instance += '.'; + instance += mapping.instance || mapping.mode || mapping.cmd || 'mode'; + return instance; +} +function valueForMode(device, mapping, value) { + var instance = 'fhem.'; + instance += device.name; + instance += '.'; + instance += mapping.mode || mapping.cmd || 'mode'; + instance += '.'; + instance += value; + return instance; +} + + function deviceToEndpoints(device) { + log.error(device.mappings); //console.log(device); var d = { endpointId: device.uuid_base.replace( /[^\w_\-=#;:?@&]/g, '_' ), manufacturerName: device.type, @@ -3009,6 +3048,7 @@ function deviceToEndpoints(device) { } ); + let mappings = device.mappings; if( device.isOfType('scene') ) { if( device.alexaRoom ) d.friendlyName += ' '+ device.alexaRoom @@ -3055,12 +3095,12 @@ function deviceToEndpoints(device) { } ); - } else if( device.mappings.On && device.mappings.On.cmdOn ) { + } else if( mappings.On && mappings.On.cmdOn ) { d.capabilities.push( { "type": "AlexaInterface", "interface": NAMESPACE_SceneController, "version": "3", - "supportsDeactivation": (device.mappings.On.cmdOff ? true : false) + "supportsDeactivation": (mappings.On.cmdOff ? true : false) } ); } @@ -3069,7 +3109,7 @@ function deviceToEndpoints(device) { return [d]; } - if( device.mappings.Reachable ) + if( mappings.Reachable ) d.capabilities.push( { "type": "AlexaInterface", "interface": "Alexa.EndpointHealth", @@ -3102,7 +3142,7 @@ function deviceToEndpoints(device) { d.displayCategories.push ( 'SWITCH' ); - if( device.mappings.Brightness ) { + if( mappings.Brightness ) { d.capabilities.push( { "type": "AlexaInterface", "interface": NAMESPACE_BrightnessController, @@ -3118,8 +3158,8 @@ function deviceToEndpoints(device) { ); } - if( device.isOfType('blind') && device.mappings.TargetPosition ) { - var mapping = device.mappings.TargetPosition; + if( device.isOfType('blind') && mappings.TargetPosition ) { + var mapping = mappings.TargetPosition; d.capabilities.push( { "type": "AlexaInterface", "interface": NAMESPACE_RangeController, @@ -3331,7 +3371,7 @@ function deviceToEndpoints(device) { log.error(semantics); } - } else if( device.mappings.TargetPosition ) { + } else if( mappings.TargetPosition ) { d.capabilities.push( { "type": "AlexaInterface", "interface": NAMESPACE_PercentageController, @@ -3347,7 +3387,72 @@ function deviceToEndpoints(device) { ); } - if( device.mappings.Hue || device.mappings.RGB ) { + if( device.isOfType('mode') && mappings.ModeController ) { + var mapping = mappings.ModeController; + d.capabilities.push( { + "type": "AlexaInterface", + "interface": NAMESPACE_ModeController, + "instance": instanceForMode(device, mapping), + "version": "3", + "properties": { + "supported": [ + { "name": "mode" }, + ], + "proactivelyReported": device.proactiveEvents, + "retrievable": true, + "nonControllable": false + }, + "capabilityResources": { + "friendlyNames": [ + { + "@type": "text", + "value": { + "text": mapping.mode || "mode", + "locale": mapping.locale || "de-DE" + } + } + ] + }, + "configuration": { + "ordered": (mapping.ordered?true:false) || false, + "supportedModes": [ + ] + }, + "semantics": { + } + } + ); + + + if( typeof mapping.value2homekit === 'object' ) { + let modes = []; + for( let from of Object.keys(mapping.value2homekit) ) { + let to = mapping.value2homekit[from]; + modes.push( { + "value": valueForMode(device, mapping, from), + "modeResources": { + "friendlyNames": [ + { + "@type": "text", + "value": { + "text": to, + "locale": mapping.locale || "de-DE" + } + } + ] + } + } ); + } + let configuration = d.capabilities[d.capabilities.length-1].configuration; + configuration.supportedModes = modes; + //log.error(d.capabilities); + } + + if( !d.displayCategories.length ) + d.displayCategories.push ( 'OTHER' ); + } + + if( mappings.Hue || mappings.RGB ) { d.capabilities.push( { "type": "AlexaInterface", "interface": NAMESPACE_ColorController, @@ -3357,13 +3462,13 @@ function deviceToEndpoints(device) { { "name": "color" } ], "proactivelyReported": device.proactiveEvents, - "retrievable": ((device.mappings.Saturation && device.mappings.Brightness) ? true : false) + "retrievable": ((mappings.Saturation && mappings.Brightness) ? true : false) } } ); } - if( device.mappings.ColorTemperature || device.mappings[FHEM.CustomUUIDs.ColorTemperature] || device.mappings[FHEM.CustomUUIDs.CT] ) { + if( mappings.ColorTemperature || mappings[FHEM.CustomUUIDs.ColorTemperature] || mappings[FHEM.CustomUUIDs.CT] ) { d.capabilities.push( { "type": "AlexaInterface", "interface": NAMESPACE_ColorTemperatureController, @@ -3379,7 +3484,7 @@ function deviceToEndpoints(device) { ); } - if( device.mappings.ContactSensorState || device.mappings.CurrentDoorState ) { + if( mappings.ContactSensorState || mappings.CurrentDoorState ) { d.capabilities.push( { "type": "AlexaInterface", "interface": NAMESPACE_ContactSensor, @@ -3398,7 +3503,7 @@ function deviceToEndpoints(device) { d.displayCategories.push ( 'CONTACT_SENSOR' ); } - if( device.mappings.MotionDetected ) { + if( mappings.MotionDetected ) { d.capabilities.push( { "type": "AlexaInterface", "interface": NAMESPACE_MotionSensor, @@ -3417,8 +3522,8 @@ function deviceToEndpoints(device) { d.displayCategories.push ( 'MOTION_SENSOR' ); } - if( device.mappings.Alarm ) { - var type = device.mappings.Alarm.type || 'fireAlarm'; + if( mappings.Alarm ) { + var type = mappings.Alarm.type || 'fireAlarm'; d.capabilities.push( { "type": "AlexaInterface", "interface": NAMESPACE_SecurityPanelController, @@ -3437,7 +3542,7 @@ function deviceToEndpoints(device) { d.displayCategories.push ( 'SECURITY_PANEL' ); } - if( device.mappings.TargetTemperature ) { + if( mappings.TargetTemperature ) { const capability = { "type": "AlexaInterface", "interface": NAMESPACE_ThermostatController, @@ -3456,8 +3561,8 @@ function deviceToEndpoints(device) { let tModes = []; // https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#thermostat-mode-values // additionally: CUSTOM is possible - if ( device.mappings.TargetHeatingCoolingState && device.mappings.TargetHeatingCoolingState.cmds ) { - device.mappings.TargetHeatingCoolingState.cmds.map((s)=>{ + if ( mappings.TargetHeatingCoolingState && mappings.TargetHeatingCoolingState.cmds ) { + mappings.TargetHeatingCoolingState.cmds.map((s)=>{ const m = s.match("^(AUTO|COOL|ECO|HEAT|OFF|CUSTOM):.*"); if (m) tModes.push(m[1]); @@ -3474,7 +3579,7 @@ function deviceToEndpoints(device) { d.displayCategories.push ( 'THERMOSTAT' ); } - if( device.mappings.CurrentTemperature ) { + if( mappings.CurrentTemperature ) { d.capabilities.push( { "type": "AlexaInterface", "interface": NAMESPACE_TemperatureSensor, @@ -3493,8 +3598,8 @@ function deviceToEndpoints(device) { d.displayCategories.push ( 'TEMPERATURE_SENSOR' ); } - if( device.mappings.LockTargetState - || device.mappings.LockCurrentState ) + if( mappings.LockTargetState + || mappings.LockCurrentState ) d.capabilities.push( { "type": "AlexaInterface", "interface": NAMESPACE_LockController, @@ -3509,7 +3614,7 @@ function deviceToEndpoints(device) { } ); - if( device.mappings.ChannelController ) { + if( mappings.ChannelController ) { if( d.displayCategories.indexOf('TV') === -1 ) d.displayCategories.push ( 'TV' ); d.capabilities.push( { @@ -3527,12 +3632,12 @@ function deviceToEndpoints(device) { ); } - if( device.mappings.InputController ) { + if( mappings.InputController ) { if( d.displayCategories.indexOf('TV') === -1 ) d.displayCategories.push ( 'TV' ); let inputs = []; - if( typeof device.mappings.InputController.value2homekit === 'object' ) - for( var input in device.mappings.InputController.value2homekit ) + if( typeof mappings.InputController.value2homekit === 'object' ) + for( var input in mappings.InputController.value2homekit ) inputs.push( {name : input} ); d.capabilities.push( { "type": "AlexaInterface", @@ -3543,12 +3648,12 @@ function deviceToEndpoints(device) { ); } - if( device.mappings.PlaybackController ) { + if( mappings.PlaybackController ) { if( d.displayCategories.indexOf('TV') === -1 ) d.displayCategories.push ( 'TV' ); let operations = []; - if( typeof device.mappings.PlaybackController.value2homekit === 'object' ) - for( var operation in device.mappings.PlaybackController.value2homekit ) + if( typeof mappings.PlaybackController.value2homekit === 'object' ) + for( var operation in mappings.PlaybackController.value2homekit ) operations.push( operation ); d.capabilities.push( { "type": "AlexaInterface", @@ -3560,7 +3665,7 @@ function deviceToEndpoints(device) { ); } - if( device.mappings[FHEM.CustomUUIDs.Volume] || device.mappings.Mute ) { + if( mappings[FHEM.CustomUUIDs.Volume] || mappings.Mute ) { d.displayCategories.push ( 'SPEAKER' ); var capability = { "type": "AlexaInterface", @@ -3573,12 +3678,12 @@ function deviceToEndpoints(device) { "retrievable": true } }; - if( device.mappings.Mute ) capability.properties.supported.push( { "name": "mute" } ); - if( device.mappings[FHEM.CustomUUIDs.Volume] ) capability.properties.supported.push( { "name": "volume" } ); + if( mappings.Mute ) capability.properties.supported.push( { "name": "mute" } ); + if( mappings[FHEM.CustomUUIDs.Volume] ) capability.properties.supported.push( { "name": "volume" } ); d.capabilities.push( capability ); } - if( device.mappings.On ) { + if( mappings.On ) { if( !d.displayCategories.length ) d.displayCategories.push ( 'SWITCH' ); d.capabilities.push( { "type": "AlexaInterface", @@ -3984,6 +4089,61 @@ var handleRangeController = function(event) { }// handleRangeController +var handleModeController = function(event) { + var device = this.devices[event.directive.endpoint.cookie.device.toLowerCase()]; + if( !device ) + return createError(ERROR3_NO_SUCH_ENDPOINT, undefined, event); + + var mapping; + if( device.mappings.ModeController ) + mapping = device.mappings.ModeController; + else + return createError(ERROR3_INVALID_DIRECTIVE, undefined, event); + var current = parseInt( device.fhem.cached(mapping.informId) ); + + var target; + + var requestedName = event.directive.header.name; + switch (requestedName) { + case 'AdjustMode': + if( !mapping.ordered ) + return createError(ERROR3_INVALID_DIRECTIVE, undefined, event); + + target = mapping.value2homekit.indexOf(current) + event.directive.payload.modeDelta; + if( target >= mapping.value2homekit.length ) + target = 0; + target = mapping.value2homekit[target].to; + break; + case 'SetMode': + target = event.directive.payload.mode; + break; + default: + return createError(ERROR3_INVALID_DIRECTIVE, undefined, event); + break; + } + + if( target !== undefined ) { + target = target.replace( valueForMode(device, mapping, ''), '' ); + device.command( mapping, target ); + } + + var header = createHeader("Alexa", "Response", event); + var context = { + "properties": [ { + "namespace": NAMESPACE_ModeController, + "instance": event.directive.header.instance, + "name": "mode", + "value": target, + "timeOfSample": new Date(Date.now()).toISOString(), + "uncertaintyInMilliseconds": 500 + } ] + }; + var endpoint = { "scope": event.directive.endpoint.scope, "endpointId": event.directive.endpoint.endpointId}; + + return { "context": context, "event": { "header": header, "endpoint": endpoint , "payload": {} } }; + +}// handleModeController + var handleThermostatController = function(event) { var device = this.devices[event.directive.endpoint.cookie.device.toLowerCase()]; if( !device )