Connecting the TEKTELIC Comfort LoRaWAN® Smart Room Sensor to the Kaa IoT Platform
This tutorial is the second part of a series dedicated to TEKTELIC sensors.
Check out our step-by-step guide on connecting TEKTELIC BREEZE Indoor Air Quality & CO2 Sensor to Kaa IoT Platform.
In this tutorial, we will cover one of the most popular sensors: the COMFORT LoRaWAN Smart Room sensor.
The COMFORT sensor, like other devices from TEKTELIC, integrates several sensors:
- Temperature sensor
- Humidity sensor
- Light sensor
- Magnetic contact sensor
- Impact detection sensor (detects movement or impact through a built-in accelerometer)
- Universal digital/analog input
With this array of built-in sensors, the device can be used in a wide range of applications. The universal digital/analog input is particularly convenient. One of its most common uses is connecting a special water leak detection cable (included in the package). By default, the sensor is configured as a digital input designed to connect to the water leak cable. When water contacts the cable, the sensor detects a signal change from high to low and sends an alert message.
Additionally, this universal input can be repurposed for various other applications, such as counting pulses from a meter or measuring the voltage output from a device.
(CAUTION: When using this mode, carefully review the allowable voltage thresholds and ensure level compatibility if the measured voltage exceeds the permissible parameters)
A significant advantage of this device is the extensive configuration options for each element of the device:
- You can enable/disable individual modules (to save power if they are not used),
- Customize the alerts that the sensor will send,
- Configure the operational logic of each element, and more.
Conveniently, all settings are managed through downlink commands, allowing for remote reconfiguration of the device.
Step 1: Setting Up the Device with The Things Stack
As in the previous tutorial, we will use The Things Stack to connect the device to the Kaa IoT platform.
Since the release of the previous tutorial, a new version of The Things Stack has been launched, which includes several interface changes. Therefore, we will include more images in this article than would have been necessary if the version had remained unchanged.
1. Log in to The Things Stack
If you don’t have an account, we recommend returning to the first guide in this series to create one. Remember that we already have the tektelic-equipment application, and we will be adding our new sensor to it.
2. Register the Device
- Navigate to End devices and click Register end device.
3. Search and Select Device
Like last time, we will register the device by searching for it in the device repository.
- Enter Tektelic in the End device brand field and Comfort in the Model field. You will see two variants of this sensor. We need to select the second option.
- Choose the appropriate Frequency plan. In our case, this is EU_863-870.
4. Enter Device Parameters
Next, input the required JoinEUI, DevEUI, and AppKey parameters. These parameters can be found on the device's packaging. They are available both in text format and as a QR code.
- Use the QR code for easier input.
6. Device Registration
Once you have entered all the required parameters and click Register end device - the device will be registered.
Step 2: Checking Device Revision and Firmware Version
1. Identify Device Revision
It's important to know the revision of the module and the firmware version. Depending on this information, you may need to use different versions of the documentation (see the table below).
- To obtain the firmware version, you will need to send a downlink message 0x71 to port 100.
2. Restart the Device
The first step is to restart the device (ensure that the batteries are installed in the device) so that it can register on the network.
We see that the device is online.
3. Set Up Payload Formatter
To decode the data received from the device, you need to configure a Payload formatter. The formatter can be found at the following link: Tektelic Payload Formatter.
4. Configure the Payload Formatter
- Go to the Payload formatters tab and select Custom Javascript formatter.
By default, the formatter will look like this:
- Copy the Decoder function from the provided link and use it in the decodeUplink function.
5. Save the Changes
- Click Save changes to finalize the payload formatter setup.
function decodeUplink(input) {
return { "data": Decoder(input.bytes, input.fPort)};
}
function Decoder(bytes, port) {
var decoded_data = {};
var decoder = [];
var errors = [];
bytes = convertToUint8Array(bytes);
decoded_data['raw'] = toHexString(bytes).toUpperCase();
decoded_data['port'] = port;
var input = {
"fPort": port,
}
if(input.fPort === 101){
decoder = [
{
key: [],
fn: function(arg) {
var size = arg.length;
var invalid_registers = [];
var responses = [];
while(arg.length > 0){
var downlink_fcnt = arg[0];
var num_invalid_writes = arg[1];
arg = arg.slice(2);
if(num_invalid_writes > 0) {
for(var i = 0; i < num_invalid_writes; i++){
invalid_registers.push("0x" + arg[i].toString(16));
}
arg = arg.slice(num_invalid_writes);
responses.push(num_invalid_writes + ' Invalid write command(s) from DL:' + downlink_fcnt + ' for register(s): ' + invalid_registers);
}
else {
responses.push('All write commands from DL:' + downlink_fcnt + 'were successfull');
}
invalid_registers = [];
}
decoded_data["response"] = responses;
return size;
}
}
];
}
if (input.fPort === 100) {
decoder = [
{
key: [0x00],
fn: function(arg) {
decoded_data['device_eui'] = decode_field(arg, 8, 63, 0, "hexstring");
return 8;
}
},
{
key: [0x01],
fn: function(arg) {
decoded_data['app_eui'] = decode_field(arg, 8, 63, 0, "hexstring");
return 8;
}
},
{
key: [0x02],
fn: function(arg) {
decoded_data['app_key'] = decode_field(arg, 16, 127, 0, "hexstring");
return 16;
}
},
{
key: [0x03],
fn: function(arg) {
decoded_data['device_address'] = decode_field(arg, 4, 31, 0, "hexstring");
return 4;
}
},
{
key: [0x04],
fn: function(arg) {
decoded_data['network_session_key'] = decode_field(arg, 16, 127, 0, "hexstring");
return 16;
}
},
{
key: [0x05],
fn: function(arg) {
decoded_data['app_session_key'] = decode_field(arg, 16, 127, 0, "hexstring");
return 16;
}
},
{
key: [0x10],
fn: function(arg) {
var val = decode_field(arg, 2, 15, 15, "unsigned");
{switch (val){
case 0:
decoded_data['loramac_join_mode'] = "ABP";
break;
case 1:
decoded_data['loramac_join_mode'] = "OTAA";
break;
default:
decoded_data['loramac_join_mode'] = "Invalid";
}}
return 2;
}
},
{
key: [0x11],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('loramac_opts')) {
decoded_data['loramac_opts'] = {};
}
var val = decode_field(arg, 2, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['loramac_opts']['confirm_mode'] = "Unconfirmed";
break;
case 1:
decoded_data['loramac_opts']['confirm_mode'] = "Confirmed";
break;
default:
decoded_data['loramac_opts']['confirm_mode'] = "Invalid";
}}
var val = decode_field(arg, 2, 1, 1, "unsigned");
{switch (val){
case 0:
decoded_data['loramac_opts']['sync_word'] = "Private";
break;
case 1:
decoded_data['loramac_opts']['sync_word'] = "Public";
break;
default:
decoded_data['loramac_opts']['sync_word'] = "Invalid";
}}
var val = decode_field(arg, 2, 2, 2, "unsigned");
{switch (val){
case 0:
decoded_data['loramac_opts']['duty_cycle'] = "Disable";
break;
case 1:
decoded_data['loramac_opts']['duty_cycle'] = "Enable";
break;
default:
decoded_data['loramac_opts']['duty_cycle'] = "Invalid";
}}
var val = decode_field(arg, 2, 3, 3, "unsigned");
{switch (val){
case 0:
decoded_data['loramac_opts']['adr'] = "Disable";
break;
case 1:
decoded_data['loramac_opts']['adr'] = "Enable";
break;
default:
decoded_data['loramac_opts']['adr'] = "Invalid";
}}
return 2;
}
},
{
key: [0x12],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('loramac_dr_tx')) {
decoded_data['loramac_dr_tx'] = {};
}
decoded_data['loramac_dr_tx']['dr_number'] = decode_field(arg, 2, 11, 8, "unsigned");
decoded_data['loramac_dr_tx']['tx_power_number'] = decode_field(arg, 2, 3, 0, "unsigned");
return 2;
}
},
{
key: [0x13],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('loramac_rx2')) {
decoded_data['loramac_rx2'] = {};
}
decoded_data['loramac_rx2']['frequency'] = decode_field(arg, 5, 39, 8, "unsigned");
decoded_data['loramac_rx2']['dr_number_rx2'] = decode_field(arg, 5, 7, 0, "unsigned");
return 5;
}
},
{
key: [0x19],
fn: function(arg) {
decoded_data['loramac_net_id_msb'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x1A],
fn: function(arg) {
decoded_data['loramac_net_id_lsb'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x20],
fn: function(arg) {
decoded_data['seconds_per_core_tick'] = decode_field(arg, 4, 31, 0, "unsigned");
return 4;
}
},
{
key: [0x21],
fn: function(arg) {
decoded_data['tick_per_battery'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x22],
fn: function(arg) {
decoded_data['tick_per_ambient_temperature'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x23],
fn: function(arg) {
decoded_data['tick_per_relative_humidity'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x24],
fn: function(arg) {
decoded_data['tick_per_reed_switch'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x25],
fn: function(arg) {
decoded_data['tick_per_light'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x26],
fn: function(arg) {
decoded_data['tick_per_accelerometer'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x27],
fn: function(arg) {
decoded_data['tick_per_mcu_temperature'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x28],
fn: function(arg) {
decoded_data['tick_per_pir'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x29],
fn: function(arg) {
decoded_data['tick_per_external_connector'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x2A],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('mode')) {
decoded_data['mode'] = {};
}
var val = decode_field(arg, 1, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['mode']['rising_edge_enabled'] = "Disable";
break;
case 1:
decoded_data['mode']['rising_edge_enabled'] = "Enable";
break;
default:
decoded_data['mode']['rising_edge_enabled'] = "Invalid";
}}
var val = decode_field(arg, 1, 1, 1, "unsigned");
{switch (val){
case 0:
decoded_data['mode']['falling_edge_enabled'] = "Disable";
break;
case 1:
decoded_data['mode']['falling_edge_enabled'] = "Enable";
break;
default:
decoded_data['mode']['falling_edge_enabled'] = "Invalid";
}}
return 1;
}
},
{
key: [0x2B],
fn: function(arg) {
decoded_data['reed_switch_count_threshold'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x2C],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('reed_values_to_transmit')) {
decoded_data['reed_values_to_transmit'] = {};
}
var val = decode_field(arg, 1, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['reed_values_to_transmit']['report_state_enabled'] = "Off";
break;
case 1:
decoded_data['reed_values_to_transmit']['report_state_enabled'] = "On";
break;
default:
decoded_data['reed_values_to_transmit']['report_state_enabled'] = "Invalid";
}}
var val = decode_field(arg, 1, 1, 1, "unsigned");
{switch (val){
case 0:
decoded_data['reed_values_to_transmit']['report_count_enabled'] = "Off";
break;
case 1:
decoded_data['reed_values_to_transmit']['report_count_enabled'] = "On";
break;
default:
decoded_data['reed_values_to_transmit']['report_count_enabled'] = "Invalid";
}}
return 1;
}
},
{
key: [0x2D],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('external_mode')) {
decoded_data['external_mode'] = {};
}
var val = decode_field(arg, 1, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['external_mode']['rising_edge_enabled_ex'] = "Off";
break;
case 1:
decoded_data['external_mode']['rising_edge_enabled_ex'] = "On";
break;
default:
decoded_data['external_mode']['rising_edge_enabled_ex'] = "Invalid";
}}
var val = decode_field(arg, 1, 1, 1, "unsigned");
{switch (val){
case 0:
decoded_data['external_mode']['falling_edge_enabled_ex'] = "Off";
break;
case 1:
decoded_data['external_mode']['falling_edge_enabled_ex'] = "On";
break;
default:
decoded_data['external_mode']['falling_edge_enabled_ex'] = "Invalid";
}}
var val = decode_field(arg, 1, 7, 7, "unsigned");
{switch (val){
case 0:
decoded_data['external_mode']['mode'] = "Digital";
break;
case 1:
decoded_data['external_mode']['mode'] = "Analog";
break;
default:
decoded_data['external_mode']['mode'] = "Invalid";
}}
return 1;
}
},
{
key: [0x2E],
fn: function(arg) {
decoded_data['external_connector_count_threshold'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x2F],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('external_values_to_transmit')) {
decoded_data['external_values_to_transmit'] = {};
}
var val = decode_field(arg, 1, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['external_values_to_transmit']['report_state_enabled_ex'] = "Off";
break;
case 1:
decoded_data['external_values_to_transmit']['report_state_enabled_ex'] = "On";
break;
default:
decoded_data['external_values_to_transmit']['report_state_enabled_ex'] = "Invalid";
}}
var val = decode_field(arg, 1, 1, 1, "unsigned");
{switch (val){
case 0:
decoded_data['external_values_to_transmit']['report_count_enabled_ex'] = "Off";
break;
case 1:
decoded_data['external_values_to_transmit']['report_count_enabled_ex'] = "On";
break;
default:
decoded_data['external_values_to_transmit']['report_count_enabled_ex'] = "Invalid";
}}
decoded_data['external_values_to_transmit']['count_type'] = decode_field(arg, 1, 4, 4, "unsigned");
return 1;
}
},
{
key: [0x30],
fn: function(arg) {
decoded_data['impact_event_threshold'] = (decode_field(arg, 2, 15, 0, "unsigned") * 0.001).toFixed(3);
return 2;
}
},
{
key: [0x31],
fn: function(arg) {
decoded_data['acceleration_event_threshold'] = (decode_field(arg, 2, 15, 0, "unsigned") * 0.001).toFixed(3);
return 2;
}
},
{
key: [0x32],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('values_to_transmit')) {
decoded_data['values_to_transmit'] = {};
}
var val = decode_field(arg, 1, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['values_to_transmit']['report_periodic_alarm_enabled'] = "Off";
break;
case 1:
decoded_data['values_to_transmit']['report_periodic_alarm_enabled'] = "On";
break;
default:
decoded_data['values_to_transmit']['report_periodic_alarm_enabled'] = "Invalid";
}}
var val = decode_field(arg, 1, 1, 1, "unsigned");
{switch (val){
case 0:
decoded_data['values_to_transmit']['report_periodic_magnitude_enabled'] = "Off";
break;
case 1:
decoded_data['values_to_transmit']['report_periodic_magnitude_enabled'] = "On";
break;
default:
decoded_data['values_to_transmit']['report_periodic_magnitude_enabled'] = "Invalid";
}}
var val = decode_field(arg, 1, 2, 2, "unsigned");
{switch (val){
case 0:
decoded_data['values_to_transmit']['report_periodic_vector_enabled'] = "Off";
break;
case 1:
decoded_data['values_to_transmit']['report_periodic_vector_enabled'] = "On";
break;
default:
decoded_data['values_to_transmit']['report_periodic_vector_enabled'] = "Invalid";
}}
var val = decode_field(arg, 1, 4, 4, "unsigned");
{switch (val){
case 0:
decoded_data['values_to_transmit']['report_event_magnitude_enabled'] = "Off";
break;
case 1:
decoded_data['values_to_transmit']['report_event_magnitude_enabled'] = "On";
break;
default:
decoded_data['values_to_transmit']['report_event_magnitude_enabled'] = "Invalid";
}}
var val = decode_field(arg, 1, 5, 5, "unsigned");
{switch (val){
case 0:
decoded_data['values_to_transmit']['report_event_vector_enabled'] = "Off";
break;
case 1:
decoded_data['values_to_transmit']['report_event_vector_enabled'] = "On";
break;
default:
decoded_data['values_to_transmit']['report_event_vector_enabled'] = "Invalid";
}}
return 1;
}
},
{
key: [0x33],
fn: function(arg) {
decoded_data['acceleration_impact_grace_period'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x34],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('acceleration_mode')) {
decoded_data['acceleration_mode'] = {};
}
var val = decode_field(arg, 1, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['acceleration_mode']['impact_threshold_enabled'] = "Disable";
break;
case 1:
decoded_data['acceleration_mode']['impact_threshold_enabled'] = "Enable";
break;
default:
decoded_data['acceleration_mode']['impact_threshold_enabled'] = "Invalid";
}}
var val = decode_field(arg, 1, 1, 1, "unsigned");
{switch (val){
case 0:
decoded_data['acceleration_mode']['acceleration_threshold_enabled'] = "Disable";
break;
case 1:
decoded_data['acceleration_mode']['acceleration_threshold_enabled'] = "Enable";
break;
default:
decoded_data['acceleration_mode']['acceleration_threshold_enabled'] = "Invalid";
}}
var val = decode_field(arg, 1, 4, 4, "unsigned");
{switch (val){
case 0:
decoded_data['acceleration_mode']['xaxis_enabled'] = "Disable";
break;
case 1:
decoded_data['acceleration_mode']['xaxis_enabled'] = "Enable";
break;
default:
decoded_data['acceleration_mode']['xaxis_enabled'] = "Invalid";
}}
var val = decode_field(arg, 1, 5, 5, "unsigned");
{switch (val){
case 0:
decoded_data['acceleration_mode']['yaxis_enabled'] = "Disable";
break;
case 1:
decoded_data['acceleration_mode']['yaxis_enabled'] = "Enable";
break;
default:
decoded_data['acceleration_mode']['yaxis_enabled'] = "Invalid";
}}
var val = decode_field(arg, 1, 6, 6, "unsigned");
{switch (val){
case 0:
decoded_data['acceleration_mode']['zaxis_enabled'] = "Disable";
break;
case 1:
decoded_data['acceleration_mode']['zaxis_enabled'] = "Enable";
break;
default:
decoded_data['acceleration_mode']['zaxis_enabled'] = "Invalid";
}}
var val = decode_field(arg, 1, 7, 7, "unsigned");
{switch (val){
case 0:
decoded_data['acceleration_mode']['poweron'] = "Off";
break;
case 1:
decoded_data['acceleration_mode']['poweron'] = "On";
break;
default:
decoded_data['acceleration_mode']['poweron'] = "Invalid";
}}
return 1;
}
},
{
key: [0x35],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('sensitivity')) {
decoded_data['sensitivity'] = {};
}
var val = decode_field(arg, 1, 2, 0, "unsigned");
{switch (val){
case 1:
decoded_data['sensitivity']['accelerometer_sample_rate'] = "1 Hz";
break;
case 2:
decoded_data['sensitivity']['accelerometer_sample_rate'] = "10 Hz";
break;
case 3:
decoded_data['sensitivity']['accelerometer_sample_rate'] = "25 Hz";
break;
case 4:
decoded_data['sensitivity']['accelerometer_sample_rate'] = "50 Hz";
break;
case 5:
decoded_data['sensitivity']['accelerometer_sample_rate'] = "100 Hz";
break;
case 6:
decoded_data['sensitivity']['accelerometer_sample_rate'] = "200 Hz";
break;
case 7:
decoded_data['sensitivity']['accelerometer_sample_rate'] = "400 Hz";
break;
default:
decoded_data['sensitivity']['accelerometer_sample_rate'] = "Invalid";
}}
var val = decode_field(arg, 1, 5, 4, "unsigned");
{switch (val){
case 0:
decoded_data['sensitivity']['accelerometer_measurement_range'] = "±2 g";
break;
case 1:
decoded_data['sensitivity']['accelerometer_measurement_range'] = "±4 g";
break;
case 2:
decoded_data['sensitivity']['accelerometer_measurement_range'] = "±8 g";
break;
case 3:
decoded_data['sensitivity']['accelerometer_measurement_range'] = "±16 g";
break;
default:
decoded_data['sensitivity']['accelerometer_measurement_range'] = "Invalid";
}}
return 1;
}
},
{
key: [0x36],
fn: function(arg) {
decoded_data['impact_alarm_grace_period'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x37],
fn: function(arg) {
decoded_data['impact_alarm_threshold_count'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x38],
fn: function(arg) {
decoded_data['impact_alarm_threshold_period'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x39],
fn: function(arg) {
decoded_data['temperature_relative_humidity_sample_period_idle'] = decode_field(arg, 4, 31, 0, "unsigned");
return 4;
}
},
{
key: [0x3A],
fn: function(arg) {
decoded_data['temperature_relative_humidity_sample_period_active'] = decode_field(arg, 4, 31, 0, "unsigned");
return 4;
}
},
{
key: [0x3B],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('temperature_threshold')) {
decoded_data['temperature_threshold'] = {};
}
decoded_data['temperature_threshold']['high_temp_threshold'] = decode_field(arg, 2, 15, 8, "signed");
decoded_data['temperature_threshold']['low_temp_threshold'] = decode_field(arg, 2, 7, 0, "signed");
return 2;
}
},
{
key: [0x3C],
fn: function(arg) {
var val = decode_field(arg, 1, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['ambient_temperature_threshold_enabled'] = "Disable";
break;
case 1:
decoded_data['ambient_temperature_threshold_enabled'] = "Enable";
break;
default:
decoded_data['ambient_temperature_threshold_enabled'] = "Invalid";
}}
return 1;
}
},
{
key: [0x3D],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('rh_threshold')) {
decoded_data['rh_threshold'] = {};
}
decoded_data['rh_threshold']['high_rh_threshold'] = decode_field(arg, 2, 15, 8, "unsigned");
decoded_data['rh_threshold']['low_rh_threshold'] = decode_field(arg, 2, 7, 0, "unsigned");
return 2;
}
},
{
key: [0x3E],
fn: function(arg) {
var val = decode_field(arg, 1, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['relative_humidity_threshold_enabled'] = "Disable";
break;
case 1:
decoded_data['relative_humidity_threshold_enabled'] = "Enable";
break;
default:
decoded_data['relative_humidity_threshold_enabled'] = "Invalid";
}}
return 1;
}
},
{
key: [0x40],
fn: function(arg) {
decoded_data['mcu_temperature_sample_period_idle'] = decode_field(arg, 4, 31, 0, "unsigned");
return 4;
}
},
{
key: [0x41],
fn: function(arg) {
decoded_data['mcu_temperature_sample_period_active'] = decode_field(arg, 4, 31, 0, "unsigned");
return 4;
}
},
{
key: [0x42],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('mcu_temp_threshold')) {
decoded_data['mcu_temp_threshold'] = {};
}
decoded_data['mcu_temp_threshold']['high_mcu_temp_threshold'] = decode_field(arg, 2, 15, 8, "signed");
decoded_data['mcu_temp_threshold']['low_mcu_temp_threshold'] = decode_field(arg, 2, 7, 0, "signed");
return 2;
}
},
{
key: [0x43],
fn: function(arg) {
var val = decode_field(arg, 1, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['mcu_temperature_threshold_enabled'] = "Disabled";
break;
case 1:
decoded_data['mcu_temperature_threshold_enabled'] = "Enabled";
break;
default:
decoded_data['mcu_temperature_threshold_enabled'] = "Invalid";
}}
return 1;
}
},
{
key: [0x44],
fn: function(arg) {
decoded_data['analog_sample_period_idle'] = decode_field(arg, 4, 31, 0, "unsigned");
return 4;
}
},
{
key: [0x45],
fn: function(arg) {
decoded_data['analog_sample_period_active'] = decode_field(arg, 4, 31, 0, "unsigned");
return 4;
}
},
{
key: [0x46],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('analog_threshold')) {
decoded_data['analog_threshold'] = {};
}
decoded_data['analog_threshold']['high_analog_threshold'] = (decode_field(arg, 4, 31, 16, "unsigned") * 0.001).toFixed(3);
decoded_data['analog_threshold']['low_analog_threshold'] = (decode_field(arg, 4, 15, 0, "unsigned") * 0.001).toFixed(3);
return 4;
}
},
{
key: [0x4A],
fn: function(arg) {
var val = decode_field(arg, 1, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['analog_input_threshold_enabled'] = "Disabled";
break;
case 1:
decoded_data['analog_input_threshold_enabled'] = "Enabled";
break;
default:
decoded_data['analog_input_threshold_enabled'] = "Invalid";
}}
return 1;
}
},
{
key: [0x47],
fn: function(arg) {
decoded_data['light_sample_period'] = decode_field(arg, 4, 31, 0, "unsigned");
return 4;
}
},
{
key: [0x48],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('light_thresholds')) {
decoded_data['light_thresholds'] = {};
}
var val = decode_field(arg, 1, 7, 7, "unsigned");
{switch (val){
case 0:
decoded_data['light_thresholds']['threshold'] = "Disabled";
break;
case 1:
decoded_data['light_thresholds']['threshold'] = "Enabled";
break;
default:
decoded_data['light_thresholds']['threshold'] = "Invalid";
}}
decoded_data['light_thresholds']['threshold_enabled'] = decode_field(arg, 1, 5, 0, "unsigned");
return 1;
}
},
{
key: [0x49],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('light_values_to_transmit')) {
decoded_data['light_values_to_transmit'] = {};
}
var val = decode_field(arg, 1, 1, 1, "unsigned");
{switch (val){
case 0:
decoded_data['light_values_to_transmit']['state_reported'] = "Disabled";
break;
case 1:
decoded_data['light_values_to_transmit']['state_reported'] = "Enabled";
break;
default:
decoded_data['light_values_to_transmit']['state_reported'] = "Invalid";
}}
var val = decode_field(arg, 1, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['light_values_to_transmit']['intensity_reported'] = "Disabled";
break;
case 1:
decoded_data['light_values_to_transmit']['intensity_reported'] = "Enabled";
break;
default:
decoded_data['light_values_to_transmit']['intensity_reported'] = "Invalid";
}}
return 1;
}
},
{
key: [0x50],
fn: function(arg) {
decoded_data['pir_grace_period'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x51],
fn: function(arg) {
decoded_data['pir_threshold'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x52],
fn: function(arg) {
decoded_data['pir_threshold_period'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x53],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('pir_mode')) {
decoded_data['pir_mode'] = {};
}
var val = decode_field(arg, 1, 7, 7, "unsigned");
{switch (val){
case 0:
decoded_data['pir_mode']['motion_count_reported'] = "Disabled";
break;
case 1:
decoded_data['pir_mode']['motion_count_reported'] = "Enabled";
break;
default:
decoded_data['pir_mode']['motion_count_reported'] = "Invalid";
}}
var val = decode_field(arg, 1, 6, 6, "unsigned");
{switch (val){
case 0:
decoded_data['pir_mode']['motion_state_reported'] = "Disabled";
break;
case 1:
decoded_data['pir_mode']['motion_state_reported'] = "Enabled";
break;
default:
decoded_data['pir_mode']['motion_state_reported'] = "Invalid";
}}
var val = decode_field(arg, 1, 1, 1, "unsigned");
{switch (val){
case 0:
decoded_data['pir_mode']['event_transmission_enabled'] = "Disabled";
break;
case 1:
decoded_data['pir_mode']['event_transmission_enabled'] = "Enabled";
break;
default:
decoded_data['pir_mode']['event_transmission_enabled'] = "Invalid";
}}
var val = decode_field(arg, 1, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['pir_mode']['transducer_enabled'] = "Disabled";
break;
case 1:
decoded_data['pir_mode']['transducer_enabled'] = "Enabled";
break;
default:
decoded_data['pir_mode']['transducer_enabled'] = "Invalid";
}}
return 1;
}
},
{
key: [0x54],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('hold_off_int')) {
decoded_data['hold_off_int'] = {};
}
decoded_data['hold_off_int']['post_turn_on'] = decode_field(arg, 2, 15, 8, "unsigned");
decoded_data['hold_off_int']['post_disturbance'] = decode_field(arg, 2, 7, 0, "unsigned");
return 2;
}
},
{
key: [0x6F],
fn: function(arg) {
var val = decode_field(arg, 1, 0, 0, "unsigned");
{switch (val){
case 0:
decoded_data['resp_to_dl_command_format'] = "Invalid-write response format";
break;
case 1:
decoded_data['resp_to_dl_command_format'] = "4-byte CRC";
break;
default:
decoded_data['resp_to_dl_command_format'] = "Invalid";
}}
return 1;
}
},
{
key: [0x71],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('metadata')) {
decoded_data['metadata'] = {};
}
decoded_data['metadata']['app_major_version'] = decode_field(arg, 7, 55, 48, "unsigned");
decoded_data['metadata']['app_minor_version'] = decode_field(arg, 7, 47, 40, "unsigned");
decoded_data['metadata']['app_revision'] = decode_field(arg, 7, 39, 32, "unsigned");
decoded_data['metadata']['loramac_major_version'] = decode_field(arg, 7, 31, 24, "unsigned");
decoded_data['metadata']['loramac_minor_version'] = decode_field(arg, 7, 23, 16, "unsigned");
decoded_data['metadata']['loramac_revision'] = decode_field(arg, 7, 15, 8, "unsigned");
var val = decode_field(arg, 7, 7, 0, "unsigned");
{switch (val){
case 0:
decoded_data['metadata']['region'] = "EU868";
break;
case 1:
decoded_data['metadata']['region'] = "US915";
break;
case 2:
decoded_data['metadata']['region'] = "AS923";
break;
case 3:
decoded_data['metadata']['region'] = "AU915";
break;
case 4:
decoded_data['metadata']['region'] = "IN865";
break;
case 5:
decoded_data['metadata']['region'] = "CN470";
break;
case 6:
decoded_data['metadata']['region'] = "KR920";
break;
case 7:
decoded_data['metadata']['region'] = "RU864";
break;
case 8:
decoded_data['metadata']['region'] = "DN915";
break;
default:
decoded_data['metadata']['region'] = "Invalid";
}}
return 7;
}
},
];
}
if (input.fPort === 10) {
decoder = [
{
key: [0x00, 0xBA],
fn: function(arg) {
decoded_data['battery_voltage'] = (decode_field(arg, 2, 15, 0, "signed") * 0.001).toFixed(3);
return 2;
}
},
{
key: [0x01, 0x00],
fn: function(arg) {
var val = decode_field(arg, 1, 7, 0, "unsigned");
{switch (val){
case 0:
decoded_data['hall_effect_state'] = "Magnet Present";
break;
case 255:
decoded_data['hall_effect_state'] = "Magnet Absent";
break;
default:
decoded_data['hall_effect_state'] = "Invalid";
}}
return 1;
}
},
{
key: [0x08, 0x04],
fn: function(arg) {
decoded_data['hall_effect_count'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x0C, 0x00],
fn: function(arg) {
var val = decode_field(arg, 1, 7, 0, "unsigned");
{switch (val){
case 0:
decoded_data['impact_alarm'] = "Impact Alarm Inactive";
break;
case 255:
decoded_data['impact_alarm'] = "Impact Alarm Active";
break;
default:
decoded_data['impact_alarm'] = "Invalid";
}}
return 1;
}
},
{
key: [0x05, 0x02],
fn: function(arg) {
decoded_data['impact_magnitude'] = (decode_field(arg, 2, 15, 0, "unsigned") * 0.001).toFixed(3);
return 2;
}
},
{
key: [0x07, 0x71],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('acceleration')) {
decoded_data['acceleration'] = {};
}
decoded_data['acceleration']['xaxis'] = (decode_field(arg, 6, 47, 32, "signed") * 0.001).toFixed(3);
decoded_data['acceleration']['yaxis'] = (decode_field(arg, 6, 31, 16, "signed") * 0.001).toFixed(3);
decoded_data['acceleration']['zaxis'] = (decode_field(arg, 6, 15, 0, "signed") * 0.001).toFixed(3);
return 6;
}
},
{
key: [0x0E, 0x00],
fn: function(arg) {
var val = decode_field(arg, 1, 7, 0, "unsigned");
{switch (val){
case 0:
decoded_data['extconnector_state'] = "Low(short-circuit)";
break;
case 255:
decoded_data['extconnector_state'] = "High(open-circuit)";
break;
default:
decoded_data['extconnector_state'] = "Invalid";
}}
return 1;
}
},
{
key: [0x0F, 0x04],
fn: function(arg) {
decoded_data['extconnector_count'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
{
key: [0x12, 0x04],
fn: function(arg) {
decoded_data['extconnector_total_count'] = decode_field(arg, 4, 31, 0, "unsigned");
return 4;
}
},
{
key: [0x11, 0x02],
fn: function(arg) {
decoded_data['extconnector_analog'] = (decode_field(arg, 2, 15, 0, "signed") * 0.001).toFixed(3);
return 2;
}
},
{
key: [0x0B, 0x67],
fn: function(arg) {
decoded_data['mcu_temperature'] = (decode_field(arg, 2, 15, 0, "signed") * 0.1).toFixed(1);
return 2;
}
},
{
key: [0x03, 0x67],
fn: function(arg) {
decoded_data['ambient_temperature'] = (decode_field(arg, 2, 15, 0, "signed") * 0.1).toFixed(1);
return 2;
}
},
{
key: [0x04, 0x68],
fn: function(arg) {
decoded_data['relative_humidity'] = (decode_field(arg, 1, 7, 0, "unsigned") * 0.5).toFixed(1);
return 1;
}
},
{
key: [0x02, 0x00],
fn: function(arg) {
var val = decode_field(arg, 1, 7, 0, "unsigned");
{switch (val){
case 0:
decoded_data['light_detected'] = "Dark";
break;
case 255:
decoded_data['light_detected'] = "Bright";
break;
default:
decoded_data['light_detected'] = "Invalid";
}}
return 1;
}
},
{
key: [0x10, 0x02],
fn: function(arg) {
decoded_data['light_intensity'] = decode_field(arg, 1, 7, 0, "unsigned");
return 1;
}
},
{
key: [0x0A, 0x00],
fn: function(arg) {
var val = decode_field(arg, 1, 7, 0, "unsigned");
{switch (val){
case 0:
decoded_data['motion_event_state'] = "None";
break;
case 255:
decoded_data['motion_event_state'] = "Detected";
break;
default:
decoded_data['motion_event_state'] = "Invalid";
}}
return 1;
}
},
{
key: [0x0D, 0x04],
fn: function(arg) {
decoded_data['motion_event_count'] = decode_field(arg, 2, 15, 0, "unsigned");
return 2;
}
},
];
}
if (input.fPort === 5) {
decoder = [
{
key: [0x40, 0x06],
fn: function(arg) {
if(!decoded_data.hasOwnProperty('reset_diagnostics')) {
decoded_data['reset_diagnostics'] = {};
}
var val = decode_field(arg, 5, 39, 32, "unsigned");
{switch (val){
case 1:
decoded_data['reset_diagnostics']['reset_reason'] = "Push-button reset";
break;
case 2:
decoded_data['reset_diagnostics']['reset_reason'] = "DL command rest";
break;
case 4:
decoded_data['reset_diagnostics']['reset_reason'] = "Independent watchdog reset";
break;
case 8:
decoded_data['reset_diagnostics']['reset_reason'] = "Power loss reset";
break;
default:
decoded_data['reset_diagnostics']['reset_reason'] = "Invalid";
}}
decoded_data['reset_diagnostics']['power_loss_reset_count'] = decode_field(arg, 5, 31, 24, "unsigned");
decoded_data['reset_diagnostics']['watchdog_reset_count'] = decode_field(arg, 5, 23, 16, "unsigned");
decoded_data['reset_diagnostics']['dl_reset_count'] = decode_field(arg, 5, 15, 8, "unsigned");
decoded_data['reset_diagnostics']['button_reset_count'] = decode_field(arg, 5, 7, 0, "unsigned");
return 5;
}
},
];
}
try {
for (var bytes_left = bytes.length; bytes_left > 0;) {
var found = false;
for (var i = 0; i < decoder.length; i++) {
var item = decoder[i];
var key = item.key;
var keylen = key.length;
var header = slice(bytes, 0, keylen);
if (is_equal(header, key)) { // Header in the data matches to what we expect
var f = item.fn;
var consumed = f(slice(bytes, keylen, bytes.length)) + keylen;
bytes_left -= consumed;
bytes = slice(bytes, consumed, bytes.length);
found = true;
break;
}
}
if (!found) {
errors.push("Unable to decode header " + toHexString(header).toUpperCase());
break;
}
}
} catch (error) {
errors = "Fatal decoder error";
}
function slice(a, f, t) {
var res = [];
for (var i = 0; i < t - f; i++) {
res[i] = a[f + i];
}
return res;
}
// Extracts bits from a byte array
function extract_bytes(chunk, startBit, endBit) {
var array = new Array(0);
var totalBits = startBit - endBit + 1;
var totalBytes = Math.ceil(totalBits / 8);
var endBits = 0;
var startBits = 0;
for (var i = 0; i < totalBytes; i++) {
if(totalBits > 8) {
endBits = endBit;
startBits = endBits + 7;
endBit = endBit + 8;
totalBits -= 8;
} else {
endBits = endBit;
startBits = endBits + totalBits - 1;
totalBits = 0;
}
var endChunk = chunk.length - Math.ceil((endBits + 1) / 8);
var startChunk = chunk.length - Math.ceil((startBits + 1) / 8);
var word = 0x0;
if (startChunk == endChunk){
var endOffset = endBits % 8;
var startOffset = startBits % 8;
var mask = 0xFF >> (8 - (startOffset - endOffset + 1));
word = (chunk[startChunk] >> endOffset) & mask;
array.unshift(word);
} else {
var endChunkEndOffset = endBits % 8;
var endChunkStartOffset = 7;
var endChunkMask = 0xFF >> (8 - (endChunkStartOffset - endChunkEndOffset + 1));
var endChunkWord = (chunk[endChunk] >> endChunkEndOffset) & endChunkMask;
var startChunkEndOffset = 0;
var startChunkStartOffset = startBits % 8;
var startChunkMask = 0xFF >> (8 - (startChunkStartOffset - startChunkEndOffset + 1));
var startChunkWord = (chunk[startChunk] >> startChunkEndOffset) & startChunkMask;
var startChunkWordShifted = startChunkWord << (endChunkStartOffset - endChunkEndOffset + 1);
word = endChunkWord | startChunkWordShifted;
array.unshift(word);
}
}
return array;
}
// Applies data type to a byte array
function apply_data_type(bytes, data_type) {
var output = 0;
if (data_type === "unsigned") {
for (var i = 0; i < bytes.length; ++i) {
output = (to_uint(output << 8)) | bytes[i];
}
return output;
}
if (data_type === "signed") {
for (var i = 0; i < bytes.length; ++i) {
output = (output << 8) | bytes[i];
}
// Convert to signed, based on value size
if (output > Math.pow(2, 8 * bytes.length - 1)) {
output -= Math.pow(2, 8 * bytes.length);
}
return output;
}
if (data_type === "bool") {
return !(bytes[0] === 0);
}
if (data_type === "hexstring") {
return toHexString(bytes);
}
return null; // Incorrect data type
}
// Decodes bitfield from the given chunk of bytes
function decode_field(chunk, size, start_bit, end_bit, data_type) {
var new_chunk = chunk.slice(0, size);
var chunk_size = new_chunk.length;
if (start_bit >= chunk_size * 8) {
return null; // Error: exceeding boundaries of the chunk
}
if (start_bit < end_bit) {
return null; // Error: invalid input
}
var array = extract_bytes(new_chunk, start_bit, end_bit);
return apply_data_type(array, data_type);
}
// Converts value to unsigned
function to_uint(x) {
return x >>> 0;
}
// Checks if two arrays are equal
function is_equal(arr1, arr2) {
if (arr1.length != arr2.length) {
return false;
}
for (var i = 0; i != arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// Converts array of bytes to hex string
function toHexString(byteArray) {
var arr = [];
for (var i = 0; i < byteArray.length; ++i) {
arr.push(('0' + (byteArray[i] & 0xFF).toString(16)).slice(-2));
}
return arr.join(' ');
}
// Converts array of bytes to 8 bit array
function convertToUint8Array(byteArray) {
var arr = [];
for (var i = 0; i < byteArray.length; i++) {
arr.push(to_uint(byteArray[i]) & 0xff);
}
return arr;
}
decoded_data["errors"] = errors;
return decoded_data;
}
6. Test the Device
To verify that the data is being decoded correctly, you can perform a test by dropping some water on the water leak cable. You should see the events from the sensor appear in the Live data tab.
Step 3: Connecting the TEKTELIC Smart Room Sensor to Kaa IoT Platform
The integration of LoRaWAN devices with the Kaa IoT platform is done through the creation of application and device integrations.
1. Create Device Integration
In the previous tutorial, we created an Application integration for Breeze sensors. Now, let's create a device integration for the Comfort sensor.
2. Create an Application Version
In the previous tutorial we created an application version for Breeze sensors. Let's create an Application version for Comfort sensors
- Navigate to the Kaa Cloud -> Applications and create a new Application version specifically for the Comfort sensor.
3. Enable Autoextract
- Don't forget to enable autoextract for digital data.
4. Add the Device:
- Go to the LoRaWAN tab, select the tektelic-sensors application integration, and click Add devices.
- In the newly opened window, you will see the Comfort sensor listed. Select the appropriate application version, choose your sensor, and click Add Device.
5. Integration Confirmation:
After completing these steps, the device integration should be successfully added.
6. Accessing the Sensor Endpoint:
- Click on your sensor to navigate to the endpoint corresponding to this sensor.
If you refer back to the data seen in The Things Stack, you'll notice that the state of the universal input is received as a string. To ensure the platform correctly interprets this data, you need to set up value mapping.
7. Set Up Value Mapping:
- Navigate to Device management -> Applications -> tektelic-sensors -> Base configuration -> epts.
8. Add Time Series Value Mapping:
- Click on Add time series value mapping and add a mapping for the extconnector_state.
This will help the platform understand and correctly display the state of the universal input.
9. Check Data Display:
After configuring the mapping, perform another test (e.g., by simulating a water leak). You should now see the data from the sensor displayed correctly in the time-series table.
10. Mapping Additional Parameters:
You'll also notice that battery voltage is received as a string. Similar to the extconnector_state, you need to add a mapping for the battery voltage.
The same applies to other parameters such as ambient_temperature, relative_humidity, and several others. Each of these parameters needs to be mapped similarly to ensure they are correctly interpreted by the platform.
11. Review Mapped Data:
After setting up the mappings for all these parameters, you should see them displayed correctly in the time-series table.
Step 4: Configuring Your Solution with the Water Leak Widget
We'll use the solution created in the previous tutorial as a base and add a new widget for the Water Leak sensor.
1. Create Widget
- Create a new widget named Waterleak.
The widget will be similar to the one created in the previous tutorial, with the main difference being that it will display the water leak status as an image.
2. Prepare Images
You need to prepare three images to represent different statuses:
- Leak detected
- No leakage
- Unknown status
Save the following images to your computer:
Additionally, prepare a main background image for the widget.
Save the following image to your computer:
3. Set Up the Widget
- Set up the widget with the configuration shown in the screenshots below to display the water leak status using the images you downloaded.
The widget should switch between the images depending on the sensor's input, indicating whether a leak is detected, not detected, or if the status is unknown.
4. Final Widget Display
The final widget should visually indicate the leak status using the images:
5. Widget Configuration Code Example
Here's an example of the widget configuration code that you can use to set up the Water Leak Widget. (you will need to select the appropriate Application, Endpoint ID and upload image files or provide links to them):
{
"config": {
"header": {
"stylingCustomization": {
"textPlacement": "start",
"headerBackgroundColor": "",
"titleTextColor": "",
"additionalStyles": {
"title": {},
"header": {}
}
},
"displayTitle": true,
"title": "Waterleak",
"displayIcon": false
},
"serviceIntegration": {
"metadataServiceName": "epr",
"serviceName": "epts",
"appName": "cqtg58am6fhc73co67u0",
"updateInterval": 3000,
"endpoint": "2ecf7ebc-6316-46a3-bf89-5edf2ebca841"
},
"backgroundImage": {
"backgroundType": "image",
"url": "https://minio.cloud.kaaiot.com/d29eeb91-131a-4831-b541-7b781201d6d4-public/__wd_resources/426156eb-218e-4f9a-929a-66493d7691fd.png",
"values": [],
"size": {
"height": 300,
"width": 300
}
},
"fields": [
{
"coordinates": {
"x1": "480",
"y1": "70"
},
"name": "Temperature",
"styling": {
"additionalStyles": {},
"redirectBlockPlacement": {}
},
"timeSeriesName": "ambient_temperature",
"values": [
{
"path": "values.ambient_temperature",
"displayUnit": "Best",
"displayScale": 1,
"unitStyle": {
"color": "808080",
"font-size": "20"
},
"style": {
"color": "808080",
"font-size": "35"
},
"displayPostfix": "%",
"valueUnit": "C",
"type": "path",
"hoverStyles": {},
"measure": "Temperature"
}
]
},
{
"coordinates": {
"x1": "500",
"y1": "200"
},
"name": "humidity",
"styling": {
"additionalStyles": {},
"color": "#90EE90",
"redirectBlockPlacement": {}
},
"timeSeriesName": "relative_humidity",
"values": [
{
"displayPostfix": "%",
"displayScale": 0,
"hoverStyles": {},
"measure": "Custom",
"path": "values.relative_humidity",
"style": {
"color": "808080",
"font-size": "35"
},
"type": "path",
"unitStyle": {
"color": "808080",
"font-size": "20"
}
}
]
},
{
"coordinates": {
"x1": "460",
"y1": "315"
},
"name": "Battery",
"styling": {
"additionalStyles": {},
"redirectBlockPlacement": {}
},
"timeSeriesName": "battery_voltage",
"values": [
{
"displayPostfix": "V",
"hoverStyles": {},
"measure": "Custom",
"path": "values.battery_voltage",
"style": {
"color": "808080",
"font-size": "35"
},
"type": "path",
"unitStyle": {
"color": "808080",
"font-size": "20"
},
"displayScale": 3
}
]
},
{
"name": "leak",
"values": [
{
"type": "icon",
"path": "values.extconnector_state",
"style": {
"width": "250",
"height": "300"
},
"hoverStyles": {},
"icons": [
{
"operator": "==",
"condition": "High(open-circuit)",
"icon": "https://minio.cloud.kaaiot.com/d29eeb91-131a-4831-b541-7b781201d6d4-public/__wd_resources/9a0f4e5b-74aa-455d-bd30-e06dd7e2ee08.png"
},
{
"operator": "==",
"condition": "Low(short-circuit)",
"icon": "https://minio.cloud.kaaiot.com/d29eeb91-131a-4831-b541-7b781201d6d4-public/__wd_resources/949d4e07-d63a-4ed2-a8bd-56f96a3eec8c.png"
},
{
"operator": "==",
"condition": "default",
"icon": "https://minio.cloud.kaaiot.com/d29eeb91-131a-4831-b541-7b781201d6d4-public/__wd_resources/285d057a-9a55-45ac-8cef-dc2a12df2542.png"
}
],
"measure": "Custom"
}
],
"coordinates": {
"x1": "40",
"y1": "50"
},
"styling": {
"redirectBlockPlacement": {},
"additionalStyles": {}
},
"timeSeriesName": "extconnector_state"
}
],
"appearance": {
"contentFit": "container"
},
"type": "epLabel"
},
"lastUpdated": 1724442740779,
"layout": {
"x": 5,
"y": 0,
"w": 5,
"h": 9,
"minH": 3.5,
"minW": 2
},
"order": 1
}
Conclusion
Congratulations on successfully completing the setup of your TEKTELIC COMFORT LoRaWAN Smart Room sensor with the Kaa IoT platform! Through this tutorial, you've learned how to:
- Register the sensor with The Things Stack and configure its payload formatter.
- Create and map time-series data to ensure accurate data representation.
- Integrate the sensor with KaaIoT and configure a dashboard widget to visually monitor water leak statuses.
By following these steps, you now have a fully functional solution for monitoring environmental parameters and detecting potential water leaks. This setup enhances your ability to manage and respond to environmental changes in real-time, providing a reliable solution for various applications.
If you encounter any issues or wish to explore further customization options, don't hesitate to refer back to this guide or Contact Us.
Thank you for following along, and enjoy your newly configured solution!