Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion html/portal.css
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ label {
margin-bottom: 0.5rem;
font-weight: 600;
}
input[type='text'], input[type='password'] {
input[type='text'], input[type='password'], input[type='url'] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
Expand Down
22 changes: 22 additions & 0 deletions html/portal.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,31 @@ <h2>PostHog API configuration</h2>
</form>
</div>

<h2>Home Assistant</h2>

<div class="config-section">
<form id="home-assistant-form" onsubmit="return saveHomeAssistantConfig()">
<div class="form-group
<label for="haUrl">Home Assistant URL</label>
<input type="url" name="haUrl" id="haUrl" placeholder="http://homeassistant.local:8123" required>
<p class="tip">This is the URL of your Home Assistant instance.</p>
</div>
<div class="form-group
<label for="haToken">Long-lived access token</label>
<input type="text" name="haApiKey" id="haApiKey" required>
<p class="tip">Create a <a href="https://www.home-assistant.io/docs/authentication/long-lived-access-token/" target="_blank">long-lived access token</a> in your Home Assistant user profile.</p>
</div>
<div class="button-container">
<button type="submit">Save Home Assistant configuration</button>
</div>
</form>
</div>


<h2>Add cards</h2>

<div class="config-section">
<h3>Available cards</h3>
<div id="available-cards-list">
<!-- Will be populated by JavaScript -->
<p>Loading available card types...</p>
Expand Down
83 changes: 82 additions & 1 deletion html/portal.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,68 @@ function saveDeviceConfig() {
return false;
}

// Handle Home Assistant config form submission
function saveHomeAssistantConfig() {
const form = document.getElementById('home-assistant-form');
const formData = new FormData(form);
const globalActionStatusEl = document.getElementById('global-action-status');

fetch('/save-ha-config', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data && data.success) {
console.log("Home Assistant config saved successfully");
if (globalActionStatusEl) {
globalActionStatusEl.textContent = "Home Assistant configuration saved successfully.";
globalActionStatusEl.className = 'status-message success';
globalActionStatusEl.style.display = 'block';
setTimeout(() => {
if (globalActionStatusEl.textContent === "Home Assistant configuration saved successfully.") {
globalActionStatusEl.style.display = 'none';
globalActionStatusEl.textContent = '';
globalActionStatusEl.className = 'status-message';
}
}, 5000);
}
} else {
const errorMessage = (data && data.message) ? data.message : "Failed to save Home Assistant configuration.";
console.error("Failed to save Home Assistant config:", errorMessage);
if (globalActionStatusEl) {
globalActionStatusEl.textContent = errorMessage;
globalActionStatusEl.className = 'status-message error';
globalActionStatusEl.style.display = 'block';
setTimeout(() => {
if (globalActionStatusEl.className.includes('error')) {
globalActionStatusEl.style.display = 'none';
globalActionStatusEl.textContent = '';
globalActionStatusEl.className = 'status-message';
}
}, 7000);
}
}
})
.catch(() => {
console.error("Communication error saving Home Assistant config.");
if (globalActionStatusEl) {
globalActionStatusEl.textContent = "Communication error saving Home Assistant config.";
globalActionStatusEl.className = 'status-message error';
globalActionStatusEl.style.display = 'block';
setTimeout(() => {
if (globalActionStatusEl.className.includes('error')) {
globalActionStatusEl.style.display = 'none';
globalActionStatusEl.textContent = '';
globalActionStatusEl.className = 'status-message';
}
}, 7000);
}
});

return false;
}

// Toggle API key visibility
function toggleApiKeyVisibility() {
const apiKeyInput = document.getElementById('apiKey');
Expand Down Expand Up @@ -810,6 +872,11 @@ function pollApiStatus() {
_updateDeviceConfigUI(data.device_config);
}

// 3a. Update Home Assistant Config Info
if (data.ha_config && document.getElementById('haUrl').value === '') {
_updateHomeAssistantConfigUI(data.ha_config);
}

// 4. Update Insights List (legacy - remove if cards are working)
if (data.insights) {
_updateInsightsListUI(data.insights);
Expand Down Expand Up @@ -862,6 +929,20 @@ function _updateDeviceConfigUI(config) {
}
}

// Load current Home Assistant configuration - UI update part will be in pollApiStatus
let initialHaConfigLoaded = false;
function _updateHomeAssistantConfigUI(config) {
if (!initialHaConfigLoaded) {
if (config.ha_url !== undefined) {
document.getElementById('haUrl').value = config.ha_url;
}
if (config.ha_api_key_display !== undefined) {
document.getElementById('haApiKey').value = config.ha_api_key_display;
}
initialHaConfigLoaded = true;
}
}

// Initialize page
document.addEventListener('DOMContentLoaded', function() {
const hash = window.location.hash.substr(1);
Expand Down Expand Up @@ -895,7 +976,7 @@ document.addEventListener('DOMContentLoaded', function() {
setTimeout(() => {
loadConfiguredCards();
}, 500);
}, 1000);
}, 10000);
});

// Enum for OtaManager::UpdateStatus::State (mirror from C++)
Expand Down
4 changes: 3 additions & 1 deletion include/EventQueue.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ enum class EventType {
INSIGHT_ADDED,
INSIGHT_DELETED,
INSIGHT_DATA_RECEIVED,
HA_ENTITY_STATE_RECEIVED,
HA_SERVICE_CALLED,
WIFI_CREDENTIALS_FOUND,
NEED_WIFI_CREDENTIALS,
WIFI_CONNECTING,
Expand Down Expand Up @@ -138,4 +140,4 @@ class EventQueue {
* @brief Stop the event processing task
*/
void end();
};
};
107 changes: 105 additions & 2 deletions include/html_portal.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ static const char PORTAL_HTML[] PROGMEM = "<!DOCTYPE html>\n"
" margin-bottom: 0.5rem; \n"
" font-weight: 600;\n"
"}\n"
"input[type='text'], input[type='password'] { \n"
"input[type='text'], input[type='password'], input[type='url'] { \n"
" width: 100%; \n"
" padding: 8px; \n"
" border: 1px solid #ddd; \n"
Expand Down Expand Up @@ -516,6 +516,68 @@ static const char PORTAL_HTML[] PROGMEM = "<!DOCTYPE html>\n"
" return false;\n"
"}\n"
"\n"
"// Handle Home Assistant config form submission\n"
"function saveHomeAssistantConfig() {\n"
" const form = document.getElementById('home-assistant-form');\n"
" const formData = new FormData(form);\n"
" const globalActionStatusEl = document.getElementById('global-action-status');\n"
" \n"
" fetch('/save-ha-config', { \n"
" method: 'POST',\n"
" body: formData\n"
" })\n"
" .then(response => response.json())\n"
" .then(data => {\n"
" if (data && data.success) {\n"
" console.log(\"Home Assistant config saved successfully\");\n"
" if (globalActionStatusEl) {\n"
" globalActionStatusEl.textContent = \"Home Assistant configuration saved successfully.\";\n"
" globalActionStatusEl.className = 'status-message success';\n"
" globalActionStatusEl.style.display = 'block';\n"
" setTimeout(() => {\n"
" if (globalActionStatusEl.textContent === \"Home Assistant configuration saved successfully.\") {\n"
" globalActionStatusEl.style.display = 'none';\n"
" globalActionStatusEl.textContent = '';\n"
" globalActionStatusEl.className = 'status-message';\n"
" }\n"
" }, 5000);\n"
" }\n"
" } else {\n"
" const errorMessage = (data && data.message) ? data.message : \"Failed to save Home Assistant configuration.\";\n"
" console.error(\"Failed to save Home Assistant config:\", errorMessage);\n"
" if (globalActionStatusEl) {\n"
" globalActionStatusEl.textContent = errorMessage;\n"
" globalActionStatusEl.className = 'status-message error';\n"
" globalActionStatusEl.style.display = 'block';\n"
" setTimeout(() => {\n"
" if (globalActionStatusEl.className.includes('error')) {\n"
" globalActionStatusEl.style.display = 'none';\n"
" globalActionStatusEl.textContent = '';\n"
" globalActionStatusEl.className = 'status-message';\n"
" }\n"
" }, 7000);\n"
" }\n"
" }\n"
" })\n"
" .catch(() => {\n"
" console.error(\"Communication error saving Home Assistant config.\");\n"
" if (globalActionStatusEl) {\n"
" globalActionStatusEl.textContent = \"Communication error saving Home Assistant config.\";\n"
" globalActionStatusEl.className = 'status-message error';\n"
" globalActionStatusEl.style.display = 'block';\n"
" setTimeout(() => {\n"
" if (globalActionStatusEl.className.includes('error')) {\n"
" globalActionStatusEl.style.display = 'none';\n"
" globalActionStatusEl.textContent = '';\n"
" globalActionStatusEl.className = 'status-message';\n"
" }\n"
" }, 7000);\n"
" }\n"
" });\n"
" \n"
" return false;\n"
"}\n"
"\n"
"// Toggle API key visibility\n"
"function toggleApiKeyVisibility() {\n"
" const apiKeyInput = document.getElementById('apiKey');\n"
Expand Down Expand Up @@ -1191,6 +1253,11 @@ static const char PORTAL_HTML[] PROGMEM = "<!DOCTYPE html>\n"
" _updateDeviceConfigUI(data.device_config);\n"
" }\n"
"\n"
" // 3a. Update Home Assistant Config Info\n"
" if (data.ha_config && document.getElementById('haUrl').value === '') {\n"
" _updateHomeAssistantConfigUI(data.ha_config);\n"
" }\n"
"\n"
" // 4. Update Insights List (legacy - remove if cards are working)\n"
" if (data.insights) {\n"
" _updateInsightsListUI(data.insights);\n"
Expand Down Expand Up @@ -1243,6 +1310,20 @@ static const char PORTAL_HTML[] PROGMEM = "<!DOCTYPE html>\n"
" }\n"
"}\n"
"\n"
"// Load current Home Assistant configuration - UI update part will be in pollApiStatus\n"
"let initialHaConfigLoaded = false;\n"
"function _updateHomeAssistantConfigUI(config) {\n"
" if (!initialHaConfigLoaded) {\n"
" if (config.ha_url !== undefined) {\n"
" document.getElementById('haUrl').value = config.ha_url;\n"
" }\n"
" if (config.ha_api_key_display !== undefined) { \n"
" document.getElementById('haApiKey').value = config.ha_api_key_display;\n"
" }\n"
" initialHaConfigLoaded = true;\n"
" }\n"
"}\n"
"\n"
"// Initialize page\n"
"document.addEventListener('DOMContentLoaded', function() {\n"
" const hash = window.location.hash.substr(1);\n"
Expand Down Expand Up @@ -1276,7 +1357,7 @@ static const char PORTAL_HTML[] PROGMEM = "<!DOCTYPE html>\n"
" setTimeout(() => {\n"
" loadConfiguredCards();\n"
" }, 500);\n"
" }, 1000);\n"
" }, 10000);\n"
"});\n"
"\n"
"// Enum for OtaManager::UpdateStatus::State (mirror from C++)\n"
Expand Down Expand Up @@ -1502,9 +1583,31 @@ static const char PORTAL_HTML[] PROGMEM = "<!DOCTYPE html>\n"
" </form>\n"
" </div>\n"
"\n"
" <h2>Home Assistant</h2>\n"
"\n"
" <div class=\"config-section\">\n"
" <form id=\"home-assistant-form\" onsubmit=\"return saveHomeAssistantConfig()\">\n"
" <div class=\"form-group\n"
" <label for=\"haUrl\">Home Assistant URL</label>\n"
" <input type=\"url\" name=\"haUrl\" id=\"haUrl\" placeholder=\"http://homeassistant.local:8123\" required>\n"
" <p class=\"tip\">This is the URL of your Home Assistant instance.</p>\n"
" </div>\n"
" <div class=\"form-group\n"
" <label for=\"haToken\">Long-lived access token</label>\n"
" <input type=\"text\" name=\"haApiKey\" id=\"haApiKey\" required>\n"
" <p class=\"tip\">Create a <a href=\"https://www.home-assistant.io/docs/authentication/long-lived-access-token/\" target=\"_blank\">long-lived access token</a> in your Home Assistant user profile.</p>\n"
" </div>\n"
" <div class=\"button-container\">\n"
" <button type=\"submit\">Save Home Assistant configuration</button>\n"
" </div>\n"
" </form>\n"
" </div>\n"
"\n"
"\n"
" <h2>Add cards</h2>\n"
"\n"
" <div class=\"config-section\">\n"
" <h3>Available cards</h3>\n"
" <div id=\"available-cards-list\">\n"
" <!-- Will be populated by JavaScript -->\n"
" <p>Loading available card types...</p>\n"
Expand Down
45 changes: 45 additions & 0 deletions src/ConfigManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,4 +324,49 @@ bool ConfigManager::saveCardConfigs(const std::vector<CardConfig>& configs) {
}

return true;
}

// Home Assistant Configuration Methods
bool ConfigManager::setHomeAssistantUrl(const String& url) {
if (url.length() > MAX_HA_URL_LENGTH) {
Serial.printf("Home Assistant URL too long: %u > %u\n", url.length(), MAX_HA_URL_LENGTH);
return false;
}

_preferences.putString(_haUrlKey, url);

// Commit changes
commit();

return true;
}

String ConfigManager::getHomeAssistantUrl() {
return _preferences.getString(_haUrlKey, "");
}

bool ConfigManager::setHomeAssistantApiKey(const String& apiKey) {
if (apiKey.length() > MAX_HA_API_KEY_LENGTH) {
Serial.printf("Home Assistant API key too long: %u > %u\n", apiKey.length(), MAX_HA_API_KEY_LENGTH);
return false;
}

_preferences.putString(_haApiKeyKey, apiKey);

// Commit changes
commit();

return true;
}

String ConfigManager::getHomeAssistantApiKey() {
return _preferences.getString(_haApiKeyKey, "");
}

void ConfigManager::clearHomeAssistantConfig() {
_preferences.remove(_haUrlKey);
_preferences.remove(_haApiKeyKey);

// Commit changes
commit();
}
Loading