feat: add GroundValueChecker and WeatherSensorDevice with public API integration

This commit introduces two new device types, GroundValueChecker and WeatherSensorDevice, which leverage public APIs (BORIS NRW and Open-Meteo) for real-time data collection. It also includes several API enhancements and fixes to support these new integrations.

Detailed changes:
- **New Device Types**:
  - Added GroundValueChecker schema and integration with BORIS NRW WMS via Zabbix Script items.
  - Added WeatherSensorDevice schema and integration with Open-Meteo via Zabbix HTTP Agent items.
- **API Enhancements**:
  - Added error field to ZabbixItem for item-level error reporting.
  - Updated CreateTemplateItem mutation input to support params (for Script items) and timeout.
  - Registered missing scalar resolvers: JSONObject, DateTime, and Time.
- **Performance & Reliability**:
  - Implemented batch fetching for item preprocessing in both host and template queries to reduce Zabbix API calls and ensure data visibility.
  - Updated template_importer.ts to correctly handle Script item parameters.
- **Documentation**:
  - Consolidated public API device recipes in docs/howtos/cookbook.md.
  - Added guidance on analyzing data update frequency and setting reasonable update intervals (e.g., 1h for weather, 1d for ground values).
- **Testing**:
  - Added new regression test REG-ITEM-META to verify item metadata (units, description, error, preprocessing) and JSONObject scalar support.
  - Enhanced RegressionTestExecutor with more detailed host-item relationship verification.
This commit is contained in:
Andreas Hilbig 2026-02-01 21:07:21 +01:00
parent 41e4c4da1f
commit ad104acde2
13 changed files with 378 additions and 45 deletions

View file

@ -5,7 +5,7 @@ import {TemplateImporter} from "./template_importer.js";
import {TemplateDeleter} from "./template_deleter.js";
import {logger} from "../logging/logger.js";
import {zabbixAPI} from "../datasources/zabbix-api.js";
import {ZabbixQueryHostsGenericRequest} from "../datasources/zabbix-hosts.js";
import {ZabbixQueryHostsGenericRequest, ZabbixQueryHostsGenericRequestWithItems} from "../datasources/zabbix-hosts.js";
import {ZabbixQueryTemplatesRequest} from "../datasources/zabbix-templates.js";
import {ParsedArgs} from "../datasources/zabbix-request.js";
@ -198,6 +198,83 @@ export class RegressionTestExecutor {
}
}
// Regression 6: Item Metadata (preprocessing, units, description, error)
const metaTempName = "REG_META_TEMP_" + Math.random().toString(36).substring(7);
const metaHostName = "REG_META_HOST_" + Math.random().toString(36).substring(7);
const metaTempResult = await TemplateImporter.importTemplates([{
host: metaTempName,
name: "Regression Meta Template",
groupNames: [regGroupName],
items: [{
name: "Meta Item",
type: 2, // Zabbix trapper
key: "meta.item",
value_type: 0, // Float
units: "TEST_UNIT",
description: "Test Description",
history: "1d",
preprocessing: [
{
type: 12, // JSONPath
params: ["$.value"]
}
]
}]
}], zabbixAuthToken, cookie);
const metaTempSuccess = !!metaTempResult?.length && !metaTempResult[0].error;
let metaHostSuccess = false;
let metaVerifySuccess = false;
if (metaTempSuccess) {
const metaHostResult = await HostImporter.importHosts([{
deviceKey: metaHostName,
deviceType: "RegressionHost",
groupNames: [hostGroupName],
templateNames: [metaTempName]
}], zabbixAuthToken, cookie);
metaHostSuccess = !!metaHostResult?.length && !!metaHostResult[0].hostid;
if (metaHostSuccess) {
// Verify item metadata
const verifyResult = await new ZabbixQueryHostsGenericRequestWithItems("host.get", zabbixAuthToken, cookie)
.executeRequestReturnError(zabbixAPI, new ParsedArgs({
filter_host: metaHostName
}));
if (Array.isArray(verifyResult) && verifyResult.length > 0) {
const host = verifyResult[0] as any;
const item = host.items?.find((i: any) => i.key_ === "meta.item");
if (item) {
const hasUnits = item.units === "TEST_UNIT";
const hasDesc = item.description === "Test Description";
// Zabbix might return type as string or number depending on version/API, but usually it's string in JSON result if not cast
const hasPreproc = Array.isArray(item.preprocessing) && item.preprocessing.length > 0 &&
String(item.preprocessing[0].type) === "12";
const hasErrorField = item.hasOwnProperty("error");
metaVerifySuccess = hasUnits && hasDesc && hasPreproc && hasErrorField;
if (!metaVerifySuccess) {
logger.error(`REG-META: Verification failed. Units: ${hasUnits}, Desc: ${hasDesc}, Preproc: ${hasPreproc}, ErrorField: ${hasErrorField}. Item: ${JSON.stringify(item)}`);
}
}
}
}
}
const metaOverallSuccess = metaTempSuccess && metaHostSuccess && metaVerifySuccess;
steps.push({
name: "REG-ITEM-META: Item metadata (preprocessing, units, description, error)",
success: metaOverallSuccess,
message: metaOverallSuccess
? "Item metadata successfully retrieved including preprocessing and units"
: `Failed: TempImport=${metaTempSuccess}, HostImport=${metaHostSuccess}, Verify=${metaVerifySuccess}`
});
if (!metaOverallSuccess) success = false;
// Step 1: Create Host Group (Legacy test kept for compatibility)
const groupResult = await HostImporter.importHostGroups([{
groupName: groupName
@ -214,9 +291,11 @@ export class RegressionTestExecutor {
// Cleanup
await HostDeleter.deleteHosts(null, hostName, zabbixAuthToken, cookie);
await HostDeleter.deleteHosts(null, macroHostName, zabbixAuthToken, cookie);
await HostDeleter.deleteHosts(null, metaHostName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, regTemplateName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, httpTempName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, macroTemplateName, zabbixAuthToken, cookie);
await TemplateDeleter.deleteTemplates(null, metaTempName, zabbixAuthToken, cookie);
// We don't delete the group here as it might be shared or used by other tests in this run
} catch (error: any) {