From a96585b3cdd2d4207a376fea7027c864760e1979 Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Fri, 2 Sep 2022 01:33:37 +0200 Subject: [PATCH] feat: Add heating_schedule resource --- .../provider/heating_schedule_resource.go | 599 ++++++++++++++++++ .../heating_schedule_resource_test.go | 226 +++++++ internal/provider/provider.go | 1 + internal/provider/util.go | 11 + internal/provider/util_test.go | 11 + 5 files changed, 848 insertions(+) create mode 100644 internal/provider/heating_schedule_resource.go create mode 100644 internal/provider/heating_schedule_resource_test.go diff --git a/internal/provider/heating_schedule_resource.go b/internal/provider/heating_schedule_resource.go new file mode 100644 index 0000000..5ab3d3c --- /dev/null +++ b/internal/provider/heating_schedule_resource.go @@ -0,0 +1,599 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "github.com/gonzolino/gotado/v2" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure provider defined types fully satisfy framework interfaces +var _ resource.Resource = &HeatingScheduleResource{} +var _ resource.ResourceWithImportState = &HeatingScheduleResource{} + +func NewHeatingScheduleResource() resource.Resource { + return &HeatingScheduleResource{} +} + +type HeatingScheduleResource struct { + client *gotado.Tado + username string + password string +} + +type TimeBlockModel struct { + Heating types.Bool `tfsdk:"heating"` + Temperature types.Float64 `tfsdk:"temperature"` + Start types.String `tfsdk:"start"` + End types.String `tfsdk:"end"` + GeofencingControl types.Bool `tfsdk:"geofencing_control"` +} + +type HeatingScheduleResourceModel struct { + ID types.String `tfsdk:"id"` + HomeName types.String `tfsdk:"home_name"` + ZoneName types.String `tfsdk:"zone_name"` + MonSun []TimeBlockModel `tfsdk:"mon_sun"` + MonFri []TimeBlockModel `tfsdk:"mon_fri"` + Mon []TimeBlockModel `tfsdk:"mon"` + Tue []TimeBlockModel `tfsdk:"tue"` + Wed []TimeBlockModel `tfsdk:"wed"` + Thu []TimeBlockModel `tfsdk:"thu"` + Fri []TimeBlockModel `tfsdk:"fri"` + Sat []TimeBlockModel `tfsdk:"sat"` + Sun []TimeBlockModel `tfsdk:"sun"` +} + +var timeBlockAttributes = map[string]tfsdk.Attribute{ + "heating": { + MarkdownDescription: "Whether heating should be turned on or off", + Type: types.BoolType, + Required: true, + }, + "temperature": { + MarkdownDescription: "The temperature to set the heating to. Required when 'heating' is true", + Type: types.Float64Type, + Optional: true, + }, + "start": { + MarkdownDescription: "When the timeblock starts. Format must be 'hh:mm'.", + Type: types.StringType, + Required: true, + }, + "end": { + MarkdownDescription: "When the timeblock ends. Format must be 'hh:mm'.", + Type: types.StringType, + Required: true, + }, + "geofencing_control": { + MarkdownDescription: "Whether the settings of this time block are overwritten by the tado away settings. Defaults to 'true'.", + Type: types.BoolType, + Optional: true, + Computed: true, + }, +} + +func (*HeatingScheduleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_heating_schedule" +} + +func (HeatingScheduleResource) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "The heating schedule of a zone.", + + Attributes: map[string]tfsdk.Attribute{ + "id": { + MarkdownDescription: "ID of this heating schedule resource.", + Type: types.StringType, + Computed: true, + }, + "home_name": { + MarkdownDescription: "Name of the home this heating schedule resource belongs to.", + Type: types.StringType, + Required: true, + }, + "zone_name": { + MarkdownDescription: "Name of the zone of this heating schedule.", + Type: types.StringType, + Required: true, + }, + "mon_sun": { + MarkdownDescription: "Schedule for Monday - Sunday.", + Optional: true, + Attributes: tfsdk.ListNestedAttributes(timeBlockAttributes), + }, + "mon_fri": { + MarkdownDescription: "Schedule for Monday - Friday.", + Optional: true, + Attributes: tfsdk.ListNestedAttributes(timeBlockAttributes), + }, + "mon": { + MarkdownDescription: "Schedule for Monday.", + Optional: true, + Attributes: tfsdk.ListNestedAttributes(timeBlockAttributes), + }, + "tue": { + MarkdownDescription: "Schedule for Tuesday.", + Optional: true, + Attributes: tfsdk.ListNestedAttributes(timeBlockAttributes), + }, + "wed": { + MarkdownDescription: "Schedule for Wednesday.", + Optional: true, + Attributes: tfsdk.ListNestedAttributes(timeBlockAttributes), + }, + "thu": { + MarkdownDescription: "Schedule for Thursday.", + Optional: true, + Attributes: tfsdk.ListNestedAttributes(timeBlockAttributes), + }, + "fri": { + MarkdownDescription: "Schedule for Friday.", + Optional: true, + Attributes: tfsdk.ListNestedAttributes(timeBlockAttributes), + }, + "sat": { + MarkdownDescription: "Schedule for Saturday.", + Optional: true, + Attributes: tfsdk.ListNestedAttributes(timeBlockAttributes), + }, + "sun": { + MarkdownDescription: "Schedule for Sunday.", + Optional: true, + Attributes: tfsdk.ListNestedAttributes(timeBlockAttributes), + }, + }, + }, nil +} + +func (r *HeatingScheduleResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + data, ok := req.ProviderData.(*tadoProviderData) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *tadoProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = data.client + r.username = data.username + r.password = data.password +} + +func (r HeatingScheduleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data HeatingScheduleResourceModel + + diags := req.Config.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + me, err := r.client.Me(ctx, r.username, r.password) + if err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to authenticate with Tado: %v", err)) + return + } + + homeName := data.HomeName.ValueString() + home, err := me.GetHome(ctx, homeName) + if err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to get home '%s': %v", homeName, err)) + return + } + + zoneName := data.ZoneName.ValueString() + zone, err := home.GetZone(ctx, zoneName) + if err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to get zone '%s': %v", zoneName, err)) + return + } + + schedule, diags := heatingScheduleResourceModelToObject(ctx, data, zone) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + if err := zone.SetHeatingSchedule(ctx, schedule); err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to create heating schedule for zone '%s': %v", zone.Name, err)) + return + } + + schedule, err = zone.GetHeatingSchedule(ctx) + if err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to get created heating schedule for zone '%s': %v", zone.Name, err)) + return + } + + heatingScheduleToResourceData(ctx, schedule, &data) + diags = resp.State.Set(ctx, &data) + resp.Diagnostics.Append(diags...) +} + +func (r HeatingScheduleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data HeatingScheduleResourceModel + + diags := req.State.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + me, err := r.client.Me(ctx, r.username, r.password) + if err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to authenticate with Tado: %v", err)) + return + } + + homeName := data.HomeName.ValueString() + home, err := me.GetHome(ctx, homeName) + if err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to get home '%s': %v", homeName, err)) + return + } + + zoneName := data.ZoneName.ValueString() + zone, err := home.GetZone(ctx, zoneName) + if err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to get zone '%s': %v", zoneName, err)) + return + } + + schedule, err := zone.GetHeatingSchedule(ctx) + if err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to get heating schedule for zone '%s': %v", zone.Name, err)) + return + } + + heatingScheduleToResourceData(ctx, schedule, &data) + + diags = resp.State.Set(ctx, &data) + resp.Diagnostics.Append(diags...) +} + +func (r HeatingScheduleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data HeatingScheduleResourceModel + + diags := req.Plan.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + me, err := r.client.Me(ctx, r.username, r.password) + if err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to authenticate with Tado: %v", err)) + return + } + + homeName := data.HomeName.ValueString() + home, err := me.GetHome(ctx, homeName) + if err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to get home '%s': %v", homeName, err)) + return + } + + zoneName := data.ZoneName.ValueString() + zone, err := home.GetZone(ctx, zoneName) + if err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to get zone '%s': %v", zoneName, err)) + return + } + + schedule, diags := heatingScheduleResourceModelToObject(ctx, data, zone) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + if err := zone.SetHeatingSchedule(ctx, schedule); err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to create heating schedule for zone '%s': %v", zone.Name, err)) + return + } + + schedule, err = zone.GetHeatingSchedule(ctx) + if err != nil { + resp.Diagnostics.AddError("Tado API Error", fmt.Sprintf("Unable to get created heating schedule for zone '%s': %v", zone.Name, err)) + return + } + + heatingScheduleToResourceData(ctx, schedule, &data) + diags = resp.State.Set(ctx, &data) + resp.Diagnostics.Append(diags...) +} + +func (r HeatingScheduleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data HeatingScheduleResourceModel + + diags := req.State.Get(ctx, &data) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + // A schedule can't be deleted, so we simply 'forget' it +} + +func (r HeatingScheduleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + splittedID := strings.Split(req.ID, "/") + if len(splittedID) != 2 { + resp.Diagnostics.AddError("Resource Import ID invalid", fmt.Sprintf("ID '%s' should be in format 'home_name/zone_name'", req.ID)) + return + } + + homeName, zoneName := splittedID[0], splittedID[1] + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("home_name"), homeName)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("zone_name"), zoneName)...) +} + +// isMonSunSchedule checks if the heating schedule has a valid Monday - Sunday schedule +func isMonSunSchedule(data HeatingScheduleResourceModel) bool { + return data.MonSun != nil && data.MonFri == nil && data.Mon == nil && data.Tue == nil && data.Wed == nil && data.Thu == nil && data.Fri == nil && data.Sat == nil && data.Sun == nil +} + +// isMonFriSatSunSchedule checks if the heating schedule has a valid Monday - Friday, Saturday, Sunday schedule +func isMonFriSatSunSchedule(data HeatingScheduleResourceModel) bool { + return data.MonSun == nil && data.MonFri != nil && data.Mon == nil && data.Tue == nil && data.Wed == nil && data.Thu == nil && data.Fri == nil && data.Sat != nil && data.Sun != nil +} + +// isMonTueWedThuFriSatSunSchedule checks if the heating schedule has a valid Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday schedule +func isMonTueWedThuFriSatSunSchedule(data HeatingScheduleResourceModel) bool { + return data.MonSun == nil && data.MonFri == nil && data.Mon != nil && data.Tue != nil && data.Wed != nil && data.Thu != nil && data.Fri != nil && data.Sat != nil && data.Sun != nil +} + +func heatingScheduleToResourceData(ctx context.Context, schedule *gotado.HeatingSchedule, data *HeatingScheduleResourceModel) { + homeName, zoneName := data.HomeName.ValueString(), data.ZoneName.ValueString() + data.ID = types.String{Value: fmt.Sprintf("%s/%s", homeName, zoneName)} + data.HomeName = types.String{Value: homeName} + data.ZoneName = types.String{Value: zoneName} + + sortedBlocks := sortTimeBlocksByDayType(schedule.Blocks) + + switch schedule.ScheduleDays { + case gotado.ScheduleDaysMonToSun: + data.MonSun = make([]TimeBlockModel, len(sortedBlocks[gotado.DayTypeMondayToSunday])) + for i, block := range sortedBlocks[gotado.DayTypeMondayToSunday] { + timeBlockObjectToTimeBlockModel(ctx, block, &data.MonSun[i]) + } + case gotado.ScheduleDaysMonToFriSatSun: + data.MonFri = make([]TimeBlockModel, len(sortedBlocks[gotado.DayTypeMondayToFriday])) + data.Sat = make([]TimeBlockModel, len(sortedBlocks[gotado.DayTypeSaturday])) + data.Sun = make([]TimeBlockModel, len(sortedBlocks[gotado.DayTypeSunday])) + for i, block := range sortedBlocks[gotado.DayTypeMondayToFriday] { + timeBlockObjectToTimeBlockModel(ctx, block, &data.MonFri[i]) + } + for i, block := range sortedBlocks[gotado.DayTypeSaturday] { + timeBlockObjectToTimeBlockModel(ctx, block, &data.Sat[i]) + } + for i, block := range sortedBlocks[gotado.DayTypeSunday] { + timeBlockObjectToTimeBlockModel(ctx, block, &data.Sun[i]) + } + case gotado.ScheduleDaysMonTueWedThuFriSatSun: + data.Mon = make([]TimeBlockModel, len(sortedBlocks[gotado.DayTypeMonday])) + data.Tue = make([]TimeBlockModel, len(sortedBlocks[gotado.DayTypeTuesday])) + data.Wed = make([]TimeBlockModel, len(sortedBlocks[gotado.DayTypeWednesday])) + data.Thu = make([]TimeBlockModel, len(sortedBlocks[gotado.DayTypeThursday])) + data.Fri = make([]TimeBlockModel, len(sortedBlocks[gotado.DayTypeFriday])) + data.Sat = make([]TimeBlockModel, len(sortedBlocks[gotado.DayTypeSaturday])) + data.Sun = make([]TimeBlockModel, len(sortedBlocks[gotado.DayTypeSunday])) + for i, block := range sortedBlocks[gotado.DayTypeMonday] { + timeBlockObjectToTimeBlockModel(ctx, block, &data.Mon[i]) + } + for i, block := range sortedBlocks[gotado.DayTypeTuesday] { + timeBlockObjectToTimeBlockModel(ctx, block, &data.Tue[i]) + } + for i, block := range sortedBlocks[gotado.DayTypeWednesday] { + timeBlockObjectToTimeBlockModel(ctx, block, &data.Wed[i]) + } + for i, block := range sortedBlocks[gotado.DayTypeThursday] { + timeBlockObjectToTimeBlockModel(ctx, block, &data.Thu[i]) + } + for i, block := range sortedBlocks[gotado.DayTypeFriday] { + timeBlockObjectToTimeBlockModel(ctx, block, &data.Fri[i]) + } + for i, block := range sortedBlocks[gotado.DayTypeSaturday] { + timeBlockObjectToTimeBlockModel(ctx, block, &data.Sat[i]) + } + for i, block := range sortedBlocks[gotado.DayTypeSunday] { + timeBlockObjectToTimeBlockModel(ctx, block, &data.Sun[i]) + } + } +} + +func heatingScheduleResourceModelToObject(ctx context.Context, data HeatingScheduleResourceModel, zone *gotado.Zone) (*gotado.HeatingSchedule, diag.Diagnostics) { + var err error + var schedule *gotado.HeatingSchedule + diags := diag.Diagnostics{} + switch { + case isMonSunSchedule(data): + schedule, err = zone.ScheduleMonToSun(ctx) + if err != nil { + diags.AddError("Tado API Error", fmt.Sprintf("Unable to initialize schedule for zone '%s': %v", zone.Name, err)) + return nil, diags + } + first := data.MonSun[0] + power := boolToPower(first.Heating.ValueBool()) + schedule.NewTimeBlock(ctx, gotado.DayTypeMondayToSunday, + first.Start.ValueString(), + first.End.ValueString(), + first.GeofencingControl.ValueBool(), + power, + first.Temperature.ValueFloat64()) + for _, block := range data.MonSun[1:] { + power := boolToPower(block.Heating.ValueBool()) + schedule.AddTimeBlock(ctx, gotado.DayTypeMondayToSunday, + block.Start.ValueString(), + block.End.ValueString(), + block.GeofencingControl.ValueBool(), + power, + block.Temperature.ValueFloat64()) + } + case isMonFriSatSunSchedule(data): + schedule, err = zone.ScheduleMonToFriSatSun(ctx) + if err != nil { + diags.AddError("Tado API Error", fmt.Sprintf("Unable to initialize schedule for zone '%s': %v", zone.Name, err)) + return nil, diags + } + first := data.MonFri[0] + power := boolToPower(first.Heating.ValueBool()) + schedule.NewTimeBlock(ctx, gotado.DayTypeMondayToFriday, + first.Start.ValueString(), + first.End.ValueString(), + first.GeofencingControl.ValueBool(), + power, + first.Temperature.ValueFloat64()) + for _, block := range data.MonFri[1:] { + power := boolToPower(block.Heating.ValueBool()) + schedule.AddTimeBlock(ctx, gotado.DayTypeMondayToFriday, + block.Start.ValueString(), + block.End.ValueString(), + block.GeofencingControl.ValueBool(), + power, + block.Temperature.ValueFloat64()) + } + for _, block := range data.Sat { + power := boolToPower(block.Heating.ValueBool()) + schedule.AddTimeBlock(ctx, gotado.DayTypeSaturday, + block.Start.ValueString(), + block.End.ValueString(), + block.GeofencingControl.ValueBool(), + power, + block.Temperature.ValueFloat64()) + } + for _, block := range data.Sun { + power := boolToPower(block.Heating.ValueBool()) + schedule.AddTimeBlock(ctx, gotado.DayTypeSunday, + block.Start.ValueString(), + block.End.ValueString(), + block.GeofencingControl.ValueBool(), + power, + block.Temperature.ValueFloat64()) + } + case isMonTueWedThuFriSatSunSchedule(data): + schedule, err = zone.ScheduleAllDays(ctx) + if err != nil { + diags.AddError("Tado API Error", fmt.Sprintf("Unable to initialize schedule for zone '%s': %v", zone.Name, err)) + return nil, diags + } + first := data.Mon[0] + power := boolToPower(first.Heating.ValueBool()) + schedule.NewTimeBlock(ctx, gotado.DayTypeMonday, + first.Start.ValueString(), + first.End.ValueString(), + first.GeofencingControl.ValueBool(), + power, + first.Temperature.ValueFloat64()) + for _, block := range data.Mon[1:] { + power := boolToPower(block.Heating.ValueBool()) + schedule.AddTimeBlock(ctx, gotado.DayTypeMonday, + block.Start.ValueString(), + block.End.ValueString(), + block.GeofencingControl.ValueBool(), + power, + block.Temperature.ValueFloat64()) + } + for _, block := range data.Tue { + power := boolToPower(block.Heating.ValueBool()) + schedule.AddTimeBlock(ctx, gotado.DayTypeTuesday, + block.Start.ValueString(), + block.End.ValueString(), + block.GeofencingControl.ValueBool(), + power, + block.Temperature.ValueFloat64()) + } + for _, block := range data.Wed { + power := boolToPower(block.Heating.ValueBool()) + schedule.AddTimeBlock(ctx, gotado.DayTypeWednesday, + block.Start.ValueString(), + block.End.ValueString(), + block.GeofencingControl.ValueBool(), + power, + block.Temperature.ValueFloat64()) + } + for _, block := range data.Thu { + power := boolToPower(block.Heating.ValueBool()) + schedule.AddTimeBlock(ctx, gotado.DayTypeThursday, + block.Start.ValueString(), + block.End.ValueString(), + block.GeofencingControl.ValueBool(), + power, + block.Temperature.ValueFloat64()) + } + for _, block := range data.Fri { + power := boolToPower(block.Heating.ValueBool()) + schedule.AddTimeBlock(ctx, gotado.DayTypeFriday, + block.Start.ValueString(), + block.End.ValueString(), + block.GeofencingControl.ValueBool(), + power, + block.Temperature.ValueFloat64()) + } + for _, block := range data.Sat { + power := boolToPower(block.Heating.ValueBool()) + schedule.AddTimeBlock(ctx, gotado.DayTypeSaturday, + block.Start.ValueString(), + block.End.ValueString(), + block.GeofencingControl.ValueBool(), + power, + block.Temperature.ValueFloat64()) + } + for _, block := range data.Sun { + power := boolToPower(block.Heating.ValueBool()) + schedule.AddTimeBlock(ctx, gotado.DayTypeSunday, + block.Start.ValueString(), + block.End.ValueString(), + block.GeofencingControl.ValueBool(), + power, + block.Temperature.ValueFloat64()) + } + default: + diags.AddError("Invalid Heating Schedule", fmt.Sprintf("Unable to create heating schedule for zone '%s': No valid schedule provided", zone.Name)) + return nil, diags + } + return schedule, nil +} + +func sortTimeBlocksByDayType(blocks []*gotado.ScheduleTimeBlock) map[gotado.DayType][]*gotado.ScheduleTimeBlock { + sortedBlocks := make(map[gotado.DayType][]*gotado.ScheduleTimeBlock, len(blocks)) + + for _, block := range blocks { + if _, ok := sortedBlocks[block.DayType]; !ok { + sortedBlocks[block.DayType] = make([]*gotado.ScheduleTimeBlock, 0) + } + sortedBlocks[block.DayType] = append(sortedBlocks[block.DayType], block) + } + + return sortedBlocks +} + +func timeBlockObjectToTimeBlockModel(ctx context.Context, block *gotado.ScheduleTimeBlock, model *TimeBlockModel) { + model.Heating = types.Bool{Value: block.Setting.Power == "ON"} + if block.Setting.Temperature != nil { + model.Temperature = types.Float64{Value: block.Setting.Temperature.Celsius} + } + model.Start = types.String{Value: block.Start} + model.End = types.String{Value: block.End} + model.GeofencingControl = types.Bool{Value: !block.GeolocationOverride} +} diff --git a/internal/provider/heating_schedule_resource_test.go b/internal/provider/heating_schedule_resource_test.go new file mode 100644 index 0000000..074352a --- /dev/null +++ b/internal/provider/heating_schedule_resource_test.go @@ -0,0 +1,226 @@ +package provider + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestIsMonSunSchedule(t *testing.T) { + timeBlock := TimeBlockModel{Heating: types.Bool{}, Temperature: types.Float64{}, Start: types.String{}, End: types.String{}} + cases := []struct { + schedule HeatingScheduleResourceModel + expected bool + }{ + // valid mon-sun schedule + { + schedule: HeatingScheduleResourceModel{ + MonSun: []TimeBlockModel{timeBlock}, + }, + expected: true, + }, + // valid mon-fri, sat, sun schedule + { + schedule: HeatingScheduleResourceModel{ + MonFri: []TimeBlockModel{timeBlock}, + Sat: []TimeBlockModel{timeBlock}, + Sun: []TimeBlockModel{timeBlock}, + }, + expected: false, + }, + // valid mon, tue, wed, thu, fri, sat, sun schedule + { + schedule: HeatingScheduleResourceModel{ + Mon: []TimeBlockModel{timeBlock}, + Tue: []TimeBlockModel{timeBlock}, + Wed: []TimeBlockModel{timeBlock}, + Thu: []TimeBlockModel{timeBlock}, + Fri: []TimeBlockModel{timeBlock}, + Sat: []TimeBlockModel{timeBlock}, + Sun: []TimeBlockModel{timeBlock}, + }, + expected: false, + }, + // invalid empty schedule + { + schedule: HeatingScheduleResourceModel{}, + expected: false, + }, + // invalid mixed schedule + { + schedule: HeatingScheduleResourceModel{ + MonSun: []TimeBlockModel{timeBlock}, + MonFri: []TimeBlockModel{timeBlock}, + }, + expected: false, + }, + // invalid full schedule + { + schedule: HeatingScheduleResourceModel{ + MonSun: []TimeBlockModel{timeBlock}, + MonFri: []TimeBlockModel{timeBlock}, + Mon: []TimeBlockModel{timeBlock}, + Tue: []TimeBlockModel{timeBlock}, + Wed: []TimeBlockModel{timeBlock}, + Thu: []TimeBlockModel{timeBlock}, + Fri: []TimeBlockModel{timeBlock}, + Sat: []TimeBlockModel{timeBlock}, + Sun: []TimeBlockModel{timeBlock}, + }, + expected: false, + }, + } + + for _, c := range cases { + actual := isMonSunSchedule(c.schedule) + if actual != c.expected { + t.Fatalf("Expected: %t, got: %t", c.expected, actual) + } + } +} + +func TestIsMonFriSatSunSchedule(t *testing.T) { + timeBlock := TimeBlockModel{Heating: types.Bool{}, Temperature: types.Float64{}, Start: types.String{}, End: types.String{}} + cases := []struct { + schedule HeatingScheduleResourceModel + expected bool + }{ + // valid mon-sun schedule + { + schedule: HeatingScheduleResourceModel{ + MonSun: []TimeBlockModel{timeBlock}, + }, + expected: false, + }, + // valid mon-fri, sat, sun schedule + { + schedule: HeatingScheduleResourceModel{ + MonFri: []TimeBlockModel{timeBlock}, + Sat: []TimeBlockModel{timeBlock}, + Sun: []TimeBlockModel{timeBlock}, + }, + expected: true, + }, + // valid mon, tue, wed, thu, fri, sat, sun schedule + { + schedule: HeatingScheduleResourceModel{ + Mon: []TimeBlockModel{timeBlock}, + Tue: []TimeBlockModel{timeBlock}, + Wed: []TimeBlockModel{timeBlock}, + Thu: []TimeBlockModel{timeBlock}, + Fri: []TimeBlockModel{timeBlock}, + Sat: []TimeBlockModel{timeBlock}, + Sun: []TimeBlockModel{timeBlock}, + }, + expected: false, + }, + // invalid empty schedule + { + schedule: HeatingScheduleResourceModel{}, + expected: false, + }, + // invalid mixed schedule + { + schedule: HeatingScheduleResourceModel{ + MonSun: []TimeBlockModel{timeBlock}, + MonFri: []TimeBlockModel{timeBlock}, + }, + expected: false, + }, + // invalid full schedule + { + schedule: HeatingScheduleResourceModel{ + MonSun: []TimeBlockModel{timeBlock}, + MonFri: []TimeBlockModel{timeBlock}, + Mon: []TimeBlockModel{timeBlock}, + Tue: []TimeBlockModel{timeBlock}, + Wed: []TimeBlockModel{timeBlock}, + Thu: []TimeBlockModel{timeBlock}, + Fri: []TimeBlockModel{timeBlock}, + Sat: []TimeBlockModel{timeBlock}, + Sun: []TimeBlockModel{timeBlock}, + }, + expected: false, + }, + } + + for _, c := range cases { + actual := isMonFriSatSunSchedule(c.schedule) + if actual != c.expected { + t.Fatalf("Expected: %t, got: %t", c.expected, actual) + } + } +} + +func TestIsMonTueWedThuFriSatSunSchedule(t *testing.T) { + timeBlock := TimeBlockModel{Heating: types.Bool{}, Temperature: types.Float64{}, Start: types.String{}, End: types.String{}} + cases := []struct { + schedule HeatingScheduleResourceModel + expected bool + }{ + // valid mon-sun schedule + { + schedule: HeatingScheduleResourceModel{ + MonSun: []TimeBlockModel{timeBlock}, + }, + expected: false, + }, + // valid mon-fri, sat, sun schedule + { + schedule: HeatingScheduleResourceModel{ + MonFri: []TimeBlockModel{timeBlock}, + Sat: []TimeBlockModel{timeBlock}, + Sun: []TimeBlockModel{timeBlock}, + }, + expected: false, + }, + // valid mon, tue, wed, thu, fri, sat, sun schedule + { + schedule: HeatingScheduleResourceModel{ + Mon: []TimeBlockModel{timeBlock}, + Tue: []TimeBlockModel{timeBlock}, + Wed: []TimeBlockModel{timeBlock}, + Thu: []TimeBlockModel{timeBlock}, + Fri: []TimeBlockModel{timeBlock}, + Sat: []TimeBlockModel{timeBlock}, + Sun: []TimeBlockModel{timeBlock}, + }, + expected: true, + }, + // invalid empty schedule + { + schedule: HeatingScheduleResourceModel{}, + expected: false, + }, + // invalid mixed schedule + { + schedule: HeatingScheduleResourceModel{ + MonSun: []TimeBlockModel{timeBlock}, + MonFri: []TimeBlockModel{timeBlock}, + }, + expected: false, + }, + // invalid full schedule + { + schedule: HeatingScheduleResourceModel{ + MonSun: []TimeBlockModel{timeBlock}, + MonFri: []TimeBlockModel{timeBlock}, + Mon: []TimeBlockModel{timeBlock}, + Tue: []TimeBlockModel{timeBlock}, + Wed: []TimeBlockModel{timeBlock}, + Thu: []TimeBlockModel{timeBlock}, + Fri: []TimeBlockModel{timeBlock}, + Sat: []TimeBlockModel{timeBlock}, + Sun: []TimeBlockModel{timeBlock}, + }, + expected: false, + }, + } + + for _, c := range cases { + actual := isMonTueWedThuFriSatSunSchedule(c.schedule) + if actual != c.expected { + t.Fatalf("Expected: %t, got: %t", c.expected, actual) + } + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index fb4546a..32394e1 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -116,6 +116,7 @@ func (*TadoProvider) Configure(ctx context.Context, req provider.ConfigureReques func (*TadoProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ NewGeofencingResource, + NewHeatingScheduleResource, } } diff --git a/internal/provider/util.go b/internal/provider/util.go index 26e3912..0606f58 100644 --- a/internal/provider/util.go +++ b/internal/provider/util.go @@ -1,6 +1,7 @@ package provider import ( + "github.com/gonzolino/gotado/v2" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -12,3 +13,13 @@ func toTypesString(s *string) types.String { } return types.StringValue(*s) } + +// boolToPower converts a bool to a gotado.Power. +// If the bool is true, the gotado.Power will be set to On. +// If it is false, it will be set to Off. +func boolToPower(b bool) gotado.Power { + if b { + return gotado.PowerOn + } + return gotado.PowerOff +} diff --git a/internal/provider/util_test.go b/internal/provider/util_test.go index e81ffd3..69466a9 100644 --- a/internal/provider/util_test.go +++ b/internal/provider/util_test.go @@ -3,6 +3,7 @@ package provider import ( "testing" + "github.com/gonzolino/gotado/v2" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -24,3 +25,13 @@ func TestToTypesStr(t *testing.T) { } } } + +func TestBoolToPower(t *testing.T) { + if boolToPower(true) != gotado.PowerOn { + t.Fatalf("Expected: %s, got: %s", gotado.PowerOn, boolToPower(true)) + } + + if boolToPower(false) != gotado.PowerOff { + t.Fatalf("Expected: %s, got: %s", gotado.PowerOff, boolToPower(false)) + } +}