add support to build zones and PIDs from json

Add support to build zones and PIDs from a json configuration file.

Change-Id: If8608dcd3e084cebabb71fc39851044df4d5d7c2
Signed-off-by: Patrick Venture <venture@google.com>
diff --git a/Makefile.am b/Makefile.am
index 09a52d5..cce4f4c 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -73,6 +73,7 @@
 	pid/stepwisecontroller.cpp \
 	pid/builder.cpp \
 	pid/builderconfig.cpp \
+	pid/buildjson.cpp \
 	pid/zone.cpp \
 	pid/util.cpp \
 	pid/pidthread.cpp \
diff --git a/pid/buildjson.cpp b/pid/buildjson.cpp
new file mode 100644
index 0000000..d89da92
--- /dev/null
+++ b/pid/buildjson.cpp
@@ -0,0 +1,106 @@
+/**
+ * Copyright 2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "pid/buildjson.hpp"
+
+#include "conf.hpp"
+
+#include <map>
+#include <nlohmann/json.hpp>
+#include <tuple>
+
+using json = nlohmann::json;
+
+void from_json(const json& j, ControllerInfo& c)
+{
+    j.at("type").get_to(c.type);
+    j.at("inputs").get_to(c.inputs);
+    j.at("setpoint").get_to(c.setpoint);
+
+    /* TODO: We need to handle parsing other PID controller configurations.
+     * We can do that by checking for different keys and making the decision
+     * accordingly.
+     */
+    auto p = j.at("pid");
+    p.at("samplePeriod").get_to(c.pidInfo.ts);
+    p.at("proportionalCoeff").get_to(c.pidInfo.proportionalCoeff);
+    p.at("integralCoeff").get_to(c.pidInfo.integralCoeff);
+    p.at("feedFwdOffOffsetCoeff").get_to(c.pidInfo.feedFwdOffset);
+    p.at("feedFwdGainCoeff").get_to(c.pidInfo.feedFwdGain);
+    p.at("integralLimit_min").get_to(c.pidInfo.integralLimit.min);
+    p.at("integralLimit_max").get_to(c.pidInfo.integralLimit.max);
+    p.at("outLim_min").get_to(c.pidInfo.outLim.min);
+    p.at("outLim_max").get_to(c.pidInfo.outLim.max);
+    p.at("slewNeg").get_to(c.pidInfo.slewNeg);
+    p.at("slewPos").get_to(c.pidInfo.slewPos);
+
+    auto positiveHysteresis = p.find("positiveHysteresis");
+    if (positiveHysteresis == p.end())
+    {
+        c.pidInfo.positiveHysteresis = 0.0;
+    }
+    else
+    {
+        j.at("positiveHysteresis").get_to(c.pidInfo.positiveHysteresis);
+    }
+
+    auto negativeHysteresis = p.find("negativeHysteresis");
+    if (negativeHysteresis == p.end())
+    {
+        c.pidInfo.negativeHysteresis = 0.0;
+    }
+    else
+    {
+        j.at("negativeHysteresis").get_to(c.pidInfo.negativeHysteresis);
+    }
+}
+
+std::pair<std::map<int64_t, PIDConf>, std::map<int64_t, struct ZoneConfig>>
+    buildPIDsFromJson(const json& data)
+{
+    // zone -> pids
+    std::map<int64_t, PIDConf> pidConfig;
+    // zone -> configs
+    std::map<int64_t, struct ZoneConfig> zoneConfig;
+
+    /* TODO: if zones is empty, that's invalid. */
+    auto zones = data["zones"];
+    for (const auto& zone : zones)
+    {
+        int64_t id;
+        PIDConf thisZone;
+        struct ZoneConfig thisZoneConfig;
+
+        /* TODO: using at() throws a specific exception we can catch */
+        id = zone["id"];
+        thisZoneConfig.minThermalRpm = zone["minThermalRpm"];
+        thisZoneConfig.failsafePercent = zone["failsafePercent"];
+
+        auto pids = zone["pids"];
+        for (const auto& pid : pids)
+        {
+            auto name = pid["name"];
+            auto item = pid.get<ControllerInfo>();
+
+            thisZone[name] = item;
+        }
+
+        pidConfig[id] = thisZone;
+        zoneConfig[id] = thisZoneConfig;
+    }
+
+    return std::make_pair(pidConfig, zoneConfig);
+}
diff --git a/pid/buildjson.hpp b/pid/buildjson.hpp
new file mode 100644
index 0000000..626c982
--- /dev/null
+++ b/pid/buildjson.hpp
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "conf.hpp"
+
+#include <map>
+#include <nlohmann/json.hpp>
+#include <tuple>
+
+using json = nlohmann::json;
+
+/**
+ * Given the json "zones" data, create the map of PIDs and the map of zones.
+ *
+ * @param[in] data - the json data
+ * @return the pidConfig, and the zoneConfig
+ */
+std::pair<std::map<int64_t, PIDConf>, std::map<int64_t, struct ZoneConfig>>
+    buildPIDsFromJson(const json& data);
diff --git a/test/Makefile.am b/test/Makefile.am
index fb1d58d..5375eb4 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -18,7 +18,7 @@
  pid_thermalcontroller_unittest pid_fancontroller_unittest \
  pid_stepwisecontroller_unittest \
  dbus_passive_unittest dbus_active_unittest \
- sensors_json_unittest
+ sensors_json_unittest pid_json_unittest
 TESTS = $(check_PROGRAMS)
 
 # Until libconfig is mocked out or replaced, include it.
@@ -62,3 +62,6 @@
 
 sensors_json_unittest_SOURCES = sensors_json_unittest.cpp
 sensors_json_unittest_LDADD = $(top_builddir)/sensors/buildjson.o
+
+pid_json_unittest_SOURCES = pid_json_unittest.cpp
+pid_json_unittest_LDADD = $(top_builddir)/pid/buildjson.o
diff --git a/test/pid_json_unittest.cpp b/test/pid_json_unittest.cpp
new file mode 100644
index 0000000..f0effbd
--- /dev/null
+++ b/test/pid_json_unittest.cpp
@@ -0,0 +1,68 @@
+#include "pid/buildjson.hpp"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+TEST(ZoneFromJson, emptyZone)
+{
+    // There is a zone key, but it's empty.
+    // This is technically invalid.
+
+    std::map<int64_t, PIDConf> pidConfig;
+    std::map<int64_t, struct ZoneConfig> zoneConfig;
+
+    auto j2 = R"(
+      {
+        "zones": []
+      }
+    )"_json;
+
+    std::tie(pidConfig, zoneConfig) = buildPIDsFromJson(j2);
+
+    EXPECT_TRUE(pidConfig.empty());
+    EXPECT_TRUE(zoneConfig.empty());
+}
+
+TEST(ZoneFromJson, oneZoneOnePid)
+{
+    // Parse a valid configuration with one zone and one PID.
+
+    std::map<int64_t, PIDConf> pidConfig;
+    std::map<int64_t, struct ZoneConfig> zoneConfig;
+
+    auto j2 = R"(
+      {
+        "zones" : [{
+          "id": 1,
+          "minThermalRpm": 3000.0,
+          "failsafePercent": 75.0,
+          "pids": [{
+            "name": "fan1-5",
+            "type": "fan",
+            "inputs": ["fan1", "fan5"],
+            "setpoint": 90.0,
+            "pid": {
+              "samplePeriod": 0.1,
+              "proportionalCoeff": 0.0,
+              "integralCoeff": 0.0,
+              "feedFwdOffOffsetCoeff": 0.0,
+              "feedFwdGainCoeff": 0.010,
+              "integralLimit_min": 0.0,
+              "integralLimit_max": 0.0,
+              "outLim_min": 30.0,
+              "outLim_max": 100.0,
+              "slewNeg": 0.0,
+              "slewPos": 0.0
+            }
+          }]
+        }]
+      }
+    )"_json;
+
+    std::tie(pidConfig, zoneConfig) = buildPIDsFromJson(j2);
+    EXPECT_EQ(pidConfig.size(), 1);
+    EXPECT_EQ(zoneConfig.size(), 1);
+
+    EXPECT_EQ(pidConfig[1]["fan1-5"].type, "fan");
+    EXPECT_DOUBLE_EQ(zoneConfig[1].minThermalRpm, 3000.0);
+}