Skip to content

Commit

Permalink
Add totalizator
Browse files Browse the repository at this point in the history
  • Loading branch information
kozaktomas committed Nov 12, 2024
1 parent 102ed80 commit ed52fc9
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 88 deletions.
24 changes: 24 additions & 0 deletions backend/pkg/scale/keg.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ func GetFullWeights() KegWeights {

// CalcBeersLeft calculates the number of beers left in a keg based on its size and current weight
func CalcBeersLeft(keg int, weight float64) int {
if keg == 0 {
return 0
}
kegWeight, found := GetEmptyWeights()[keg]
if !found {
kegWeight = 0
Expand All @@ -43,6 +46,27 @@ func CalcBeersLeft(keg int, weight float64) int {
return int(math.Floor((weight/1000 - kegWeight/1000) * 2))
}

// CalcBeersConsumed calculates the number of beers consumed from a keg based on its size and current weight
func CalcBeersConsumed(keg int, weight float64) int {
kegWeight, found := GetEmptyWeights()[keg]
if !found {
return 0
}
fullKeg := float64(keg) * 2 // how many beers do we have in full keg

w := weight - kegWeight

if w <= 0 {
return keg * 2
}

if w >= float64(keg)*1000 {
return int(fullKeg)
}

return int(math.Floor(fullKeg - (w / 500)))
}

func IsKegLow(keg int, weight float64) bool {
if keg == 0 {
return true // no keg is set - is low for a new one
Expand Down
25 changes: 25 additions & 0 deletions backend/pkg/scale/keg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func TestCalcBeersLeft(t *testing.T) {
{30, 11200, 2},
{50, 10100, 0},
{50, 12100, 2},
{0, 6100, 0}, // no active keg - always 0
{90, 10000, 20}, // unknown keg - ignore weight
}

Expand All @@ -33,6 +34,30 @@ func TestCalcBeersLeft(t *testing.T) {
}
}

func TestCalcBeersConsumed(t *testing.T) {
type testcase struct {
keg int
weight float64
beers int
}

testcases := []testcase{
{10, 5900, 20},
{10, 7100, 17},
{10, 40000, 20},
{15, 7250, 29},
{15, 600, 30},
{15, 0, 30},
{50, 0, 100},
{90, 10000, 0}, // unknown keg - always 0
}

for _, tc := range testcases {
beers := CalcBeersConsumed(tc.keg, tc.weight)
assert.Equal(t, tc.beers, beers, "Keg %d with weight %f - Expected beers to be %d, got %d", tc.keg, tc.weight, tc.beers, beers)
}
}

func TestIsKegLow(t *testing.T) {
type testcase struct {
keg int
Expand Down
165 changes: 109 additions & 56 deletions backend/pkg/scale/scale.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ func (s *Scale) loadDataFromStore() {
beersTotal, err := s.store.GetBeersTotal()
if err == nil {
s.beersTotal = beersTotal
s.monitor.BeersTotal.WithLabelValues().Add(float64(beersTotal))
}

isLow, err := s.store.GetIsLow()
Expand Down Expand Up @@ -167,8 +166,12 @@ func (s *Scale) loadDataFromStore() {
if err == nil {
s.pub.closedAt = closeAt
}

s.monitor.BeersTotal.WithLabelValues().Add(float64(s.getBeersTotal()))
}

// AddMeasurement handles a new measurement from the scale
// the most important function in the scale
func (s *Scale) AddMeasurement(weight float64) error {
if weight < 6000 || weight > 65000 {
s.logger.Infof("Invalid weight: %.0f", weight)
Expand All @@ -178,6 +181,7 @@ func (s *Scale) AddMeasurement(weight float64) error {
s.mux.Lock()
defer s.mux.Unlock()

// set new values to the structure
s.weight = weight
s.weightAt = time.Now()
if serr := s.store.SetWeight(weight); serr != nil {
Expand All @@ -187,6 +191,23 @@ func (s *Scale) AddMeasurement(weight float64) error {
return fmt.Errorf("could not store weight_at: %w", serr)
}

// recalculate beers left
s.beersLeft = CalcBeersLeft(s.activeKeg, weight)
if serr := s.store.SetBeersLeft(s.beersLeft); serr != nil {
return fmt.Errorf("could not store beers_left: %w", serr)
}

// check empty keg
if s.beersLeft == 0 {
if serr := s.addCurrentKegToTotal(); serr != nil {
return fmt.Errorf("could not add current keg to total: %w", serr)
}
s.activeKeg = 0
if serr := s.store.SetActiveKeg(s.activeKeg); serr != nil {
return fmt.Errorf("could not store active_keg: %w", serr)
}
}

// check if keg is low
if !s.isLow {
s.isLow = IsKegLow(s.activeKeg, weight)
Expand All @@ -197,65 +218,18 @@ func (s *Scale) AddMeasurement(weight float64) error {
}
}

// we expect a new keg
// we need at least two measurements to be sure
// first measurement sets the candidate keg
// second measurement sets the active keg
// check if we expect a new keg
if s.activeKeg == 0 || s.isLow {
keg, err := GuessNewKegSize(weight)
if err == nil {
// we found a good candidate

if s.candidateKeg > 0 && s.candidateKeg == keg {
// we have two measurements with the same keg
s.candidateKeg = 0
s.activeKeg = keg
if serr := s.store.SetActiveKeg(keg); serr != nil {
return fmt.Errorf("could not store active_keg: %w", serr)
}

s.isLow = false
if serr := s.store.SetIsLow(false); serr != nil {
return fmt.Errorf("could not store is_low: %w", serr)
}

// remove keg from warehouse
index, err := GetWarehouseIndex(keg)
if err != nil {
return err
}
if s.warehouse[index] > 0 {
s.warehouse[index]--
if serr := s.store.SetWarehouse(s.warehouse); serr != nil {
return fmt.Errorf("could not update store warehouse: %w", serr)
}
} else {
s.logger.Warnf("Keg %d is not available in the warehouse", keg)
}

s.logger.Infof("New keg (%d l) CONFIRMED with current value %.0f", keg, weight)
s.discord.SendKeg(keg) // async
} else {
// new candidate keg
// we already know that the new keg is there, but we need to confirm it
s.logger.Infof("New keg candidate (%d l) REGISTERED with current value %.0f", keg, weight)
s.candidateKeg = keg
s.activeKeg = 0
}
if serr := s.tryNewKeg(); serr != nil {
return fmt.Errorf("could not try new keg: %w", serr)
}
}

// calculate values only if we know active keg
if s.activeKeg > 0 {
s.beersLeft = CalcBeersLeft(s.activeKeg, weight)
if serr := s.store.SetBeersLeft(s.beersLeft); serr != nil {
return fmt.Errorf("could not store beers_left: %w", serr)
}

s.monitor.Weight.WithLabelValues().Set(s.weight)
s.monitor.BeersLeft.WithLabelValues().Set(float64(s.beersLeft))
s.monitor.ActiveKeg.WithLabelValues().Set(float64(s.activeKeg))
}
// publish new values for prometheus
s.monitor.Weight.WithLabelValues().Set(s.weight)
s.monitor.BeersLeft.WithLabelValues().Set(float64(s.beersLeft))
s.monitor.ActiveKeg.WithLabelValues().Set(float64(s.activeKeg))
s.monitor.BeersTotal.WithLabelValues().Add(float64(s.getBeersTotal()))

return nil
}
Expand Down Expand Up @@ -380,3 +354,82 @@ func (s *Scale) updatePub(isOpen bool) {

s.monitor.PubIsOpen.WithLabelValues().Set(fIsOpen)
}

// tryNewKeg tries to find a new keg based on the current weight
// we need at least two measurements to be sure
// first measurement sets the candidate keg
// second measurement sets the active keg
func (s *Scale) tryNewKeg() error {
keg, err := GuessNewKegSize(s.weight)
if err == nil {
// we found a good candidate
if s.candidateKeg > 0 && s.candidateKeg == keg {
// we have two measurements with the same keg - rekeg successful !!!

if serr := s.addCurrentKegToTotal(); serr != nil {
return fmt.Errorf("could not add current keg to total: %w", serr)
}

s.candidateKeg = 0
s.activeKeg = keg
if serr := s.store.SetActiveKeg(keg); serr != nil {
return fmt.Errorf("could not store active_keg: %w", serr)
}

s.isLow = false
if serr := s.store.SetIsLow(false); serr != nil {
return fmt.Errorf("could not store is_low: %w", serr)
}

// remove keg from warehouse
index, err := GetWarehouseIndex(keg)
if err != nil {
return err
}
if s.warehouse[index] > 0 {
s.warehouse[index]--
if serr := s.store.SetWarehouse(s.warehouse); serr != nil {
return fmt.Errorf("could not update store warehouse: %w", serr)
}
} else {
s.logger.Warnf("Keg %d is not available in the warehouse", keg)
}

s.logger.Infof("New keg (%d l) CONFIRMED with current value %.0f", keg, s.weight)
s.discord.SendKeg(keg) // async
} else {
// new candidate keg
// we already know that the new keg is there, but we need to confirm it
s.logger.Infof("New keg candidate (%d l) REGISTERED with current value %.0f", keg, s.weight)
s.candidateKeg = keg
}
}

return nil
}

// getBeersTotal calculates the total amount of beers consumed
// adds two values together - total from the store and the current active keg
func (s *Scale) getBeersTotal() int {
total := s.beersTotal

if s.activeKeg > 0 {
total += CalcBeersConsumed(s.activeKeg, s.weight)
}

return total
}

func (s *Scale) addCurrentKegToTotal() error {
if s.activeKeg == 0 {
return nil // there is no active keg
}

s.beersTotal += s.activeKeg * 2 // liters to beers
s.monitor.BeersTotal.WithLabelValues().Add(float64(s.getBeersTotal()))
if err := s.store.SetBeersTotal(s.beersTotal); err != nil {
return fmt.Errorf("could not store beers_total: %w", err)
}

return nil
}
2 changes: 2 additions & 0 deletions backend/pkg/scale/scale_full_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type PubOutput struct {
type FullOutput struct {
IsOk bool `json:"is_ok"`
BeersLeft int `json:"beers_left"`
BeersTotal int `json:"beers_total"`
LastWeight float64 `json:"last_weight"`
LastWeightFormated string `json:"last_weight_formated"`
LastAt string `json:"last_at"`
Expand Down Expand Up @@ -51,6 +52,7 @@ func (s *Scale) GetScale() FullOutput {
output := FullOutput{
IsOk: s.isOk(),
BeersLeft: s.beersLeft,
BeersTotal: s.getBeersTotal(),
LastWeight: s.weight,
LastWeightFormated: fmt.Sprintf("%.2f", s.weight/1000),
LastAt: utils.FormatDate(s.weightAt),
Expand Down
3 changes: 0 additions & 3 deletions backend/pkg/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,4 @@ type Storage interface {

SetIsOpen(isOpen bool) error // set is open flag
GetIsOpen() (bool, error) // get is open flag

SetTotalBeers(totalBeers int) error // set total beers
GetTotalBeers() (int, error) // get total beers
}
8 changes: 0 additions & 8 deletions backend/pkg/store/store_fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,3 @@ func (s *FakeStore) SetIsOpen(_ bool) error {
func (s *FakeStore) GetIsOpen() (bool, error) {
return false, nil
}

func (s *FakeStore) SetTotalBeers(_ int) error {
return nil
}

func (s *FakeStore) GetTotalBeers() (int, error) {
return 0, nil
}
31 changes: 11 additions & 20 deletions backend/pkg/store/store_redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,17 @@ import (
)

const (
WeightKey = "weight"
WeightAtKey = "weight_at"
ActiveKegKey = "active_keg"
IsLowKey = "is_low"
BeersLeftKey = "beers_left"
BeersTotal = "beers_total"
WarehouseKey = "warehouse"
LastOkKey = "last_ok"
OpenAtKey = "open_at"
CloseAtKey = "close_at"
IsOpenKey = "is_open"
TotalBeersKey = "total_beers"
WeightKey = "weight"
WeightAtKey = "weight_at"
ActiveKegKey = "active_keg"
IsLowKey = "is_low"
BeersLeftKey = "beers_left"
BeersTotal = "beers_total"
WarehouseKey = "warehouse"
LastOkKey = "last_ok"
OpenAtKey = "open_at"
CloseAtKey = "close_at"
IsOpenKey = "is_open"
)

type RedisStore struct {
Expand Down Expand Up @@ -156,11 +155,3 @@ func (s *RedisStore) SetIsOpen(isOpen bool) error {
func (s *RedisStore) GetIsOpen() (bool, error) {
return s.Client.Get(s.ctx, IsOpenKey).Bool()
}

func (s *RedisStore) SetTotalBeers(totalBeers int) error {
return s.Client.Set(s.ctx, TotalBeersKey, totalBeers, 0).Err()
}

func (s *RedisStore) GetTotalBeers() (int, error) {
return s.Client.Get(s.ctx, TotalBeersKey).Int()
}
16 changes: 15 additions & 1 deletion backend/test.http
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,21 @@ POST http://localhost:8080/api/scale/push
Content-Type: text/plain
Authorization: test

push|1234|-74|32000.0
push|1234|-74|7100.0

### Value
POST http://localhost:8080/api/scale/push
Content-Type: text/plain
Authorization: test

push|1234|-74|14100.0

### Value
POST http://localhost:8080/api/scale/push
Content-Type: text/plain
Authorization: test

push|1234|-74|15800.0

### Ping
POST http://localhost:8080/api/scale/push
Expand Down
Loading

0 comments on commit ed52fc9

Please sign in to comment.