Skip to content

Commit

Permalink
wip: working prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
Denifia committed Oct 18, 2022
1 parent 3a63b15 commit eb02385
Show file tree
Hide file tree
Showing 12 changed files with 1,324 additions and 9 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vscode
45 changes: 45 additions & 0 deletions README.md
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).
5 changes: 0 additions & 5 deletions readme.md

This file was deleted.

8 changes: 7 additions & 1 deletion src/.gitignore
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
30 changes: 30 additions & 0 deletions src/Finder.cs
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");
}
}
134 changes: 132 additions & 2 deletions src/Program.cs
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);
}
}
61 changes: 61 additions & 0 deletions src/Recorder.cs
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;
}
}
}
Loading

0 comments on commit eb02385

Please sign in to comment.