Synology Docker MariaDb / .NET Core 6 / Homewizzard Electrical meter reading

HomeWizard P1 meter

Recently I came across a small device that can be connected to the Smart Energy Meter as it is used in the Netherlands, among other places:

HomeWizard.nl

This small device is connected to the P1 port of the smart meter. The HomeWizzard is then connected to the WiFi network.

The HomeWizzard device publishes a REST api at this endpoint https://[your-homewizzard-device-ip]/api/v1/data.

The specification of this API can be found here.

In this post we are going to write a little console program that reads data from the HomeWizard API and stores this data in a MariaDB database.

I’m using Docker on a Synology NAS (DS216+). That way I can make use of the .NET Core 6 framework and all its great libraries. In this post I use Docker on a Synology NAS but docker is also available on a lot of other NAS brands.

Install Docker on your Synology NAS

Log into the NAS and install the Docker package with help of the Package Center that comes with DSM.

From this point on we are going to work in a ssh shell on the NAS so be sure to enable the ssh service on the NAS (goto Control panel -> Terminal & SNMP: Enable SSH service).

Create MariaDB container

Open a command prompt on your client computer and ssh to the NAS

# Log into your synology
ssh username@synology.local

First create the host folder that will hold our MariaDB database.

# Create folder to hold the mysql database(s)
mkdir -p /volume1/docker/data/homewizard/mysql-data/

Then we create the MariaDB container. The latest image will be downloaded if not already locally available. Next we will start the Docker container and check the logs for any errors.

# Create mariadb container from latest image with data directory on the host
# (use single quotes for env to prevent history expansion in bash)
docker create -v /volume1/docker/data/homewizard/mysql-data/:/var/lib/mysql --env='MARIADB_ROOT_PASSWORD=**********' --name mariadb --network=bridge -p3306:3306 mariadb
Downloading latest mariadb image

Check if the container is created

docker ps -a
mariadb container is created

Start the container

docker start mariadb

Check the container log

docker logs mariadb
Container log

Configure Docker container timezone

We want our container to use the local timezone; not the UTC timezone. The default setting for the docker containers is to use the timezone info from /etc/localtime. Change this link to the timezone you are in, for me it is Europe/Amsterdam

mv /etc/localtime /etc/localtime-old
ln -s /usr/share/zoneinfo/Europe/Amsterdam /etc/localtime

Now when you ask linux what time it is it will respond in the localtime

root@4d07a4c538ef:/app# date
Thu Mar 31 15:09:38 CEST 2022

Now test the connection to your MariaDB container with, for example, the MySQL Workbench.

MariaDB is configured to use the SYSTEM timezone. If you have just changed the timezone you need to start and stop the linux container for changes to take effect (it is not enough to restart the MariaDB server).

To check if MariaDB uses the correct timezone execute the query below; MariaDB should show the localtime.

MariaDB [homewizzard]> select now();
+---------------------+
| now()               |
+---------------------+
| 2022-03-31 15:10:28 |
+---------------------+
1 row in set (0.001 sec)

MariaDB [homewizzard]>

Create .NET Core 6 solution

Create a new C# NET Core 6 console program. I use Visual Studio but of course you can use Visual Code or any other IDE.

Create a new NET Core 6 console program
Create a new NET Core 6 console program

Remove all code from the generated Program.cs and replace it with the code as shown below.

using HomeWizzard.Config;
using HomeWizzard.Docker;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;

var builder = new ServiceCollection()
    .AddDbContext<ApplicationDbContext>()
    .AddSingleton<IAppConfig, AppConfig>()
    .BuildServiceProvider();

Console.WriteLine("Download meter data started.");

using (ApplicationDbContext? context = builder.GetRequiredService<ApplicationDbContext>())
{
    // context.Database.EnsureDeleted();
    Console.WriteLine(context.Database.EnsureCreated() ? "Database created" : "Database exists");

    IAppConfig config = builder.GetRequiredService<IAppConfig>();
    Console.WriteLine($"Database         : {config.ConnectionString}");
    Console.WriteLine($"API url          : {config.ApiUrl}");
    Console.WriteLine($"Measure interval : {config.MeasureInterval}");
    Console.WriteLine($"Log interval     : {config.LogInterval}");
    Console.WriteLine($"Commit interval  : {config.CommitInterval}");

    using HttpClient client = new();
    ulong counter = 1;
    while (true)
    {
        using (var response = await client.GetAsync(config.ApiUrl))
        {
            string apiResponse = await response.Content.ReadAsStringAsync();
            if (apiResponse != null)
            {
                MeterData? data = JsonConvert.DeserializeObject<MeterData>(apiResponse);
                if (context.Measurements != null && data != null)
                {
                    data.Response = apiResponse;
                    context.Measurements.Add(data);
                }
            }
        }

        if ((counter % config.LogInterval) == 0)
        {
            Console.WriteLine($"{counter} measurements stored in database.");
        }

        if ((counter % config.CommitInterval) == 0)
        {
            Console.WriteLine($"{counter} commit.");
            await context.SaveChangesAsync();
        }

        counter++;

        await Task.Delay(TimeSpan.FromMilliseconds(config.MeasureInterval));
    }
}

Add a file called Dockerfile to this project with the contents as shown below. Be sure to set “Copy if newer” in the properties window of this file for the property “Copy to Output Directory”.

FROM mcr.microsoft.com/dotnet/aspnet:6.0
COPY . /app
WORKDIR /app
EXPOSE 80
ENTRYPOINT ["dotnet", "HomeWizzard.Docker.dll"]

Add a class called MeterData to this project that represents a single measurement. For this application it is stored in the main project but you could add a separate Model project. Use the definition as shown below.

namespace HomeWizzard.Docker
{
    using System.ComponentModel.DataAnnotations;

    /*
     * Example API response:
     * 
     * {
     *      "smr_version":50,
     *      "meter_model":"Sagemcom T210-D ESMR5.0",
     *      "wifi_ssid":"TELE2-3DFA4A_2.4G",
     *      "wifi_strength":100,
     *      "total_power_import_t1_kwh":6888.519,
     *      "total_power_import_t2_kwh":7456.22,
     *      "total_power_export_t1_kwh":5352.048,
     *      "total_power_export_t2_kwh":12348.325,
     *      "active_power_w":-1409,
     *      "active_power_l1_w":-1463,
     *      "active_power_l2_w":-508,
     *      "active_power_l3_w":563,
     *      "total_gas_m3":8261.986,
     *      "gas_timestamp":220327122500
     *  }
     */
    public class MeterData
    {
        public MeterData()
        {
            this.DateTime = DateTime.Now;
        }
        public ulong Id { get; set; }
        [Required]
        public DateTime DateTime { get; set; }
        [MaxLength(50)]
        [Required]
        public string? meter_model { get; set; }
        [MaxLength(50)]
        [Required]
        public string? wifi_ssid { get; set; }
        [Required]
        public int? wifi_strength { get; set; }
        [Required]
        public double? total_power_import_t1_kwh { get; set; }
        [Required]
        public double? total_power_import_t2_kwh { get; set; }
        [Required]
        public double? total_power_export_t1_kwh { get; set; }
        [Required]
        public double? total_power_export_t2_kwh { get; set; }
        [Required]
        public double? active_power_w { get; set; }
        [Required]
        public double? active_power_l1_w { get; set; }
        [Required]
        public double? active_power_l2_w { get; set; }
        [Required]
        public double? active_power_l3_w { get; set; }
        [Required]
        public double? total_gas_m3 { get; set; }
        [Required]
        public long gas_timestamp { get; set; }
        [MaxLength(2048)]
        [Required]
        public string? Response { get; set; } = null;
    }
}

Add a new class library HomeWizzard.Config to the solution. Add a new interface called IAppConfig to this project with the definition as shown below.

namespace HomeWizzard.Config
{
    public interface IAppConfig
    {
        string ApiUrl { get; set; }
        string ConnectionString { get; }
        ulong LogInterval { get; }
        ulong CommitInterval { get; }
        ulong MeasureInterval { get; }
    }
}

Add a class AppConfig to the HomeWizzard.Config project that implements the IAppConfig interface. Code is shown below.

#nullable disable

namespace HomeWizzard.Config
{
    using Microsoft.Extensions.Configuration;
    using MySqlConnector;
    using System;

    public class AppConfig : IAppConfig
    {
        public AppConfig()
        {
            IConfiguration configProvider = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json", true, true)
                .AddUserSecrets<AppConfig>()
                .AddEnvironmentVariables()
                .Build();

            // Defaults
            this.CommitInterval = 10;
            this.LogInterval = 1;
            this.MeasureInterval = 0;
            configProvider.Bind(this);

            if (this.MySql == null)
            {
                throw new InvalidOperationException("MySql configuration is missing");
            }
        }

        public ulong LogInterval { get; set; }

        public ulong CommitInterval { get; set; }

        public ulong MeasureInterval { get; set; }

        public string ApiUrl { get; set; }

        public MySqlConnectionStringBuilder MySql { get; set; }

        public string ConnectionString
        {
            get
            {
                return MySql.ConnectionString;
            }
        }
    }
}

We also need an appsettings.json file for the application settings. Add it to the HomeWizzard.Config project with the contents as shown below. Be sure to set “Copy if newer” in the properties window of this file for the property “Copy to Output Directory”.

{
  "ApiUrl": "http://192.168.1.6/api/v1/data",
  "CommitInterval" :  20,
  "MySql": {
    "Server": "localhost",
    "Port": 3306,
    "Database": "homewizzard",
    "UserID": "root",
    "Password": "**********"
  }
}

Now we are ready to publish our solution. Publish the solution to the @Docker share on the NAS. This share is created by the Docker installation. Create a subfolder on the share called HomeWizzard.

Next in Visual Studio right click your main project and select Publish. Choose folder as your publish target

Publish your application

Choose Folder as your Specific target

Choose folder as Specific target

Browse to the correct location on your NAS and press Finish. Next press Publish to start publishing your application to the NAS folder.

Publish application

Create the Docker image from your application

Now that we have the published application on our NAS share we can create a Docker image from it. In a ssh session on your NAS execute the commands below.

cd /volume1/docker/HomeWizzard
docker build -t homewizzard -f ./Dockerfile .

The new image is created. To list the images on your docker instance execute the command below

docker images

As you can see we have three images on our system. The aspnet image is created when the homewizzard image was created.

The nex step is to create a new container from the newly created homewizzard image. Execute the command below.

docker create --name=homewizzard --env="MeasureInterval=1000" --env="CommitInterval=10" --env="LogInterval=10" homewizzard

The command creates an image with tag (name) homewizzard. It also defines three environment variables that are used in our application. Finally the image homewizzard is used as the base for this container.

To list all containers in the registry execute the command below

docker container ls -a

All available containers will be shown (stopped and executing containers).

All containers in local registry

The final step is to start our container. Execute the command below.

docker start homewizzard

Check the log with this command

docker logs homewizzard

The log will show something like this

Application log

Final result

On the synology you can check in the DSM that the two Docker images are running.

Docker images are running on Synology NAS

You can also view the log of each container in the DSM GUI. Go to the Container tab and double click on the container whose log you want to view.

The log overview of the homewizzard container

Some usefull Docker commands

In this section, I’ll show you some Docker commands that you’ll probably use most often.

# build image from Dockerfile and files in current folder (.)
docker build -t homewizzard -f ./Dockerfile .

# List docker images
docker images

# Show all docker containers (running and stopped)
docker ps -a

# Start / stop a container
docker start [imagename]
docker stop [imagename]

# Remove a container
docker container rm [containername]

# Remove an image
docker rm [imagename]

# Rename an image
docker rename [oldname] [newname]

# Start bash shell on container
docker exec -it [imagename] bash

Share

Leave a Reply

Your email address will not be published. Required fields are marked *