Phase 6: fix the live Home Assistant write (Local Calendar, no event id)
The first live run against the new calendar.school entity failed loudly twice, and both causes are fixed here. - The Local Calendar rejects `return_response` on calendar.create_event. The client retries the call without it and ignores the changed-states array that Home Assistant returns instead. - Home Assistant then reports no event id. The item keeps the description marker ([compass-nhs:<id>]) as its reference, so it leaves the candidate set and is never written twice. The live retry proved it: both events already in the calendar were adopted by their markers, with no duplicate. - calendar.get_events reports `start` / `end` (a date-time or a bare date), not `start_date_time`; the reader now accepts all three shapes. A model must no longer classify events. The calendar write depends on the school-event rule, and the LLM labelled every event "normal"; events now keep the deterministic rule under both classifiers. Also fix the Phase 5 screenshot script: the calendar banner shares the `.scan` class, so select each banner by its labelled section. Docs record the accepted decision: Sam chose a Home Assistant Local Calendar, so the target is calendar.school. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -166,6 +166,11 @@ type Event struct {
|
||||
// Assistant event id when the service reports one. It asks for the service
|
||||
// response, so the id can be stored on the item and a re-run cannot duplicate
|
||||
// the event.
|
||||
//
|
||||
// Not every calendar answers with a response: the Local Calendar integration
|
||||
// rejects `return_response` outright. That is not a write failure, so the call
|
||||
// is retried without it and the empty id is reported to the caller, which then
|
||||
// falls back to the description marker for idempotency.
|
||||
func (c *Client) CreateEvent(ctx context.Context, ev Event) (string, error) {
|
||||
if strings.TrimSpace(ev.Summary) == "" {
|
||||
return "", errors.New("calendar: event summary is empty")
|
||||
@@ -186,13 +191,29 @@ func (c *Client) CreateEvent(ctx context.Context, ev Event) (string, error) {
|
||||
body["location"] = ev.Location
|
||||
}
|
||||
|
||||
resp, err := c.callService(ctx, "calendar", "create_event", body)
|
||||
resp, err := c.callService(ctx, "calendar", "create_event", body, true)
|
||||
if err != nil {
|
||||
if noResponseSupport(err) {
|
||||
if _, rerr := c.callService(ctx, "calendar", "create_event", body, false); rerr != nil {
|
||||
return "", rerr
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return serviceUID(resp, c.entity), nil
|
||||
}
|
||||
|
||||
// noResponseSupport reports whether Home Assistant refused the request only
|
||||
// because this calendar does not answer with a service response.
|
||||
func noResponseSupport(err error) bool {
|
||||
var haErr *Error
|
||||
if !errors.As(err, &haErr) {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(strings.ToLower(haErr.Message), "does not support responses")
|
||||
}
|
||||
|
||||
// DeleteEvent removes an event by its Home Assistant uid. It exists so a test
|
||||
// event can be cleaned up, and so a wrong write can be undone.
|
||||
func (c *Client) DeleteEvent(ctx context.Context, uid string) error {
|
||||
@@ -202,7 +223,7 @@ func (c *Client) DeleteEvent(ctx context.Context, uid string) error {
|
||||
_, err := c.callService(ctx, "calendar", "delete_event", map[string]any{
|
||||
"entity_id": c.entity,
|
||||
"uid": uid,
|
||||
})
|
||||
}, false)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -214,7 +235,7 @@ func (c *Client) Events(ctx context.Context, start, end time.Time) ([]Event, err
|
||||
"entity_id": c.entity,
|
||||
"start_date_time": start.Format(time.RFC3339),
|
||||
"end_date_time": end.Format(time.RFC3339),
|
||||
})
|
||||
}, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -223,12 +244,20 @@ func (c *Client) Events(ctx context.Context, start, end time.Time) ([]Event, err
|
||||
|
||||
// ---------- transport ----------
|
||||
|
||||
// callService posts to /api/services/<domain>/<service> with return_response,
|
||||
// so a service that reports data (the created event id) answers with it.
|
||||
func (c *Client) callService(ctx context.Context, domain, service string, body map[string]any) (map[string]any, error) {
|
||||
path := fmt.Sprintf("/api/services/%s/%s?return_response", domain, service)
|
||||
// callService posts to /api/services/<domain>/<service>. When withResponse is
|
||||
// true it adds ?return_response, so a service that reports data (the created
|
||||
// event id) answers with it. Without a response Home Assistant replies with the
|
||||
// changed states, a JSON array, which the caller does not need.
|
||||
func (c *Client) callService(ctx context.Context, domain, service string, body map[string]any, withResponse bool) (map[string]any, error) {
|
||||
path := fmt.Sprintf("/api/services/%s/%s", domain, service)
|
||||
if !withResponse {
|
||||
if err := c.do(ctx, http.MethodPost, path, body, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
var out map[string]any
|
||||
if err := c.do(ctx, http.MethodPost, path, body, &out); err != nil {
|
||||
if err := c.do(ctx, http.MethodPost, path+"?return_response", body, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
@@ -372,13 +401,19 @@ func servicePayloads(resp map[string]any, entity string) []map[string]any {
|
||||
return out
|
||||
}
|
||||
|
||||
// parseHATime reads an event time from a calendar.get_events result. Home
|
||||
// Assistant reports `start` / `end` as either a date-time with an offset or a
|
||||
// bare date for an all-day event; the *_date_time / *_date shapes are also
|
||||
// accepted for older replies.
|
||||
func parseHATime(m map[string]any, prefix string) (*time.Time, string) {
|
||||
if raw := str(m[prefix+"_date_time"]); raw != "" {
|
||||
for _, key := range []string{prefix + "_date_time", prefix + "_date", prefix} {
|
||||
raw := str(m[key])
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, raw); err == nil {
|
||||
return &t, raw
|
||||
}
|
||||
}
|
||||
if raw := str(m[prefix+"_date"]); raw != "" {
|
||||
if t, err := time.ParseInLocation("2006-01-02", raw, time.Local); err == nil {
|
||||
return &t, raw
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user