implement grpc gateway for http api
All checks were successful
Build and Publish / release (push) Has been skipped
Build and Publish / check-chart (push) Successful in 10s
Build and Publish / helm-release (push) Has been skipped

This commit is contained in:
2025-03-24 16:47:54 -04:00
parent 2c78dcc133
commit 9077ae354a
15 changed files with 668 additions and 187 deletions

View File

@ -0,0 +1,66 @@
package ambientgrpc
import (
"context"
"go.opentelemetry.io/otel/attribute"
otelcodes "go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel"
pb "gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/api/v1alpha1/weather"
"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/weather/recorder"
)
type GRPCWeather struct {
ctx context.Context
recorder *recorder.WeatherRecorder
tracer trace.Tracer
*pb.UnimplementedAmbientLocalWeatherServiceServer
}
func NewGRPCWeather(ctx context.Context, recorder *recorder.WeatherRecorder) *GRPCWeather {
return &GRPCWeather{
ctx: ctx,
recorder: recorder,
tracer: otel.GetTracer(ctx, "grpcWeather"),
}
}
func (w *GRPCWeather) GetWeather(ctx context.Context, req *pb.GetWeatherRequest) (
*pb.GetWeatherResponse, error,
) {
ctx, span := w.tracer.Start(ctx, "getWeather")
defer span.End()
limit := 1
if req.Limit != nil {
if *req.Limit > 1 {
limit = int(*req.Limit)
}
}
span.SetAttributes(attribute.Int("limit", limit))
updates, err := w.recorder.Get(ctx, req)
if err != nil {
span.RecordError(err)
span.SetStatus(otelcodes.Error, err.Error())
return nil, status.Error(codes.Internal, err.Error())
} else if len(updates) < 1 {
return nil, status.Error(codes.OutOfRange, "no weather available")
}
span.SetStatus(otelcodes.Ok, "")
return &pb.GetWeatherResponse{
LastUpdated: &timestamppb.Timestamp{
Seconds: updates[0].DateUTC.Unix(),
},
WeatherUpdates: UpdatesToPbUpdates(updates),
}, nil
}

View File

@ -0,0 +1,90 @@
package ambientgrpc
import (
"slices"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
pb "gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/api/v1alpha1/weather"
"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/util"
"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/weather"
)
// Converts a slice of weather.WeatherUpdate to proto structs
func UpdatesToPbUpdates(u []*weather.WeatherUpdate) []*pb.WeatherUpdate {
updates := make([]*pb.WeatherUpdate, len(u))
for i, update := range u {
updates[i] = UpdateToPbUpdate(update)
}
return updates
}
// Maps a weather.WeatherUpdate to the proto version.
//
// This should be auto-generated code, but as the proto should be
// stable, it is probably not worth the effort.
func UpdateToPbUpdate(u *weather.WeatherUpdate) *pb.WeatherUpdate {
if u.DateUTC == nil {
now := time.Now()
u.DateUTC = &now
}
return &pb.WeatherUpdate{
StationName: u.StationConfig.Name,
StationType: util.DerefStr(u.StationType),
StationId: util.DerefStr(u.StationID),
TempOutdoorF: u.TempOutdoorF,
TempIndoorF: u.TempIndoorF,
HumidityOutdoor: util.Int32ptr(u.HumidityOudoor),
HumidityIndoor: util.Int32ptr(u.HumidityIndoor),
WindSpeedMph: u.WindSpeedMPH,
WindGustMph: u.WindGustMPH,
MaxDailyGust: u.MaxDailyGust,
WindDir: util.Int32ptr(u.WindDir),
WindDirAvg_10M: util.Int32ptr(u.WindDirAvg10m),
Uv: util.Int32ptr(u.UV),
SolarRadiation: u.SolarRadiation,
HourlyRainIn: u.HourlyRainIn,
EventRainIn: u.EventRainIn,
DailyRainIn: u.DailyRainIn,
WeeklyRainIn: u.WeeklyRainIn,
MonthlyRainIn: u.MonthlyRainIn,
YearlyRainIn: u.YearlyRainIn,
TotalRainIn: u.TotalRainIn,
Batteries: batteriesToPbBatteries(u.Batteries),
BaromRelativeIn: u.BaromRelativeIn,
BaromAbsoluteIn: u.BaromAbsoluteIn,
DewPointF: u.DewPointF,
WindChillF: u.WindChillF,
TempHumiditySensors: thSensorsToPbSensors(u.TempHumiditySensors),
UpdateTimestamp: timestamppb.New(*u.DateUTC),
}
}
func batteriesToPbBatteries(batteries []weather.BatteryStatus) []*pb.BatteryStatus {
pbBatteries := make([]*pb.BatteryStatus, 0, len(batteries))
for _, b := range batteries {
if b.Status != nil {
pbBatteries = append(pbBatteries, &pb.BatteryStatus{
Component: b.Component,
Status: util.Int32ptr(b.Status),
})
}
}
return slices.Clip(pbBatteries)
}
func thSensorsToPbSensors(sensors []*weather.TempHumiditySensor) []*pb.TempHumiditySensor {
pbSensors := make([]*pb.TempHumiditySensor, 0, len(sensors))
for _, s := range sensors {
if s.TempF != nil || s.Humidity != nil {
pbSensors = append(pbSensors, &pb.TempHumiditySensor{
Name: s.Name,
TempF: s.TempF,
Humidity: util.Int32ptr(s.Humidity),
})
}
}
return slices.Clip(pbSensors)
}

View File

@ -0,0 +1,117 @@
package ambientgrpc
import (
"reflect"
"testing"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
"k8s.io/utils/ptr"
pb "gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/api/v1alpha1/weather"
"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/ambient/config"
"gitea.libretechconsulting.com/rmcguire/ambient-local-exporter/pkg/weather"
)
var now = time.Now()
var mockUpdate = &weather.WeatherUpdate{
DateUTC: &now,
StationConfig: &config.WeatherStation{
Name: "50W",
Equipment: "WS-5000",
},
StationID: ptr.To("50W"),
StationType: ptr.To("WS-5000"),
TempOutdoorF: ptr.To(97.6),
TempIndoorF: ptr.To(77.6),
HumidityOudoor: ptr.To(50),
HumidityIndoor: ptr.To(50),
WindSpeedMPH: ptr.To(20.5),
WindGustMPH: ptr.To(30.5),
MaxDailyGust: ptr.To(40.0),
WindDir: ptr.To(180),
WindDirAvg10m: ptr.To(180),
UV: nil,
SolarRadiation: ptr.To(9.999),
HourlyRainIn: ptr.To(9.999),
EventRainIn: ptr.To(9.999),
DailyRainIn: ptr.To(9.999),
WeeklyRainIn: ptr.To(9.999),
MonthlyRainIn: ptr.To(9.999),
YearlyRainIn: ptr.To(9.999),
TotalRainIn: ptr.To(9.999),
Batteries: []weather.BatteryStatus{
{Component: "battery1", Status: ptr.To(1)},
{Component: "battery2", Status: ptr.To(1)},
},
BaromRelativeIn: ptr.To(9.999),
BaromAbsoluteIn: ptr.To(9.999),
DewPointF: ptr.To(9.999),
WindChillF: ptr.To(9.999),
TempHumiditySensors: []*weather.TempHumiditySensor{
{Name: "sensor1", TempF: ptr.To(99.999), Humidity: nil},
},
}
func TestUpdateToPbUpdate(t *testing.T) {
type args struct {
u *weather.WeatherUpdate
}
tests := []struct {
name string
args args
want *pb.WeatherUpdate
}{
{
name: "Map Update to PB",
args: args{u: mockUpdate},
want: &pb.WeatherUpdate{
StationName: mockUpdate.StationConfig.Name,
StationType: *mockUpdate.StationType,
StationId: *mockUpdate.StationID,
TempOutdoorF: mockUpdate.TempOutdoorF,
TempIndoorF: mockUpdate.TempIndoorF,
HumidityOutdoor: ptr.To(int32(*mockUpdate.HumidityOudoor)),
HumidityIndoor: ptr.To(int32(*mockUpdate.HumidityIndoor)),
WindSpeedMph: mockUpdate.WindSpeedMPH,
WindGustMph: mockUpdate.WindGustMPH,
MaxDailyGust: mockUpdate.MaxDailyGust,
WindDir: ptr.To(int32(*mockUpdate.WindDir)),
WindDirAvg_10M: ptr.To(int32(*mockUpdate.WindDirAvg10m)),
Uv: nil,
SolarRadiation: mockUpdate.SolarRadiation,
HourlyRainIn: mockUpdate.HourlyRainIn,
EventRainIn: mockUpdate.EventRainIn,
DailyRainIn: mockUpdate.DailyRainIn,
WeeklyRainIn: mockUpdate.WeeklyRainIn,
MonthlyRainIn: mockUpdate.MonthlyRainIn,
YearlyRainIn: mockUpdate.YearlyRainIn,
TotalRainIn: mockUpdate.TotalRainIn,
Batteries: []*pb.BatteryStatus{
{Component: mockUpdate.Batteries[0].Component, Status: ptr.To(int32(*mockUpdate.Batteries[0].Status))},
{Component: mockUpdate.Batteries[1].Component, Status: ptr.To(int32(*mockUpdate.Batteries[1].Status))},
},
BaromRelativeIn: mockUpdate.BaromRelativeIn,
BaromAbsoluteIn: mockUpdate.BaromAbsoluteIn,
DewPointF: mockUpdate.DewPointF,
WindChillF: mockUpdate.WindChillF,
TempHumiditySensors: []*pb.TempHumiditySensor{
{
Name: mockUpdate.TempHumiditySensors[0].Name,
TempF: mockUpdate.TempHumiditySensors[0].TempF,
Humidity: nil,
},
},
UpdateTimestamp: timestamppb.New(now),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := UpdateToPbUpdate(tt.args.u); !reflect.DeepEqual(got, tt.want) {
t.Errorf("UpdateToPbUpdate() = %v, want %v", got, tt.want)
}
})
}
}