Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support SmartMeter #14

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
70 changes: 70 additions & 0 deletions _example/smartmeter/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package main

import (
"context"
"fmt"
"log"
"os"
"time"

"github.com/tenntenn/natureremo"
)

type Item struct {
Value float64
Time time.Time
}

func main() {
cli := natureremo.NewClient(os.Args[1])
ctx := context.Background()
prevNormalCumulative := map[string]Item{}
prevReverseCumulative := map[string]Item{}
for {
as, err := cli.ApplianceService.GetAll(ctx)
if err != nil {
log.Fatal(err)
}

for _, a := range as {
if a.Type == natureremo.ApplianceTypeSmartMeter {
fmt.Println("===", a.Device.Name, "===")
if a.SmartMeter != nil {
vi, _, e := a.SmartMeter.GetMeasuredInstantaneousWatt()
if e != nil {
log.Fatal(e)
}
fmt.Println("Instantaneous:", vi, "W")
v, t, e := a.SmartMeter.GetNormalDirectionCumulativeElectricEnergyWattHour()
if e != nil {
log.Fatal(e)
}
fmt.Println("NormalCumulative:", v/1000, "kWh")
if prev, ok := prevNormalCumulative[a.ID]; ok && prev.Time.Before(t) {
diff, err := a.SmartMeter.CalcCumulativeDiff(v, prev.Value)
if err != nil {
log.Fatal(err)
}
fmt.Println("NormalCumulativeDiff:", t.Sub(prev.Time), diff/1000, "kWh")
}
prevNormalCumulative[a.ID] = Item{v, t}
v, t, e = a.SmartMeter.GetReverseDirectionCumulativeElectricEnergyWattHour()
if e != nil {
log.Fatal(e)
}
fmt.Println("ReverseCumulative:", v/1000, "kWh")
if prev, ok := prevReverseCumulative[a.ID]; ok && prev.Time.Before(t) {
diff, err := a.SmartMeter.CalcCumulativeDiff(v, prev.Value)
if err != nil {
log.Fatal(err)
}
fmt.Println("ReverseCumulativeDiff:", t.Sub(prev.Time), diff/1000, "kWh")
}
prevReverseCumulative[a.ID] = Item{v, t}
}
}
}
d := cli.LastRateLimit.Reset.Sub(time.Now())
time.Sleep(d / time.Duration(cli.LastRateLimit.Remaining))
}
}
3 changes: 3 additions & 0 deletions appliance.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Appliance struct {
AirCon *AirCon `json:"aircon"`
TV *TV `json:"tv"`
Light *Light `json:"light"`
SmartMeter *SmartMeter `json:"smart_meter"`
}

// SignalByName gets a signal by name from Signals.
Expand All @@ -40,6 +41,8 @@ const (
ApplianceTypeLight ApplianceType = "LIGHT"
// ApplianceTypeIR represents a device which is controlled by infrared.
ApplianceTypeIR ApplianceType = "IR"
// ApplianceTypeSmartMeter represents a smart meter
ApplianceTypeSmartMeter ApplianceType = "EL_SMART_METER"
)

// ApplianceModel is device information of appliance
Expand Down
141 changes: 141 additions & 0 deletions smartmeter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package natureremo

import (
"fmt"
"math"
"strconv"
"time"
)

type SmartMeter struct {
Properties []EchonetliteProperty `json:"echonetlite_properties"`
}

type EchonetliteProperty struct {
Name string `json:"name"`
Epc EchonetliteEPC `json:"epc"`
Value string `json:"val"`
UpdatedAt time.Time `json:"updated_at"`
}

type EchonetliteEPC int

const (
EPCNormalDirectionCumulativeElectricEnergy EchonetliteEPC = 0xE0
EPCReverseDirectionCumulativeElectricEnergy EchonetliteEPC = 0xE3
EPCCoefficient EchonetliteEPC = 0xD3
EPCCumulativeElectricEnergyUnit EchonetliteEPC = 0xE1
EPCCumulativeElectricEnergyEffectiveDigits EchonetliteEPC = 0xD7
EPCMeasuredInstantaneous EchonetliteEPC = 0xE7
)

// Find specified EPC object.
// It returns reference to the object if found. Otherwise, returns nil.
func (s *SmartMeter) Find(epc EchonetliteEPC) *EchonetliteProperty {
for _, p := range s.Properties {
if p.Epc == epc {
return &p
}
}
return nil
}

// Get MeasuredInstantaneous in Watt(W).
func (s *SmartMeter) GetMeasuredInstantaneousWatt() (int64, time.Time, error) {
p := s.Find(EPCMeasuredInstantaneous)
if p == nil {
return 0, time.Time{}, fmt.Errorf("MeasuredInstantaneous property not found")
}
v, err := strconv.ParseInt(p.Value, 10, 64)
if err != nil {
return 0, time.Time{}, err
}
return v, p.UpdatedAt, nil
}

// Get NormalDirectionCumulativeElectricEnergy in watt hour(Wh).
func (s *SmartMeter) GetNormalDirectionCumulativeElectricEnergyWattHour() (float64, time.Time, error) {
coefficient, err := s.getCumulativeElectricEnergyCoefficientWattHour()
if err != nil {
return 0, time.Time{}, err
}
p := s.Find(EPCNormalDirectionCumulativeElectricEnergy)
if p == nil {
return 0, time.Time{}, fmt.Errorf("NormalDirectionCumulativeElectricEnergy property not found")
}
v, err := strconv.ParseUint(p.Value, 10, 64)
if err != nil {
return 0, time.Time{}, err
}
return float64(v) * coefficient, p.UpdatedAt, nil
}

// Get ReverseDirectionCumulativeElectricEnergy in watt hour(Wh).
func (s *SmartMeter) GetReverseDirectionCumulativeElectricEnergyWattHour() (float64, time.Time, error) {
coefficient, err := s.getCumulativeElectricEnergyCoefficientWattHour()
if err != nil {
return 0, time.Time{}, err
}
p := s.Find(EPCReverseDirectionCumulativeElectricEnergy)
if p == nil {
return 0, time.Time{}, fmt.Errorf("ReverseDirectionCumulativeElectricEnergy property not found")
}
v, err := strconv.ParseUint(p.Value, 10, 64)
if err != nil {
return 0, time.Time{}, err
}
return float64(v) * coefficient, p.UpdatedAt, nil
}

// Calculate diff of two cumulative value considering over flow.
func (s *SmartMeter) CalcCumulativeDiff(nextWattHour float64, prevWattHour float64) (float64, error) {
coefficient, err := s.getCumulativeElectricEnergyCoefficientWattHour()
if err != nil {
return 0, err
}
effectiveDigitsProp := s.Find(EPCCumulativeElectricEnergyEffectiveDigits)
if effectiveDigitsProp == nil {
return 0, fmt.Errorf("CumulativeElectricEnergyEffectiveDigits property not found")
}
effectiveDigits, err := strconv.ParseUint(effectiveDigitsProp.Value, 10, 8)
if err != nil {
return 0, err
}
upperBound := math.Pow10(int(effectiveDigits+1)) * coefficient
if nextWattHour >= prevWattHour {
return nextWattHour - prevWattHour, nil
} else {
return upperBound - prevWattHour + nextWattHour, nil
}
}

// Get coefficient to convert cumulative electric energy unit to watt hour
func (s *SmartMeter) getCumulativeElectricEnergyCoefficientWattHour() (value float64, err error) {
unitProp := s.Find(EPCCumulativeElectricEnergyUnit)
var unitCode uint64
if unitProp == nil {
unitCode = 0x00 // assume unit is kWh
} else {
unitCode, err = strconv.ParseUint(unitProp.Value, 10, 8)
if err != nil {
return 0.0, err
}
}
var unitCoefficient float64
if unitCode < 0x0A {
unitCoefficient = math.Pow10(0x03 - int(unitCode))
} else {
unitCoefficient = math.Pow10(int(unitCode) - 0x0A + 4)
}
coefficientProp := s.Find(EPCCoefficient)
var coefficient uint64
if coefficientProp == nil {
coefficient = 1
} else {
coefficient, err = strconv.ParseUint(coefficientProp.Value, 10, 64)
if err != nil {
return 0.0, err
}
}
return unitCoefficient * float64(coefficient), nil
}
76 changes: 76 additions & 0 deletions smartmeter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package natureremo_test

import (
"encoding/json"
"testing"

"github.com/tenntenn/natureremo"
)

func TestSmartMeter(t *testing.T) {
jsonString := `{
"echonetlite_properties": [
{
"name": "coefficient",
"epc": 211,
"val": "1",
"updated_at": "2020-04-27T15:24:03Z"
},
{
"name": "cumulative_electric_energy_effective_digits",
"epc": 215,
"val": "6",
"updated_at": "2020-04-27T15:24:03Z"
},
{
"name": "normal_direction_cumulative_electric_energy",
"epc": 224,
"val": "5167",
"updated_at": "2020-04-27T15:24:03Z"
},
{
"name": "cumulative_electric_energy_unit",
"epc": 225,
"val": "1",
"updated_at": "2020-04-27T15:24:03Z"
},
{
"name": "reverse_direction_cumulative_electric_energy",
"epc": 227,
"val": "3606",
"updated_at": "2020-04-27T15:24:03Z"
},
{
"name": "measured_instantaneous",
"epc": 231,
"val": "360",
"updated_at": "2020-04-27T15:24:03Z"
}
]
}`
s := natureremo.SmartMeter{}
if err := json.Unmarshal([]byte(jsonString), &s); err != nil {
t.Fatal(err)
}
instant, _, err := s.GetMeasuredInstantaneousWatt()
if err != nil {
t.Fatal(err)
}
if instant != 360 {
t.Fatalf("instant value expects 360, but %v", instant)
}
normal, _, err := s.GetNormalDirectionCumulativeElectricEnergyWattHour()
if err != nil {
t.Fatal(err)
}
if normal != 516700.0 {
t.Fatalf("normal cumulative value expects 516700, but %v", normal)
}
reverse, _, err := s.GetReverseDirectionCumulativeElectricEnergyWattHour()
if err != nil {
t.Fatal(err)
}
if reverse != 360600.0 {
t.Fatalf("reverse cumulative value expects 360600, but %v", reverse)
}
}