From 83e67738620972ec98e3c7cdca2e7cc2ff4d8ab1 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 29 Dec 2022 10:14:24 +0100 Subject: [PATCH] Add price zones to fixed tariff (#5583) --- evcc.dist.yaml | 8 ++- go.mod | 7 +- go.sum | 28 ++++++++ tariff/fixed.go | 108 ++++++++++++++++++++++++++---- tariff/fixed/day.go | 116 +++++++++++++++++++++++++++++++++ tariff/fixed/day_enumer.go | 98 ++++++++++++++++++++++++++++ tariff/fixed/day_test.go | 53 +++++++++++++++ tariff/fixed/timerange.go | 93 ++++++++++++++++++++++++++ tariff/fixed/timerange_test.go | 49 ++++++++++++++ tariff/fixed/zone.go | 60 +++++++++++++++++ tariff/fixed/zone_test.go | 41 ++++++++++++ tariff/fixed_test.go | 35 ++++++++++ 12 files changed, 683 insertions(+), 13 deletions(-) create mode 100644 tariff/fixed/day.go create mode 100644 tariff/fixed/day_enumer.go create mode 100644 tariff/fixed/day_test.go create mode 100644 tariff/fixed/timerange.go create mode 100644 tariff/fixed/timerange_test.go create mode 100644 tariff/fixed/zone.go create mode 100644 tariff/fixed/zone_test.go create mode 100644 tariff/fixed_test.go diff --git a/evcc.dist.yaml b/evcc.dist.yaml index 2ff30a230..d30249ecd 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -139,9 +139,15 @@ loadpoints: tariffs: currency: EUR # three letter ISO-4217 currency code (default EUR) grid: - # either static grid price + # either static grid price (or price zones) type: fixed price: 0.294 # EUR/kWh + zones: + - days: Mo-Fr + hours: 2-5 + price: 0.2 # EUR/kWh + - days: Sa,So + price: 0.15 # EUR/kWh # # or variable via tibber # type: tibber diff --git a/go.mod b/go.mod index 1ad18f48f..91916673e 100644 --- a/go.mod +++ b/go.mod @@ -31,10 +31,12 @@ require ( github.com/fatih/structs v1.1.0 github.com/foogod/go-powerwall v0.2.0 github.com/glebarez/sqlite v1.6.0 + github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-ping/ping v1.1.0 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/godbus/dbus/v5 v5.1.0 github.com/gokrazy/updater v0.0.0-20211121155532-30ae8cd650ea + github.com/golang-module/carbon v1.7.2 github.com/golang/mock v1.6.0 github.com/google/go-github/v32 v32.1.0 github.com/google/uuid v1.3.0 @@ -116,13 +118,15 @@ require ( github.com/felixge/httpsnoop v1.0.3 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/glebarez/go-sqlite v1.20.0 // indirect - github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 // indirect github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/gobuffalo/envy v1.7.0 // indirect + github.com/gobuffalo/packd v0.3.0 // indirect + github.com/gobuffalo/packr v1.30.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-querystring v1.1.0 // indirect @@ -164,6 +168,7 @@ require ( github.com/rickb777/date v1.20.1 // indirect github.com/rickb777/plural v1.4.1 // indirect github.com/rivo/uniseg v0.4.3 // indirect + github.com/rogpeppe/go-internal v1.6.1 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/afero v1.9.3 // indirect diff --git a/go.sum b/go.sum index 2b353fc90..eaa5561ac 100644 --- a/go.sum +++ b/go.sum @@ -182,7 +182,9 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE github.com/containrrr/shoutrrr v0.6.1 h1:6ih7jA6mo3t6C97MZbd3SxL/kRizOE3bI9CpBQZ6wzg= github.com/containrrr/shoutrrr v0.6.1/go.mod h1:ye9jGX5YzMnJ76waaNVWlJ4luhMEyt1EWU5unYTQSb0= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-oidc/v3 v3.4.0 h1:xz7elHb/LDwm/ERpwHd+5nb7wFHL32rsr6bBOgaeu6g= github.com/coreos/go-oidc/v3 v3.4.0/go.mod h1:eHUXhZtXPQLgEaDrOVTgwbgmz1xGOkJNye6h3zkD2Pw= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -190,6 +192,7 @@ github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -304,6 +307,14 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= +github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= +github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= @@ -322,6 +333,8 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gokrazy/updater v0.0.0-20211121155532-30ae8cd650ea h1:NM/sLCKWWc9mnMtJZGbwOtaAwEpCEe6tlcXWKp3LEKk= github.com/gokrazy/updater v0.0.0-20211121155532-30ae8cd650ea/go.mod h1:PYOvzGOL4nlBmuxu7IyKQTFLaxr61+WPRNRzVtuYOHw= +github.com/golang-module/carbon v1.7.2 h1:92v/5h0YORJPQqAi+dDuqnN/YWJ6rA8nsfgW5dx9N/M= +github.com/golang-module/carbon v1.7.2/go.mod h1:nUMnXq90Rv8a7h2+YOo2BGKS77Y0w/hMPm4/a8h19N8= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -530,6 +543,7 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joeshaw/carwings v0.0.0-20220711204617-d90801cf2848 h1:mUoi+B77PKkv/pYCR7uOh5ouZdgzFAwOZ3HU8Jr1P+4= github.com/joeshaw/carwings v0.0.0-20220711204617-d90801cf2848/go.mod h1:tB0OlpicmRVTL1Vksc5XRiYo+wkK2kl/GI7eGMIl6Rs= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -552,6 +566,7 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -561,6 +576,7 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e github.com/klauspost/compress v1.15.13 h1:NFn1Wr8cfnenSJSA46lLq4wHCcBzKTSjnBIexDMMOV0= github.com/klauspost/compress v1.15.13/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= @@ -586,6 +602,7 @@ github.com/lorenzodonini/ocpp-go v0.16.1-0.20220927200117-d4afed542168/go.mod h1 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= @@ -777,8 +794,11 @@ github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGG github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -816,6 +836,7 @@ github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= @@ -826,6 +847,7 @@ github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= @@ -861,6 +883,7 @@ github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDH github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -914,9 +937,11 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1079,6 +1104,7 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1086,6 +1112,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1208,6 +1235,7 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/tariff/fixed.go b/tariff/fixed.go index d3306f14f..b4e3d9ba8 100644 --- a/tariff/fixed.go +++ b/tariff/fixed.go @@ -1,14 +1,19 @@ package tariff import ( - "time" + "fmt" + "sort" + "github.com/benbjohnson/clock" "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/tariff/fixed" "github.com/evcc-io/evcc/util" + "github.com/golang-module/carbon" ) type Fixed struct { - Price float64 + clock clock.Clock + zones fixed.Zones } var _ api.Tariff = (*Fixed)(nil) @@ -18,23 +23,104 @@ func init() { } func NewFixedFromConfig(other map[string]interface{}) (api.Tariff, error) { - var cc Fixed + var cc struct { + Price float64 + Zones []struct { + Price float64 + Days, Hours string + } + } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - return &cc, nil + t := &Fixed{ + clock: clock.New(), + zones: []fixed.Zone{ + {Price: cc.Price}, // full week is implicit + }, + } + + for _, z := range cc.Zones { + days, err := fixed.ParseDays(z.Days) + if err != nil { + return nil, err + } + + hours, err := fixed.ParseTimeRanges(z.Hours) + if err != nil && z.Hours != "" { + return nil, err + } + + if len(hours) == 0 { + t.zones = append(t.zones, fixed.Zone{ + Price: z.Price, + Days: days, + }) + continue + } + + for _, h := range hours { + t.zones = append(t.zones, fixed.Zone{ + Price: z.Price, + Days: days, + Hours: h, + }) + } + } + + sort.Sort(t.zones) + + return t, nil } // Rates implements the api.Tariff interface func (t *Fixed) Rates() (api.Rates, error) { - start := time.Now().Truncate(time.Hour) - rr := api.Rates{{ - Start: start, - End: start.Add(time.Duration(24*7) * time.Hour), - Price: t.Price, - }} + var res api.Rates - return rr, nil + start := carbon.Time2Carbon(t.clock.Now().Local()).StartOfDay() + for i := 0; i < 7; i++ { + dow := fixed.Day((start.DayOfWeek() + i) % 7) + + zones := t.zones.ForDay(dow) + if len(zones) == 0 { + return nil, fmt.Errorf("no zones for weekday %d", dow) + } + + dayStart := start.AddDays(i) + markers := zones.TimeTableMarkers() + + for i, m := range markers { + ts := dayStart.AddMinutes(m.Minutes()) + + var zone *fixed.Zone + for j := len(zones) - 1; j >= 0; j-- { + if zones[j].Hours.Contains(m) { + zone = &zones[j] + break + } + } + + if zone == nil { + panic("could not find zone") + } + + // end rate at end of day or next marker + end := dayStart.AddDay() + if i+1 < len(markers) { + end = dayStart.AddMinutes(markers[i+1].Minutes()) + } + + rate := api.Rate{ + Price: zone.Price, + Start: ts.Carbon2Time(), + End: end.Carbon2Time(), + } + + res = append(res, rate) + } + } + + return res, nil } diff --git a/tariff/fixed/day.go b/tariff/fixed/day.go new file mode 100644 index 000000000..27d567338 --- /dev/null +++ b/tariff/fixed/day.go @@ -0,0 +1,116 @@ +package fixed + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "golang.org/x/exp/slices" +) + +//go:generate enumer -type Day +type Day int + +const ( + Sunday Day = iota + Monday + Tuesday + Wednesday + Thursday + Friday + Saturday +) + +var Week = []Day{Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday} + +var shortDays = map[string]Day{ + // english + "sun": Sunday, + "mon": Monday, + "tue": Tuesday, + "wed": Wednesday, + "thu": Thursday, + "fri": Friday, + "sat": Saturday, + // german + "so": Sunday, + "mo": Monday, + "di": Tuesday, + "mi": Wednesday, + "do": Thursday, + "fr": Friday, + "sa": Saturday, +} + +// ParseDay parses a single day +func ParseDay(s string) (Day, error) { + s = strings.ToLower(strings.TrimSpace(s)) + + // full string + if d, err := DayString(s); err == nil { + return d, nil + } + + // short string + if d, ok := shortDays[s]; ok { + return d, nil + } + + d, err := strconv.Atoi(s) + if d < 0 || d > 7 || err != nil { + return 0, fmt.Errorf("invalid day: %s", s) + } + + return Day(d % 7), nil +} + +// ParseDays converts a days string into a slice of individual days +// Days format: +// +// day[-day][, ...] +func ParseDays(s string) ([]Day, error) { + var res []Day + + for _, segment := range strings.Split(s, ",") { + fromto := strings.SplitN(segment, "-", 2) + if len(fromto) == 0 { + return nil, fmt.Errorf("invalid day range: %s", segment) + } + + from, err := ParseDay(fromto[0]) + if err != nil { + return nil, err + } + res = append(res, Day(from%7)) + + if len(fromto) == 2 { + to, err := ParseDay(fromto[1]) + if err != nil { + return nil, err + } + + if to < from { + to += 7 + } + + for d := from + 1; d <= to; d++ { + res = append(res, d%7) + } + } + } + + if len(res) > 7 { + return nil, errors.New("too many days") + } + + sorted := make([]Day, len(res)) + copy(sorted, res) + slices.Sort(sorted) + + if len(slices.Compact(sorted)) < len(res) { + return nil, errors.New("duplicate days") + } + + return res, nil +} diff --git a/tariff/fixed/day_enumer.go b/tariff/fixed/day_enumer.go new file mode 100644 index 000000000..0c8c0c4a1 --- /dev/null +++ b/tariff/fixed/day_enumer.go @@ -0,0 +1,98 @@ +// Code generated by "enumer -type Day"; DO NOT EDIT. + +package fixed + +import ( + "fmt" + "strings" +) + +const _DayName = "SundayMondayTuesdayWednesdayThursdayFridaySaturday" + +var _DayIndex = [...]uint8{0, 6, 12, 19, 28, 36, 42, 50} + +const _DayLowerName = "sundaymondaytuesdaywednesdaythursdayfridaysaturday" + +func (i Day) String() string { + if i < 0 || i >= Day(len(_DayIndex)-1) { + return fmt.Sprintf("Day(%d)", i) + } + return _DayName[_DayIndex[i]:_DayIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _DayNoOp() { + var x [1]struct{} + _ = x[Sunday-(0)] + _ = x[Monday-(1)] + _ = x[Tuesday-(2)] + _ = x[Wednesday-(3)] + _ = x[Thursday-(4)] + _ = x[Friday-(5)] + _ = x[Saturday-(6)] +} + +var _DayValues = []Day{Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday} + +var _DayNameToValueMap = map[string]Day{ + _DayName[0:6]: Sunday, + _DayLowerName[0:6]: Sunday, + _DayName[6:12]: Monday, + _DayLowerName[6:12]: Monday, + _DayName[12:19]: Tuesday, + _DayLowerName[12:19]: Tuesday, + _DayName[19:28]: Wednesday, + _DayLowerName[19:28]: Wednesday, + _DayName[28:36]: Thursday, + _DayLowerName[28:36]: Thursday, + _DayName[36:42]: Friday, + _DayLowerName[36:42]: Friday, + _DayName[42:50]: Saturday, + _DayLowerName[42:50]: Saturday, +} + +var _DayNames = []string{ + _DayName[0:6], + _DayName[6:12], + _DayName[12:19], + _DayName[19:28], + _DayName[28:36], + _DayName[36:42], + _DayName[42:50], +} + +// DayString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func DayString(s string) (Day, error) { + if val, ok := _DayNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _DayNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Day values", s) +} + +// DayValues returns all values of the enum +func DayValues() []Day { + return _DayValues +} + +// DayStrings returns a slice of all String values of the enum +func DayStrings() []string { + strs := make([]string, len(_DayNames)) + copy(strs, _DayNames) + return strs +} + +// IsADay returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Day) IsADay() bool { + for _, v := range _DayValues { + if i == v { + return true + } + } + return false +} diff --git a/tariff/fixed/day_test.go b/tariff/fixed/day_test.go new file mode 100644 index 000000000..9c63438ac --- /dev/null +++ b/tariff/fixed/day_test.go @@ -0,0 +1,53 @@ +package fixed + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseDays(t *testing.T) { + d, err := ParseDays(" sunday ") + assert.NoError(t, err) + assert.Equal(t, []Day{Sunday}, d) + + d, err = ParseDays("sun") + assert.NoError(t, err) + assert.Equal(t, []Day{Sunday}, d) + + d, err = ParseDays("so") + assert.NoError(t, err) + assert.Equal(t, []Day{Sunday}, d) + + d, err = ParseDays("0 ") + assert.NoError(t, err) + assert.Equal(t, []Day{Sunday}, d) + + d, err = ParseDays(" 7") + assert.NoError(t, err) + assert.Equal(t, []Day{Sunday}, d) + + d, err = ParseDays(" 6-7 ") + assert.NoError(t, err) + assert.Equal(t, []Day{Saturday, Sunday}, d) + + d, err = ParseDays("1- 7") + assert.NoError(t, err) + assert.Equal(t, []Day{Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday}, d) + + d, err = ParseDays("1, 3-7") + assert.NoError(t, err) + assert.Equal(t, []Day{Monday, Wednesday, Thursday, Friday, Saturday, Sunday}, d) + + _, err = ParseDays("-1") + assert.EqualError(t, err, "invalid day: ") + + _, err = ParseDays(" 8 ") + assert.EqualError(t, err, "invalid day: 8") + + _, err = ParseDays("1, 1") + assert.EqualError(t, err, "duplicate days") + + _, err = ParseDays("0,1,2,3,4,5,6,7") + assert.EqualError(t, err, "too many days") +} diff --git a/tariff/fixed/timerange.go b/tariff/fixed/timerange.go new file mode 100644 index 000000000..ec91c2dbe --- /dev/null +++ b/tariff/fixed/timerange.go @@ -0,0 +1,93 @@ +package fixed + +import ( + "fmt" + "strings" + "time" +) + +type HourMin struct { + Hour, Min int +} + +func (hm HourMin) Minutes() int { + return 60*hm.Hour + hm.Min +} + +func (hm HourMin) IsNil() bool { + return hm.Hour+hm.Min == 0 +} + +func (hm HourMin) String() string { + return fmt.Sprintf("%02d:%02d", hm.Hour, hm.Min) +} + +type TimeRange struct { + From, To HourMin +} + +func (tr TimeRange) Contains(hm HourMin) bool { + ts := hm.Minutes() + return tr.From.Minutes() <= ts && (tr.To.IsNil() || tr.To.Minutes() > ts) +} + +func (tr TimeRange) IsNil() bool { + return tr.From.IsNil() && tr.To.IsNil() +} + +func (tr TimeRange) String() string { + to := tr.To.String() + if tr.To.IsNil() { + to += "(+1)" + } + return tr.From.String() + "-" + to +} + +func parseTime(s string) (HourMin, error) { + s = strings.TrimSpace(s) + + t, err := time.ParseInLocation("15:04", s, time.Local) + if err == nil { + return HourMin{t.Hour(), t.Minute()}, nil + } + + t, err = time.ParseInLocation("15", s, time.Local) + if err == nil { + return HourMin{t.Hour(), 0}, nil + } + + return HourMin{}, fmt.Errorf("invalid time: %s", s) +} + +func ParseTimeRange(s string) (TimeRange, error) { + fromto := strings.SplitN(s, "-", 2) + if len(fromto) != 2 { + return TimeRange{}, fmt.Errorf("invalid time range: %s", s) + } + + from, err := parseTime(fromto[0]) + if err != nil { + return TimeRange{}, err + } + + to, err := parseTime(fromto[1]) + if err != nil { + return TimeRange{}, err + } + + return TimeRange{from, to}, nil +} + +func ParseTimeRanges(s string) ([]TimeRange, error) { + var res []TimeRange + + for _, segment := range strings.Split(s, ",") { + tr, err := ParseTimeRange(segment) + if err != nil { + return nil, err + } + res = append(res, tr) + } + + return res, nil +} diff --git a/tariff/fixed/timerange_test.go b/tariff/fixed/timerange_test.go new file mode 100644 index 000000000..9af731217 --- /dev/null +++ b/tariff/fixed/timerange_test.go @@ -0,0 +1,49 @@ +package fixed + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseTimeRange(t *testing.T) { + d, err := ParseTimeRanges(" 7:00 - 12:30 ") + assert.NoError(t, err) + assert.Equal(t, []TimeRange{{HourMin{7, 0}, HourMin{12, 30}}}, d, "hour:min range") + + d, err = ParseTimeRanges("8-10") + assert.NoError(t, err) + assert.Equal(t, []TimeRange{{HourMin{8, 0}, HourMin{10, 0}}}, d, "hour range") + + d, err = ParseTimeRanges("8-10,20-22:30") + assert.NoError(t, err) + assert.Equal(t, []TimeRange{ + {HourMin{8, 0}, HourMin{10, 0}}, + {HourMin{20, 0}, HourMin{22, 30}}, + }, d) +} + +func TestTimeRangeContains(t *testing.T) { + tr := TimeRange{ + From: HourMin{1, 0}, + To: HourMin{2, 0}, + } + + assert.False(t, tr.Contains(HourMin{0, 0})) + assert.True(t, tr.Contains(HourMin{1, 0})) + assert.True(t, tr.Contains(HourMin{1, 59})) + assert.False(t, tr.Contains(HourMin{2, 0})) + assert.False(t, tr.Contains(HourMin{3, 0})) +} + +func TestTimeRangeOpenEndContains(t *testing.T) { + tr := TimeRange{ + From: HourMin{1, 0}, + } + + assert.False(t, tr.Contains(HourMin{0, 0})) + assert.True(t, tr.Contains(HourMin{1, 0})) + assert.True(t, tr.Contains(HourMin{1, 59})) + assert.True(t, tr.Contains(HourMin{2, 0})) + assert.True(t, tr.Contains(HourMin{23, 0})) +} diff --git a/tariff/fixed/zone.go b/tariff/fixed/zone.go new file mode 100644 index 000000000..efbf1c528 --- /dev/null +++ b/tariff/fixed/zone.go @@ -0,0 +1,60 @@ +package fixed + +import ( + "sort" + + "golang.org/x/exp/slices" +) + +type Zone struct { + Price float64 + Days []Day + Hours TimeRange +} + +type Zones []Zone + +// implement sort.Interface +func (r Zones) Len() int { + return len(r) +} + +func (r Zones) Less(i, j int) bool { + if r[i].Hours.From.Minutes() == r[j].Hours.From.Minutes() { + return r[i].Hours.To.Minutes() > r[j].Hours.To.Minutes() + } + return r[i].Hours.From.Minutes() < r[j].Hours.From.Minutes() +} + +func (r Zones) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +// ForDay returns the zones for given day in ascending order +func (r Zones) ForDay(day Day) Zones { + var zones Zones + for _, z := range r { + if slices.Contains(z.Days, day) || len(z.Days) == 0 { + zones = append(zones, z) + } + } + + sort.Sort(zones) + return zones +} + +// TimeTableMarkers returns list of zone start/end markers +func (r Zones) TimeTableMarkers() []HourMin { + res := []HourMin{{Hour: 0, Min: 0}} + + for _, z := range r { + if !z.Hours.From.IsNil() { + res = append(res, z.Hours.From) + } + if !z.Hours.To.IsNil() { + res = append(res, z.Hours.To) + } + } + + return res +} diff --git a/tariff/fixed/zone_test.go b/tariff/fixed/zone_test.go new file mode 100644 index 000000000..aaa025f5c --- /dev/null +++ b/tariff/fixed/zone_test.go @@ -0,0 +1,41 @@ +package fixed + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestZonesForDay(t *testing.T) { + zones := Zones{ + {Days: nil}, + {Days: []Day{Monday, Tuesday, Wednesday}}, + {Days: []Day{Wednesday, Thursday, Friday}}, + } + + assert.Len(t, zones.ForDay(Sunday), 1) + assert.Len(t, zones.ForDay(Monday), 2) + assert.Len(t, zones.ForDay(Wednesday), 3) + assert.Len(t, zones.ForDay(Thursday), 2) +} + +func TestZonesTimeTableMarkers(t *testing.T) { + zones := Zones{ + {Hours: TimeRange{ + From: HourMin{1, 0}, + To: HourMin{2, 0}, + }}, + {Hours: TimeRange{ + From: HourMin{3, 30}, + To: HourMin{4, 30}, + }}, + } + + assert.Equal(t, []HourMin{ + {0, 0}, + {1, 0}, + {2, 0}, + {3, 30}, + {4, 30}, + }, zones.TimeTableMarkers()) +} diff --git a/tariff/fixed_test.go b/tariff/fixed_test.go new file mode 100644 index 000000000..f1dd90958 --- /dev/null +++ b/tariff/fixed_test.go @@ -0,0 +1,35 @@ +package tariff + +import ( + "testing" + + "github.com/benbjohnson/clock" + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/tariff/fixed" + "github.com/golang-module/carbon" + "github.com/stretchr/testify/assert" +) + +func TestFixed(t *testing.T) { + tf := &Fixed{ + clock: clock.NewMock(), + zones: []fixed.Zone{ + {Price: 0.3}, + }, + } + + var expect api.Rates + for i := 0; i < 7; i++ { + dayStart := carbon.Time2Carbon(tf.clock.Now()).StartOfDay().AddDays(i) + + expect = append(expect, api.Rate{ + Price: 0.3, + Start: dayStart.Carbon2Time(), + End: dayStart.AddDay().Carbon2Time(), + }) + } + + rates, err := tf.Rates() + assert.NoError(t, err) + assert.Equal(t, expect, rates) +}