Files
magistrala/reports/generator.go
T
Dušan Borovčanin 61d0427898 NOISSUE - Rename to Magistrala (#3427)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-04-06 15:23:42 +02:00

343 lines
9.2 KiB
Go

// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package reports
import (
"bytes"
"context"
"encoding/csv"
"fmt"
"html/template"
"io"
"log/slog"
"mime/multipart"
"net/http"
"sort"
"strings"
"time"
_ "time/tzdata" // Embed timezone database
"github.com/absmach/magistrala/pkg/errors"
svcerr "github.com/absmach/magistrala/pkg/errors/service"
pkglog "github.com/absmach/magistrala/pkg/logger"
"github.com/absmach/magistrala/pkg/transformers/senml"
)
const nanosecondThreshold = float64(10 * time.Second / time.Nanosecond)
type ReportData struct {
Title string
GeneratedTime string
GeneratedDate string
Reports []Report
Timezone string
}
func (r *report) generatePDFReport(ctx context.Context, title string, reports []Report, template ReportTemplate, timezone string) ([]byte, error) {
for i := range reports {
sort.Slice(reports[i].Messages, func(j, k int) bool {
return reports[i].Messages[j].Time < reports[i].Messages[k].Time
})
}
loc, err := resolveTimezone(timezone)
if err != nil {
r.runInfo <- pkglog.RunInfo{
Level: slog.LevelWarn,
Message: fmt.Sprintf("failed to resolve timezone '%s', falling back to UTC: %s", timezone, err),
Details: []slog.Attr{
slog.String("report_title", title),
slog.Time("time", time.Now().UTC()),
},
}
}
now := time.Now().In(loc)
displayTZ := timezone
if strings.TrimSpace(displayTZ) == "" {
displayTZ = "UTC"
}
data := ReportData{
Title: title,
GeneratedTime: now.Format("15:04:05"),
GeneratedDate: now.Format("02 Jan 2006"),
Reports: reports,
Timezone: displayTZ,
}
templateContent := r.defaultTemplate.String()
if template.String() != "" {
templateContent = template.String()
}
return r.generate(ctx, templateContent, data)
}
func (r *report) generate(ctx context.Context, templateContent string, data ReportData) ([]byte, error) {
tmpl := template.New("report").Funcs(template.FuncMap{
"formatTime": func(t float64) string { return r.formatTimeWithTimezone(t, data.Timezone) },
"formatValue": formatValue,
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"iterate": func(count int) []int { return makeRange(count) },
"ge": func(a, b int) bool { return a >= b },
"lt": func(a, b int) bool { return a < b },
"eq": func(a, b int) bool { return a == b },
"div": func(a, b int) int {
if b == 0 {
return 0
}
return a / b
},
"mod": func(a, b int) int {
if b == 0 {
return 0
}
return a % b
},
"getStartRow": getStartRow,
"getEndRow": getEndRow,
})
tmpl, err := tmpl.Parse(templateContent)
if err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
var htmlBuf bytes.Buffer
if err := tmpl.Execute(&htmlBuf, data); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
htmlContent := htmlBuf.String()
pdfBytes, err := r.htmlToPDF(ctx, htmlContent)
if err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
return pdfBytes, nil
}
func (r *report) htmlToPDF(ctx context.Context, htmlContent string) ([]byte, error) {
var requestBody bytes.Buffer
writer := multipart.NewWriter(&requestBody)
htmlPart, err := writer.CreateFormFile("files", "index.html")
if err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if _, err := htmlPart.Write([]byte(htmlContent)); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("marginTop", "0"); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("marginBottom", "0"); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("marginLeft", "0"); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("marginRight", "0"); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("printBackground", "true"); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("preferCSSPageSize", "true"); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("emulatedMediaType", "print"); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("waitForSelector", "body"); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Close(); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.converterURL, &requestBody)
if err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
defer resp.Body.Close()
pdfBytes, err := io.ReadAll(resp.Body)
if err != nil || resp.StatusCode != http.StatusOK {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
return pdfBytes, nil
}
func (r *report) formatTimeWithTimezone(t float64, timezone string) string {
loc, err := resolveTimezone(timezone)
if err != nil {
r.runInfo <- pkglog.RunInfo{
Level: slog.LevelWarn,
Message: fmt.Sprintf("failed to resolve timezone '%s', falling back to UTC: %s", timezone, err),
Details: []slog.Attr{slog.Time("time", time.Now().UTC())},
}
}
var timeVal time.Time
switch {
case t > nanosecondThreshold:
timeVal = time.Unix(0, int64(t)).In(loc)
default:
timeVal = time.Unix(int64(t), 0).In(loc)
}
return timeVal.Format("2006-01-02 15:04:05")
}
func formatValue(msg senml.Message) string {
switch {
case msg.Value != nil:
return fmt.Sprintf("%.2f", *msg.Value)
case msg.StringValue != nil:
return *msg.StringValue
case msg.BoolValue != nil:
return fmt.Sprintf("%t", *msg.BoolValue)
case msg.DataValue != nil:
return *msg.DataValue
default:
return "N/A"
}
}
func makeRange(n int) []int {
result := make([]int, n)
for i := range result {
result[i] = i
}
return result
}
func getStartRow(pageNum, firstPageRows, continuationPageRows int) int {
if pageNum == 0 {
return 0
}
return firstPageRows + (pageNum-1)*continuationPageRows
}
func getEndRow(pageNum, firstPageRows, continuationPageRows, totalMessages int) int {
var end int
if pageNum == 0 {
end = firstPageRows
} else {
start := firstPageRows + (pageNum-1)*continuationPageRows
end = start + continuationPageRows
}
if end > totalMessages {
end = totalMessages
}
return end
}
func (r *report) generateCSVReport(_ context.Context, title string, reports []Report, timezone string) ([]byte, error) {
var buf bytes.Buffer
writer := csv.NewWriter(&buf)
headers := []string{"Time", "Value", "Unit", "Protocol", "Subtopic"}
for i, report := range reports {
if i > 0 {
if err := writer.Write([]string{""}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{"=== NEW REPORT ==="}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{""}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
} else {
if err := writer.Write([]string{title}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{""}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
}
if err := writer.Write([]string{"Report Information:"}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{"Name", report.Metric.Name}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if report.Metric.ClientID != "" {
if err := writer.Write([]string{"Device ID", report.Metric.ClientID}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
}
if err := writer.Write([]string{"Channel ID", report.Metric.ChannelID}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{""}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write(headers); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
sort.Slice(report.Messages, func(i, j int) bool {
return report.Messages[i].Time < report.Messages[j].Time
})
for _, msg := range report.Messages {
timeStr := r.formatTimeWithTimezone(msg.Time, timezone)
var valueStr string
if msg.Value != nil {
valueStr = fmt.Sprintf("%.2f", *msg.Value)
} else if msg.StringValue != nil {
valueStr = *msg.StringValue
} else if msg.BoolValue != nil {
valueStr = fmt.Sprintf("%v", *msg.BoolValue)
} else if msg.DataValue != nil {
valueStr = *msg.DataValue
} else {
valueStr = "N/A"
}
row := []string{
timeStr,
valueStr,
msg.Unit,
msg.Protocol,
msg.Subtopic,
}
if err := writer.Write(row); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
}
}
writer.Flush()
if err := writer.Error(); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
return buf.Bytes(), nil
}