Fujitsu Airstage Control

In this tutorial, I would like to share with you how I configured Openhab to control my Fujitsu Airstage air conditioner.

My indoor unit: Fujitsu ASYG12KMCF (Comes with WIFI, no other/additional dongle required)

Note: I only want to control my air conditioner on the local network and am NOT using the Fujitsu cloud connection.

The air conditioning system offers a REST interface, which unfortunately is not officially documented. I used the following repository as reference: GitHub - danielkaldheim/ha_airstage: Connects your Fujitsu Airstage air conditioner to Home Assistant.

REST Request to retrieve the current status:
The device_id is the MAC address of your device, without colons.

curl --location 'http://192.168.xxx.xx/GetParam' \
--header 'Content-Type: text/plain' \
--data '{
    "device_id": "94BBxxxxxx529",
    "device_sub_id": 0,
    "req_id": "",
    "modified_by": "",
    "set_level": "03",
    "list": [
        "iu_set_tmp",
        "iu_onoff",
        "iu_op_mode"
    ]
}'

REST Reqeust to tun on the AC:
iu_onoff 1 represents ON, whereby iu_onoff = represents OFF

curl --location 'http://192.168.xxx.xx/SetParam' \
--header 'Content-Type: text/plain' \
--data '{
    "device_id": "94BBxxxxxx529",
    "device_sub_id": 0,
    "req_id": "",
    "modified_by": "",
    "set_level": "02",
    "value": {
        "iu_onoff": "1"
    }
}'

REST Request to change the target temperatur:
The target temperatur needs to be handed over multiplied by 10, e.g 190 for 19°C

curl --location 'http://192.168.178.55/SetParam' \
--header 'Content-Type: text/plain' \
--data '{
    "device_id": "94BB4393442F",
    "device_sub_id": 0,
    "req_id": "",
    "modified_by": "",
    "set_level": "02",
    "value": {
        "iu_set_tmp": "190"
    }
}'

REST Request to change the mode:
“AUTO”: “0”,
“COOL”: “1”,
“DRY”: “2”,
“FAN”: “3”,
“HEAT”: “4”

curl --location 'http://192.168.178.55/SetParam' \
--header 'Content-Type: text/plain' \
--data '{
    "device_id": "94BB4393442F",
    "device_sub_id": 0,
    "req_id": "",
    "modified_by": "",
    "set_level": "02",
    "value": {
        "iu_op_mode":"1"
    }
}'

Setup in Openhab:

I’m using the HTTP Binding, JSONPATH Transformation & Java Script. Please install the same, if not done already.

HTTP Thing configuration:

Channels:

Off/On:

  • State URL Extension: GetParam
  • Command URL Extenson: SetParam
  • State Content: { “device_id”: “yourMAC”, “device_sub_id”: 0, “req_id”: “”, “modified_by”: “”, “set_level”: “03”, “list”: [ “iu_onoff” ] }
  • State Transformation: JS:airstageOnoffState.js
  • Command Transformation: JS:airstageSendOnoff.js
  • Mode: Read & Write

Temperature:

  • State URL Extension: GetParam
  • Command URL Extenson: SetParam
  • State Content: { “device_id”: “yourMAC”, “device_sub_id”: 0, “req_id”: “”, “modified_by”: “”, “set_level”: “03”, “list”: [ “iu_set_tmp” ] }
  • State Transformation: JSONPATH:$.value.iu_set_tmp ∩ JS:airstageDivideBy10.js
  • Command Transformation: JS:airstageSendTemp.js
  • Mode: Read & Write

Mode:

  • State URL Extension: GetParam
  • Command URL Extenson: SetParam
  • State Content: { “device_id”: “yourMAC”, “device_sub_id”: 0, “req_id”: “”, “modified_by”: “”, “set_level”: “03”, “list”: [ “iu_op_mode” ] }
  • State Transformation: JS:airstageModeState.js
  • Command Transformation: JS:airstageMode.js
  • Mode: Read & Write

JS files to be stored in your openHAB-conf\transform folder:

airstageDivideBy10.js

(function(i) {
    if (isNaN(i)) return i;
    return parseFloat(i) / 10.0;
})(input)

airstageMode.js

(function(input) {
    var modes = {
        "AUTO": "0",
        "COOL": "1",
        "DRY": "2",
        "FAN": "3",
        "HEAT": "4"
    };
    
    var val = modes[input] || "0";

    var body = {
        "device_id": "yourMAC",
        "device_sub_id": 0,
        "req_id": "",
        "modified_by": "",
        "set_level": "02",
        "value": {
            "iu_op_mode": val
        }
    };
    return JSON.stringify(body);
})(input)

airstageModeState.js

(function(input) {
    var json = JSON.parse(input);
    var val = json.value.iu_op_mode; // Das ist z.B. "1"
    
    var map = {
        "0": "AUTO",
        "1": "COOL",
        "2": "DRY",
        "3": "FAN",
        "4": "HEAT"
    };
    
    return map[val] || "UNKNOWN";
})(input)

airstageOnoffState.js

(function(input) {
    var json = JSON.parse(input);
    var val = json.value.iu_onoff;
    return (val == "1") ? "ON" : "OFF";
})(input)

airstageSendOnoff.js

(function(input) {
    var val = (input == "ON") ? "1" : "0";
    
    var body = {
        "device_id": "yourMAC",
        "device_sub_id": 0,
        "req_id": "",
        "modified_by": "",
        "set_level": "02",
        "value": {
            "iu_onoff": val
        }
    };
    
    return JSON.stringify(body);
})(input)

airstageSendTemp.js

(function(input) {
    var numericValue = parseFloat(input);
    var transformedValue = (numericValue * 10).toFixed(0);

    var body = {
        "device_id": "yourMAC",
        "device_sub_id": 0,
        "req_id": "",
        "modified_by": "",
        "set_level": "02",
        "value": {
            "iu_set_tmp": transformedValue.toString()
        }
    };

    return JSON.stringify(body);
})(input)

In my specific case, I have three devices, which means that the file names of the JS files have been slightly modified.

UID: http:url:88d294d182
label: Fujitsu Airstage Wohnzimmer
thingTypeUID: http:url
configuration:
  authMode: BASIC
  ignoreSSLErrors: false
  baseURL: http://192.168.178.54/
  delay: 0
  stateMethod: POST
  refresh: 60
  commandMethod: POST
  timeout: 3000
  bufferSize: 2048
channels:
  - id: last-failure
    channelTypeUID: http:request-date-time
    label: Last Failure
    configuration: {}
  - id: last-success
    channelTypeUID: http:request-date-time
    label: Last Success
    configuration: {}
  - id: Soll-Temperatur
    channelTypeUID: http:number
    label: Soll-Temperatur
    configuration:
      stateContent: '{     "device_id": "94BB4379C529",     "device_sub_id":
        0,     "req_id": "",     "modified_by": "",     "set_level":
        "03",     "list": [         "iu_set_tmp"     ] }'
      commandTransformation:
        - JS:airstageSendTemp_Wohnzimmer.js
      stateExtension: GetParam
      commandExtension: SetParam
      stateTransformation:
        - JSONPATH:$.value.iu_set_tmp ∩ JS:airstageDivideBy10.js
  - id: Schalten
    channelTypeUID: http:switch
    label: Aus/Ein
    configuration:
      onValue: ON
      stateContent: '{     "device_id":
        "94BB4379C529",     "device_sub_id":         0,     "req_id":
        "",     "modified_by": "",     "set_level":         "03",     "list":
        [         "iu_onoff"     ] }'
      commandTransformation:
        - JS:airstageOnoffState_Wohnzimmer.js
      offValue: OFF
      stateExtension: GetParam
      commandExtension: SetParam
      stateTransformation:
        - JS:airstageOnoffState.js
  - id: Mode
    channelTypeUID: http:string
    label: Mode
    configuration:
      stateContent: '{     "device_id": "94BB4379C529",     "device_sub_id":
        0,     "req_id": "",     "modified_by": "",     "set_level":
        "03",     "list": [         "iu_op_mode"     ] }'
      commandTransformation:
        - JS:airstageMode_Wohnzimmer.js
      stateExtension: GetParam
      commandExtension: SetParam
      stateTransformation:
        - JS:airstageModeState.js

Thanks for posting! If you click on the code tab of your HTTP Thing you’ll get a text based version of the Thing. Posting that (either the YAML or the DSL) will make it easier for users to create their own Thing without error.

Thank you for the hint! I added the code to the initial post.

Thanks a lot for this tutorial!
It inspired me to use a little different approach, but it’s still based on the HTTP binding. I use a combination of HTTP binding, JINJA transformation and a JavaScript rule.

In the HTTP binding, I configured one generic “status” channel which calls all the properties from the API. It’s a read-only channel.
Then with a JavaScript rule I parse the JSON response and post updates to the single items if the status has changed on the device side.

In the HTTP binding, I defined separate channels for the items, but these channels are write-only.
If I change the value of an item, the new value is sent via the channel to the device.

With this approach I only need to do one API call to the device to update all the items instead of doing separate API calls for each item.

Additionally I use number items for the mode or the fan speed instead of string items. The mapping to “words” is done in the state description meta data of the specific items.

This is the current code:
HTTP binding

version: 1
things:
  http:url:d8c48f2424:
    label: Fujitsu Airstage
    location: Küche
    config:
      baseURL: http://<SET DEVICE IP HERE>
      refresh: 30
      timeout: 3000
      delay: 0
      bufferSize: 2048
      authMode: BASIC
      stateMethod: POST
      commandMethod: POST
      contentType: text/plain
      ignoreSSLErrors: false
    channels:
      last-failure:
        type: request-date-time
        label: Last Failure
      last-success:
        type: request-date-time
        label: Last Success
      api:
        type: string
        label: Status API Call
        description: "READ: Get the current device status"
        config:
          stateExtension: GetParam
          stateContent: "{\"device_id\": \"<SET DEVICE ID (MAC) HERE>\",\"device_sub_id\":0,\"req_id\":\"\",\"modified_by\":\"\",\"set_level\":\"03\",\"list\":[\"iu_onoff\",\"iu_op_mode\",\"iu_fan_spd\",\"iu_set_tmp\",\"iu_af_inc_vrt\",\"iu_af_dir_vrt\",\"iu_af_swg_vrt\",\"iu_af_swg_hrz\",\"iu_af_dir_hrz\",\"ou_low_noise\",\"iu_fan_ctrl\",\"iu_hmn_det_auto_save\",\"iu_min_heat\",\"iu_powerful\",\"iu_economy\",\"iu_err_code\",\"iu_demand\",\"iu_fltr_sign_reset\"]}"
          mode: READONLY
      apisend:
        type: string
        label: Command API Call
        description: "WRITE: Set device status (for testing purposes via API Explorer)"
        config:
          commandTransformation:
            - "JINJA:{\"device_id\": \"<SET DEVICE ID (MAC) HERE>\",\"device_sub_id\":0,\"req_id\":\"\",\"modified_by\":\"\",\"set_level\":\"02\",\"value\":{{ value }}}"
          commandExtension: SetParam
          mode: WRITEONLY
      onoff:
        type: switch
        label: On/Off
        description: "WRITE: On/off status"
        config:
          commandTransformation:
            - "JINJA:{\"device_id\": \"<SET DEVICE ID (MAC) HERE>\",\"device_sub_id\":0,\"req_id\":\"\",\"modified_by\":\"\",\"set_level\":\"02\",\"value\":{\"iu_onoff\":\"{{value}}\"}}"
          commandExtension: SetParam
          onValue: "1"
          offValue: "0"
          mode: WRITEONLY
      targetTemperature:
        type: number
        label: Target Temperature
        description: "WRITE: Temperature setpoint"
        config:
          commandTransformation:
            - "JINJA:{\"device_id\": \"<SET DEVICE ID (MAC) HERE>\",\"device_sub_id\":0,\"req_id\":\"\",\"modified_by\":\"\",\"set_level\":\"02\",\"value\":{\"iu_set_tmp\":\"{{value * 10}}\"}}"
          commandExtension: SetParam
          mode: WRITEONLY
      mode:
        type: number
        label: Mode
        description: "WRITE: Device mode (0=AUTO, 1=COOL, 2=DRY, 3=FAN, 4=HEAT)"
        config:
          commandTransformation:
            - "JINJA:{\"device_id\": \"<SET DEVICE ID (MAC) HERE>\",\"device_sub_id\":0,\"req_id\":\"\",\"modified_by\":\"\",\"set_level\":\"02\",\"value\":{\"iu_op_mode\":\"{{value}}\"}}"
          commandExtension: SetParam
          mode: WRITEONLY
      fanspeed:
        type: number
        label: Fan Speed
        description: "WRITE: Fan speed (0=AUTO, 2=LOW, 5=NORMAL, 8=MEDIUM, 11=HIGH)"
        config:
          commandTransformation:
            - "JINJA:{\"device_id\": \"<SET DEVICE ID (MAC) HERE>\",\"device_sub_id\":0,\"req_id\":\"\",\"modified_by\":\"\",\"set_level\":\"02\",\"value\":{\"iu_fan_spd\":\"{{value}}\"}}"
          commandExtension: SetParam
          mode: WRITEONLY
      directionvertical:
        type: number
        label: Ventilation Direction Vertical
        description: "WRITE: Vertical ventilation direction (1=0°, 2=30° 3=60°, 4=90°)"
        config:
          commandTransformation:
            - "JINJA:{\"device_id\": \"<SET DEVICE ID (MAC) HERE>\",\"device_sub_id\":0,\"req_id\":\"\",\"modified_by\":\"\",\"set_level\":\"02\",\"value\":{\"iu_af_dir_vrt\":\"{{value}}\"}}"
          commandExtension: SetParam
          mode: WRITEONLY
      energysavemode:
        type: switch
        label: Energy Saver Mode
        description: "WRITE: Energy saver on/off status"
        config:
          commandTransformation:
            - "JINJA:{\"device_id\": \"<SET DEVICE ID (MAC) HERE>\",\"device_sub_id\":0,\"req_id\":\"\",\"modified_by\":\"\",\"set_level\":\"02\",\"value\":{\"iu_economy\":\"{{value}}\"}}"
          commandExtension: SetParam
          onValue: "1"
          offValue: "0"
          mode: WRITEONLY
      powerful:
        type: switch
        label: Powerful Mode
        description: "WRITE: Powerful mode on/off status"
        config:
          commandTransformation:
            - "JINJA:{\"device_id\": \"<SET DEVICE ID (MAC) HERE>\",\"device_sub_id\":0,\"req_id\":\"\",\"modified_by\":\"\",\"set_level\":\"02\",\"value\":{\"iu_powerful\":\"{{value}}\"}}"
          commandExtension: SetParam
          onValue: "1"
          offValue: "0"
          mode: WRITEONLY
      minheat:
        type: switch
        label: Minimum Heating Mode
        description: "WRITE: Minimum heating on/off status"
        config:
          commandTransformation:
            - "JINJA:{\"device_id\": \"<SET DEVICE ID (MAC) HERE>\",\"device_sub_id\":0,\"req_id\":\"\",\"modified_by\":\"\",\"set_level\":\"02\",\"value\":{\"iu_min_heat\":\"{{value}}\"}}"
          commandExtension: SetParam
          onValue: "1"
          offValue: "0"
          mode: WRITEONLY
      lownoiseout:
        type: switch
        label: Low Noise Mode
        description: "WRITE: Low Noise Mode on/off status"
        config:
          commandTransformation:
            - "JINJA:{\"device_id\": \"<SET DEVICE ID (MAC) HERE>\",\"device_sub_id\":0,\"req_id\":\"\",\"modified_by\":\"\",\"set_level\":\"02\",\"value\":{\"ou_low_noise\":\"{{value}}\"}}"
          commandExtension: SetParam
          onValue: "1"
          offValue: "0"
          mode: WRITEONLY
      ventilatorenergysave:
        type: switch
        label: Ventilator Energy Save Mode
        description: "WRITE: Ventilator energy save on/off status"
        config:
          commandTransformation:
            - "JINJA:{\"device_id\": \"<SET DEVICE ID (MAC) HERE>\",\"device_sub_id\":0,\"req_id\":\"\",\"modified_by\":\"\",\"set_level\":\"02\",\"value\":{\"iu_fan_ctrl\":\"{{value}}\"}}"
          commandExtension: SetParam
          onValue: "1"
          offValue: "0"
          mode: WRITEONLY

JavaScript rule:

configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: Fujitsu_Airstage_Status
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >-
        // Fujitsu Airstage Status

        const status = JSON.parse(event.newState);

        if (status) {
          const value = status.value;
          // ON/OFF
          const onOffStatus = parseInt(value.iu_onoff) === 1 ? 'ON' : 'OFF';
          const onOffItem = items.getItem('Fujitsu_Airstage_OnOff');
          if (onOffItem.state !== onOffStatus) {
            onOffItem.postUpdate(onOffStatus);
          }
          // Temperature
          const temperatureStatus = parseInt(value.iu_set_tmp);
          const temperatureItem = items.getItem('Fujitsu_Airstage_Target_Temperature');
          if (temperatureStatus && temperatureItem.numericState * 10 !== temperatureStatus) {
            const targetTemperature = temperatureStatus / 10;
            temperatureItem.postUpdate(targetTemperature);
          }
          // Mode
          const modeStatus = parseInt(value.iu_op_mode);
          const modeItem = items.getItem('Fujitsu_Airstage_Mode');
          if (modeStatus && modeItem.numericState !== modeStatus) {
            modeItem.postUpdate(modeStatus);
          }
          // Fan speed
          const fanSpeedStatus = parseInt(value.iu_fan_spd);
          const fanSpeedItem = items.getItem('Fujitsu_Airstage_Fan_Speed');
          if (fanSpeedStatus && fanSpeedItem.numericState !== fanSpeedStatus) {
            fanSpeedItem.postUpdate(fanSpeedStatus);
          }
          // Ventilation direction vertical
          const dirVerticalStatus = parseInt(value.iu_af_dir_vrt);
          const dirVerticalItem = items.getItem('Fujitsu_Airstage_Ventilation_Direction_Vertical');
          if (dirVerticalStatus && dirVerticalItem.numericState !== dirVerticalStatus) {
            dirVerticalItem.postUpdate(dirVerticalStatus);
          }
          // Energy saver
          const energySaverStatus = parseInt(value.iu_economy) === 1 ? 'ON' : 'OFF';
          const energySaverItem = items.getItem('Fujitsu_Airstage_Energy_Saver_Mode');
          if (energySaverItem.state !== energySaverStatus) {
            energySaverItem.postUpdate(energySaverStatus);
          }
          // Power mode
          const powerModeStatus = parseInt(value.iu_powerful) === 1 ? 'ON' : 'OFF';
          const powerModeItem = items.getItem('Fujitsu_Airstage_Powerful_Mode');
          if (powerModeItem.state !== powerModeStatus) {
            powerModeItem.postUpdate(powerModeStatus);
          }
          // Minimum heating mode
          const minimumModeStatus = parseInt(value.iu_min_heat) === 1 ? 'ON' : 'OFF';
          const minimumModeItem = items.getItem('Fujitsu_Airstage_Minimum_Heating_Mode');
          if (minimumModeItem.state !== minimumModeStatus) {
            minimumModeItem.postUpdate(minimumModeStatus);
          }
          // Low noise mode
          const lowNoiseStatus = parseInt(value.ou_low_noise) === 1 ? 'ON' : 'OFF';
          const lowNoiseItem = items.getItem('Fujitsu_Airstage_Low_Noise_Mode');
          if (lowNoiseItem.state !== lowNoiseStatus) {
            lowNoiseItem.postUpdate(lowNoiseStatus);
          }
          // Ventilator energy saver
          const ventEnergySaverStatus = parseInt(value.iu_fan_ctrl) === 1 ? 'ON' : 'OFF';
          const ventEnergySaverItem = items.getItem('Fujitsu_Airstage_Ventilator_Energy_Save_Mode');
          if (ventEnergySaverItem.state !== ventEnergySaverStatus) {
            ventEnergySaverItem.postUpdate(ventEnergySaverStatus);
          }
        }
    type: script.ScriptAction

And e.g. the Mode item with the state descriptions metadata:

version: 1
items:
  Fujitsu_Airstage_Mode:
    type: Number
    label: Fujitsu Airstage Modus
    icon: if:mdi:air-conditioner
    groups:
      - Fujitsu_Airstage
    tags:
      - Airconditioning
      - Status
    metadata:
      stateDescription:
        value: ' '
        config:
          options: "0=Automatik,1=Kühlen,2=Trocknen,3=Lüften,4=Heizen"

Similar for ventilation direction:

version: 1
items:
  Fujitsu_Airstage_Ventilation_Direction_Vertical:
    type: Number
    label: Fujitsu Airstage Lüfterrichung vertikal
    icon: if:material-symbols:fan-indirect-rounded
    groups:
      - Fujitsu_Airstage
    tags:
      - Status
      - Ventilation
    metadata:
      stateDescription:
        value: ' '
        config:
          options: "1=0°,2=30°,3=60°,4=90°"

These items are tagged with semantic point “Status” instead of “Control”.

Hi @engine ,

I appreciate you shared your approach and I’m glad I inspired you!

I see this definitely as an improvement compared to the initial version. When I created the Things and Channels, I didn’t like the fact that multiple requests are sent instead of a single one. Your improvement solves this problem.

I will give it a try, when I have some time left.

I wondered if it is also possible to link the items created from the WRITE channels to the READ channel and then pick the correct value from the status JSON with a JINJA transformation for the linked item.
In theory it should be possible and no more rule would be required.

I’m just not sure if this would lead to unneccessary commands towards the device.
If e.g. the target temperature is updated on the device (via app) and therefore the target temperature item is updated by the READ channel and its item value changes, does this then cause a command towards the WRITE channel? I.e. the temperature which is already set on device side is set again.

That’s why I use postUpdate in the rule instead of sendCommand to avoid this.