/* Copyright (C) 2018-2024 Fredrik Öhrström (gpl-3.0-or-later) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include"address.h" #include"aes.h" #include"aescmac.h" #include"cmdline.h" #include"config.h" #include"formula_implementation.h" #include"meters.h" #include"printer.h" #include"serial.h" #include"translatebits.h" #include"util.h" #include"wmbus.h" #include"dvparser.h" #include #include using namespace std; // This is test specific verbose. bool verbose_ = false; #define LIST_OF_TESTS \ X(addresses) \ X(dynamic_loading) \ X(crc) \ X(dvparser) \ X(devices) \ X(linkmodes) \ X(ids) \ X(kdf) \ X(periods) \ X(device_parsing) \ X(meters) \ X(months) \ X(aes) \ X(sbc) \ X(hex) \ X(translate) \ X(slip) \ X(dvs) \ X(ascii_detection) \ X(status_join) \ X(status_sort) \ X(field_matcher) \ X(units_extraction) \ X(si_units_siexp) \ X(si_units_basic) \ X(si_units_conversion) \ X(formulas_building_consts) \ X(formulas_building_meters) \ X(formulas_datetimes) \ X(formulas_parsing_1) \ X(formulas_parsing_2) \ X(formulas_multiply_constants) \ X(formulas_divide_constants) \ X(formulas_sqrt_constants) \ X(formulas_errors) \ X(formulas_dventries) \ X(formulas_stringinterpolation) \ #define X(t) void test_##t(); LIST_OF_TESTS #undef X // Test if we should run this test based on the command line pattern. bool test(const char *test_name, const char *pattern) { if (pattern == NULL) { if (verbose_) printf("Test %s\n", test_name); return true; } bool ok = strstr(test_name, pattern) != NULL; if (ok) info("Test %s\n", test_name); return ok; } int main(int argc, char **argv) { const char *pattern = NULL; int i = 1; while (i < argc) { if (!strcmp(argv[i], "--verbose")) { verbose_ = true; } else if (!strcmp(argv[i], "--debug")) { verbose_ = true; debugEnabled(true); } else if (!strcmp(argv[i], "--trace")) { verbose_ = true; debugEnabled(true); traceEnabled(true); } else { pattern = argv[i]; } i++; } onExit([](){}); #define X(x) if (test(#x, pattern)) test_##x(); LIST_OF_TESTS #undef X return 0; } void test_crc() { unsigned char data[4]; data[0] = 0x01; data[1] = 0xfd; data[2] = 0x1f; data[3] = 0x01; uint16_t crc = crc16_EN13757(data, 4); if (crc != 0xcc22) { printf("ERROR! %4x should be cc22\n", crc); } data[3] = 0x00; crc = crc16_EN13757(data, 4); if (crc != 0xf147) { printf("ERROR! %4x should be f147\n", crc); } uchar block[10]; block[0]=0xEE; block[1]=0x44; block[2]=0x9A; block[3]=0xCE; block[4]=0x01; block[5]=0x00; block[6]=0x00; block[7]=0x80; block[8]=0x23; block[9]=0x07; crc = crc16_EN13757(block, 10); if (crc != 0xaabc) { printf("ERROR! %4x should be aabc\n", crc); } block[0]='1'; block[1]='2'; block[2]='3'; block[3]='4'; block[4]='5'; block[5]='6'; block[6]='7'; block[7]='8'; block[8]='9'; crc = crc16_EN13757(block, 9); if (crc != 0xc2b7) { printf("ERROR! %4x should be c2b7\n", crc); } } bool tst_parse(const char *data, std::map> *dv_entries, int testnr) { debug("\n\nTest nr %d......\n\n", testnr); bool b; Telegram t; vector databytes; hex2bin(data, &databytes); vector::iterator i = databytes.begin(); b = parseDV(&t, databytes, i, databytes.size(), dv_entries); return b; } void tst_double(map> &values, const char *key, double v, int testnr) { int offset; double value; bool b = extractDVdouble(&values, key, &offset, &value); if (!b || value != v) { fprintf(stderr, "Error in dvparser testnr %d: got %lf but expected value %lf for key %s\n", testnr, value, v, key); } } void tst_string(map> &values, const char *key, const char *v, int testnr) { int offset; string value; bool b = extractDVHexString(&values, key, &offset, &value); if (!b || value != v) { fprintf(stderr, "Error in dvparser testnr %d: got \"%s\" but expected value \"%s\" for key %s\n", testnr, value.c_str(), v, key); } } void tst_date(map> &values, const char *key, string date_expected, int testnr) { int offset; struct tm value; bool b = extractDVdate(&values, key, &offset, &value); char buf[256]; strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &value); string date_got = buf; if (!b || date_got != date_expected) { fprintf(stderr, "Error in dvparser testnr %d:\ngot >%s< but expected >%s< for key %s\n\n", testnr, date_got.c_str(), date_expected.c_str(), key); } } void test_dvparser() { map> dv_entries; int testnr = 1; tst_parse("2F 2F 0B 13 56 34 12 8B 82 00 93 3E 67 45 23 0D FD 10 0A 30 31 32 33 34 35 36 37 38 39 0F 88 2F", &dv_entries, testnr); tst_double(dv_entries, "0B13", 123.456, testnr); tst_double(dv_entries, "8B8200933E", 234.567, testnr); tst_string(dv_entries, "0DFD10", "30313233343536373839", testnr); testnr++; dv_entries.clear(); tst_parse("82046C 5f1C", &dv_entries, testnr); tst_date(dv_entries, "82046C", "2010-12-31 00:00:00", testnr); // 2010-dec-31 testnr++; dv_entries.clear(); tst_parse("0C1348550000426CE1F14C130000000082046C21298C0413330000008D04931E3A3CFE3300000033000000330000003300000033000000330000003300000033000000330000003300000033000000330000004300000034180000046D0D0B5C2B03FD6C5E150082206C5C290BFD0F0200018C4079678885238310FD3100000082106C01018110FD610002FD66020002FD170000", &dv_entries, testnr); tst_double(dv_entries, "0C13", 5.548, testnr); tst_date(dv_entries, "426C", "2127-01-01 00:00:00", testnr); // 2127-jan-1 tst_date(dv_entries, "82106C", "2000-01-01 00:00:00", testnr); // 2000-jan-1 testnr++; dv_entries.clear(); tst_parse("426C FE04", &dv_entries, testnr); tst_date(dv_entries, "426C", "2007-04-30 00:00:00", testnr); // 2010-dec-31 } void test_devices() { shared_ptr manager = createSerialCommunicationManager(0, false); shared_ptr serial1 = manager->createSerialDeviceSimulator(); /* shared_ptr wmbus_im871a = openIM871A("", manager, serial1); manager->stop();*/ } void test_linkmodes() { /* LinkModeCalculationResult lmcr; auto manager = createSerialCommunicationManager(0, false); auto serial1 = manager->createSerialDeviceSimulator(); auto serial2 = manager->createSerialDeviceSimulator(); auto serial3 = manager->createSerialDeviceSimulator(); auto serial4 = manager->createSerialDeviceSimulator(); auto serial5 = manager->createSerialDeviceSimulator(); vector no_meter_shells, no_meter_jsons; Detected de; SpecifiedDevice sd; shared_ptr wmbus_im871a = openIM871A(de, manager, serial1); shared_ptr wmbus_amb8465 = openAMB8465(de, manager, serial2); shared_ptr wmbus_rtlwmbus = openRTLWMBUS(de, "", false, manager, serial3); shared_ptr wmbus_rawtty = openRawTTY(de, manager, serial4); shared_ptr wmbus_amb3665 = openAMB3665(de, manager, serial5); Configuration nometers_config; // Check that if no meters are supplied then you must set a link mode. lmcr = calculateLinkModes(&nometers_config, wmbus_im871a.get()); if (lmcr.type != LinkModeCalculationResultType::NoMetersMustSupplyModes) { printf("ERROR! Expected failure due to automatic deduction! Got instead:\n%s\n", lmcr.msg.c_str()); } debug("test0 OK\n\n"); nometers_config.default_device_linkmodes.addLinkMode(LinkMode::T1); lmcr = calculateLinkModes(&nometers_config, wmbus_im871a.get()); if (lmcr.type != LinkModeCalculationResultType::Success) { printf("ERROR! Expected succcess! Got instead:\n%s\n", lmcr.msg.c_str()); } debug("test0.0 OK\n\n"); Configuration apator_config; string apator162 = "apator162"; vector ids = { "12345678" }; apator_config.meters.push_back(MeterInfo("", // bus "m1", // name MeterDriver::APATOR162, // driver/type "", // extras ids, // ids "", // Key toMeterLinkModeSet(apator162), // link mode set 0, // baud no_meter_shells, // shells no_meter_jsons)); // jsons // Check that if no explicit link modes are provided to apator162, then // automatic deduction will fail, since apator162 can be configured to transmit // either C1 or T1 telegrams. lmcr = calculateLinkModes(&apator_config, wmbus_im871a.get()); if (lmcr.type != LinkModeCalculationResultType::AutomaticDeductionFailed) { printf("ERROR! Expected failure due to automatic deduction! Got instead:\n%s\n", lmcr.msg.c_str()); } debug("test1 OK\n\n"); // Check that if we supply the link mode T1 when using an apator162, then // automatic deduction will succeeed. apator_config.default_device_linkmodes = LinkModeSet(); apator_config.default_device_linkmodes.addLinkMode(LinkMode::T1); apator_config.default_device_linkmodes.addLinkMode(LinkMode::C1); lmcr = calculateLinkModes(&apator_config, wmbus_im871a.get()); if (lmcr.type != LinkModeCalculationResultType::DongleCannotListenTo) { printf("ERROR! Expected dongle cannot listen to! Got instead:\n%s\n", lmcr.msg.c_str()); } debug("test2 OK\n\n"); lmcr = calculateLinkModes(&apator_config, wmbus_rtlwmbus.get()); if (lmcr.type != LinkModeCalculationResultType::Success) { printf("ERROR! Expected success! Got instead:\n%s\n", lmcr.msg.c_str()); } debug("test3 OK\n\n"); Configuration multical21_and_supercom587_config; string multical21 = "multical21"; string supercom587 = "supercom587"; multical21_and_supercom587_config.meters.push_back(MeterInfo("", "m1", MeterDriver::MULTICAL21, "", ids, "", toMeterLinkModeSet(multical21), 0, no_meter_shells, no_meter_jsons)); multical21_and_supercom587_config.meters.push_back(MeterInfo("", "m2", MeterDriver::UNKNOWN, "supercom587", ids, "", toMeterLinkModeSet(supercom587), 0, no_meter_shells, no_meter_jsons)); // Check that meters that transmit on two different link modes cannot be listened to // at the same time using im871a. lmcr = calculateLinkModes(&multical21_and_supercom587_config, wmbus_im871a.get()); if (lmcr.type != LinkModeCalculationResultType::AutomaticDeductionFailed) { printf("ERROR! Expected failure due to automatic deduction! Got instead:\n%s\n", lmcr.msg.c_str()); } debug("test4 OK\n\n"); // Explicitly set T1 multical21_and_supercom587_config.default_device_linkmodes = LinkModeSet(); multical21_and_supercom587_config.default_device_linkmodes.addLinkMode(LinkMode::T1); lmcr = calculateLinkModes(&multical21_and_supercom587_config, wmbus_im871a.get()); if (lmcr.type != LinkModeCalculationResultType::MightMissTelegrams) { printf("ERROR! Expected might miss telegrams! Got instead:\n%s\n", lmcr.msg.c_str()); } debug("test5 OK\n\n"); // Explicitly set N1a, but the meters transmit on C1 and T1. multical21_and_supercom587_config.default_device_linkmodes = LinkModeSet(); multical21_and_supercom587_config.default_device_linkmodes.addLinkMode(LinkMode::N1a); lmcr = calculateLinkModes(&multical21_and_supercom587_config, wmbus_im871a.get()); if (lmcr.type != LinkModeCalculationResultType::MightMissTelegrams) { printf("ERROR! Expected no meter can be heard! Got instead:\n%s\n", lmcr.msg.c_str()); } debug("test6 OK\n\n"); // Explicitly set N1a, but it is an amber dongle. multical21_and_supercom587_config.default_device_linkmodes = LinkModeSet(); multical21_and_supercom587_config.default_device_linkmodes.addLinkMode(LinkMode::N1a); lmcr = calculateLinkModes(&multical21_and_supercom587_config, wmbus_amb8465.get()); if (lmcr.type != LinkModeCalculationResultType::DongleCannotListenTo) { printf("ERROR! Expected dongle cannot listen to! Got instead:\n%s\n", lmcr.msg.c_str()); } debug("test7 OK\n\n"); manager->stop(); */ } void test_valid_match_expression(string s, bool expected) { bool b = isValidSequenceOfAddressExpressions(s); if (b == expected) return; if (expected == true) { printf("ERROR! Expected \"%s\" to be valid! But it was not!\n", s.c_str()); } else { printf("ERROR! Expected \"%s\" to be invalid! But it was reported as valid!\n", s.c_str()); } } void test_does_id_match_expression(string id, string mes, bool expected, bool expected_uw) { Address a; a.id = id; vector
as; as.push_back(a); vector expressions = splitAddressExpressions(mes); bool uw = false; bool b = doesTelegramMatchExpressions(as, expressions, &uw); if (b != expected) { if (expected == true) { printf("ERROR! Expected \"%s\" to match \"%s\" ! But it did not!\n", id.c_str(), mes.c_str()); } else { printf("ERROR! Expected \"%s\" to NOT match \"%s\" ! But it did!\n", id.c_str(), mes.c_str()); } } if (expected_uw != uw) { printf("ERROR! Matching \"%s\" \"%s\" and expecte used_wildcard %d but got %d!\n", id.c_str(), mes.c_str(), expected_uw, uw); } } void test_ids() { test_valid_match_expression("12345678", true); test_valid_match_expression("*", true); test_valid_match_expression("!12345678", true); test_valid_match_expression("12345*", true); test_valid_match_expression("!123456*", true); test_valid_match_expression("1234567", false); test_valid_match_expression("", false); test_valid_match_expression("z1234567", false); test_valid_match_expression("123456789", false); test_valid_match_expression("!!12345678", false); test_valid_match_expression("12345678*", false); test_valid_match_expression("**", false); test_valid_match_expression("123**", false); test_valid_match_expression("2222*,!22224444", true); test_does_id_match_expression("12345678", "12345678", true, false); test_does_id_match_expression("12345678", "*", true, true); test_does_id_match_expression("12345678", "2*", false, false); test_does_id_match_expression("12345678", "*,!2*", true, true); test_does_id_match_expression("22222222", "22*,!22222222", false, false); test_does_id_match_expression("22222223", "22*,!22222222", true, true); test_does_id_match_expression("22222223", "*,!22*", false, false); test_does_id_match_expression("12333333", "123*,!1234*,!1235*,!1236*", true, true); test_does_id_match_expression("12366666", "123*,!1234*,!1235*,!1236*", false, false); test_does_id_match_expression("11223344", "22*,33*,44*,55*", false, false); test_does_id_match_expression("55223344", "22*,33*,44*,55*", true, true); test_does_id_match_expression("78563413", "78563412,78563413", true, false); test_does_id_match_expression("78563413", "*,!00156327,!00048713", true, true); } void tst_address(string s, bool valid, string id, bool has_wildcard, string mfct, uchar version, uchar type, bool mbus_primary, bool filter_out) { AddressExpression a; bool ok = a.parse(s); if (ok != valid) { printf("Expected parse of address \"%s\" to return %s, but returned %s\n", s.c_str(), valid?"valid":"bad", ok?"valid":"bad"); } if (ok) { string smfct = manufacturerFlag(a.mfct); if (id != a.id || has_wildcard != a.has_wildcard || mfct != smfct || version != a.version || type != a.type || mbus_primary != a.mbus_primary || filter_out != a.filter_out) { printf("Expected parse of address \"%s\" to return\n" "(id=%s haswild=%d mfct=%s version=%02x type=%02x mbus=%d negt=%d)\n" "but got\n" "(id=%s haswild=%d mfct=%s version=%02x type=%02x mbus=%d negt=%d)\n", s.c_str(), id.c_str(), has_wildcard, mfct.c_str(), version, type, mbus_primary, filter_out, a.id.c_str(), a.has_wildcard, smfct.c_str(), a.version, a.type, a.mbus_primary, a.filter_out); } } } void tst_address_match(string expr, string id, uint16_t m, uchar v, uchar t, bool match, bool filter_out) { AddressExpression e; bool ok = e.parse(expr); assert(ok); bool test = e.match(id, m, v, t); if (test != match) { printf("Expected address %s %04x %02x %02x to %smatch expression %s\n", id.c_str(), m, v, t, match?"":"not ", expr.c_str()); } if (match && e.filter_out != filter_out) { printf("Expected %s from match expression %s\n", filter_out?"FILTERED OUT":"NOT filtered", expr.c_str()); } } void tst_telegram_match(string addresses, string expressions, bool match, bool uw) { vector exprs = splitAddressExpressions(expressions); vector as = splitAddressExpressions(addresses); vector
addrs; for (auto &ad : as) { Address a; a.id = ad.id; a.mfct = ad.mfct; a.version = ad.version; a.type = ad.type; addrs.push_back(a); } bool used_wildcard = false; bool m = doesTelegramMatchExpressions(addrs, exprs, &used_wildcard); if (m != match) { printf("Expected addresses %s to %smatch expressions %s\n", addresses.c_str(), match?"":"NOT ", expressions.c_str()); } if (uw != used_wildcard) { printf("Expected addresses %s from match expression %s %susing wildcard\n", addresses.c_str(), expressions.c_str(), uw?"":"NOT "); } } void test_addresses() { tst_address("12345678", true, "12345678", // id false, // has wildcard "___", // mfct 0xff, // type 0xff, // version false, // mbus primary found false // negate test ); tst_address("123k45678", false, "", false, "", 0xff, 0xff, false, false); tst_address("1234", false, "", false, "", 0xff, 0xff, false, false); tst_address("p0", true, "p0", false, "___", 0xff, 0xff, true, false); tst_address("p250", true, "p250", false, "___", 0xff, 0xff, true, false); tst_address("p251", false, "", false, "", 0xff, 0xff, false, false); tst_address("p0.M=PII.V=01.T=1b", true, "p0", false, "PII", 0x01, 0x1b, true, false); tst_address("p123.V=11.M=FOO.T=ff", true, "p123", false, "FOO", 0x11, 0xff, true, false); tst_address("p123.M=FOO", true, "p123", false, "FOO", 0xff, 0xff, true, false); tst_address("p123.M=FOO.V=33", true, "p123", false, "FOO", 0x33, 0xff, true, false); tst_address("p123.T=33", true, "p123", false, "___", 0xff, 0x33, true, false); tst_address("p1.V=33", true, "p1", false, "___", 0x33, 0xff, true, false); tst_address("p16.M=BAR", true, "p16", false, "BAR", 0xff, 0xff, true, false); tst_address("12345678.M=ABB.V=66.T=16", true, "12345678", false, "ABB", 0x66, 0x16, false, false); tst_address("!12345678.M=ABB.V=66.T=16", true, "12345678", false, "ABB", 0x66, 0x16, false, true); tst_address("!*.M=ABB", true, "*", true, "ABB", 0xff, 0xff, false, true); tst_address("!*.V=66.T=06", true, "*", true, "___", 0x66, 0x06, false, true); tst_address("12*", true, "12*", true, "___", 0xff, 0xff, false, false); tst_address("!1234567*", true, "1234567*", true, "___", 0xff, 0xff, false, true); tst_address_match("12345678", "12345678", 1, 1, 1, true, false); tst_address_match("12345678.M=ABB.V=77", "12345678", MANUFACTURER_ABB, 0x77, 88, true, false); tst_address_match("1*.V=77", "12345678", MANUFACTURER_ABB, 0x77, 1, true, false); tst_address_match("12345678.M=ABB.V=67.T=06", "12345678", MANUFACTURER_ABB, 0x67, 0x06, true, false); tst_address_match("12345678.M=ABB.V=67.T=06", "12345678", MANUFACTURER_ABB, 0x68, 0x06, false, false); tst_address_match("12345678.M=ABB.V=67.T=06", "12345678", MANUFACTURER_ABB, 0x67, 0x07, false, false); tst_address_match("12345678.M=ABB.V=67.T=06", "12345678", MANUFACTURER_ABB+1, 0x67, 0x06, false, false); tst_address_match("12345678.M=ABB.V=67.T=06", "12345677", MANUFACTURER_ABB, 0x67, 0x06, false, false); // Now verify filter out ! character. The filter out does notchange the test. It is still the same // test, but the match will be used as a filter out. Ie if the match triggers, then the telegram will be filtered out. tst_address_match("!12345678", "12345677", 1, 1, 1, false, false); tst_address_match("!*.M=ABB", "99999999", MANUFACTURER_ABB, 1, 1, true, true); tst_address_match("*.M=ABB", "99999999", MANUFACTURER_ABB, 1, 1, true, false); // Test that both id wildcard matches and the version. tst_address_match("9*.V=06", "99999999", MANUFACTURER_ABB, 0x06, 1, true, false); tst_address_match("9*.V=06", "89999999", MANUFACTURER_ABB, 0x06, 1, false, false); tst_address_match("9*.V=06", "99999999", MANUFACTURER_ABB, 0x07, 1, false, false); tst_address_match("9*.V=06", "89999999", MANUFACTURER_ABB, 0x07, 1, false, false); // Test the same, expect same answers but check that filtered out is set. tst_address_match("!9*.V=06", "99999999", MANUFACTURER_ABB, 0x06, 1, true, true); tst_address_match("!9*.V=06", "89999999", MANUFACTURER_ABB, 0x06, 1, false, true); tst_address_match("!9*.V=06", "99999999", MANUFACTURER_ABB, 0x07, 1, false, true); tst_address_match("!9*.V=06", "89999999", MANUFACTURER_ABB, 0x07, 1, false, true); tst_telegram_match("12345678", "12345678", true, false); tst_telegram_match("11111111,22222222", "12345678,22*", true, true); tst_telegram_match("11111111,22222222", "12345678,22222222", true, false); tst_telegram_match("11111111.M=KAM,22222222.M=PII", "11111111.M=KAM", true, false); tst_telegram_match("11111111.M=KAF", "11111111.M=KAM", false, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAM", true, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAF", false, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111", true, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAM", true, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.V=1b", true, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.T=16", true, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAM.T=16", true, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAM.V=1b", true, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.T=16.V=1b", true, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAL", false, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.V=1c", false, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.T=17", false, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAM.T=17", false, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAL.V=1b", false, false); tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.T=17.V=1b", false, false); // Test * matches both 11111111 and 2222222 but the only the 111111 matches the filter out V=1b. // Verify that the filter out !1*.V=1b will override successfull match (with no filter out) * for 22222222. tst_telegram_match("11111111.M=KAM.V=1b.T=16,22222222.M=XXX.V=aa.T=99", "*,!1*.V=1b", false, true); } void eq(string a, string b, const char *tn) { if (a != b) { printf("ERROR in test %s expected \"%s\" to be equal to \"%s\"\n", tn, a.c_str(), b.c_str()); } } void eqn(int a, int b, const char *tn) { if (a != b) { printf("ERROR in test %s expected %d to be equal to %d\n", tn, a, b); } } void test_kdf() { vector key; vector input; vector mac; hex2bin("2b7e151628aed2a6abf7158809cf4f3c", &key); mac.resize(16); AES_CMAC(safeButUnsafeVectorPtr(key), safeButUnsafeVectorPtr(input), 0, safeButUnsafeVectorPtr(mac)); string s = bin2hex(mac); string ex = "BB1D6929E95937287FA37D129B756746"; if (s != ex) { printf("ERROR in aes-cmac expected \"%s\" but got \"%s\"\n", ex.c_str(), s.c_str()); } input.clear(); hex2bin("6bc1bee22e409f96e93d7e117393172a", &input); AES_CMAC(safeButUnsafeVectorPtr(key), safeButUnsafeVectorPtr(input), 16, safeButUnsafeVectorPtr(mac)); s = bin2hex(mac); ex = "070A16B46B4D4144F79BDD9DD04A287C"; if (s != ex) { printf("ERROR in aes-cmac expected \"%s\" but got \"%s\"\n", ex.c_str(), s.c_str()); } } void testp(time_t now, string period, bool expected) { bool rc = isInsideTimePeriod(now, period); char buf[256]; struct tm now_tm {}; localtime_r(&now, &now_tm); strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M %A", &now_tm); string nows = buf; if (expected == true && rc == false) { printf("ERROR in period test is \"%s\" in period \"%s\"? Expected true but got false!\n", nows.c_str(), period.c_str()); } if (expected == false && rc == true) { printf("ERROR in period test is \"%s\" in period \"%s\"? Expected false but got true!\n", nows.c_str(), period.c_str()); } } void test_periods() { // 3600*24*7+3600 means 1970-01-08 01:00 Thursday in Greenwich. // However your local time is adjusted with your timezone. // Get your timezone offset tm_gmtoff into the value. time_t t = 3600*24*7+3600; struct tm value {}; localtime_r(&t, &value); // if tm_gmtoff is zero, then we are in Greenwich! // if tm_gmtoff is 3600 then we are in Stockholm! t -= value.tm_gmtoff; // We have now adjusted the t so that it should be thursday at 1 am. // The following test should therefore work inependently on // where in the world this test is run. testp(t, "mon-sun(00-23)", true); testp(t, "mon(00-23)", false); testp(t, "thu-fri(01-01)", true); testp(t, "mon-wed(00-23),thu(02-23),fri-sun(00-23)", false); testp(t, "mon-wed(00-23),thu(01-23),fri-sun(00-23)", true); testp(t, "thu(00-00)", false); testp(t, "thu(01-01)", true); } void testd(string arg, bool xok, string xalias, string xfile, string xtype, string xid, string xextras, string xfq, string xbps, string xlm, string xcmd) { SpecifiedDevice d; bool ok = d.parse(arg); if (ok != xok) { printf("ERROR in device parse test \"%s\" expected %s but got %s\n", arg.c_str(), xok?"OK":"FALSE", ok?"OK":"FALSE"); return; } if (ok == false) return; if (d.bus_alias != xalias || d.file != xfile || toString(d.type) != xtype || d.id != xid || d.extras != xextras || d.fq != xfq || d.bps != xbps || d.linkmodes.hr() != xlm || d.command != xcmd) { printf("ERROR in bus device parsing parts \"%s\" - got\n" "alias: \"%s\", file: \"%s\", type: \"%s\", id: \"%s\", extras: \"%s\", fq: \"%s\", bps: \"%s\", lm: \"%s\", cmd: \"%s\"\n" "but expected:\n" "alias: \"%s\", file: \"%s\", type: \"%s\", id: \"%s\", extras: \"%s\", fq: \"%s\", bps: \"%s\", lm: \"%s\", cmd: \"%s\"\n", arg.c_str(), d.bus_alias.c_str(), d.file.c_str(), toString(d.type), d.id.c_str(), d.extras.c_str(), d.fq.c_str(), d.bps.c_str(), d.linkmodes.hr().c_str(), d.command.c_str(), xalias.c_str(), xfile.c_str(), xtype.c_str(), xid.c_str(), xextras.c_str(), xfq.c_str(), xbps.c_str(), xlm.c_str(), xcmd.c_str()); } } void test_device_parsing() { testd("Bus_4711=/dev/ttyUSB0:im871a[12345678]:9600:868.95M:c1,t1", true, "Bus_4711", // alias "/dev/ttyUSB0", // file "im871a", // type "12345678", // id "", // extras "868.95M", // fq "9600", // bps "t1,c1", // linkmodes ""); // command testd("/dev/ttyUSB0:im871a:c1", true, "", // alias "/dev/ttyUSB0", // file "im871a", // type "", // id "", // extras "", // fq "", // bps "c1", // linkmodes ""); // command testd("im871a[12345678]:c1", true, "", // alias "", // file "im871a", // type "12345678", // id "", // extras "", // fq "", // bps "c1", // linkmodes ""); // command testd("im871a(track=7,pi=3.14):c1", true, "", // alias "", // file "im871a", // type "", // id "track=7,pi=3.14", // extras "", // fq "", // bps "c1", // linkmodes ""); // command testd("rtlwmbus:c1,t1:CMD(gurka)", true, "", // alias "", // file "rtlwmbus", // type "", // id "", // extras "", // fq "", // bps "t1,c1", // linkmodes "gurka"); // command testd("rtlwmbus[plast]:c1,t1", true, "", // alias "", // file "rtlwmbus", // type "plast", // id "", // extras "", // fq "", // bps "t1,c1", // linkmodes ""); // command testd("ANTENNA1=rtlwmbus[plast](ppm=5):c1,t1", true, "ANTENNA1", // alias "", // file "rtlwmbus", // type "plast", // id "ppm=5", // extras "", // fq "", // bps "t1,c1", // linkmodes ""); // command testd("stdin:rtlwmbus", true, "", // alias "stdin", // file "rtlwmbus", // type "", // id "", // extras "", // fq "", // bps "none", // linkmodes ""); // command testd("/dev/ttyUSB0:rawtty:9600", true, "", // alias "/dev/ttyUSB0", // file "rawtty", // type "", // id "", // extras "", // fq "9600", // bps "none", // linkmodes ""); // command // testinternals must be run from a location where // there is a Makefile. testd("Makefile:simulation", true, "", // alias "Makefile", // file "simulation", // type "", // id "", // extras "", // fq "", // bps "none", // linkmodes ""); // command testd("auto:c1,t1", true, "", // alias "", // file "auto", // type "", // id "", // extras "", // fq "", // bps "t1,c1", // linkmodes ""); // command testd("auto:Makefile:c1,t1", false, "", // alias "", // file "", // type "", // id "", // extras "", // fq "", // bps "none", // linkmodes ""); // command testd("Vatten", false, "", // alias "", // file "", // type "", // id "", // extras "", // fq "", // bps "none", // linkmodes ""); // command testd("main=/dev/ttyUSB0:mbus:2400", true, "main", // alias "/dev/ttyUSB0", // file "mbus", // type "", // id "", // extras "", // fq "2400", // bps "none", // linkmodes ""); // command // Support : inside the command. testd("cul:c1:CMD(socat TCP:CUNO:2323 STDIO)", true, "", // alias "", // file "cul", // type "", // id "", // extras "", // fq "", // bps "c1", // linkmodes "socat TCP:CUNO:2323 STDIO"); // command } void test_month(int y, int m, int day, int mdiff, string from, string to) { struct tm date {}; date.tm_year = y-1900; date.tm_mon = m-1; date.tm_mday = day; string s = strdate(&date); struct tm d; d = date; addMonths(&d, mdiff); string os = strdate(&d); if (s != from || os != to) { printf("ERROR! Expected %s + %d months should be %s\n" "But got %s - 11 = %s\n", from.c_str(), mdiff, to.c_str(), s.c_str(), os.c_str()); } } void test_months() { test_month(2020,12,31, 2, "2020-12-31", "2021-02-28"); test_month(2020,12,31,-10, "2020-12-31", "2020-02-29"); test_month(2021,01,31,-1, "2021-01-31", "2020-12-31"); test_month(2021,01,31,-2, "2021-01-31", "2020-11-30"); test_month(2021,01,31,-24, "2021-01-31", "2019-01-31"); test_month(2021,01,31, 24, "2021-01-31", "2023-01-31"); test_month(2021,01,31, 22, "2021-01-31", "2022-11-30"); // 2020 was a leap year. test_month(2021,02,28, -12, "2021-02-28", "2020-02-29"); // 2000 was a leap year %100=0 but %400=0 overrides. test_month(2001,02,28, -12, "2001-02-28", "2000-02-29"); // 2100 is not a leap year since %100=0 and not overriden %400 != 0. test_month(2000,02,29, 12*100, "2000-02-29", "2100-02-28"); } // Vatten multical21:BUS1:c1 12345678 KEY // Tempmeter piigth(info=123):BUS2:2400 0 NOKEY void testm(string arg, bool xok, string xdriver, string xextras, string xbus, string xbps, string xlm) { MeterInfo mi; bool ok = mi.parse("", arg, "12345678", ""); if (ok != xok) { printf("ERROR in meter parse test \"%s\" expected %s but got %s\n", arg.c_str(), xok?"OK":"FALSE", ok?"OK":"FALSE"); return; } if (ok == false) return; bool driver_ok = mi.driverName().str() == xdriver; bool extras_ok = mi.extras == xextras; bool bus_ok = mi.bus == xbus; bool bps_ok = to_string(mi.bps) == xbps; bool link_modes_ok = mi.link_modes.hr() == xlm; if (!driver_ok || !extras_ok || !bus_ok || !bps_ok || !link_modes_ok) { printf("ERROR in meterc parsing parts \"%s\" got\n" "driver: \"%s\", extras: \"%s\", bus: \"%s\", bbps: \"%s\", linkmodes: \"%s\"\n" "but expected\n" "driver: \"%s\", extras: \"%s\", bus: \"%s\", bbps: \"%s\", linkmodes: \"%s\"\n", arg.c_str(), mi.driverName().str().c_str(), mi.extras.c_str(), mi.bus.c_str(), to_string(mi.bps).c_str(), mi.link_modes.hr().c_str(), xdriver.c_str(), xextras.c_str(), xbus.c_str(), xbps.c_str(), xlm.c_str() ); } } void testc(string file, string file_content, string xdriver, string xextras, string xbus, string xbps, string xlm) { MeterInfo mi; Configuration c; vector meter_conf(file_content.begin(), file_content.end()); meter_conf.push_back('\n'); parseMeterConfig(&c, meter_conf, file); assert(c.meters.size() > 0); mi = c.meters.back(); if (mi.driverName().str() != xdriver || mi.extras != xextras || mi.bus != xbus || to_string(mi.bps) != xbps || mi.link_modes.hr() != xlm) { printf("ERROR in meterc parsing parts \"%s\" got\n" "driver: \"%s\", extras: \"%s\", bus: \"%s\", bbps: \"%s\", linkmodes: \"%s\"\n" "but expected\n" "driver: \"%s\", extras: \"%s\", bus: \"%s\", bbps: \"%s\", linkmodes: \"%s\"\n", file.c_str(), mi.driverName().str().c_str(), mi.extras.c_str(), mi.bus.c_str(), to_string(mi.bps).c_str(), mi.link_modes.hr().c_str(), xdriver.c_str(), xextras.c_str(), xbus.c_str(), xbps.c_str(), xlm.c_str() ); } } void test_meters() { string config_content; testm("piigth:BUS1:2400", true, "piigth", // driver "", // extras "BUS1", // bus "2400", // bps "none"); // linkmodes testm("c5isf:MAINO:9600:mbus", true, "c5isf", // driver "", // extras "MAINO", // bus "9600", // bps "mbus"); // linkmodes testm("c5isf:DONGLE:t1", true, "c5isf", // driver "", // extras "DONGLE", // bus "0", // bps "t1"); // linkmodes testm("c5isf:t1,c1,mbus", true, "c5isf", // driver "", // extras "", // bus "0", // bps "mbus,t1,c1"); // linkmodes /* config_content = "name=test\n" "driver=piigth:BUS1:2400:mbus\n" "id=01234567\n"; testc("meter/piigth:BUS1:2400", config_content, "piigth", // driver "", // extras "BUS1", // bus "2400", // bps "mbus"); // linkmodes) */ testm("multical21:c1", true, "multical21", // driver "", // extras "", // bus "0", // bps "c1"); // linkmodes config_content = "name=test\n" "driver=multical21:c1\n" "id=01234567\n"; testc("meter/multical21:c1", config_content, "multical21", // driver "", // extras "", // bus "0", // bps "c1"); // linkmodes) testm("apator162(offset=162)", true, "apator162", // driver "offset=162", // extras "", // bus "0", // bps "none"); // linkmodes config_content = "name=test\n" "driver=apator162(offset=99)\n" "id=01234567\n" "key=00000000000000000000000000000000\n"; testc("meter/apatortest", config_content, "apator162", // driver "offset=99", // extras "", // bus "0", // bps "none"); // linkmodes) } void tests(string arg, bool expect, LinkMode link_mode, TelegramFormat format, string bus, string content) { SendBusContent sbc; bool rc = sbc.parse(arg); if (rc != expect && rc == false) { printf("ERROR could not parse send bus content \"%s\"\n", arg.c_str()); return; } if (rc != expect && rc == true) { printf("ERROR could parse send bus content \"%s\" but expected failure!\n", arg.c_str()); return; } if (expect == false && rc == false) return; // It failed, which was expected. if (sbc.link_mode != link_mode || sbc.format != format || sbc.bus != bus || sbc.content != content) { printf("ERROR in parsing send bus content \"%s\"\n" "got (link_mode: %s format: %s bus: %s, data: %s)\n" "expected (link_mode: %s format: %s bus: %s, data: %s)\n", arg.c_str(), toString(sbc.link_mode), toString(sbc.format), sbc.bus.c_str(), sbc.content.c_str(), toString(link_mode), toString(format), bus.c_str(), content.c_str()); } } void test_sbc() { tests("send:t1:wmbus_c_field:BUS1:11223344", true, LinkMode::T1, TelegramFormat::WMBUS_C_FIELD, "BUS1", // bus "11223344"); // content tests("send:c1:wmbus_ci_field:alfa:11", true, LinkMode::C1, TelegramFormat::WMBUS_CI_FIELD, "alfa", // bus "11"); // content tests("send:t2:wmbus_c_field:OUTBUS:1122334455", true, LinkMode::T2, TelegramFormat::WMBUS_C_FIELD, "OUTBUS", // bus "1122334455"); // content tests("alfa:t1", false, LinkMode::UNKNOWN, TelegramFormat::UNKNOWN, "", ""); tests("send", false, LinkMode::UNKNOWN, TelegramFormat::UNKNOWN, "", ""); tests("send:::::::::::", false, LinkMode::UNKNOWN, TelegramFormat::UNKNOWN, "", ""); tests("send:foo", false, LinkMode::UNKNOWN, TelegramFormat::UNKNOWN, "", ""); tests("send:t2:wmbus_c_field:OUT:", false, LinkMode::UNKNOWN, TelegramFormat::UNKNOWN, "", ""); tests("send:t2:wmbus_c_field:OUT:1", false, LinkMode::UNKNOWN, TelegramFormat::UNKNOWN, "", ""); tests("send:mbus:mbus_short_frame:out:5b00", true, LinkMode::MBUS, TelegramFormat::MBUS_SHORT_FRAME, "out", // bus "5b00"); // content tests("send:mbus:mbus_long_frame:mbus2:1122334455", true, LinkMode::MBUS, TelegramFormat::MBUS_LONG_FRAME, "mbus2", // bus "1122334455"); // content*/ } void test_aes() { vector key; hex2bin("0123456789abcdef0123456789abcdef", &key); string poe = "Once upon a midnight dreary, while I pondered, weak and weary,\n" "Over many a quaint and curious volume of forgotten lore\n"; while (poe.length() % 16 != 0) { poe += "."; } uchar iv[16]; memset(iv, 0xaa, 16); uchar in[poe.length()]; memcpy(in, &poe[0], poe.size()); debug("(aes) input: \"%s\"\n", poe.c_str()); uchar out[sizeof(in)]; AES_CBC_encrypt_buffer(out, in, sizeof(in), safeButUnsafeVectorPtr(key), iv); vector outv(out, out+sizeof(out)); string s = bin2hex(outv); debug("(aes) encrypted: \"%s\"\n", s.c_str()); uchar back[sizeof(in)]; AES_CBC_decrypt_buffer(back, out, sizeof(in), safeButUnsafeVectorPtr(key), iv); string b = string(back, back+sizeof(back)); debug("(aes) decrypted: \"%s\"\n", b.c_str()); if (poe != b) { printf("ERROR! aes with IV encrypt decrypt failed!\n"); } AES_ECB_encrypt(in, safeButUnsafeVectorPtr(key), out, sizeof(in)); AES_ECB_decrypt(out, safeButUnsafeVectorPtr(key), back, sizeof(in)); if (memcmp(back, in, sizeof(in))) { printf("ERROR! aes encrypt decrypt (no iv) failed!\n"); } } void test_is_hex(const char *hex, bool expected_ok, bool expected_invalid, bool strict) { bool got_invalid; bool got_ok; if (strict) got_ok = isHexStringStrict(hex, &got_invalid); else got_ok = isHexStringFlex(hex, &got_invalid); if (got_ok != expected_ok || got_invalid != expected_invalid) { printf("ERROR! hex string %s was expected to be %d (invalid %d) but got %d (invalid %d)\n", hex, expected_ok, expected_invalid, got_ok, got_invalid); } } void test_hex() { test_is_hex("00112233445566778899aabbccddeeff", true, false, true); test_is_hex("00112233445566778899AABBCCDDEEFF", true, false, true); test_is_hex("00112233445566778899AABBCCDDEEF", true, true, true); test_is_hex("00112233445566778899AABBCCDDEEFG", false, false, true); test_is_hex("00 11 22 33#44|55#66 778899aabbccddeeff", true, false, false); test_is_hex("00 11 22 33#4|55#66 778899aabbccddeeff", true, true, false); } void test_translate() { Translate::Lookup lookup1 = Translate::Lookup() .add(Translate::Rule("ACCESS_BITS", Translate::MapType::BitToString) .set(MaskBits(0xf0)) .add(Translate::Map(0x10, "NO_ACCESS", TestBit::Set)) .add(Translate::Map(0x20, "ALL_ACCESS", TestBit::Set)) .add(Translate::Map(0x40, "TEMP_ACCESS", TestBit::Set)) ) .add(Translate::Rule("ACCESSOR_TYPE", Translate::MapType::IndexToString) .set(MaskBits(0x0f)) .add(Translate::Map(0x00, "ACCESSOR_RED", TestBit::Set)) .add(Translate::Map(0x07, "ACCESSOR_GREEN", TestBit::Set)) ) ; Translate::Lookup lookup2 = { { { "FLOW_FLAGS", Translate::MapType::BitToString, AlwaysTrigger, MaskBits(0x3f), "OOOK", { { 0x01, "BACKWARD_FLOW" }, { 0x02, "DRY" }, { 0x10, "TRIG" }, { 0x20, "COS" }, } }, }, }; Translate::Lookup lookup3 = { { { "NO_FLAGS", Translate::MapType::BitToString, AlwaysTrigger, MaskBits(0x03), "OK", { // Test that 0x01 is set, means OK (ie installed) // Not set means not installed. { 0x01, "NOT_INSTALLED", TestBit::NotSet }, { 0x02, "FOO" }, } }, }, }; string s, e; uint8_t bits; bits = 0xa0; s = sortStatusString(lookup1.translate(bits)); e = sortStatusString("ALL_ACCESS ACCESS_BITS_80 ACCESSOR_RED"); if (s != e) { printf("ERROR lookup1 0x%02x expected \"%s\" but got \"%s\"\n", bits, e.c_str(), s.c_str()); } bits = 0x35; s = sortStatusString(lookup1.translate(bits)); e = sortStatusString("NO_ACCESS ALL_ACCESS ACCESSOR_TYPE_5"); if (s != e) { printf("ERROR lookup1 0x%02x expected \"%s\" but got \"%s\"\n", bits, e.c_str(), s.c_str()); } bits = 0x02; s = lookup2.translate(0x02); e = "DRY"; if (s != e) { printf("ERROR lookup2 0x%02x expected \"%s\" but got \"%s\"\n", bits, e.c_str(), s.c_str()); } bits = 0x00; s = lookup2.translate(bits); e = "OOOK"; if (s != e) { printf("ERROR lookup2 0x%02x expected \"%s\" but got \"%s\"\n", bits, e.c_str(), s.c_str()); } // Verify that the not set 0x01 bit translates to NOT_INSTALLED // The set bit 0x02 translates to FOO. bits = 0x02; s = sortStatusString(lookup3.translate(0x02)); e = sortStatusString("NOT_INSTALLED FOO"); if (s != e) { printf("ERROR lookup3 0x%02x expected \"%s\" but got \"%s\"\n", bits, e.c_str(), s.c_str()); } bits = 0x01; s = lookup3.translate(bits); e = "OK"; if (s != e) { printf("ERROR lookup3 0x%02x expected \"%s\" but got \"%s\"\n", bits, e.c_str(), s.c_str()); } } void test_slip() { vector from = { 1, 0xc0, 3, 4, 5, 0xdb }; vector expected_to = { 0xc0, 1, 0xdb, 0xdc, 3, 4, 5, 0xdb, 0xdd, 0xc0 }; vector to; vector back; addSlipFraming(from, to); if (expected_to != to) { printf("ERROR slip 1\n"); } size_t frame_length = 0; removeSlipFraming(to, &frame_length, back); if (back != from) { printf("ERROR slip 2\n"); } if (to.size() != frame_length) { printf("ERROR slip 3\n"); } vector more = { 0xc0, 0xc0, 0xc0, 1, 2, 3, 4, 5, 6, 7, 8 }; addSlipFraming(more, to); frame_length = 0; removeSlipFraming(to, &frame_length, back); if (back != from) { printf("ERROR slip 4\n"); } to.erase(to.begin(), to.begin()+frame_length); removeSlipFraming(to, &frame_length, back); if (back != more) { printf("ERROR slip 5\n"); } vector again = { 0xc0 }; removeSlipFraming(again, &frame_length, back); if (frame_length != 0) { printf("ERROR slip 6\n"); } vector againn = { 0xc0, 1, 2, 3, 4, 5 }; removeSlipFraming(againn, &frame_length, back); if (frame_length != 0) { printf("ERROR slip 7\n"); } } void test_dvs() { DifVifKey dvk("0B2B"); if (dvk.dif() != 0x0b || dvk.vif() != 0x2b || dvk.hasDifes() || dvk.hasVifes()) { printf("ERROR test_dvs 1\n"); } } void test_ascii_detection() { string s = "000008"; if (isLikelyAscii(s)) { printf("ERROR >%s< should not be likely ascii\n", s.c_str()); } s = "41424344"; if (!isLikelyAscii(s)) { printf("ERROR >%s< should be likely ascii\n", s.c_str()); } s = "000041424344"; if (!isLikelyAscii(s)) { printf("ERROR >%s< should be likely ascii\n", s.c_str()); } s = "000041194300"; if (isLikelyAscii(s)) { printf("ERROR >%s< should not be likely ascii\n", s.c_str()); } } void test_join(string a, string b, string s) { string t = joinStatusOKStrings(a, b); if (t != s) { printf("Expected joinStatusString(\"%s\",\"%s\") to be \"%s\" but got \"%s\"\n", a.c_str(), b.c_str(), s.c_str(), t.c_str()); } } void test_status_join() { test_join("OK", "OK", "OK"); test_join("", "", "OK"); test_join("OK", "", "OK"); test_join("", "OK", "OK"); test_join("null", "OK", "OK"); test_join("null", "null", "OK"); test_join("ERROR FLOW", "OK", "ERROR FLOW"); test_join("ERROR FLOW", "", "ERROR FLOW"); test_join("OK", "ERROR FLOW", "ERROR FLOW"); test_join("", "ERROR FLOW", "ERROR FLOW"); test_join("ERROR", "FLOW", "ERROR FLOW"); test_join("ERROR", "null", "ERROR"); test_join("A B C", "D E F G", "A B C D E F G"); } void test_sort(string in, string out) { string t = sortStatusString(in); if (t != out) { printf("Expected sortStatusString(\"%s\") to be \"%s\" but got \"%s\"\n", in.c_str(), out.c_str(), t.c_str()); } } void test_status_sort() { test_sort("C B A", "A B C"); test_sort("ERROR BUSY FLOW ERROR", "BUSY ERROR FLOW"); test_sort("X X X Y Y Z A B C A A AAAA AA AAA", "A AA AAA AAAA B C X Y Z"); } void test_field_matcher() { // 04 dif (32 Bit Integer/Binary Instantaneous value) // 13 vif (Volume l) // 2F4E0000 ("total_m3":20.015) FieldMatcher m1 = FieldMatcher::build() .set(MeasurementType::Instantaneous) .set(VIFRange::Volume); string v1 = "2F4E0000"; DVEntry e1(0, DifVifKey("0413"), MeasurementType::Instantaneous, Vif(0x13), { }, { }, StorageNr(0), TariffNr(0), SubUnitNr(0), v1); if (!m1.matches(e1)) { printf("ERROR expected match for field matcher test 1 !\n"); } // 81 dif (8 Bit Integer/Binary Instantaneous value) // 01 dife (subunit=0 tariff=0 storagenr=2) // 10 vif (Volume) // FC combinable vif (Extension) // 0C combinable vif (DeltaBetween...) // 03 ("external_temperature_c":3) FieldMatcher m2 = FieldMatcher::build() .set(MeasurementType::Instantaneous) .set(StorageNr(2)) .set(VIFRange::Volume) .add(VIFCombinable::Any); string v2 = "03"; DVEntry e2(0, DifVifKey("810110FC0C"), MeasurementType::Instantaneous, Vif(0x10), { VIFCombinable::DeltaBetweenImportAndExport }, { }, StorageNr(2), TariffNr(0), SubUnitNr(0), v2); if (!m2.matches(e2)) { printf("ERROR expected match for field matcher test 2 !\n"); } FieldMatcher m3 = FieldMatcher::build() .set(MeasurementType::Instantaneous) .set(StorageNr(2)) .set(VIFRange::Volume) .add(VIFCombinable::DeltaBetweenImportAndExport); if (!m3.matches(e2)) { printf("ERROR expected match for field matcher test 3 !\n"); } FieldMatcher m4 = FieldMatcher::build() .set(MeasurementType::Instantaneous) .set(StorageNr(2)) .set(VIFRange::Volume) .add(VIFCombinable::ValueDuringUpperLimitExceeded); if (m4.matches(e2)) { printf("ERROR expected NO match for field matcher test 4 !\n"); } } void test_unit(string in, bool expected_ok, string expected_vname, Unit expected_unit) { Unit unit; string vname; bool ok = extractUnit(in, &vname, &unit); if (ok != expected_ok || vname != expected_vname || unit != expected_unit) { printf("ERROR expected ok=%d vname=%s unit=%s but got\n" " but got ok=%d vname=%s unit=%s\n", expected_ok, expected_vname.c_str(), unitToStringUpperCase(expected_unit).c_str(), ok, vname.c_str(), unitToStringUpperCase(unit).c_str()); } } void test_units_extraction() { test_unit("total_kwh", true, "total", Unit::KWH); test_unit("total_", false, "", Unit::Unknown); test_unit("total", false, "", Unit::Unknown); test_unit("", false, "", Unit::Unknown); test_unit("_c", false, "", Unit::Unknown); test_unit("work__c", true, "work_", Unit::C); test_unit("water_c", true, "water", Unit::C); test_unit("walk_counter", true, "walk", Unit::COUNTER); test_unit("work_kvarh", true, "work", Unit::KVARH); test_unit("current_power_consumption_phase1_kw", true, "current_power_consumption_phase1", Unit::KW); } void test_expected_failed_si_convert(Unit from_unit, Unit to_unit, Quantity q) { SIUnit from_si_unit(from_unit); SIUnit to_si_unit(to_unit); string fu = unitToStringLowerCase(from_si_unit.asUnit()); string tu = unitToStringLowerCase(to_si_unit.asUnit()); if (q != from_si_unit.quantity() || q != to_si_unit.quantity()) { printf("ERROR! Not the expected quantities!\n"); } if (from_si_unit.convertTo(0, to_si_unit, NULL)) { printf("ERROR! Should not be able to convert from %s to %s !\n", fu.c_str(), tu.c_str()); } } void test_si_convert(double from_value, double expected_value, Unit from_unit, string expected_from_unit, Unit to_unit, string expected_to_unit, Quantity q, set *from_set, set *to_set) { debug("test_si_convert from %.17g %s to %.17g %s\n", from_value, expected_from_unit.c_str(), expected_value, expected_to_unit.c_str()); string evs = tostrprintf("%.15g", expected_value); SIUnit from_si_unit(from_unit); SIUnit to_si_unit(to_unit); string fu = unitToStringLowerCase(from_si_unit.asUnit(q)); string tu = unitToStringLowerCase(to_si_unit.asUnit(q)); from_set->erase(from_unit); to_set->erase(to_unit); double e {}; from_si_unit.convertTo(from_value, to_si_unit, &e); string es = tostrprintf("%.15g", e); if (canConvert(from_unit, to_unit)) { // Test if conversion was the same using 16 significant digits. // I.e. slightly less than the maximum 17 significant digits. // Takes up the slack between the old style conversion and the new style conversion // which can introduce minor changes in the final digit. double ee = convert(from_value, from_unit, to_unit); string ees = tostrprintf("%.15g", ee); if (es != ees) { printf("ERROR! SI unit conversion %.15g (%s) from %.15g differs from unit conversion %.15g (%s)! \n", e, es.c_str(), from_value, ee, ees.c_str()); } } if (fu != expected_from_unit) { printf("ERROR! Expected from unit %s (but got %s) when converting si unit %s\n", expected_from_unit.c_str(), fu.c_str(), from_si_unit.str().c_str()); } if (tu != expected_to_unit) { printf("ERROR! Expected to unit %s (but got %s) when converting si unit %s\n", expected_to_unit.c_str(), tu.c_str(), to_si_unit.str().c_str()); } if (es != evs) { printf("ERROR! Expected %.17g [%s] (but got %.17g [%s]) when converting %.17g from %s (%s) to %s (%s)\n", expected_value, evs.c_str(), e, es.c_str(), from_value, from_si_unit.str().c_str(), fu.c_str(), to_si_unit.str().c_str(), tu.c_str()); } } void test_si_units_siexp() { // m3/s SIExp e = SIExp::build().s(-1).m(3); if (e.str() != "m³s⁻¹") { printf("ERROR Expected m³s⁻¹ but got \"%s\"\n", e.str().c_str()); } SIExp f = SIExp::build().s(1); if (f.str() != "s") { printf("ERROR Expected s but got \"%s\"\n", f.str().c_str()); } SIExp g = e.mul(f); if (g.str() != "m³") { printf("ERROR Expected m³ but got \"%s\"\n", g.str().c_str()); } SIExp h = SIExp::build().s(127); // Test overflow of exponent for seconds! SIExp i = h.mul(f); if (i.str() != "!s⁻¹²⁸-Invalid!") { printf("ERROR Expected !s⁻¹²⁸-Invalid! but got \"%s\"\n", i.str().c_str()); } SIExp j = e.div(e); if (j.str() != "") { printf("ERROR Expected \"\" but got \"%s\"\n", j.str().c_str()); } SIExp bad = SIExp::build().k(1).c(1); if (bad.str() != "!kc-Invalid!") { printf("ERROR Expected !kc-Invalid! but got \"%s\"\n", bad.str().c_str()); } } void test_si_units_basic() { // A kilowatt unit generated from scratch: SIUnit kwh(Quantity::Energy, 3.6E6, SIExp().kg(1).m(2).s(-2)); string expected = "3.6×10⁶kgm²s⁻²"; if (kwh.str() != expected) printf("ERROR expected kwh to be %s but got %s\n", expected.c_str(), kwh.str().c_str()); // A kilowatt unit from the unit lookup table. SIUnit kwh2(Unit::KWH); if (kwh2.str() != expected) printf("ERROR expected second kwh to be %s but got %s\n", expected.c_str(), kwh2.str().c_str()); // A Celsius unit generated from scratch: SIUnit celsius(Quantity::Temperature, 1, SIExp().c(1)); expected = "1c"; if (celsius.str() != expected) printf("ERROR expected celsius to be %s but got %s\n", expected.c_str(), celsius.str().c_str()); // A celsius unit from the Unit. SIUnit celsius2(Unit::C); if (celsius2.str() != expected) printf("ERROR expected second celsius to be %s but got %s\n", expected.c_str(), celsius2.str().c_str()); } void fill_with_units_from(Quantity q, set *s) { s->clear(); #define X(cname,lcname,hrname,quantity,explanation) if (q == Quantity::quantity) s->insert(Unit::cname); LIST_OF_UNITS #undef X } void check_units_tested(set &from_set, set &to_set, Quantity q) { if (from_set.size() > 0) { printf("ERROR not all units as source in quantity %s tested! Remaining: ", toString(q)); for (Unit u : from_set) printf("%s ", unitToStringLowerCase(u).c_str()); printf("\n"); } if (to_set.size() > 0) { printf("ERROR not all units as targets in quantity %s tested! Remaining: ", toString(q)); for (Unit u : to_set) printf("%s ", unitToStringLowerCase(u).c_str()); printf("\n"); } } void check_quantities_tested(set &s) { if (s.size() > 0) { printf("ERROR not all quantities tested! Remaining: "); for (Quantity q : s) printf("%s ", toString(q)); printf("\n"); } } void test_si_units_conversion() { set q_set; set from_set; set to_set; #define X(quantity,default_unit) q_set.insert(Quantity::quantity); LIST_OF_QUANTITIES #undef X // Test time units: s, min, h, d, y ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Time); fill_with_units_from(Quantity::Time, &from_set); fill_with_units_from(Quantity::Time, &to_set); // 60 seconds is one minute. test_si_convert(60.0, 1.0, Unit::Second, "s", Unit::Minute, "min", Quantity::Time, &from_set, &to_set); // 3600 seconds is one hour. test_si_convert(3600.0, 1.0, Unit::Second, "s", Unit::Hour, "h", Quantity::Time, &from_set, &to_set); // 3600 seconds is 1/24 of a day which is 0.041666666666666664. test_si_convert(3600.0, 0.041666666666666664, Unit::Second, "s", Unit::Day, "d", Quantity::Time, &from_set, &to_set); // Same test again. test_si_convert(3600.0, 1.0/24.0, Unit::Second, "s", Unit::Day, "d", Quantity::Time, &from_set, &to_set); // 1 min is 60 seconds. test_si_convert(1.0, 60.0, Unit::Minute, "min", Unit::Second, "s", Quantity::Time, &from_set, &to_set); // 1 day is 24 hours test_si_convert(1.0, 24, Unit::Day, "d", Unit::Hour, "h", Quantity::Time, &from_set, &to_set); // 1 month is 1 month. test_si_convert(1.0, 1.0, Unit::Month, "month", Unit::Month, "month", Quantity::Time, &from_set, &to_set); test_si_convert(1.0, 1.0, Unit::Year, "y", Unit::Year, "y", Quantity::Time, &from_set, &to_set); // 100 hours is 100/24 days. test_si_convert(100.0, 100.0/24.0, Unit::Hour, "h", Unit::Day, "d", Quantity::Time, &from_set, &to_set); // 1 year is 365.2425 days. // test_si_convert(1.0, 365.2425, Unit::Year, "y", Unit::Day, "d", Quantity::Time, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Time); // Test length units: m ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Length); fill_with_units_from(Quantity::Length, &from_set); fill_with_units_from(Quantity::Length, &to_set); test_si_convert(111.1, 111.1, Unit::M, "m", Unit::M, "m", Quantity::Length, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Length); // Test mass units: kg ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Mass); fill_with_units_from(Quantity::Mass, &from_set); fill_with_units_from(Quantity::Mass, &to_set); test_si_convert(222.1, 222.1, Unit::KG, "kg", Unit::KG, "kg", Quantity::Mass, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Mass); // Test electrical current units: a ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Amperage); fill_with_units_from(Quantity::Amperage, &from_set); fill_with_units_from(Quantity::Amperage, &to_set); test_si_convert(999.9, 999.9, Unit::Ampere, "a", Unit::Ampere, "a", Quantity::Amperage, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Amperage); // Test temperature units: c k f ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Temperature); fill_with_units_from(Quantity::Temperature, &from_set); fill_with_units_from(Quantity::Temperature, &to_set); test_si_convert(0, 273.15, Unit::C, "c", Unit::K, "k", Quantity::Temperature, &from_set, &to_set); test_si_convert(10.85, 284.0, Unit::C, "c", Unit::K, "k", Quantity::Temperature, &from_set, &to_set); test_si_convert(100.0, -173.15, Unit::K, "k", Unit::C, "c", Quantity::Temperature, &from_set, &to_set); test_si_convert(100.0, -279.67, Unit::K, "k", Unit::F, "f", Quantity::Temperature, &from_set, &to_set); test_si_convert(100.0, 37.77777777777777, Unit::F, "f", Unit::C, "c", Quantity::Temperature, &from_set, &to_set); test_si_convert(0.0, -17.7777777777778, Unit::F, "f", Unit::C, "c", Quantity::Temperature, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Temperature); // Test energy units: kwh, mj, gj ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Energy); fill_with_units_from(Quantity::Energy, &from_set); fill_with_units_from(Quantity::Energy, &to_set); // 1 kwh is 3.6 mj test_si_convert(1.0, 3.6, Unit::KWH, "kwh", Unit::MJ, "mj", Quantity::Energy, &from_set, &to_set); // 1 kwh is 0.0036 gj test_si_convert(1.0, 0.0036, Unit::KWH, "kwh", Unit::GJ, "gj", Quantity::Energy, &from_set, &to_set); // 1 gj is 1000 mj test_si_convert(1.0, 1000.0, Unit::GJ, "gj", Unit::MJ, "mj", Quantity::Energy, &from_set, &to_set); // 10 mj is 2.77777 kwh test_si_convert(10, 2.7777777777777777, Unit::MJ, "mj", Unit::KWH, "kwh", Quantity::Energy, &from_set, &to_set); // 1 ws = 1/3600000 kwh is 1 j = 0.000001 MJ test_si_convert(1.0/3600000.0, 0.000001, Unit::KWH, "kwh", Unit::MJ, "mj", Quantity::Energy, &from_set, &to_set); // 99 m3c = 99 m3c this is the only test we can do with the m3c energy unit, // which cannot be converted into other energy units since we lack the density of the water etc. test_si_convert(99.0, 99.0, Unit::M3C, "m3c", Unit::M3C, "m3c", Quantity::Energy, &from_set, &to_set); test_expected_failed_si_convert(Unit::M3C, Unit::KWH, Quantity::Energy); check_units_tested(from_set, to_set, Quantity::Energy); // Test reactive energy kvarh unit: kvarh ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Reactive_Energy); fill_with_units_from(Quantity::Reactive_Energy, &from_set); fill_with_units_from(Quantity::Reactive_Energy, &to_set); // 1 kvarh is 1kwh test_si_convert(1.0, 1.0, Unit::KVARH, "kvarh", Unit::KWH, "kvarh", Quantity::Reactive_Energy, &from_set, &to_set); test_si_convert(1.0, 1.0, Unit::KWH, "kvarh", Unit::KVARH, "kvarh", Quantity::Reactive_Energy, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Reactive_Energy); // Test apparent energy kvah unit: kvah ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Apparent_Energy); fill_with_units_from(Quantity::Apparent_Energy, &from_set); fill_with_units_from(Quantity::Apparent_Energy, &to_set); // 1 kvah is 1kwh test_si_convert(1.0, 1.0, Unit::KVAH, "kvah", Unit::KWH, "kvah", Quantity::Apparent_Energy, &from_set, &to_set); test_si_convert(1.0, 1.0, Unit::KWH, "kvah", Unit::KVAH, "kvah", Quantity::Apparent_Energy, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Apparent_Energy); // Test volume units: m3 l ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Volume); fill_with_units_from(Quantity::Volume, &from_set); fill_with_units_from(Quantity::Volume, &to_set); test_si_convert(1, 1000.0, Unit::M3, "m3", Unit::L, "l", Quantity::Volume, &from_set, &to_set); test_si_convert(1, 1.0/1000.0, Unit::L, "l", Unit::M3, "m3", Quantity::Volume, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Volume); // Test voltage unit: v ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Voltage); fill_with_units_from(Quantity::Voltage, &from_set); fill_with_units_from(Quantity::Voltage, &to_set); test_si_convert(1, 1, Unit::Volt, "v", Unit::Volt, "v", Quantity::Voltage, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Voltage); // Test power unit: kw ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Power); fill_with_units_from(Quantity::Power, &from_set); fill_with_units_from(Quantity::Power, &to_set); test_si_convert(1, 1, Unit::KW, "kw", Unit::KW, "kw", Quantity::Power, &from_set, &to_set); // The power variant is m3ch. test_si_convert(99.0, 99.0, Unit::M3CH, "m3ch", Unit::M3CH, "m3ch", Quantity::Power, &from_set, &to_set); test_expected_failed_si_convert(Unit::M3CH, Unit::KW, Quantity::Power); check_units_tested(from_set, to_set, Quantity::Power); // Test volume flow units: m3h lh ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Flow); fill_with_units_from(Quantity::Flow, &from_set); fill_with_units_from(Quantity::Flow, &to_set); test_si_convert(1, 1000.0, Unit::M3H, "m3h", Unit::LH, "lh", Quantity::Flow, &from_set, &to_set); test_si_convert(1000.0, 1.0, Unit::LH, "lh", Unit::M3H, "m3h", Quantity::Flow, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Flow); // Test amount of substance: mol ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::AmountOfSubstance); fill_with_units_from(Quantity::AmountOfSubstance, &from_set); fill_with_units_from(Quantity::AmountOfSubstance, &to_set); test_si_convert(1.1717, 1.1717, Unit::MOL, "mol", Unit::MOL, "mol", Quantity::AmountOfSubstance, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::AmountOfSubstance); // Test luminous intensity: cd ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::LuminousIntensity); fill_with_units_from(Quantity::LuminousIntensity, &from_set); fill_with_units_from(Quantity::LuminousIntensity, &to_set); test_si_convert(1.1717, 1.1717, Unit::CD, "cd", Unit::CD, "cd", Quantity::LuminousIntensity, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::LuminousIntensity); // Test relative humidity: rh ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::RelativeHumidity); fill_with_units_from(Quantity::RelativeHumidity, &from_set); fill_with_units_from(Quantity::RelativeHumidity, &to_set); test_si_convert(1.1717, 1.1717, Unit::RH, "rh", Unit::RH, "rh", Quantity::RelativeHumidity, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::RelativeHumidity); // Test heat cost allocation: hca ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::HCA); fill_with_units_from(Quantity::HCA, &from_set); fill_with_units_from(Quantity::HCA, &to_set); test_si_convert(11717, 11717, Unit::HCA, "hca", Unit::HCA, "hca", Quantity::HCA, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::HCA); // Test pressure: bar pa ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Pressure); fill_with_units_from(Quantity::Pressure, &from_set); fill_with_units_from(Quantity::Pressure, &to_set); test_si_convert(1.1717, 117170, Unit::BAR, "bar", Unit::PA, "pa", Quantity::Pressure, &from_set, &to_set); test_si_convert(1.1717, 1.1717e-05, Unit::PA, "pa", Unit::BAR, "bar", Quantity::Pressure, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Pressure); // Test frequency: hz ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Frequency); fill_with_units_from(Quantity::Frequency, &from_set); fill_with_units_from(Quantity::Frequency, &to_set); test_si_convert(440, 440, Unit::HZ, "hz", Unit::HZ, "hz", Quantity::Frequency, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Frequency); // Test counter: counter ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Dimensionless); fill_with_units_from(Quantity::Dimensionless, &from_set); fill_with_units_from(Quantity::Dimensionless, &to_set); test_si_convert(2211717, 2211717, Unit::COUNTER, "counter", Unit::FACTOR, "counter", Quantity::Dimensionless, &from_set, &to_set); test_si_convert(2211717, 2211717, Unit::FACTOR, "counter", Unit::COUNTER, "counter", Quantity::Dimensionless, &from_set, &to_set); test_si_convert(2211717, 2211717, Unit::NUMBER, "counter", Unit::COUNTER, "counter", Quantity::Dimensionless, &from_set, &to_set); test_si_convert(2211717, 2211717, Unit::FACTOR, "counter", Unit::NUMBER, "counter", Quantity::Dimensionless, &from_set, &to_set); test_si_convert(2211717, 2211717, Unit::PERCENTAGE, "counter", Unit::NUMBER, "counter", Quantity::Dimensionless, &from_set, &to_set); test_si_convert(2211717, 2211717, Unit::NUMBER, "counter", Unit::PERCENTAGE, "counter", Quantity::Dimensionless, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Dimensionless); // Test angles: deg rad ///////////////////////////////////////////////////////////////////////////////////////////////////// q_set.erase(Quantity::Angle); fill_with_units_from(Quantity::Angle, &from_set); fill_with_units_from(Quantity::Angle, &to_set); test_si_convert(180, 3.1415926535897931l, Unit::DEGREE, "deg", Unit::RADIAN, "rad", Quantity::Angle, &from_set, &to_set); test_si_convert(3.1415926535897931l, 180, Unit::RADIAN, "rad", Unit::DEGREE, "deg", Quantity::Angle, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Angle); // Test point in time units: ut utc lt ///////////////////////////////////////////////////////////////////////////////////////////////////// // I do not know how to handle the point in time units yet. // Mark them as tested.... q_set.erase(Quantity::PointInTime); // Test text unit: text ///////////////////////////////////////////////////////////////////////////////////////////////////// // I do not know how to handle the text unit yet. // Mark it as tested.... q_set.erase(Quantity::Text); check_quantities_tested(q_set); } void test_formulas_building_consts() { unique_ptr f = unique_ptr(new FormulaImplementation()); double v, expected; f->doConstant(Unit::KWH, 17); f->doConstant(Unit::KWH, 1); f->doAddition(SI_KWH); v = f->calculate(Unit::KWH); if (v != 18.0) { printf("ERROR in test formula 1 expected 18.0 but got %lf\n", v); } f->clear(); f->doConstant(Unit::KWH, 10); v = f->calculate(Unit::MJ); if (v != 36.0) { printf("ERROR in test formula 2 expected 36.0 but got %lf\n", v); } f->clear(); f->doConstant(Unit::GJ, 10); f->doConstant(Unit::MJ, 10); f->doAddition(SI_GJ); v = f->calculate(Unit::GJ); if (v != 10.01) { printf("ERROR in test formula 3 expected 10.01 but got %lf\n", v); } f->clear(); f->doConstant(Unit::C, 10); f->doConstant(Unit::C, 20); f->doAddition(SI_C); f->doConstant(Unit::C, 22); f->doAddition(SI_C); v = f->calculate(Unit::C); if (v != 52) { printf("ERROR in test formula 4 expected 52 but got %lf\n", v); } f->clear(); f->doConstant(Unit::Month, 2); f->doConstant(Unit::COUNTER, 3); f->doMultiplication(); v = f->calculate(Unit::Month); if (v != 6) { printf("ERROR in test formula 5 expected 6 but got %g\n", v); } f->clear(); f->doConstant(Unit::UnixTimestamp, 3600*24*11); // mon 12 jan 1970 01:00:00 CET f->doConstant(Unit::Second, 9); f->doAddition(SIUnit(Unit::UnixTimestamp)); v = f->calculate(Unit::UnixTimestamp); expected = 3600*24*11+9; if (v != expected) { printf("ERROR in test formula 6 expected %g but got %g\n", expected, v); } f->clear(); f->doConstant(Unit::UnixTimestamp, 3600*24*11); // mon 12 jan 1970 01:00:00 CET f->doConstant(Unit::Month, 1); f->doAddition(SIUnit(Unit::UnixTimestamp)); v = f->calculate(Unit::UnixTimestamp); expected = 3600*24*(31+11); // mon 12 feb 1970 01:00:00 CET if (v != expected) { printf("ERROR in test formula 7 expected %g but got %g\n", expected, v); } } void test_formulas_building_meters() { unique_ptr f = unique_ptr(new FormulaImplementation()); //////////////////////////////////////////////////////////////////////////////////////////////////// { MeterInfo mi; assert(lookupDriverInfo("multical21")); mi.parse("testur", "multical21", "12345678", ""); shared_ptr meter = createMeter(&mi); FieldInfo *fi_flow = meter->findFieldInfo("flow_temperature", Quantity::Temperature); FieldInfo *fi_ext = meter->findFieldInfo("external_temperature", Quantity::Temperature); assert(fi_flow != NULL); assert(fi_ext != NULL); vector frame; hex2bin("2a442d2c785634121B168d2091d37cac217f2d7802ff207100041308190000441308190000615B1f616713", &frame); Telegram t; MeterKeys mk; t.parse(frame, &mk, true); vector
addresses; bool match; meter->handleTelegram(t.about, frame, true, &addresses, &match, &t); f->clear(); f->setMeter(meter.get()); f->doMeterField(Unit::C, fi_flow); double v = f->calculate(Unit::C); if (v != 31) { printf("ERROR in test formula 5 expected 31 but got %lf\n", v); } f->clear(); f->setMeter(meter.get()); f->doMeterField(Unit::C, fi_flow); f->doMeterField(Unit::C, fi_ext); f->doAddition(SIUnit(Unit::C)); v = f->calculate(Unit::C); if (v != 50) { printf("ERROR in test formula 6 expected 50 but got %lf\n", v); } // Check that trying to add a field reference expecting a non-convertible unit, will fail! // f->clear(); // assert(false == f->doField(Unit::M3, meter.get(), fi_flow)); } //////////////////////////////////////////////////////////////////////////////////////////////////// { MeterInfo mi; mi.parse("testur", "ebzwmbe", "22992299", ""); shared_ptr meter = createMeter(&mi); FieldInfo *fi_p1 = meter->findFieldInfo("current_power_consumption_phase1", Quantity::Power); FieldInfo *fi_p2 = meter->findFieldInfo("current_power_consumption_phase2", Quantity::Power); FieldInfo *fi_p3 = meter->findFieldInfo("current_power_consumption_phase3", Quantity::Power); assert(fi_p1 != NULL); assert(fi_p2 != NULL); assert(fi_p3 != NULL); vector frame; hex2bin("5B445a149922992202378c20f6900f002c25Bc9e0000BBBBBBBBBBBBBBBB72992299225a140102f6003007102f2f040330f92a0004a9ff01ff24000004a9ff026a29000004a9ff03460600000dfd11063132333435362f2f2f2f2f2f", &frame); Telegram t; MeterKeys mk; t.parse(frame, &mk, true); vector
id; bool match; meter->handleTelegram(t.about, frame, true, &id, &match, &t); unique_ptr f = unique_ptr(new FormulaImplementation()); f->clear(); f->setMeter(meter.get()); f->doMeterField(Unit::KW, fi_p1); f->doMeterField(Unit::KW, fi_p2); f->doAddition(SI_KW); f->doMeterField(Unit::KW, fi_p3); f->doAddition(SI_KW); double v = f->calculate(Unit::KW); if (v != 0.21679) { printf("ERROR in test formula 7 expected 0.21679 but got %lf\n", v); } } } void test_formula_tree(FormulaImplementation *f, Meter *m, string formula, string tree) { f->clear(); f->parse(m, formula); string t = f->tree(); if (t != tree) { printf("ERROR when parsing \"%s\" expected tree to be \"%s\"\nbut got \"%s\"\n", formula.c_str(), tree.c_str(), t.c_str()); } } void test_formula_value(FormulaImplementation *f, Meter *m, string formula, double val, Unit unit) { f->clear(); bool ok = f->parse(m, formula); assert(ok); double v = f->calculate(unit); debug("(formula) %s\n", f->tree().c_str()); if (v != val) { printf("ERROR when evaluating \"%s\"\nERROR expected %.17g but got %.17g\n", formula.c_str(), val, v); } } void test_formula_error(FormulaImplementation *f, Meter *m, string formula, Unit unit, string errors) { f->clear(); bool ok = f->parse(m, formula); string es = f->errors(); if (es != errors) { printf("ERROR when parsing \"%s\"\nExpected errors:\n%sBut got errors:\n%s", formula.c_str(), errors.c_str(), f->errors().c_str()); } assert(!ok); } double totime(int year, int month = 1, int day = 1, int hour = 0, int min = 0, int sec = 0) { struct tm date {}; date.tm_year = year-1900; date.tm_mon = month-1; date.tm_mday = day; date.tm_hour = hour; date.tm_min = min; date.tm_sec = sec; // This t timestamp is dependent on the local time zone. time_t t = mktime(&date); /* // Extract the local time zone. struct tm tz_adjust {}; localtime_r(&t, &tz_adjust); // if tm_gmtoff is zero, then we are in Greenwich! // if tm_gmtoff is 3600 then we are in Stockholm! // Now adjust the t timestamp so that we execute this this, as if we are in Greenwich. // This way, the test will work wherever in the world we run it. t -= tz_adjust.tm_gmtoff; */ return (double)t; } void test_datetime(FormulaImplementation *f, string formula, int year, int month=1, int day=1, int hour=0, int min=0, int sec=0) { f->clear(); double expected = totime(year,month,day,hour,min,sec); f->parse(NULL, formula); if (!f->valid()) { printf("%s\n", f->errors().c_str()); } double v = f->calculate(Unit::UnixTimestamp); if (v != expected) { time_t t = v; struct tm time {}; localtime_r(&t, &time); string gs = strdatetimesec(&time); printf("ERROR Expected datetime %.17g %04d-%02d-%02d %02d:%02d:%02d " "but got %.17g (%s) when testing \"%s\"\n", expected, year, month, day, hour, min, sec, v, gs.c_str(), formula.c_str()); } } void test_time(FormulaImplementation *f, string formula, int hour=0, int min=0, int sec=0) { f->clear(); double expected = hour*3600+min*60+sec; f->parse(NULL, formula); if (!f->valid()) { printf("%s\n", f->errors().c_str()); } double v = f->calculate(Unit::Second); if (v != expected) { printf("ERROR Expected time %.17g but got %.17g when testing %s %02d:%02d.%02d\n", expected, v, formula.c_str(), hour, min, sec); } } void test_formulas_datetimes() { unique_ptr f = unique_ptr(new FormulaImplementation()); test_datetime(f.get(), "'2022-02-02'", 2022, 02, 02); test_datetime(f.get(), "'2021-02-28'", 2021, 02, 28); test_datetime(f.get(), "'1970-01-01 01:00:00'", 1970, 01, 01, 01, 00, 00); test_datetime(f.get(), "'1970-01-01 01:00'", 1970, 01, 01, 01, 00); test_datetime(f.get(), "'1970-01-01'", 1970, 01, 01); test_time(f.get(), "'00:15'", 0, 15, 0); test_time(f.get(), "'00:00:16'", 0, 0, 16); test_datetime(f.get(), "'2022-01-01 00:00:00' + 1s", 2022,1,1,0,0,1); test_datetime(f.get(), "'1971-10-01 02:17' +7d+1h+2min+1s", 1971, 10, 8, 3, 19, 1); test_datetime(f.get(), "'2000-01-01' + 1month", 2000, 2, 1); test_datetime(f.get(), "'2020-12-31' + 2month", 2021, 2, 28); test_datetime(f.get(), "'2020-12-31' - 10month", 2020, 2, 29); test_datetime(f.get(), "'2021-01-31' - 1month", 2020, 12,31); test_datetime(f.get(), "'2021-01-31' - 2month", 2020, 11, 30); test_datetime(f.get(), "'2021-01-31' - 24month", 2019, 1, 31); test_datetime(f.get(), "'2021-01-31' + 24month", 2023, 1, 31); test_datetime(f.get(), "'2021-01-31' + 22month", 2022, 11, 30); // 2020 was a leap year. test_datetime(f.get(), "'2021-02-28' -12month", 2020,2,29); // 2000 was a leap year %100=0 but %400=0 overrides. test_datetime(f.get(), "'2001-02-28' -12month", 2000, 2, 29); // 2100 is not a leap year since %100=0 and not overriden %400 != 0. test_datetime(f.get(), "'2000-02-29' +(12month * 100counter)", 2100,2,28); } void test_formulas_parsing_1() { MeterInfo mi; mi.parse("testur", "ebzwmbe", "22992299", ""); shared_ptr meter = createMeter(&mi); vector frame; hex2bin("5B445a149922992202378c20f6900f002c25Bc9e0000BBBBBBBBBBBBBBBB72992299225a140102f6003007102f2f040330f92a0004a9ff01ff24000004a9ff026a29000004a9ff03460600000dfd11063132333435362f2f2f2f2f2f", &frame); Telegram t; MeterKeys mk; t.parse(frame, &mk, true); vector
id; bool match; meter->handleTelegram(t.about, frame, true, &id, &match, &t); unique_ptr f = unique_ptr(new FormulaImplementation()); test_formula_value(f.get(), meter.get(), "10 kwh + 100 kwh", 110, Unit::KWH); test_formula_value(f.get(), meter.get(), "current_power_consumption_phase1_kw + " "current_power_consumption_phase2_kw + " "current_power_consumption_phase3_kw + " "100 kw", 100.21679, Unit::KW); test_formula_tree(f.get(), meter.get(), "5 c + 7 c + 10 c", " > >"); test_formula_tree(f.get(), meter.get(), "(5 c + 7 c) + 10 c", " > >"); test_formula_tree(f.get(), meter.get(), "5 c + (7 c + 10 c)", " > >"); test_formula_tree(f.get(), meter.get(), "sqrt(22 m * 22 m)", " > >"); } void test_formulas_parsing_2() { MeterInfo mi; mi.parse("testur", "em24", "66666666", ""); shared_ptr meter = createMeter(&mi); vector frame; hex2bin("35442D2C6666666633028D2070806A0520B4D378_0405F208000004FB82753F00000004853C0000000004FB82F53CCA01000001FD1722", &frame); Telegram t; MeterKeys mk; t.parse(frame, &mk, true); vector
id; bool match; meter->handleTelegram(t.about, frame, true, &id, &match, &t); unique_ptr f = unique_ptr(new FormulaImplementation()); test_formula_value(f.get(), meter.get(), "total_energy_consumption_kwh + 18 kwh", 247, Unit::KWH); } void test_formulas_multiply_constants() { FormulaImplementation fi; test_formula_value(&fi, NULL, "100.5 counter * 22 kwh", 2211, Unit::KWH); test_formula_value(&fi, NULL, "5 kw * 10 h", 50, Unit::KWH); test_formula_value(&fi, NULL, "5000 v * 10 a * 700 h", 35000, Unit::KVAH); } void test_formulas_divide_constants() { FormulaImplementation fi; test_formula_value(&fi, NULL, "22 kwh / 11 h", 2, Unit::KW); } void test_formulas_sqrt_constants() { FormulaImplementation fi; test_formula_value(&fi, NULL, "sqrt(22 m * 22 m)", 22, Unit::M); test_formula_value(&fi, NULL, "sqrt((2 kwh * 2 kwh) + (3 kvarh * 3 kvarh))", 3.6055512754639891, Unit::KVAH); } void test_formulas_date_constants() { FormulaImplementation fi; // test_formula_value(&fi, NULL, "2022-01-01 + 1 month", "2022-02-01"); } void test_formulas_errors() { { MeterInfo mi; mi.parse("testur", "em24", "66666666", ""); auto meter = createMeter(&mi); auto formula = unique_ptr(new FormulaImplementation()); test_formula_error(formula.get(), meter.get(), "10 kwh + 20 kw", Unit::KWH, "Cannot add [kwh|Energy|3.6×10⁶kgm²s⁻²] to [kw|Power|1000kgm²s⁻³]!\n" "10 kwh + 20 kw\n" " ^~~~~\n"); } } void test_formulas_dventries() { FormulaImplementation fi; DVEntry dve; dve.storage_nr = 17; dve.tariff_nr = 3; dve.subunit_nr = 2; unique_ptr f = unique_ptr(new FormulaImplementation()); string s = "(storage_counter - 12 counter) * tariff_counter - subunit_counter"; f->parse(NULL, s); f->setDVEntry(&dve); double v = f->calculate(Unit::COUNTER); if (v != 13.0) { printf("ERROR when calculating:\n%s\nExpected: %g but got: %g\n", s.c_str(), 13.0, v); } dve.storage_nr = 18; dve.tariff_nr = 0; dve.subunit_nr = 0; s = "(storage_counter - 8counter) / 2counter"; f->parse(NULL, s); f->setDVEntry(&dve); v = f->calculate(Unit::COUNTER); if (v != 5.0) { printf("ERROR when calculating:\n%s\nExpected: %g but got: %g\n", s.c_str(), 5.0, v); } } void test_formulas_stringinterpolation() { DVEntry dve; dve.storage_nr = 17; dve.tariff_nr = 3; dve.subunit_nr = 2; unique_ptr f = unique_ptr(new StringInterpolatorImplementation()); string p = "history_{storage_counter-12counter}_value"; f->parse(p); string s = f->apply(&dve); string e = "history_5_value"; if (s != e) { printf("ERROR when interpolating\n%s\nExpected: %s but got: %s\n", p.c_str(), e.c_str(), s.c_str()); } p = "{storage_counter}_{tariff_counter}_{2counter*subunit_counter}"; f->parse(p); s = f->apply(&dve); e = "17_3_4"; if (s != e) { printf("ERROR when interpolating\n%s\nExpected: %s but got: %s\n", p.c_str(), e.c_str(), s.c_str()); } } void test_dynamic_loading() { VIFRange vr = toVIFRange("Date"); if (vr != VIFRange::Date) { printf("ERROR in dynamic loading got %s but expected %s!\n", toString(vr), toString(VIFRange::Date)); } vr = toVIFRange("DateTime"); if (vr != VIFRange::DateTime) { printf("ERROR in dynamic loading got %s but expected %s!\n", toString(vr), toString(VIFRange::DateTime)); } }