-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
1,324 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.vscode |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
## Goodwe Sems History Home Assistant | ||
|
||
Downloads daily import, export and load stats from the sems portal for a given day and imports them into long and short term statistics tables in Home Assistant. | ||
|
||
> __Use at your own risk__. This app was built for me and may cause issues your unique setup. | ||
> __BUG!__ This app nicely imports the data in the date range you specify but every day after that has a weird spike in the 12-1am time slot. | ||
### Requirements | ||
|
||
- Your Sems username, password and plant id | ||
- Home Assistant using sqlite | ||
- Dotnet 6 runtime installed | ||
|
||
### Usage | ||
|
||
> __STOP HOME ASSISTANT BEFORE PROCEEDING__ because if you don't there is a fair chance this'll corrupt your database. | ||
```bash | ||
.\sems-history-importer record \ | ||
--username your-sems-username \ | ||
--password your-sems-password \ | ||
--plant your-sems-plant-id \ | ||
--sqlite path/to/home-assistant_v2.db \ | ||
--start yyyy-MM-dd \ | ||
--end yyyy-MM-dd \ | ||
--timezone hh:mm | ||
``` | ||
|
||
- `start` to `end` is the date range of data you want to pull from Sems, inclusive. | ||
- All Sems HomeKit import/export stats in Home Assistant before and including `end` will be deleted. | ||
|
||
### How it works | ||
|
||
1. Logs into Sems | ||
2. Finds your HomeKit serial number | ||
3. Looks in Home Assistant for import/export sensors with that serial number | ||
4. Downloads HomeKit data for your date range | ||
5. Remove HomeKit import/export statistics in Home Assistant on or before `end` date | ||
6. Import the new data | ||
7. Recalculate _all_ HomeKit import/export sums (including stats outside of your date range) | ||
|
||
### Credits | ||
|
||
This is a spin-off project from [goodwe-sems-home-assistant](https://github.com/TimSoethout/goodwe-sems-home-assistant). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,8 @@ | ||
bin/ | ||
obj/ | ||
obj/ | ||
|
||
# cached sems payloads | ||
dump/ | ||
|
||
# test db | ||
home-assistant_v2.db |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
using Hass; | ||
|
||
namespace Goodwe.Sems.Sinks.HomeAssistant; | ||
|
||
public class Finder | ||
{ | ||
public async Task FindSemsSensors(string semsUsername, string semsPassword, string plantId) | ||
{ | ||
var semsApi = new SemsApi(semsUsername, semsPassword, plantId); | ||
var sensors = await semsApi.GetSensorsAsync(); | ||
System.Console.WriteLine($"[1/1] Finding Sems sensors..."); | ||
foreach (var sensor in sensors) | ||
{ | ||
System.Console.WriteLine($"> [INFO]: found {sensor}."); | ||
} | ||
System.Console.WriteLine("Done"); | ||
} | ||
|
||
public async Task FindHassSensors(FileInfo dbfile) | ||
{ | ||
var hassHelper = new HassHelper(dbfile); | ||
System.Console.WriteLine($"[1/1] Finding Home Assistant sensors..."); | ||
var sensors = await hassHelper.GetSemsSensorsAsync(); | ||
foreach (var sensor in sensors) | ||
{ | ||
System.Console.WriteLine($"> [INFO]: found {sensor.id}:{sensor.statistic_id}"); | ||
} | ||
System.Console.WriteLine("Done"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,132 @@ | ||
// See https://aka.ms/new-console-template for more information | ||
Console.WriteLine("Hello, World!"); | ||
using System.CommandLine; | ||
using Microsoft.Extensions.Configuration; | ||
|
||
namespace Goodwe.Sems.Sinks.HomeAssistant; | ||
|
||
internal class Program | ||
{ | ||
public static async Task<int> Main(string[] args) | ||
{ | ||
var configuration = new ConfigurationBuilder() | ||
.AddUserSecrets<Program>() | ||
.Build(); | ||
|
||
var username = configuration["sems:username"]; | ||
var password = configuration["sems:password"]; | ||
var plantId = configuration["sems:plant"]; | ||
|
||
var sqliteFileOption = new Option<FileInfo?>( | ||
name: "--sqlite", | ||
description: "Path to Home Assistant sqlite db file.", | ||
isDefault: true, | ||
parseArgument: result => | ||
{ | ||
if (result.Tokens.Count == 0) | ||
{ | ||
return new FileInfo("home-assistant_v2.db"); | ||
|
||
} | ||
string? filePath = result.Tokens.Single().Value; | ||
if (!File.Exists(filePath)) | ||
{ | ||
result.ErrorMessage = "File does not exist"; | ||
return null; | ||
} | ||
else | ||
{ | ||
return new FileInfo(filePath); | ||
} | ||
}) | ||
{ IsRequired = true }; | ||
|
||
var startAtOption = new Option<DateTimeOffset>( | ||
aliases: new[] { "--start", "-s" }, | ||
description: "First day of statistics to fetch from Sems, provided in format yyyy-MM-dd.") | ||
{ IsRequired = true }; | ||
|
||
var endAtOption = new Option<DateTimeOffset>( | ||
aliases: new[] { "--end", "-e" }, | ||
description: "Last day of statistics to fetch from Sems, provided in format yyyy-MM-dd.") | ||
{ IsRequired = true }; | ||
|
||
var timezoneOption = new Option<TimeSpan>( | ||
aliases: new[] { "--timezone", "-z" }, | ||
description: "Your Sems & Home Assistant Timezone offset in format ±hh:mm.") | ||
{ IsRequired = true }; | ||
|
||
var semsUsernameOption = new Option<string>( | ||
aliases: new[] { "--username", "-u" }, | ||
description: "Sems portal username.", | ||
getDefaultValue: () => username) | ||
{ IsRequired = true }; | ||
|
||
var semsPasswordOption = new Option<string>( | ||
aliases: new[] { "--password", "-p" }, | ||
description: "Sems portal password.", | ||
getDefaultValue: () => password) | ||
{ IsRequired = true }; | ||
|
||
var semsPlantIdOption = new Option<string>( | ||
aliases: new[] { "--plant", "-i" }, | ||
description: "Sems plant id.", | ||
getDefaultValue: () => plantId) | ||
{ IsRequired = true }; | ||
|
||
var rootCommand = new RootCommand("Import Goodwe Sems HomeKit data into Home Assistant"); | ||
|
||
var recordCommand = new Command("record", "Record Sems statistics in Home Assistant.") | ||
{ | ||
sqliteFileOption, | ||
semsUsernameOption, | ||
semsPasswordOption, | ||
semsPlantIdOption, | ||
startAtOption, | ||
endAtOption, | ||
timezoneOption, | ||
}; | ||
rootCommand.AddCommand(recordCommand); | ||
|
||
var getSemsSensorsCommand = new Command("sensors", "Get Sems sensors.") | ||
{ | ||
semsUsernameOption, | ||
semsPasswordOption, | ||
semsPlantIdOption | ||
}; | ||
|
||
var semsCommand = new Command("sems", "Sems utilities."); | ||
semsCommand.AddCommand(getSemsSensorsCommand); | ||
rootCommand.AddCommand(semsCommand); | ||
|
||
var getHassSensorsCommand = new Command("sensors", "Get Home Assistant sensors.") | ||
{ | ||
sqliteFileOption | ||
}; | ||
|
||
var hassCommand = new Command("hass", "Home Assistatnt utilities."); | ||
hassCommand.AddCommand(getHassSensorsCommand); | ||
rootCommand.AddCommand(hassCommand); | ||
|
||
recordCommand.SetHandler(async (dbfile, semsUsername, semsPassword, semsPlantId, startAt, endAt, timezoneOffset) => | ||
{ | ||
var recorder = new Recorder(); | ||
await recorder.Record(dbfile!, semsUsername, semsPassword, semsPlantId, startAt.ToOffset(timezoneOffset), endAt.ToOffset(timezoneOffset)); | ||
}, | ||
sqliteFileOption, semsUsernameOption, semsPasswordOption, semsPlantIdOption, startAtOption, endAtOption, timezoneOption); | ||
|
||
getSemsSensorsCommand.SetHandler(async (semsUsername, semsPassword, semsPlantId) => | ||
{ | ||
var finder = new Finder(); | ||
await finder.FindSemsSensors(semsUsername, semsPassword, semsPlantId); | ||
}, | ||
semsUsernameOption, semsPasswordOption, semsPlantIdOption); | ||
|
||
getHassSensorsCommand.SetHandler(async (dbFile) => | ||
{ | ||
var finder = new Finder(); | ||
await finder.FindHassSensors(dbFile!); | ||
}, | ||
sqliteFileOption); | ||
|
||
return await rootCommand.InvokeAsync(args); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
using Hass; | ||
|
||
namespace Goodwe.Sems.Sinks.HomeAssistant | ||
{ | ||
public class Recorder | ||
{ | ||
public async Task Record( | ||
FileInfo dbfile, | ||
string semsUsername, | ||
string semsPassword, | ||
string plantId, | ||
DateTimeOffset startDate, | ||
DateTimeOffset endDate) | ||
{ | ||
System.Console.WriteLine($"[1/5] Finding sensors..."); | ||
var semsApi = new SemsApi(semsUsername, semsPassword, plantId); | ||
var semsSensors = await semsApi.GetSensorsAsync(); | ||
|
||
var hassHelper = new HassHelper(dbfile); | ||
var hassSensors = await hassHelper.GetSemsSensorsAsync(); | ||
|
||
var matchingSensors = hassSensors | ||
.ToArray() | ||
.Where(x => semsSensors.Any(y => string.Equals(x.statistic_id, $"sensor.{y}", StringComparison.CurrentCultureIgnoreCase))) | ||
.ToArray(); | ||
|
||
foreach (var item in matchingSensors) | ||
{ | ||
System.Console.WriteLine($"> [INFO]: Found {item.statistic_id}"); | ||
} | ||
|
||
System.Console.WriteLine($"[2/5] Downloading Goodwe Sems HomeKit data from {startDate:yyyy-MM-dd} to {endDate:yyyy-MM-dd}..."); | ||
var fullExtract = new FullExtract(); | ||
foreach (DateTimeOffset day in EachDay(startDate, endDate)) | ||
{ | ||
var dayExtract = await semsApi.GetStatsByDateAsync(day); | ||
fullExtract.Import.Add(day, dayExtract.Import); | ||
fullExtract.Export.Add(day, dayExtract.Export); | ||
fullExtract.Load.Add(day, dayExtract.Load); | ||
System.Console.WriteLine($"> [INFO]: Downloaded {day:yyyy-MM-dd}"); | ||
} | ||
|
||
System.Console.WriteLine($"[3/5] Removing statistics on and before {endDate:yyyy-MM-dd}..."); | ||
await hassHelper.ClearStatsPriorTooAsync(endDate.AddDays(1), matchingSensors); | ||
|
||
System.Console.WriteLine("[4/5] Importing statistics..."); | ||
await hassHelper.RecordHistoricalStatsAsync(fullExtract, matchingSensors); | ||
|
||
System.Console.WriteLine("[5/5] Recalculating statistic sums..."); | ||
await hassHelper.RecalculateSumsAsync(matchingSensors, startDate.Offset); | ||
|
||
System.Console.WriteLine("Done"); | ||
} | ||
|
||
private IEnumerable<DateTimeOffset> EachDay(DateTimeOffset from, DateTimeOffset thru) | ||
{ | ||
for (var day = from.Date; day.Date <= thru.Date; day = day.AddDays(1)) | ||
yield return day; | ||
} | ||
} | ||
} |
Oops, something went wrong.