From 328a1467090e7c2d6c89bb800f7b9e2d33d153a6 Mon Sep 17 00:00:00 2001 From: joshuaterk Date: Mon, 9 Mar 2026 10:28:02 -0400 Subject: [PATCH 1/5] Add DTE Rates for Home Assistant to portfolio --- frontend/src/pages/Portfolio.tsx | 34 +++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/Portfolio.tsx b/frontend/src/pages/Portfolio.tsx index 7e32098..17db358 100644 --- a/frontend/src/pages/Portfolio.tsx +++ b/frontend/src/pages/Portfolio.tsx @@ -2,6 +2,13 @@ import { motion } from 'framer-motion'; const Portfolio = () => { const projects = [ + { + title: 'DTE Rates for Home Assistant', + description: 'A Home Assistant custom integration that pulls the official DTE residential electric rate card PDF, parses rates dynamically, and exposes import/export price entities that track time-of-day and season.', + tech: ['Python', 'Home Assistant', 'HACS', 'Lovelace'], + gradient: 'from-yellow-500 to-orange-500', + link: 'https://github.com/javaDevJT/DTE-Rates-for-Home-Assistant' + }, { title: 'Interactive Dashboard', description: 'A sleek dashboard for monitoring automotive performance data with real-time visualizations.', @@ -81,13 +88,26 @@ const Portfolio = () => { ))} - - View Project - + {project.link ? ( + + View Project + + ) : ( + + View Project + + )} ))} From 16df46d480abb467d558470dc330cf38a938c93a Mon Sep 17 00:00:00 2001 From: joshuaterk Date: Mon, 9 Mar 2026 10:32:50 -0400 Subject: [PATCH 2/5] Add DTE Rates for Home Assistant portfolio entry --- .../portfolio/DTE-Rates-for-Home-Assistant.md | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/main/resources/directories/portfolio/DTE-Rates-for-Home-Assistant.md diff --git a/src/main/resources/directories/portfolio/DTE-Rates-for-Home-Assistant.md b/src/main/resources/directories/portfolio/DTE-Rates-for-Home-Assistant.md new file mode 100644 index 0000000..b9c484d --- /dev/null +++ b/src/main/resources/directories/portfolio/DTE-Rates-for-Home-Assistant.md @@ -0,0 +1,71 @@ +title: DTE-Rates-for-Home-Assistant +type: Home Automation +year: 2025 +technologies: Python, Home Assistant, HACS, Lovelace, PDF Parsing, Custom Integration + +# DTE Rates for Home Assistant + +## Real-Time DTE Electric Rates in Your Smart Home + +DTE Rates for Home Assistant is a custom integration that pulls the official DTE residential electric rate card PDF, parses plans and time-of-use windows dynamically, and exposes import/export price sensor entities that update based on time-of-day and season. + +## Why It Matters +- Eliminates manually hardcoding rate schedules that change with every DTE tariff update. +- Enables energy automations and dashboards to react to live pricing (off-peak charging, export decisions, cost tracking). +- Integrates directly with the Home Assistant Energy Dashboard as a `USD/kWh` price entity. +- HACS-installable — no manual file copying required for most users. + +## What It Exposes + +| Entity | Type | Description | +|---|---|---| +| `sensor.dte_import_rate` | monetary | Current import price (USD/kWh) | +| `sensor.dte_export_rate` | monetary | Current export/generation price (USD/kWh) | +| `sensor.dte_current_rate_name` | string | Active period name (e.g. `Winter Off-Peak`) | +| `sensor.dte_rate_schedule` | structured | Full parsed schedule with attributes for dashboards | + +## Architecture +``` +DTE Rate Card PDF (dteenergy.com) + ↓ (weekly refresh) +PDF Parser → Plan/Period/Window/Component extraction + ↓ +Home Assistant Config Flow (rate plan + net metering selection) + ↓ +Sensor entities (updated on time boundary events) + ↓ +Energy Dashboard / Automations / Lovelace Cards +``` + +## Key Features +- **Dynamic PDF parsing:** Rates are parsed live from the official DTE PDF — no hardcoded values that go stale. +- **Time-of-use awareness:** Sensors reflect the correct rate for the current time window and season automatically. +- **Net metering support:** Export calculation adjusts based on whether net metering is enabled. +- **Warning system:** Persistent notifications and `warning` attributes fire when a previously selected rate plan disappears from the latest card. +- **Custom Lovelace card:** Ships a `custom:dte-rates-card` for at-a-glance rate display on any dashboard. +- **Service calls:** Force refresh, inspect the parsed schedule, and generate card YAML examples via HA services. + +## Custom Lovelace Card +```yaml +type: custom:dte-rates-card +title: DTE Residential Rates +import_entity: sensor.dte_import_rate +export_entity: sensor.dte_export_rate +name_entity: sensor.dte_current_rate_name +schedule_entity: sensor.dte_rate_schedule +``` + +## Services + +| Service | Description | +|---|---| +| `dte_rates.refresh_rate_card` | Force re-download and re-parse of the DTE PDF | +| `dte_rates.show_rate_schedule` | Dumps parsed schedule to a persistent notification | +| `dte_rates.show_lovelace_card_example` | Generates card resource + YAML example notification | + +## Status +- **State:** Active, publicly available via HACS. +- **Role:** Full design + implementation (PDF parsing pipeline, HA integration, config flow, Lovelace card). +- **Next Up:** Support for additional DTE rate plans, multi-utility expansion. + +[GitHub](https://github.com/javaDevJT/DTE-Rates-for-Home-Assistant) From 5b6806a84e6b9c9a2c6553082a7edbd635ec1db2 Mon Sep 17 00:00:00 2001 From: joshuaterk Date: Mon, 9 Mar 2026 10:38:24 -0400 Subject: [PATCH 3/5] Fix markdown table rendering: enable TablesExtension and fix unassigned cleanup chain --- .../java/com/jtdev/website/service/ContentService.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/jtdev/website/service/ContentService.java b/src/main/java/com/jtdev/website/service/ContentService.java index 09ed447..4e264ee 100644 --- a/src/main/java/com/jtdev/website/service/ContentService.java +++ b/src/main/java/com/jtdev/website/service/ContentService.java @@ -2,6 +2,7 @@ import com.jtdev.website.model.BlogMetadata; import com.jtdev.website.model.PortfolioMetadata; +import com.vladsch.flexmark.ext.tables.TablesExtension; import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.ast.Node; @@ -72,6 +73,7 @@ public String getMarkdownContent(String path) throws IOException { // Parse markdown and convert to ASCII-friendly format MutableDataSet options = new MutableDataSet(); + options.set(Parser.EXTENSIONS, java.util.Arrays.asList(TablesExtension.create())); Parser parser = Parser.builder(options).build(); HtmlRenderer renderer = HtmlRenderer.builder(options).build(); @@ -150,8 +152,8 @@ private String convertHtmlToAscii(String html, String dir) { // Handle images without src text = text.replaceAll("]*>", "[Image]"); - text + text = text // Remove any remaining HTML tags .replaceAll("<[^>]+>", "") @@ -162,8 +164,7 @@ private String convertHtmlToAscii(String html, String dir) { .replaceAll("\\n{3,}", "\n\n") .trim(); - // The formatting is already handled in the regex replacements above - return text.trim(); + return text; } private String formatNestedLists(String html) { From c3eb510bf9ad84cfa951b91cf6a56c01997085c9 Mon Sep 17 00:00:00 2001 From: joshuaterk Date: Mon, 9 Mar 2026 10:49:45 -0400 Subject: [PATCH 4/5] Rewrite table renderer: proper column-width-aware ASCII box drawing --- .../jtdev/website/service/ContentService.java | 101 +++++++++++++++--- 1 file changed, 89 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/jtdev/website/service/ContentService.java b/src/main/java/com/jtdev/website/service/ContentService.java index 4e264ee..b3c0b50 100644 --- a/src/main/java/com/jtdev/website/service/ContentService.java +++ b/src/main/java/com/jtdev/website/service/ContentService.java @@ -90,9 +90,12 @@ public String getMarkdownContent(String path) throws IOException { private String convertHtmlToAscii(String html, String dir) { // First pass: Convert headers with dynamic borders html = convertDynamicHeaders(html); - + html = formatNestedLists(html); + // Convert tables before the general regex pass + html = convertTablesToAscii(html); + // Convert HTML elements to ASCII formatting String text = html // Headers already converted above @@ -126,17 +129,7 @@ private String convertHtmlToAscii(String html, String dir) { // Bold and italic .replaceAll("]*>([^<]*)", "**$1**") - .replaceAll("]*>([^<]*)", "*$1*") - - // Tables (simplified ASCII representation) - .replaceAll("]*>", "\n┌") - .replaceAll("", "┘\n") - .replaceAll("]*>", "") - .replaceAll("", "\n├") - .replaceAll("]*>", "─ ") - .replaceAll("", " ─") - .replaceAll("]*>", "│ ") - .replaceAll("", " │"); + .replaceAll("]*>([^<]*)", "*$1*"); // Handle images with ASCII art Pattern imgPattern = Pattern.compile("]*src=\"([^\"]*)\"[^>]*>"); @@ -251,6 +244,90 @@ int nextIndex() { } } + private String convertTablesToAscii(String html) { + Pattern tablePattern = Pattern.compile("]*>(.*?)", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); + Matcher tableMatcher = tablePattern.matcher(html); + StringBuffer sb = new StringBuffer(); + while (tableMatcher.find()) { + String asciiTable = renderTableAsAscii(tableMatcher.group(1)); + tableMatcher.appendReplacement(sb, Matcher.quoteReplacement(asciiTable)); + } + tableMatcher.appendTail(sb); + return sb.toString(); + } + + private String renderTableAsAscii(String tableHtml) { + List> rows = new ArrayList<>(); + + Pattern rowPattern = Pattern.compile("]*>(.*?)", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); + Matcher rowMatcher = rowPattern.matcher(tableHtml); + while (rowMatcher.find()) { + List cells = new ArrayList<>(); + Pattern cellPattern = Pattern.compile("]*>(.*?)", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); + Matcher cellMatcher = cellPattern.matcher(rowMatcher.group(1)); + while (cellMatcher.find()) { + String cell = cellMatcher.group(1) + .replaceAll("<[^>]+>", "") + .replaceAll("&", "&").replaceAll("<", "<") + .replaceAll(">", ">").replaceAll(""", "\"") + .replaceAll("'", "'").replaceAll(" ", " ") + .trim(); + cells.add(cell); + } + if (!cells.isEmpty()) rows.add(cells); + } + + if (rows.isEmpty()) return ""; + + int numCols = rows.stream().mapToInt(List::size).max().orElse(0); + int[] colWidths = new int[numCols]; + for (List row : rows) { + for (int i = 0; i < row.size(); i++) { + colWidths[i] = Math.max(colWidths[i], row.get(i).length()); + } + } + + StringBuilder ascii = new StringBuilder("\n"); + + // Top border + ascii.append("┌"); + for (int i = 0; i < numCols; i++) { + ascii.append("─".repeat(colWidths[i] + 2)); + ascii.append(i < numCols - 1 ? "┬" : "┐"); + } + ascii.append("\n"); + + for (int r = 0; r < rows.size(); r++) { + List row = rows.get(r); + ascii.append("│"); + for (int i = 0; i < numCols; i++) { + String cell = i < row.size() ? row.get(i) : ""; + ascii.append(" ").append(cell).append(" ".repeat(colWidths[i] - cell.length() + 1)).append("│"); + } + ascii.append("\n"); + + if (r == 0 && rows.size() > 1) { + // Header separator + ascii.append("├"); + for (int i = 0; i < numCols; i++) { + ascii.append("─".repeat(colWidths[i] + 2)); + ascii.append(i < numCols - 1 ? "┼" : "┤"); + } + ascii.append("\n"); + } else if (r == rows.size() - 1) { + // Bottom border + ascii.append("└"); + for (int i = 0; i < numCols; i++) { + ascii.append("─".repeat(colWidths[i] + 2)); + ascii.append(i < numCols - 1 ? "┴" : "┘"); + } + ascii.append("\n"); + } + } + + return ascii.toString(); + } + private String convertDynamicHeaders(String html) { // Convert H1 headers with dynamic borders Pattern h1Pattern = Pattern.compile("]*>(.*?)", Pattern.DOTALL); From 40df859aeb2b1d10d23565d624143d3ba49c6d04 Mon Sep 17 00:00:00 2001 From: joshuaterk Date: Mon, 9 Mar 2026 12:17:25 -0400 Subject: [PATCH 5/5] Update --- frontend/node_modules/.package-lock.json | 8 -------- frontend/package-lock.json | 8 -------- .../directories/portfolio/DTE-Rates-for-Home-Assistant.md | 2 +- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/frontend/node_modules/.package-lock.json b/frontend/node_modules/.package-lock.json index 96a5322..f381d31 100644 --- a/frontend/node_modules/.package-lock.json +++ b/frontend/node_modules/.package-lock.json @@ -48,7 +48,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -522,7 +521,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -642,7 +640,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -1342,7 +1339,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1370,7 +1366,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -1411,7 +1406,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -1421,7 +1415,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -1645,7 +1638,6 @@ "integrity": "sha512-C/Naxf8H0pBx1PA4BdpT+c/5wdqI9ILMdwjSMILw7tVIh3JsxzZqdeTLmmdaoh5MYUEOyBnM9K3o0DzoZ/fe+w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ecddae2..cf8ae9f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -72,7 +72,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1465,7 +1464,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1585,7 +1583,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2495,7 +2492,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2523,7 +2519,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2564,7 +2559,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2574,7 +2568,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2798,7 +2791,6 @@ "integrity": "sha512-C/Naxf8H0pBx1PA4BdpT+c/5wdqI9ILMdwjSMILw7tVIh3JsxzZqdeTLmmdaoh5MYUEOyBnM9K3o0DzoZ/fe+w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/src/main/resources/directories/portfolio/DTE-Rates-for-Home-Assistant.md b/src/main/resources/directories/portfolio/DTE-Rates-for-Home-Assistant.md index b9c484d..092d125 100644 --- a/src/main/resources/directories/portfolio/DTE-Rates-for-Home-Assistant.md +++ b/src/main/resources/directories/portfolio/DTE-Rates-for-Home-Assistant.md @@ -1,6 +1,6 @@ title: DTE-Rates-for-Home-Assistant type: Home Automation -year: 2025 +year: 2026 technologies: Python, Home Assistant, HACS, Lovelace, PDF Parsing, Custom Integration # DTE Rates for Home Assistant