system: Turn on and off UID LED via netconf

Change-Id: Ic33a436f6bfb796379a019bd498b9919ce99a9d5
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c0ec1a1..38c274f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -412,7 +412,7 @@
             RESOURCE_LOCK sysrepo
     )
 
-    velia_test(sysrepo_system-leds velia-system)
+    velia_test(sysrepo_system-leds velia-system FsTestUtils)
     set_tests_properties(
             test-sysrepo_system-leds
             PROPERTIES FIXTURES_REQUIRED sysrepo:env:sysrepo-czechlight-system
diff --git a/src/system/LED.cpp b/src/system/LED.cpp
index b6465d0..870476b 100644
--- a/src/system/LED.cpp
+++ b/src/system/LED.cpp
@@ -9,6 +9,7 @@
 
 #include <utility>
 #include "utils/io.h"
+#include "utils/libyang.h"
 #include "utils/log.h"
 #include "utils/sysrepo.h"
 
@@ -19,6 +20,7 @@
 const auto CZECHLIGHT_SYSTEM_MODULE_NAME = "czechlight-system"s;
 const auto CZECHLIGHT_SYSTEM_LEDS_MODULE_PREFIX = "/"s + CZECHLIGHT_SYSTEM_MODULE_NAME + ":leds/"s;
 
+const auto UID_LED = "uid:blue"s;
 }
 
 namespace velia::system {
@@ -63,6 +65,35 @@
         },
         (CZECHLIGHT_SYSTEM_LEDS_MODULE_PREFIX + "*").c_str(),
         SR_SUBSCR_PASSIVE | SR_SUBSCR_OPER_MERGE | SR_SUBSCR_CTX_REUSE);
+
+    const auto uidMaxBrightness = std::to_string(velia::utils::readFileInt64(m_sysfsLeds / UID_LED / "max_brightness"));
+    const auto triggerFile = m_sysfsLeds / UID_LED / "trigger";
+    const auto brightnessFile = m_sysfsLeds / UID_LED / "brightness";
+
+    m_srSubscribe->rpc_subscribe_tree(
+        (CZECHLIGHT_SYSTEM_LEDS_MODULE_PREFIX + "uid").c_str(),
+        [this, uidMaxBrightness, triggerFile, brightnessFile](auto session, auto, auto input, auto, auto, auto) {
+            std::string val = getValueAsString(getSubtree(input, (CZECHLIGHT_SYSTEM_LEDS_MODULE_PREFIX + "uid/state").c_str()));
+
+            try {
+                if (val == "on") {
+                    utils::writeFile(triggerFile, "none");
+                    utils::writeFile(brightnessFile, uidMaxBrightness);
+                } else if (val == "off") {
+                    utils::writeFile(triggerFile, "none");
+                    utils::writeFile(brightnessFile, "0");
+                } else if (val == "blinking") {
+                    utils::writeFile(triggerFile, "timer");
+                    utils::writeFile(brightnessFile, uidMaxBrightness);
+                }
+            } catch (const std::invalid_argument& e) {
+                m_log->warn("Failed to set state of the UID LED: '{}'", e.what());
+                session->set_error("Failed to set state of the UID LED", nullptr);
+                return SR_ERR_OPERATION_FAILED;
+            }
+
+            return SR_ERR_OK;
+        });
 }
 
 }
diff --git a/src/utils/io.cpp b/src/utils/io.cpp
index 4c84a89..5de62b2 100644
--- a/src/utils/io.cpp
+++ b/src/utils/io.cpp
@@ -78,6 +78,18 @@
     return std::string(begin, end);
 }
 
+void writeFile(const std::string& path, const std::string_view& contents)
+{
+    std::ofstream ofs(path);
+    if (!ofs.is_open()) {
+        throw std::invalid_argument("File '" + std::string(path) + "' could not be opened.");
+    }
+
+    if (!(ofs << contents)) {
+        throw std::invalid_argument("File '" + std::string(path) + "' could not be written.");
+    }
+}
+
 void safeWriteFile(const std::string& filename, const std::string_view& contents)
 {
     auto throwErr = [&filename] (const auto& what) {
diff --git a/src/utils/io.h b/src/utils/io.h
index ec3286b..e2ab09b 100644
--- a/src/utils/io.h
+++ b/src/utils/io.h
@@ -17,5 +17,6 @@
 int64_t readFileInt64(const std::filesystem::path& path);
 std::vector<uint32_t> readFileWords(const std::filesystem::path& path, int valuesCount);
 std::string readFileToString(const std::filesystem::path& path);
+void writeFile(const std::string& path, const std::string_view& contents);
 void safeWriteFile(const std::string& filename, const std::string_view& contents);
 }
diff --git a/tests/sysfs/leds/uid:blue/trigger b/tests/sysfs/leds/uid:blue/trigger
new file mode 100644
index 0000000..621e94f
--- /dev/null
+++ b/tests/sysfs/leds/uid:blue/trigger
@@ -0,0 +1 @@
+none
diff --git a/tests/sysrepo_system-leds.cpp b/tests/sysrepo_system-leds.cpp
index 5248faa..db648a4 100644
--- a/tests/sysrepo_system-leds.cpp
+++ b/tests/sysrepo_system-leds.cpp
@@ -1,10 +1,12 @@
 #include "trompeloeil_doctest.h"
 #include "dbus-helpers/dbus_rauc_server.h"
+#include "fs-helpers/utils.h"
 #include "pretty_printers.h"
 #include "system/LED.h"
 #include "test_log_setup.h"
 #include "test_sysrepo_helpers.h"
 #include "tests/configure.cmake.h"
+#include "utils/io.h"
 
 using namespace std::literals;
 
@@ -16,7 +18,10 @@
     TEST_SYSREPO_INIT;
     TEST_SYSREPO_INIT_CLIENT;
 
-    auto fakeSysfsDir = std::filesystem::path {CMAKE_CURRENT_SOURCE_DIR + "/tests/sysfs/leds/"s};
+    auto fakeSysfsDir = std::filesystem::path {CMAKE_CURRENT_BINARY_DIR + "/tests/leds/"s};
+    removeDirectoryTreeIfExists(fakeSysfsDir);
+    std::filesystem::copy(CMAKE_CURRENT_SOURCE_DIR + "/tests/sysfs/leds"s, fakeSysfsDir, std::filesystem::copy_options::recursive);
+
     velia::system::LED led(srConn, fakeSysfsDir);
 
     std::this_thread::sleep_for(10ms);
@@ -35,4 +40,46 @@
                 {"/led[name='uid:red']/brightness", "100"},
                 {"/led[name='uid:red']/name", "uid:red"},
             });
+
+    std::shared_ptr<sysrepo::Vals> rpcInput = std::make_shared<sysrepo::Vals>(1);
+    std::string state;
+    std::string expectedTrigger;
+    std::string expectedBrightness;
+
+    /* This isn't what actually happens in real-life. The contents of the trigger file is usually something like this (i.e., list of available triggers).
+     *
+     *  [none] kbd-scrolllock kbd-numlock kbd-capslock kbd-kanalock kbd-shiftlock kbd-altgrlock kbd-ctrllock kbd-altlock kbd-shiftllock kbd-shiftrlock kbd-ctrlllock kbd-ctrlrlock mmc0 timer oneshot heartbeat gpio default-on transient panic netdev f1072004.mdio-mii:01:link f1072004.mdio-mii:01:1Gbps f1072004.mdio-mii:01:100Mbps f1072004.mdio-mii:01:10Mbps f1072004.mdio-mii:00:link f1072004.mdio-mii:00:1Gbps f1072004.mdio-mii:00:100Mbps f1072004.mdio-mii:00:10Mbps
+     *
+     * The value enclosed in brackets is the current active trigger. You can change it by writing a value corresponding to a trigger to the trigger file.
+     * I'm not going to simulate sysfs led behaviour here, so just test that the original contents was "none" and the value written by the RPC is the expected value.
+     * Also, I'm not implementing the 'timer' trigger behaviour, so the value written to the brightness file is static.
+     */
+    REQUIRE(velia::utils::readFileString(fakeSysfsDir / "uid:blue" / "trigger") == "none");
+
+    SECTION("UID led on")
+    {
+        state = "on";
+        expectedTrigger = "none";
+        expectedBrightness = "256";
+    }
+
+    SECTION("UID led off")
+    {
+        state = "off";
+        expectedTrigger = "none";
+        expectedBrightness = "0";
+    }
+
+    SECTION("UID led blinking")
+    {
+        state = "blinking";
+        expectedTrigger = "timer";
+        expectedBrightness = "256";
+    }
+
+    rpcInput->val(0)->set("/czechlight-system:leds/uid/state", state.c_str());
+    auto res = client->rpc_send("/czechlight-system:leds/uid", rpcInput);
+    REQUIRE(res->val_cnt() == 0);
+    REQUIRE(velia::utils::readFileString(fakeSysfsDir / "uid:blue" / "trigger") == expectedTrigger);
+    REQUIRE(velia::utils::readFileString(fakeSysfsDir / "uid:blue" / "brightness") == expectedBrightness);
 }
diff --git a/yang/czechlight-system@2021-01-13.yang b/yang/czechlight-system@2021-01-13.yang
index afd6c80..bd8449e 100644
--- a/yang/czechlight-system@2021-01-13.yang
+++ b/yang/czechlight-system@2021-01-13.yang
@@ -236,6 +236,20 @@
         type percent;
       }
     }
+
+    action uid {
+      input {
+        leaf state {
+          mandatory true;
+          description "Change state of the UID led (turn off, on, or keep blinking).";
+          type enumeration {
+            enum off;
+            enum on;
+            enum blinking;
+          }
+        }
+      }
+    }
   }
 
   deviation /sys:system-shutdown { deviate not-supported; }