Logic Machine Forum
Buderus KM200 api - Printable Version

+- Logic Machine Forum (https://forum.logicmachine.net)
+-- Forum: LogicMachine eco-system (https://forum.logicmachine.net/forumdisplay.php?fid=1)
+--- Forum: Scripting (https://forum.logicmachine.net/forumdisplay.php?fid=8)
+--- Thread: Buderus KM200 api (/showthread.php?tid=2057)



Buderus KM200 api - MichelDeLigne - 29.04.2019

Hello

Here is a script I made that allows you to interface a LM5 with a Logamatic KM200 Buderus gateway.
Logamatic web KM50, KM100, KM300 modules, and Junkers/Bosch MB LANi should also be supported in theory.
 
This script allows you to read any endpoint from the REST interface of the gateway, and assign the values to a KNX group.
It is also possible to set some of the values, ie. change from "auto" to "day" or "night" mode.
Included in an event script, and with a few customizations at the end, you could control the central heating from a few KNX address groups.

For all intent and purposes, this script gives you the same capabilities as the KNX10 module from Bosch, that is available almost nowhere, and is only in German.

Because I do not read many endpoints in my case, I run the script in a cyclic mode on a 60s cycle. 
Accessing a lot of the endpoints would probably require some changes to the script to allow more, or less, polling on some of the endpoints (ie: 10min for the outdoor temperature, 15s for the mode, ...). 
This would reduce the load on the gateway and the network.

Two scripts are required: A custom aes.lua library installed in the user library, and the km200.lua

The aes.lua is different than the one published in another thread on the forum because this one supports no-padding and can decode strings encrypted in other tools, which the original library cannot do.

The list of all endpoints that exist in my system (and some more) is included in the km200.lua script. 
Your system may have more or less depending on the hardware installed.

Of course, if you decide to use the scripts, you do it at your own risk. 
I am not responsible if something goes wrong.

That said, my house is still standing.  Wink

Enjoy.
Michel.

KM200.lua:
Code:
--
-- Control of Buderus boilers using a Logamatic web KM200 module.
-- Logamatic web KM50, KM100, KM300 modules, and Junkers/Bosch MB LANi, should also be supported.
--
-- Reading and writing from/to the gateway is supported.
--
-- v1.0.0 - 2019-04-28
-- Michel De Ligne
--

encdec = require('encdec')
aes = require('user.aes')
json = require('json')
ltn12 = require('ltn12')
http = require('socket.http')

--
-- !!! The three following parameters need to be filled in properly !!!
--
-- IP address or DNS name of the Buderus KM200.
local km200_gateway_host = "TK-850XXXNET.home" -- or "192.168.xxx.xxx"
-- Gateway password - found on the sticker on the KM200.
-- Remove the "-", and put everything in one word.
local km200_gateway_password = "gateway_password" -- ie: fjbvJH5IjYndKdhf

-- Password defined in the app.
local km200_private_password = "app_password" -- ie: qweYTR!rtyEWQ

--
-- Code
--

-- MD5 salt used for the AES key generation (do not change!)
local km200_crypt_md5_salt = string.char(
       0x86, 0x78, 0x45, 0xe9, 0x7c, 0x4e, 0x29, 0xdc,
       0xe5, 0x22, 0xb9, 0xa7, 0xd3, 0xa3, 0xe0, 0x7b,
       0x15, 0x2b, 0xff, 0xad, 0xdd, 0xbe, 0xd7, 0xf5,
       0xff, 0xd8, 0x42, 0xe9, 0x89, 0x5a, 0xd1, 0xe4
);

-- First half of the key: MD5 of Gateway password followed by km200_crypt_md5_salt
local key_1 = encdec.md5(km200_gateway_password .. km200_crypt_md5_salt, true)
-- Second part of the key - private: MD5 from salt followed by private_password
local key_2_private = encdec.md5(km200_crypt_md5_salt .. km200_private_password, true)

km200_crypt_key_private = key_1 .. key_2_private

-- PKCS7 pad/unpad
function pad(data, blocksize, optional)
   blocksize = blocksize or 16
   if type(blocksize) ~= "number" then
       return nil, "invalid block size data type"
   end
   if blocksize < 1 or blocksize > 256 then
       return nil, "invalid block size"
   end
   local ps = blocksize - #data % blocksize
   if optional and ps == blocksize then return data end
   return data .. string.rep(string.char(ps), ps)
end

-- PKCS7 unpad
function unpad(data, blocksize)
   blocksize = blocksize or 16
   if type(blocksize) ~= "number" then
       return nil, "invalid block size data type"
   end
   if blocksize < 1 or blocksize > 256 then
       return nil, "invalid block size"
   end
   local len = #data
   if len % blocksize ~= 0 then
       return nil, "data length is not a multiple of the block size"
   end
   local chr = string.sub(data, -1)
   local rem = string.byte(chr)
   if rem > 0 and rem <= blocksize then
       local chk = string.sub(data, -rem)
       if chk == string.rep(chr, rem) then
           return string.sub(data, 1, len - rem)
       end
   end
   return data
end

function km200_Encrypt(encryptData)
   -- add PKCS #7 padding
   encryptData = pad(encryptData)

   local hash = { iv = string.rep('\0', 16) } -- no hashing method for key
   local aes_256_ecb, err = aes:new(km200_crypt_key_private, nil, aes.cipher(256, 'ecb'), hash, nil, 0)
   return encdec.base64enc(aes_256_ecb:encrypt(encryptData))
   end

function km200_Decrypt(decryptData)
   local hash = { iv = string.rep('\0', 16) } -- no hashing method for key
   local aes_256_ecb, err = aes:new(km200_crypt_key_private, nil, aes.cipher(256, 'ecb'), hash, nil, 0)
   local res = aes_256_ecb:decrypt(encdec.base64dec(decryptData)) -- data block size is always 128 bits

   -- Search the position of the last "}", and remove all the remaining padding after that (I am lazy).
   local endposition = string.find(res, "}", -0)
   res = string.sub(res, 0, endposition)
   return res
end

function dorequest(url)
   local tbl = {}
   local res, err = http.request({
       url = url,
       headers = {
           ['Accept'] = 'application/json',
           ['User-Agent'] = 'TeleHeater/2.2.3'
       },
       sink = ltn12.sink.table(tbl)
   })

   if res then
       return table.concat(tbl)
   else
       log('http error: ' .. err)
       return nil
   end
end

function km200_GetData(REST_URL)
   local res = dorequest('http://' .. km200_gateway_host .. REST_URL)

   if res then
       return km200_Decrypt(res)
   else
       return nil
   end
end

function doput(url, reqbody)
   local tbl = {}
   local res, err = http.request({
       url = url,
       method = 'POST',
       source = ltn12.source.string(reqbody),
       headers = {
           ['User-Agent'] = 'TeleHeater/2.2.3',
           ['Content-Type'] = 'application/json',
           ['Content-Length'] = string.len(reqbody)
       },
       sink = ltn12.sink.table(tbl)
   })
   if res then
       return table.concat(tbl)
   else
       log('http error: ' .. err)
       return nil
   end
end

function km200_SetData(REST_URL, Value)
   local content = json.encode({ value = Value })
   local res = doput('http://' .. km200_gateway_host .. REST_URL, km200_Encrypt(content))

   if res then
       return km200_Decrypt(res)
   else
       return nil
   end
end

-- List of all the REST endpoints available.
-- The endpoints available depends on the system configuration (ie: solar vs no solar, type of regulation, ...)

--[[
/dhwCircuits/dhw1
/dhwCircuits/dhw1/actualTemp
/dhwCircuits/dhw1/charge
/dhwCircuits/dhw1/chargeDuration
/dhwCircuits/dhw1/cpStartph
/dhwCircuits/dhw1/currentSetpoint
/dhwCircuits/dhw1/operationMode
/dhwCircuits/dhw1/singleChargeSetpoint
/dhwCircuits/dhw1/status
/dhwCircuits/dhw1/switchPrograms
/dhwCircuits/dhw1/tdMode
/dhwCircuits/dhw1/tdsetPoint
/dhwCircuits/dhw1/temperatureLevels
/dhwCircuits/dhw1/temperatureLevels/high
/dhwCircuits/dhw1/temperatureLevels/off
/dhwCircuits/dhw1/waterFlow
/dhwCircuits/dhw1/workingTime
/gateway/boschSHPassword
/gateway/DateTime
/gateway/firmware
/gateway/haiPassword
/gateway/instAccess
/gateway/instPassword
/gateway/instWriteAccess
/gateway/knxPassword
/gateway/portalPassword
/gateway/update
/gateway/userpassword
/gateway/uuid
/gateway/version
/gateway/versionFirmware
/gateway/versionHardware
/heatingCircuits
/heatingCircuits/hc1
/heatingCircuits/hc1/activeSwitchProgram
/heatingCircuits/hc1/actualSupplyTemperature
/heatingCircuits/hc1/controlType
/heatingCircuits/hc1/currentOpModeInfo
/heatingCircuits/hc1/currentRoomSetpoint
/heatingCircuits/hc1/designTemp
/heatingCircuits/hc1/fastHeatupFactor
/heatingCircuits/hc1/heatCurveMax
/heatingCircuits/hc1/heatCurveMin
/heatingCircuits/hc1/manualRoomSetpoint
/heatingCircuits/hc1/nextSetpoint
/heatingCircuits/hc1/operationMode
/heatingCircuits/hc1/pumpModulation
/heatingCircuits/hc1/roomInfluence
/heatingCircuits/hc1/roomtemperature
/heatingCircuits/hc1/roomTempOffset
/heatingCircuits/hc1/setpointOptimization
/heatingCircuits/hc1/solarInfluence
/heatingCircuits/hc1/status
/heatingCircuits/hc1/suWiSwitchMode
/heatingCircuits/hc1/suWiThreshold
/heatingCircuits/hc1/switchPrograms
/heatingCircuits/hc1/switchPrograms/A
/heatingCircuits/hc1/switchPrograms/B
/heatingCircuits/hc1/temperatureLevels
/heatingCircuits/hc1/temperatureLevels/comfort2
/heatingCircuits/hc1/temperatureLevels/eco
/heatingCircuits/hc1/temperatureRoomSetpoint
/heatingCircuits/hc1/temporaryRoomSetpoint
/heatingCircuits/hc1/timeToNextSetpoint
/heatSources
/heatSources/actualCHPower
/heatSources/actualDHWPower
/heatSources/actualModulation
/heatSources/actualPower
/heatSources/actualSupplyTemperature
/heatSources/applianceSupplyTemperature
/heatSources/burnerModulationSetpoint
/heatSources/burnerPowerSetpoint
/heatSources/ChimneySweeper
/heatSources/CHpumpModulation
/heatSources/flameCurrent
/heatSources/flameStatus
/heatSources/gasAirPressure
/heatSources/hs1
/heatSources/hs1/actualCHPower
/heatSources/hs1/actualDHWPower
/heatSources/hs1/actualModulation
/heatSources/hs1/actualPower
/heatSources/hs1/CHpumpModulation
/heatSources/hs1/energyReservoir
/heatSources/hs1/flameStatus
/heatSources/hs1/fuel
/heatSources/hs1/fuel/caloricValue
/heatSources/hs1/fuel/density
/heatSources/hs1/fuelConsmptCorrFactor
/heatSources/hs1/info
/heatSources/hs1/nominalCHPower
/heatSources/hs1/nominalDHWPower
/heatSources/hs1/nominalFuelConsumption
/heatSources/hs1/numberOfStarts
/heatSources/hs1/reservoirAlert
/heatSources/hs1/supplyTemperatureSetpoint
/heatSources/hs1/type
/heatSources/info
/heatSources/nominalCHPower
/heatSources/nominalDHWPower
/heatSources/numberOfStarts
/heatSources/powerSetpoint
/heatSources/returnTemperature
/heatSources/supplyTemperatureSetpoint
/heatSources/systemPressure
/heatSources/workingTime
/heatSources/workingTime/centralHeating
/heatSources/workingTime/secondBurner
/heatSources/workingTime/totalSystem
/notifications
/recordings
/recordings/heatingCircuits
/recordings/heatingCircuits/hc1
/recordings/heatingCircuits/hc1/roomtemperature
/recordings/heatSources
/recordings/heatSources/actualCHPower
/recordings/heatSources/actualDHWPower
/recordings/heatSources/actualPower
/recordings/heatSources/hs1
/recordings/heatSources/hs1/actualPower
/recordings/system
/recordings/system/heatSources
/recordings/system/heatSources/hs1
/recordings/system/heatSources/hs1/actualPower
/recordings/system/sensors
/recordings/system/sensors/temperatures
/recordings/system/sensors/temperatures/outdoor_t1
/solarCircuits
/solarCircuits/sc1/collectorTemperature
/solarCircuits/sc1/pumpModulation
/solarCircuits/sc1/solarYield
/solarCircuits/sc1/status
/system
/system/appliance
/system/appliance/actualPower
/system/appliance/actualSupplyTemperature
/system/appliance/ChimneySweeper
/system/appliance/CHpumpModulation
/system/appliance/flameCurrent
/system/appliance/gasAirPressure
/system/appliance/nominalBurnerLoad
/system/appliance/numberOfStarts
/system/appliance/powerSetpoint
/system/appliance/systemPressure
/system/appliance/workingTime
/system/appliance/workingTime/centralHeating
/system/appliance/workingTime/secondBurner
/system/appliance/workingTime/totalSystem
/system/brand
/system/bus
/system/healthStatus
/system/heatSources
/system/heatSources/hs1
/system/heatSources/hs1/actualModulation
/system/heatSources/hs1/actualPower
/system/heatSources/hs1/energyReservoir
/system/heatSources/hs1/fuel
/system/heatSources/hs1/fuel/caloricValue
/system/heatSources/hs1/fuel/density
/system/heatSources/hs1/fuelConsmptCorrFactor
/system/heatSources/hs1/nominalFuelConsumption
/system/heatSources/hs1/reservoirAlert
/system/holidayModes
/system/holidayModes/hm1
/system/holidayModes/hm1/assignedTo
/system/holidayModes/hm1/delete
/system/holidayModes/hm1/dhwMode
/system/holidayModes/hm1/hcMode
/system/holidayModes/hm1/startStop
/system/holidayModes/hm2
/system/holidayModes/hm2/assignedTo
/system/holidayModes/hm2/delete
/system/holidayModes/hm2/dhwMode
/system/holidayModes/hm2/hcMode
/system/holidayModes/hm2/startStop
/system/holidayModes/hm3
/system/holidayModes/hm3/assignedTo
/system/holidayModes/hm3/delete
/system/holidayModes/hm3/dhwMode
/system/holidayModes/hm3/hcMode
/system/holidayModes/hm3/startStop
/system/holidayModes/hm4
/system/holidayModes/hm4/assignedTo
/system/holidayModes/hm4/delete
/system/holidayModes/hm4/dhwMode
/system/holidayModes/hm4/hcMode
/system/holidayModes/hm4/startStop
/system/holidayModes/hm5
/system/holidayModes/hm5/assignedTo
/system/holidayModes/hm5/delete
/system/holidayModes/hm5/dhwMode
/system/holidayModes/hm5/hcMode
/system/holidayModes/hm5/startStop
/system/info
/system/minOutdoorTemp
/system/sensors
/system/sensors/temperatures
/system/sensors/temperatures/chimney
/system/sensors/temperatures/hotWater_t1
/system/sensors/temperatures/hotWater_t2
/system/sensors/temperatures/outdoor_t1
/system/sensors/temperatures/return
/system/sensors/temperatures/supply_t1
/system/sensors/temperatures/supply_t1_setpoint
/system/sensors/temperatures/switch
/system/systemType
]]

--
-- Mapping of some endpoints to KNX group addresses
--

-- {"id":"/heatSources/flameStatus","type":"stringValue","writeable":0,"recordable":1,"value":"off","allowedValues":["on","off"]}
jsondata = json.decode(km200_GetData("/heatSources/flameStatus"))
if (jsondata.value == 'on') then
   grp.write('heatSources_flameStatus', true)
else
   grp.write('heatSources_flameStatus', false)
end

-- Afficher la performance actuelle Générateur de chaleur (5.001)
-- {"id":"/heatSources/actualCHPower","type":"floatValue","writeable":0,"recordable":1,"value":0.0,"unitOfMeasure":"%"}
jsondata = json.decode(km200_GetData("/heatSources/actualCHPower"))
grp.write('heatSources_actualCHPower', jsondata.value)

jsondata = json.decode(km200_GetData("/heatingCircuits/hc1/roomtemperature"))
grp.write('heatingCircuits_hc1_roomtemperature', jsondata.value)

-- {"id":"/system/sensors/temperatures/outdoor_t1","type":"floatValue","writeable":0,"recordable":1,"value":14.0,"unitOfMeasure":"C"}
jsondata = json.decode(km200_GetData("/system/sensors/temperatures/outdoor_t1"))
grp.write('temperatures_outdoor_t1', jsondata.value)

-- {"id":"/heatSources/actualSupplyTemperature","type":"floatValue","writeable":0,"recordable":0,"value":21.7,"unitOfMeasure":"C"}
jsondata = json.decode(km200_GetData("/heatSources/actualSupplyTemperature"))
grp.write('heatSources_actualSupplyTemperature', jsondata.value)

-- {"id":"/system/healthStatus","type":"stringValue","writeable":0,"recordable":0,"value":"ok"}
jsondata = json.decode(km200_GetData("/system/healthStatus"))
if (jsondata.value == 'ok') then
   grp.write('system_healthStatus', false)
else
   grp.write('system_healthStatus', true)
end

-- {"id":"/heatingCircuits/hc1/operationMode","type":"stringValue","writeable":1,"recordable":1,"value":"auto","allowedValues":["night","day","auto"]}
jsondata = json.decode(km200_GetData("/heatingCircuits/hc1/operationMode"))
if (jsondata.value == 'auto') then
   grp.write('heatingCircuits_operationMode_Auto', true)
else
   grp.write('heatingCircuits_operationMode_Auto', false)
   if (jsondata.value == 'night') then
       grp.write('heatingCircuits_operationMode_NightDay', false)
   else
       grp.write('heatingCircuits_operationMode_NightDay', true)
   end
end

-- {"id":"/heatingCircuits/hc1/currentRoomSetpoint","type":"floatValue","writeable":0,"recordable":1,"value":18.0,"unitOfMeasure":"C","minValue":0.0,"maxValue":30.0}
jsondata = json.decode(km200_GetData("/heatingCircuits/hc1/currentRoomSetpoint"))
grp.write('heatingCircuits_hc1_currentRoomSetpoint', jsondata.value)

-- {"id":"/heatingCircuits/hc1/pumpModulation","type":"floatValue","writeable":0,"recordable":1,"value":100.0,"unitOfMeasure":"%","minValue":0.0,"maxValue":100.0}
jsondata = json.decode(km200_GetData("/heatingCircuits/hc1/pumpModulation"))
if (jsondata.value == '0.0') then
   grp.write('heatingCircuits_hc1_pumpModulation', false)
else
   grp.write('heatingCircuits_hc1_pumpModulation', true)
end

-- {"id":"/dhwCircuits/dhw1/actualTemp","type":"floatValue","writeable":0,"recordable":1,"value":57.8,"unitOfMeasure":"C"}
jsondata = json.decode(km200_GetData("/dhwCircuits/dhw1/actualTemp"))
grp.write('dhwCircuits_dhw1_actualTemp', jsondata.value)


-- {"id":"/dhwCircuits/dhw1/currentSetpoint","type":"floatValue","writeable":0,"recordable":1,"value":10.0,"unitOfMeasure":"C","minValue":0.0,"maxValue":80.0}
jsondata = json.decode(km200_GetData("/dhwCircuits/dhw1/currentSetpoint"))
grp.write('dhwCircuits_dhw1_currentSetpoint', jsondata.value)

-- {"id":"/dhwCircuits/dhw1/operationMode","type":"stringValue","writeable":1,"recordable":0,"value":"auto","allowedValues":["auto","on","off"]}
jsondata = json.decode(km200_GetData("/dhwCircuits/dhw1/operationMode"))
if (jsondata.value == 'auto') then
   grp.write('dhwCircuits_dhw1_operationMode_Auto', true)
else
   grp.write('dhwCircuits_dhw1_operationMode_Auto', false)
   if (jsondata.value == 'on') then
       grp.write('dhwCircuits_dhw1_operationManualOnOff', true)
   else
       grp.write('dhwCircuits_dhw1_operationManualOnOff', false)
   end
end

-- Change the operation mode to auto mode (it needs to be one of the allowedValues):
-- km200_SetData("/heatingCircuits/hc1/operationMode", "auto")

aes.lua
Code:
-- Copyright (C) by Yichun Zhang (agentzh)


--local asn1 = require "resty.asn1"
local ffi = require "ffi"
local ffi_new = ffi.new
local ffi_gc = ffi.gc
local ffi_str = ffi.string
local ffi_copy = ffi.copy
local C = ffi.C
local setmetatable = setmetatable
--local error = error
local type = type


local _M = { _VERSION = '0.09' }

local mt = { __index = _M }

local lib = '/usr/lib/libcrypto.so'
if not io.exists(lib) then
     lib = '/usr/lib/libcrypto.so.1.0.0'
end
ffi.load(lib, true)

ffi.cdef[[
typedef struct engine_st ENGINE;

typedef struct evp_cipher_st EVP_CIPHER;
typedef struct evp_cipher_ctx_st
{
const EVP_CIPHER *cipher;
ENGINE *engine;
int encrypt;
int buf_len;

unsigned char  oiv[16];
unsigned char  iv[16];
unsigned char buf[32];
int num;

void *app_data;
int key_len;
unsigned long flags;
void *cipher_data;
int final_used;
int block_mask;
unsigned char final[32];
} EVP_CIPHER_CTX;

typedef struct env_md_ctx_st EVP_MD_CTX;
typedef struct env_md_st EVP_MD;

const EVP_MD *EVP_md5(void);
const EVP_MD *EVP_sha(void);
const EVP_MD *EVP_sha1(void);
const EVP_MD *EVP_sha224(void);
const EVP_MD *EVP_sha256(void);
const EVP_MD *EVP_sha384(void);
const EVP_MD *EVP_sha512(void);

const EVP_CIPHER *EVP_aes_128_ecb(void);
const EVP_CIPHER *EVP_aes_128_cbc(void);
const EVP_CIPHER *EVP_aes_128_cfb1(void);
const EVP_CIPHER *EVP_aes_128_cfb8(void);
const EVP_CIPHER *EVP_aes_128_cfb128(void);
const EVP_CIPHER *EVP_aes_128_ofb(void);
const EVP_CIPHER *EVP_aes_128_ctr(void);
const EVP_CIPHER *EVP_aes_192_ecb(void);
const EVP_CIPHER *EVP_aes_192_cbc(void);
const EVP_CIPHER *EVP_aes_192_cfb1(void);
const EVP_CIPHER *EVP_aes_192_cfb8(void);
const EVP_CIPHER *EVP_aes_192_cfb128(void);
const EVP_CIPHER *EVP_aes_192_ofb(void);
const EVP_CIPHER *EVP_aes_192_ctr(void);
const EVP_CIPHER *EVP_aes_256_ecb(void);
const EVP_CIPHER *EVP_aes_256_cbc(void);
const EVP_CIPHER *EVP_aes_256_cfb1(void);
const EVP_CIPHER *EVP_aes_256_cfb8(void);
const EVP_CIPHER *EVP_aes_256_cfb128(void);
const EVP_CIPHER *EVP_aes_256_ofb(void);

void EVP_CIPHER_CTX_init(EVP_CIPHER_CTX *a);
int EVP_CIPHER_CTX_cleanup(EVP_CIPHER_CTX *a);

int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *ctx, int padding);

int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx,const EVP_CIPHER *cipher,
       ENGINE *impl, unsigned char *key, const unsigned char *iv);

int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl,
       const unsigned char *in, int inl);

int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl);

int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx,const EVP_CIPHER *cipher,
       ENGINE *impl, unsigned char *key, const unsigned char *iv);

int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl,
       const unsigned char *in, int inl);

int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm, int *outl);

int EVP_BytesToKey(const EVP_CIPHER *type,const EVP_MD *md,
       const unsigned char *salt, const unsigned char *data, int datal,
       int count, unsigned char *key,unsigned char *iv);
]]

local ctx_ptr_type = ffi.typeof("EVP_CIPHER_CTX[1]")

local hash
hash = {
   md5 = C.EVP_md5(),
   sha1 = C.EVP_sha1(),
   sha224 = C.EVP_sha224(),
   sha256 = C.EVP_sha256(),
   sha384 = C.EVP_sha384(),
   sha512 = C.EVP_sha512()
}
_M.hash = hash

local cipher
cipher = function (size, _cipher)
   local _size = size or 128
   local _cipher = _cipher or "cbc"
   local func = "EVP_aes_" .. _size .. "_" .. _cipher
   if C[func] then
       return { size=_size, cipher=_cipher, method=C[func]()}
   else
       return nil
   end
end
_M.cipher = cipher

function _M.new(self, key, salt, _cipher, _hash, hash_rounds, padding)
   local encrypt_ctx = ffi_new(ctx_ptr_type)
   local decrypt_ctx = ffi_new(ctx_ptr_type)
   local _cipher = _cipher or cipher()
   local _hash = _hash or hash.md5
   local hash_rounds = hash_rounds or 1
   local padding = padding or 1
   local _cipherLength = _cipher.size/8
   local gen_key = ffi_new("unsigned char[?]",_cipherLength)
   local gen_iv = ffi_new("unsigned char[?]",_cipherLength)

   if type(_hash) == "table" then
       if not _hash.iv or #_hash.iv ~= 16 then
         return nil, "bad iv"
       end

       if _hash.method then
           local tmp_key = _hash.method(key)

           if #tmp_key ~= _cipherLength then
               return nil, "bad key length"
           end

           ffi_copy(gen_key, tmp_key, _cipherLength)

       elseif #key ~= _cipherLength then
           return nil, "bad key length"

       else
           ffi_copy(gen_key, key, _cipherLength)
       end

       ffi_copy(gen_iv, _hash.iv, 16)

   else
       if C.EVP_BytesToKey(_cipher.method, _hash, salt, key, #key,
                           hash_rounds, gen_key, gen_iv)
           ~= _cipherLength
       then
           return nil
       end
   end

   C.EVP_CIPHER_CTX_init(encrypt_ctx)
   C.EVP_CIPHER_CTX_init(decrypt_ctx)

   if C.EVP_EncryptInit_ex(encrypt_ctx, _cipher.method, nil,
     gen_key, gen_iv) == 0 or
     C.EVP_DecryptInit_ex(decrypt_ctx, _cipher.method, nil,
     gen_key, gen_iv) == 0 then
       return nil
   end

   if C.EVP_CIPHER_CTX_set_padding(encrypt_ctx, padding) == 0 then
       return nil
   end

   if C.EVP_CIPHER_CTX_set_padding(decrypt_ctx, padding) == 0 then
       return nil
   end

   ffi_gc(encrypt_ctx, C.EVP_CIPHER_CTX_cleanup)
   ffi_gc(decrypt_ctx, C.EVP_CIPHER_CTX_cleanup)

   return setmetatable({
     _encrypt_ctx = encrypt_ctx,
     _decrypt_ctx = decrypt_ctx
     }, mt)
end


function _M.encrypt(self, s)
   local s_len = #s
   local max_len = s_len + 16
   local buf = ffi_new("unsigned char[?]", max_len)
   local out_len = ffi_new("int[1]")
   local tmp_len = ffi_new("int[1]")
   local ctx = self._encrypt_ctx

   if C.EVP_EncryptInit_ex(ctx, nil, nil, nil, nil) == 0 then
       return nil
   end

   if C.EVP_EncryptUpdate(ctx, buf, out_len, s, s_len) == 0 then
       return nil
   end

   if C.EVP_EncryptFinal_ex(ctx, buf + out_len[0], tmp_len) == 0 then
       return nil
   end

   return ffi_str(buf, out_len[0] + tmp_len[0])
end


function _M.decrypt(self, s)
   local s_len = #s
   local buf = ffi_new("unsigned char[?]", s_len)
   local out_len = ffi_new("int[1]")
   local tmp_len = ffi_new("int[1]")
   local ctx = self._decrypt_ctx

   if C.EVP_DecryptInit_ex(ctx, nil, nil, nil, nil) == 0 then
     return nil
   end

   if C.EVP_DecryptUpdate(ctx, buf, out_len, s, s_len) == 0 then
     return nil
   end

   if C.EVP_DecryptFinal_ex(ctx, buf + out_len[0], tmp_len) == 0 then
       return nil
   end

   return ffi_str(buf, out_len[0] + tmp_len[0])
end


return _M



RE: Buderus KM200 api - sds - 30.10.2019

Hello Michel

Is it possible that line 101:

Code:
-- Search the position of the last "}", and remove all the remaining padding after that (I am lazy).
   local endposition = string.find(res, "}", -0)

needs to be -1 instead of -0. With your -0 I have faulty results, such as with this REST response

Code:
"id":"/solarCircuits/sc1/collectorTemperature","type":"floatValue","writeable":0,"recordable":1,"value":-0.6,"unitOfMeasure":"C","state":[{"open":-3276.8},{"short":3276.7}]}



RE: Buderus KM200 api - MichelDeLigne - 31.10.2019

Hi,

Could be that there is an error left somewhere as once in a while I see an error in the log.
I don't think it is related to the line 101. REST sends a json results, that is supposed to be enclosed in {}.
In your case, I would say the problem is probably with the first "{", that seems to be missing.

Regards.
Michel.


RE: Buderus KM200 api - sds - 01.11.2019

(31.10.2019, 08:49)MichelDeLigne Wrote: I don't think it is related to the line 101. REST sends a json results, that is supposed to be enclosed in {}.
In your case, I would say the problem is probably with the first "{", that seems to be missing.
That was a copy paste error from my side. Real JSON answer is:
Code:
{"id":"/solarCircuits/sc1/collectorTemperature","type":"floatValue","writeable":0,"recordable":1,"value":-0.6,"unitOfMeasure":"C","state":[{"open":-3276.8},{"short":3276.7}]}
By the way, as in the code you are searching for } it doesn't have anything to do with the {. 

Let me rephrase the issue.
The output that I got after the code:
Code:
local endposition = string.find(res, "}", -0)
res = string.sub(res, 0, endposition)
return res
is
Code:
{"id":"/solarCircuits/sc1/collectorTemperature","type":"floatValue","writeable":0,"recordable":1,"value":-0.6,"unitOfMeasure":"C","state":[{"open":-3276.8}
This is not a valid JSON response as the last } (and some more text) is missing. I assume the -0 in the string.find is not the correct value.

Anyway, thanks a lot for the code Smile It works like a charm so I'm happy with it!


RE: Buderus KM200 api - m.j.sorokin - 28.07.2020

Good day! I'm trying a lice script with a Buderus KM200 V2 device. When I try to run the script, I get the following error:

km200 28.07.2020 21:34:09
User library aes:178: Symbol not found: EVP_CIPHER_CTX_init
stack traceback:
[C]: in function '__index'
User library aes:178: in function 'new'
Resident script:97: in function 'km200_GetData'


What could be the reason?


RE: Buderus KM200 api - MichelDeLigne - 28.07.2020

(28.07.2020, 18:39)m.j.sorokin Wrote: Good day! I'm trying a lice script with a Buderus KM200 V2 device. When I try to run the script, I get the following error:

km200 28.07.2020 21:34:09
User library aes:178: Symbol not found: EVP_CIPHER_CTX_init
stack traceback:
[C]: in function '__index'
User library aes:178: in function 'new'
Resident script:97: in function 'km200_GetData'


What could be the reason?

Hello.
Are you running by any chances on the latest software release of LM?
I have the same problem since I updated. There is a problem with the aes library with the 2020 RC2 version.


RE: Buderus KM200 api - admin - 29.07.2020

This happened because new FW uses newer OpenSSL library which changed some of the function calls. I've attached an updated aes library see if it works for you.


RE: Buderus KM200 api - MichelDeLigne - 29.07.2020

Thanks for the updated  aes library. It does solve the problem.


RE: Buderus KM200 api - m.j.sorokin - 29.07.2020

Thanks, the new library solved the problem, some of the variables were updated, but I got the following error:


km200 29.07.2020 16:32:36
Resident script:397: Expected comma or array end but found T_END at character 156
stack traceback:
[C]: in function 'decode'


This may be due to the fact that on my device some of the functionality may be inactive?


RE: Buderus KM200 api - admin - 30.07.2020

Replace km200_Decrypt function with this. Original function removes all data after the first "}" while it should remove it after the last "}", this is why you get an error when there are multiple "}" in returned data.
Code:
function km200_Decrypt(decryptData)
    local hash = { iv = string.rep('\0', 16) } -- no hashing method for key
    local aes_256_ecb, err = aes:new(km200_crypt_key_private, nil, aes.cipher(256, 'ecb'), hash, nil, 0)
    local res = aes_256_ecb:decrypt(encdec.base64dec(decryptData)) -- data block size is always 128 bits

    -- Search the position of the last "}", and remove all the remaining padding after that
    local endposition
    while true do
      local pos = res:find("}", endposition)
      if pos then
          endposition = pos + 1
      else
          break
      end
    end

    if endposition then
        res = res:sub(1, endposition - 1)
    end
    return res
end



RE: Buderus KM200 api - m.j.sorokin - 30.07.2020

Thank you so much! A new feature allows parse all parameters, including all current temperatures!


RE: Buderus KM200 api - MichelDeLigne - 30.07.2020

(30.07.2020, 06:20)admin Wrote: Replace km200_Decrypt function with this. Original function removes all data after the first "}" while it should remove it after the last "}", this is why you get an error when there are multiple "}" in returned data.
Code:
function km200_Decrypt(decryptData)
    local hash = { iv = string.rep('\0', 16) } -- no hashing method for key
    local aes_256_ecb, err = aes:new(km200_crypt_key_private, nil, aes.cipher(256, 'ecb'), hash, nil, 0)
    local res = aes_256_ecb:decrypt(encdec.base64dec(decryptData)) -- data block size is always 128 bits

    -- Search the position of the last "}", and remove all the remaining padding after that
    local endposition
    while true do
      local pos = res:find("}", endposition)
      if pos then
          endposition = pos + 1
      else
          break
      end
    end

    if endposition then
        res = res:sub(1, endposition - 1)
    end
    return res
end

Thanks admin for the correction.

Do you know why a find with a "-" does not work?
in Lua, if I am not mistaken, a find with a "-" in the argument would start the search from the end and find the first occurence of the character. That should find the last "}".
Obviously it does not work, so I am missing something here.

Regards.
Michel


RE: Buderus KM200 api - admin - 30.07.2020

This argument is for where to start the search from. A negative value means start from "string length - abs(argument)". The search is still performed from the position to the end of the string. There's no built-in function for reverse search.


RE: Buderus KM200 api - sds - 20.05.2024

Hi all

Just upgraded from 20230607 to 20240426 version, with error message

Code:
User library aes:185: Symbol not found: EVP_CIPHER_CTX_block_size
stack traceback:
[C]: in function '__index'
User library aes:185: in function 'new'
User script:107: in function 'km200_GetData'

I updated aes.lua as suggested in https://forum.logicmachine.net/showthread.php?tid=2390&pid=34799#pid34799

However, I stumble now to another error (while the Buderus REST API did not change)

Reading the output of the error message thrown by aes decrypt function
Code:
local res, err2 = aes_256_ecb:decrypt(encdec.base64dec(decryptData)) -- data block size is always 128 bits
  alert (err2)

resulted in: "EVP_DecryptFinal_ex failed"

Which is thrown by following code

Code:
    if C.EVP_DecryptFinal_ex(ctx, buf + out_len[0], tmp_len) == 0 then
        return nil, "EVP_DecryptFinal_ex failed"
    end

Any suggestion? What info is needed to debug further?


RE: Buderus KM200 api - admin - 20.05.2024

Can you send raw HTTP response via PM (in base64 format before decryption)?


RE: Buderus KM200 api - sds - 20.05.2024

PM is sent


RE: Buderus KM200 api - admin - 21.05.2024

Try disabling padding in km200_Encrypt and km200_Decrypt:

Code:
function km200_Encrypt(encryptData)
    -- add PKCS #7 padding
    encryptData = pad(encryptData)

    local hash = { iv = string.rep('\0', 16) } -- no hashing method for key
    local aes_256_ecb, err = aes:new(km200_crypt_key_private, nil, aes.cipher(256, 'ecb'), hash, nil, 0, false) -- no padding

Code:
function km200_Decrypt(decryptData)
    local hash = { iv = string.rep('\0', 16) } -- no hashing method for key
    local aes_256_ecb, err = aes:new(km200_crypt_key_private, nil, aes.cipher(256, 'ecb'), hash, nil, 0, false) -- no padding



RE: Buderus KM200 api - sds - 21.05.2024

We have a winner! Much appreciated Admin