From 9018d8386830a3b23b4274da8a7198d0617cd210 Mon Sep 17 00:00:00 2001 From: Tyler Perkins Date: Mon, 17 Jan 2022 15:03:34 -0500 Subject: [PATCH] Add home assistant bare bones panel --- src/config.cpp | 8 +- src/config.def.hpp | 1 + src/panel/homeassistant.cpp | 225 +++++++++++++++++++++++++++++ src/panel/homeassistant.hpp | 59 ++++++++ src/panel/homeassistant_config.hpp | 31 ++++ 5 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 src/panel/homeassistant.cpp create mode 100644 src/panel/homeassistant.hpp create mode 100644 src/panel/homeassistant_config.hpp diff --git a/src/config.cpp b/src/config.cpp index 69b23b6..10f80f7 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -10,10 +10,12 @@ // A list of all panels to be displayed, in order dashboard::panel::panel* PANELS[] = { //new dashboard::panel::sample_panel(), + new dashboard::panel::homeassistant(), new dashboard::panel::plex(), new dashboard::panel::weather(), new dashboard::panel::wifi(), }; + size_t PANELS_LENGTH = sizeof(PANELS)/sizeof(PANELS[0]); // OVERLAY @@ -49,7 +51,8 @@ const char* IMAGE_LOCATIONS[] = { //plex "plex_background.jpg", - + + //homeassistant }; size_t IMAGE_LOCATIONS_LENGTH = sizeof(IMAGE_LOCATIONS)/sizeof(IMAGE_LOCATIONS[0]); @@ -76,6 +79,7 @@ const FONT_SIZE_STRING CONST_STRINGS[] = { { "Weather", { "Roboto_Mono/RobotoMono-Medium.ttf", 50 } }, { "Wireless", { "Roboto_Mono/RobotoMono-Medium.ttf", 50 } }, { "Plex", { "Roboto_Mono/RobotoMono-Medium.ttf", 50} }, + { "Homeassistant", { "Roboto_Mono/RobotoMono-Medium.ttf", 50} }, //sample panel //{ "Sample Panel", { "Roboto_Mono/RobotoMono-Medium.ttf", 50} }, @@ -101,5 +105,7 @@ const FONT_SIZE_STRING CONST_STRINGS[] = { { "Playing", {"Roboto_Mono/RobotoMono-Medium.ttf", 28} }, { "Top Users", {"Roboto_Mono/RobotoMono-Medium.ttf", 50} }, + //Home assistant + { "Home", {"Roboto_Mono/RobotoMono-Medium.ttf", 50} }, }; size_t CONST_STRINGS_LENGTH = sizeof(CONST_STRINGS)/sizeof(CONST_STRINGS[0]); diff --git a/src/config.def.hpp b/src/config.def.hpp index 4f17fd0..ad61de8 100644 --- a/src/config.def.hpp +++ b/src/config.def.hpp @@ -79,6 +79,7 @@ constexpr int IMG_FLAGS = 0 #include "panel/def_overlay.hpp" #include "panel/wifi.hpp" #include "panel/plex.hpp" +#include "panel/homeassistant.hpp" //uncomment this to use the sample panel //#include "panel/sample_panel.hpp" extern dashboard::panel::panel* PANELS[]; diff --git a/src/panel/homeassistant.cpp b/src/panel/homeassistant.cpp new file mode 100644 index 0000000..b35e2ae --- /dev/null +++ b/src/panel/homeassistant.cpp @@ -0,0 +1,225 @@ +/////////////////////////////////////////////////////////////////////////////// +// Tyler Perkins +// 14-1-22 +// homeassistant panel +// + +#include "homeassistant.hpp" +#include "homeassistant_config.hpp" + +using namespace dashboard::panel; + +/////////////////////////////////////////////////////////////////////////////// +// Constructors /////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// + +homeassistant::homeassistant(){ + std::cerr << "HOMEASSISTANT CONSTRUCTOR\n"; + _time_on_screen = HOMEASSISTANT_DEFAULT_ON_SCREEN_TIME; + _update_interval = std::chrono::milliseconds{HOMEASSISTANT_UPDATE_INTERVAL}; + _texture = nullptr; + _title = HOMEASSISTANT_TITLE; + + std::string api_string = "Authorization: Bearer "; + api_string += HOMEASSISTANT_APIKEY; + + headers = NULL; + headers = curl_slist_append(headers, api_string.c_str()); + headers = curl_slist_append(headers, "Content-Type: application/json"); + + api_string.clear(); + + api_curl = curl_easy_init(); + curl_easy_setopt(api_curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(api_curl, CURLOPT_URL, HOMEASSISTANT_URL); + curl_easy_setopt(api_curl, CURLOPT_WRITEFUNCTION, + dashboard::panel::homeassistant::curl_callback); + curl_easy_setopt(api_curl, CURLOPT_WRITEDATA, &json_string); + //Write request string in update function +} + +homeassistant::~homeassistant(){ + std::cerr << "HOMEASSISTANT DECONSTRUCTOR\n"; + if(_texture != nullptr){ + SDL_DestroyTexture(_texture); + } + if(api_curl != nullptr){ + curl_easy_cleanup(api_curl); + } +} + + +/////////////////////////////////////////////////////////////////////////////// +// Draw function ////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// + +void homeassistant::draw(){ + //create the texture if this is the first time running draw + if(_texture == nullptr){ + initTexture(); + update(); + update_texture(); + } + + //check if its time to update + if((std::chrono::high_resolution_clock::now() - _last_update) + > _update_interval){ + update(); + + update_texture(); + } + + SDL_RenderCopy(board::getRenderer(), _texture, NULL, NULL); +} + +/////////////////////////////////////////////////////////////////////////////// +// Helper functions /////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// + +/////////////////////////////////////// +// Update the information of the obj +// This DOES NOT update the display +void homeassistant::update(){ + std::cerr << "HOMEASSISTANT::UPDATE\n"; + _last_update = std::chrono::high_resolution_clock::now(); + + //perform request + curl_easy_perform(api_curl); + + std::cerr << json_string << "\n"; + + //parse the result + json_doc.Parse(json_string.c_str()); + + //update internal state + for(rapidjson::SizeType i = 0; i < json_doc.Size(); i++){ + + //if is a person entry + if(strstr(json_doc[i]["entity_id"].GetString(), "person.") != NULL){ + //get the last changed date and convert to a tm + std::string format_str = json_doc[i]["last_changed"].GetString(); + std::time_t t = datestringToTm(format_str); + std::stringstream ss; + ss << std::put_time(std::localtime(&t), "%b %e, %I:%M %p"); + + struct home_entry entry = { + json_doc[i]["attributes"]["friendly_name"].GetString(), + strcmp(json_doc[i]["state"].GetString(), "home") == 0 ? true : false, + ss.str(), + }; + + home_entries.push_back(entry); + } + + //TODO other entries from general home assistant? + + + + } + json_string.clear(); +} + +/////////////////////////////////////// +// Update the texture that is being +// displayed, based on data grabed from json +void homeassistant::update_texture(){ + std::cerr << "HOMEASSISTANT::UPDATE_TEXTURE\n"; + + SDL_Rect tgt; + + SDL_SetRenderTarget(board::getRenderer(), _texture); + SDL_RenderClear(board::getRenderer()); + + constexpr int GAP_SIZE = 10; + + //draw each name and time last seen + { + for(size_t i = 0; i < home_entries.size(); ++i){ + tgt.x = (SCREEN_WIDTH / (2 * home_entries.size())) + + i * (SCREEN_WIDTH / home_entries.size()); + tgt.y = DEF_OVERLAY_BAR_HEIGHT + GAP_SIZE; + TTF_SizeText(board::getFont( {"Roboto_Mono/RobotoMono-Medium.ttf", 50} ), + home_entries[i].name.c_str(), + &tgt.w, &tgt.h); + + tgt.x -= tgt.w / 2; + + SDL_RenderCopy(board::getRenderer(), + board::getString(home_entries[i].name, + { "Roboto_Mono/RobotoMono-Medium.ttf", 50 }), + NULL, &tgt); + + //down a new line + tgt.y += tgt.h + GAP_SIZE; + tgt.x += tgt.w / 2; + + std::string home = ""; + + //if is home + if(home_entries[i].home){ + home = "Home"; + } else { + home = home_entries[i].time; + } + + TTF_SizeText(board::getFont( {"Roboto_Mono/RobotoMono-Medium.ttf", 50} ), + home.c_str(), + &tgt.w, &tgt.h); + + tgt.x -= tgt.w / 2; + + SDL_RenderCopy(board::getRenderer(), + board::getString(home, + { "Roboto_Mono/RobotoMono-Medium.ttf", 50 }), + NULL, &tgt); + } + } + + + home_entries.clear(); + SDL_SetRenderTarget(board::getRenderer(), NULL); +} + +/////////////////////////////////////// +// Lazy load the texture object +// This is to make sure that all of SDL +// is init before we attempt to draw anything +void homeassistant::initTexture(){ + if(_texture == nullptr){ + _texture = SDL_CreateTexture(board::getRenderer(), + SDL_PIXELFORMAT_RGBA8888, + SDL_TEXTUREACCESS_TARGET, + SCREEN_WIDTH, SCREEN_HEIGHT); + + SDL_SetTextureBlendMode(_texture, SDL_BLENDMODE_BLEND); + } +} + +/////////////////////////////////////// +// Convert the homeassistant string +// format to a tm object +std::time_t homeassistant::datestringToTm(const std::string& date_string){ + int year = 0, month = 0, day = 0, hour = 0, minuite = 0, second = 0; + + std::tm tm_tmp{}; + + if(sscanf(date_string.c_str(), "%4d-%2d-%2dT%2d:%2d:%2d", + &year, &month, &day, &hour, &minuite, &second) == 6){ + tm_tmp.tm_year = year - 1900; + tm_tmp.tm_mon = month - 1; + tm_tmp.tm_mday = day; + tm_tmp.tm_hour = hour; + tm_tmp.tm_min = minuite; + tm_tmp.tm_sec = second; + } + + return std::mktime(&tm_tmp); +} + +/////////////////////////////////////// +// Curl callback function +size_t dashboard::panel::homeassistant::curl_callback(void* contents, size_t size, + size_t nmemb, void* userp){ + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; +} diff --git a/src/panel/homeassistant.hpp b/src/panel/homeassistant.hpp new file mode 100644 index 0000000..29fce36 --- /dev/null +++ b/src/panel/homeassistant.hpp @@ -0,0 +1,59 @@ +/////////////////////////////////////////////////////////////////////////////// +// Tyler Perkins +// 14-1-22 +// Home assistant panel +// + +#pragma once + +#include "panel.hpp" + +#include +#include +#include + +#include +#include "../util/rapidjson/document.h" + +#include +#include +#include +#include +#include +#include + +namespace dashboard::panel { + class homeassistant : public panel { + public: + homeassistant(); + ~homeassistant(); + + static size_t curl_callback(void*, size_t, size_t, void*); + + void draw(); + private: + void update(); + void update_texture(); + void initTexture(); + + std::time_t datestringToTm(const std::string&); + + struct home_entry { + std::string name; + bool home; + std::string time; + }; + + std::vector home_entries; + + CURL* api_curl; + struct curl_slist *headers; + std::string json_string; + rapidjson::Document json_doc; + + std::chrono::time_point _last_update; + std::chrono::milliseconds _update_interval; + }; +} + +#include "../board.hpp" diff --git a/src/panel/homeassistant_config.hpp b/src/panel/homeassistant_config.hpp new file mode 100644 index 0000000..0500411 --- /dev/null +++ b/src/panel/homeassistant_config.hpp @@ -0,0 +1,31 @@ +/////////////////////////////////////////////////////////////////////////////// +// Tyler Perkins +// 14-1-22 +// homeassistant configuration +// + +#pragma once + +#include "def_overlay_config.hpp" + +namespace dashboard::panel { + //This will be displayed at the top left on the status bar. Set to a blank + //string to not show anything + constexpr char HOMEASSISTANT_TITLE[] = "Home assistant"; + + //API key, you can get this in homeassistant under your profile + constexpr char HOMEASSISTANT_APIKEY[] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIxYTNhNDhhYTBlZDM0MjkyOTk5OWZhNGVjZGRjMGUwMCIsImlhdCI6MTY0MjE4NDU0MCwiZXhwIjoxOTU3NTQ0NTQwfQ.Zdy7-ZwX0HuwhmUGeLhF5XCluotsKmpDqmi5o-hQZCc"; + + //This API endpoint. Sub the IP with the IP of the homeassistant server on + //your network. Be sure to enable the RESTful API in homeassistant's + //configuration.yaml + constexpr char HOMEASSISTANT_URL[] = "http://192.168.1.104:8123/api/states"; + + //Default time the slide is shown on screen, in ms + //Default 15s + constexpr size_t HOMEASSISTANT_DEFAULT_ON_SCREEN_TIME = 15000; + + //How long should we wait between updates? in ms + //Default 15 + constexpr size_t HOMEASSISTANT_UPDATE_INTERVAL = 15000; +}