From 40f27cd9028d4147a066310e81920bdf3966289c Mon Sep 17 00:00:00 2001 From: Tobias Lindberg Date: Sat, 13 Feb 2021 10:50:09 +0100 Subject: [PATCH] adding endpoints for charges and drives --- src/TeslaMateAPICarsCharges.go | 198 +++++++++++++ src/TeslaMateAPICarsChargesDetails.go | 320 +++++++++++++++++++++ src/TeslaMateAPICarsDrives.go | 246 +++++++++++++++++ src/TeslaMateAPICarsDrivesDetails.go | 383 ++++++++++++++++++++++++++ src/webserver.go | 68 +++++ 5 files changed, 1215 insertions(+) create mode 100644 src/TeslaMateAPICarsCharges.go create mode 100644 src/TeslaMateAPICarsChargesDetails.go create mode 100644 src/TeslaMateAPICarsDrives.go create mode 100644 src/TeslaMateAPICarsDrivesDetails.go diff --git a/src/TeslaMateAPICarsCharges.go b/src/TeslaMateAPICarsCharges.go new file mode 100644 index 0000000..4b9b1f1 --- /dev/null +++ b/src/TeslaMateAPICarsCharges.go @@ -0,0 +1,198 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + + _ "github.com/lib/pq" +) + +// TeslaMateAPICarsCharges func +func TeslaMateAPICarsCharges(CarID int, ResultPage int, ResultShow int) (string, bool) { + + // creating structs for /cars//charges + // Car struct - child of Data + type Car struct { + CarID int `json:"car_id"` // smallint + CarName string `json:"car_name"` // text + } + // BatteryDetails struct - child of Charges + type BatteryDetails struct { + StartBatteryLevel int `json:"start_battery_level"` // int + EndBatteryLevel int `json:"end_battery_level"` // int + } + // PreferredRange struct - child of Charges + type PreferredRange struct { + StartRange float64 `json:"start_range"` // float64 + EndRange float64 `json:"end_range"` // float64 + } + // Charges struct - child of Data + type Charges struct { + ChargeID int `json:"charge_id"` // int + StartDate string `json:"start_date"` // string + EndDate string `json:"end_date"` // string + Address string `json:"address"` // string + ChargeEnergyAdded float64 `json:"charge_energy_added"` // float64 + ChargeEnergyUsed float64 `json:"charge_energy_used"` // float64 + Cost float64 `json:"cost"` // float64 + DurationMin int `json:"duration_min"` // int + DurationStr string `json:"duration_str"` // string + BatteryDetails BatteryDetails `json:"battery_details"` // BatteryDetails + RangeIdeal PreferredRange `json:"range_ideal"` // PreferredRange + RangeRated PreferredRange `json:"range_rated"` // PreferredRange + OutsideTempAvg float64 `json:"outside_temp_avg"` // float64 + } + // TeslaMateUnits struct - child of Data + type TeslaMateUnits struct { + UnitsLength string `json:"unit_of_length"` // string + UnitsTemperature string `json:"unit_of_temperature"` // string + } + // Data struct - child of JSONData + type Data struct { + Car Car `json:"car"` + Charges []Charges `json:"charges"` + TeslaMateUnits TeslaMateUnits `json:"units"` + } + // JSONData struct - main + type JSONData struct { + Data Data `json:"data"` + } + + // creating required vars + var ChargesData []Charges + var UnitsLength, UnitsTemperature, CarName string + var ValidResponse bool // default is false + + // calculate offset based on page (page 0 is not possible, since first page is minimum 1) + if ResultPage > 0 { + ResultPage-- + } else { + ResultPage = 0 + } + ResultPage = (ResultPage * ResultShow) + + // getting data from database + query := ` + SELECT + charging_processes.id AS charge_id, + start_date, + end_date, + COALESCE(geofence.name, CONCAT_WS(', ', COALESCE(address.name, nullif(CONCAT_WS(' ', address.road, address.house_number), '')), address.city)) AS address, + COALESCE(charge_energy_added, 0) AS charge_energy_added, + COALESCE(charge_energy_used, 0) AS charge_energy_used, + COALESCE(cost, 0) AS cost, + start_ideal_range_km AS start_ideal_range, + end_ideal_range_km AS end_ideal_range, + start_rated_range_km AS start_rated_range, + end_rated_range_km AS end_rated_range, + start_battery_level, + end_battery_level, + duration_min, + TO_CHAR((duration_min * INTERVAL '1 minute'), 'HH24:MI') as duration_str, + outside_temp_avg, + (SELECT unit_of_length FROM settings LIMIT 1) as unit_of_length, + (SELECT unit_of_temperature FROM settings LIMIT 1) as unit_of_temperature, + cars.name + FROM charging_processes + LEFT JOIN cars ON car_id = cars.id + LEFT JOIN addresses address ON address_id = address.id + LEFT JOIN positions position ON position_id = position.id + LEFT JOIN geofences geofence ON geofence_id = geofence.id + WHERE charging_processes.car_id=$1 + ORDER BY start_date DESC + LIMIT $2 OFFSET $3;` + rows, err := db.Query(query, CarID, ResultShow, ResultPage) + + // checking for errors in query + if err != nil { + log.Fatal(err) + } + + // defer closing rows + defer rows.Close() + + // looping through all results + for rows.Next() { + + // creating charge object based on struct + charge := Charges{} + + // scanning row and putting values into the charge + err = rows.Scan( + &charge.ChargeID, + &charge.StartDate, + &charge.EndDate, + &charge.Address, + &charge.ChargeEnergyAdded, + &charge.ChargeEnergyUsed, + &charge.Cost, + &charge.RangeIdeal.StartRange, + &charge.RangeIdeal.EndRange, + &charge.RangeRated.StartRange, + &charge.RangeRated.EndRange, + &charge.BatteryDetails.StartBatteryLevel, + &charge.BatteryDetails.EndBatteryLevel, + &charge.DurationMin, + &charge.DurationStr, + &charge.OutsideTempAvg, + &UnitsLength, + &UnitsTemperature, + &CarName, + ) + + // converting values based of settings UnitsLength + if UnitsLength == "mi" { + charge.RangeIdeal.StartRange = kilometersToMiles(charge.RangeIdeal.StartRange) + charge.RangeIdeal.EndRange = kilometersToMiles(charge.RangeIdeal.EndRange) + charge.RangeRated.StartRange = kilometersToMiles(charge.RangeRated.StartRange) + charge.RangeRated.EndRange = kilometersToMiles(charge.RangeRated.EndRange) + } + // converting values based of settings UnitsTemperature + if UnitsTemperature == "F" { + charge.OutsideTempAvg = celsiusToFahrenheit(charge.OutsideTempAvg) + } + + // adjusting to timezone differences from UTC to be userspecific + charge.StartDate = getTimeInTimeZone(charge.StartDate) + charge.EndDate = getTimeInTimeZone(charge.EndDate) + + // checking for errors after scanning + if err != nil { + log.Fatal(err) + } + + // appending charge to ChargesData + ChargesData = append(ChargesData, charge) + ValidResponse = true + } + + // checking for errors in the rows result + err = rows.Err() + if err != nil { + log.Fatal(err) + } + + // + // build the data-blob + jsonData := JSONData{ + Data{ + Car: Car{ + CarID: CarID, + CarName: CarName, + }, + Charges: ChargesData, + TeslaMateUnits: TeslaMateUnits{ + UnitsLength: UnitsLength, + UnitsTemperature: UnitsTemperature, + }, + }, + } + + // print readable output to log + log.Printf("data for /cars/%d/charges created:", CarID) + + js, _ := json.Marshal(jsonData) + fmt.Printf("%s\n", js) + return string(js), ValidResponse +} diff --git a/src/TeslaMateAPICarsChargesDetails.go b/src/TeslaMateAPICarsChargesDetails.go new file mode 100644 index 0000000..3d8756b --- /dev/null +++ b/src/TeslaMateAPICarsChargesDetails.go @@ -0,0 +1,320 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + + _ "github.com/lib/pq" +) + +// TeslaMateAPICarsChargesDetails func +func TeslaMateAPICarsChargesDetails(CarID int, ChargeID int) (string, bool) { + + // creating structs for /cars//charges/ + // Car struct - child of Data + type Car struct { + CarID int `json:"car_id"` // smallint + CarName string `json:"car_name"` // text + } + // BatteryDetails struct - child of Charges + type BatteryDetails struct { + StartBatteryLevel int `json:"start_battery_level"` // int + EndBatteryLevel int `json:"end_battery_level"` // int + } + // PreferredRange struct - child of Charges + type PreferredRange struct { + StartRange float64 `json:"start_range"` // float64 + EndRange float64 `json:"end_range"` // float64 + } + // ChargerDetails struct - child of ChargeDetails + type ChargerDetails struct { + ChargerActualCurrent int `json:"charger_actual_current"` // int + ChargerPhases int `json:"charger_phases"` // int + ChargerPilotCurrent int `json:"charger_pilot_current"` // int + ChargerPower int `json:"charger_power"` // int + ChargerVoltage int `json:"charger_voltage"` // int + } + // FastChargerInfo struct - child of ChargeDetails + type FastChargerInfo struct { + FastChargerPresent bool `json:"fast_charger_present"` // bool + FastChargerBrand string `json:"fast_charger_brand"` // string + FastChargerType string `json:"fast_charger_type"` // string + + } + // BatteryInfo struct - child of ChargeDetails + type BatteryInfo struct { + IdealBatteryRange float64 `json:"ideal_battery_range"` // float64 + RatedBatteryRange float64 `json:"rated_battery_range"` // float64 + BatteryHeater bool `json:"battery_heater"` // bool + BatteryHeaterOn bool `json:"battery_heater_on"` // bool + BatteryHeaterNoPower bool `json:"battery_heater_no_power"` // bool + } + // ChargeDetails struct - child of Charge + type ChargeDetails struct { + DetailID int `json:"detail_id"` // integer + Date string `json:"date"` // string + BatteryLevel int `json:"battery_level"` // int + UsableBatteryLevel int `json:"usable_battery_level"` // int + ChargeEnergyAdded float64 `json:"charge_energy_added"` // float64 + NotEnoughPowerToHeat bool `json:"not_enough_power_to_heat"` // bool + ChargerDetails ChargerDetails `json:"charger_details"` // struct + BatteryInfo BatteryInfo `json:"battery_info"` // struct + ConnChargeCable string `json:"conn_charge_cable"` // string + FastChargerInfo FastChargerInfo `json:"fast_charger_info"` // struct + OutsideTemp float64 `json:"outside_temp"` // float64 + } + // Charge struct - child of Data + type Charge struct { + ChargeID int `json:"charge_id"` // int + StartDate string `json:"start_date"` // string + EndDate string `json:"end_date"` // string + Address string `json:"address"` // string + ChargeEnergyAdded float64 `json:"charge_energy_added"` // float64 + ChargeEnergyUsed float64 `json:"charge_energy_used"` // float64 + Cost float64 `json:"cost"` // float64 + DurationMin int `json:"duration_min"` // int + DurationStr string `json:"duration_str"` // string + BatteryDetails BatteryDetails `json:"battery_details"` // BatteryDetails + RangeIdeal PreferredRange `json:"range_ideal"` // PreferredRange + RangeRated PreferredRange `json:"range_rated"` // PreferredRange + OutsideTempAvg float64 `json:"outside_temp_avg"` // float64 + ChargeDetails []ChargeDetails `json:"charge_details"` // struct + } + // TeslaMateUnits struct - child of Data + type TeslaMateUnits struct { + UnitsLength string `json:"unit_of_length"` // string + UnitsTemperature string `json:"unit_of_temperature"` // string + } + // Data struct - child of JSONData + type Data struct { + Car Car `json:"car"` + Charge Charge `json:"charge"` + TeslaMateUnits TeslaMateUnits `json:"units"` + } + // JSONData struct - main + type JSONData struct { + Data Data `json:"data"` + } + + // creating required vars + var ChargeData Charge + var ChargeDetailsData []ChargeDetails + var UnitsLength, UnitsTemperature, CarName string + var ValidResponse bool // default is false + + // getting data from database + query := ` + SELECT + charging_processes.id AS charge_id, + start_date, + end_date, + COALESCE(geofence.name, CONCAT_WS(', ', COALESCE(address.name, nullif(CONCAT_WS(' ', address.road, address.house_number), '')), address.city)) AS address, + COALESCE(charging_processes.charge_energy_added, 0) AS charge_energy_added, + COALESCE(charge_energy_used, 0) AS charge_energy_used, + COALESCE(cost, 0) AS cost, + start_ideal_range_km AS start_ideal_range, + end_ideal_range_km AS end_ideal_range, + start_rated_range_km AS start_rated_range, + end_rated_range_km AS end_rated_range, + start_battery_level, + end_battery_level, + duration_min, + TO_CHAR((duration_min * INTERVAL '1 minute'), 'HH24:MI') as duration_str, + outside_temp_avg, + (SELECT unit_of_length FROM settings LIMIT 1) as unit_of_length, + (SELECT unit_of_temperature FROM settings LIMIT 1) as unit_of_temperature, + cars.name + FROM charging_processes + LEFT JOIN cars ON car_id = cars.id + LEFT JOIN addresses address ON address_id = address.id + LEFT JOIN positions position ON position_id = position.id + LEFT JOIN geofences geofence ON geofence_id = geofence.id + LEFT JOIN charges ON charging_processes.id = charges.id + WHERE charging_processes.car_id=$1 AND charging_processes.id=$2 + ORDER BY start_date DESC;` + rows, err := db.Query(query, CarID, ChargeID) + + // checking for errors in query + if err != nil { + log.Fatal(err) + } + + // defer closing rows + defer rows.Close() + + // looping through all results + for rows.Next() { + + // creating charge object based on struct + charge := Charge{} + + // scanning row and putting values into the charge + err = rows.Scan( + &charge.ChargeID, + &charge.StartDate, + &charge.EndDate, + &charge.Address, + &charge.ChargeEnergyAdded, + &charge.ChargeEnergyUsed, + &charge.Cost, + &charge.RangeIdeal.StartRange, + &charge.RangeIdeal.EndRange, + &charge.RangeRated.StartRange, + &charge.RangeRated.EndRange, + &charge.BatteryDetails.StartBatteryLevel, + &charge.BatteryDetails.EndBatteryLevel, + &charge.DurationMin, + &charge.DurationStr, + &charge.OutsideTempAvg, + &UnitsLength, + &UnitsTemperature, + &CarName, + ) + + // converting values based of settings UnitsLength + if UnitsLength == "mi" { + charge.RangeIdeal.StartRange = kilometersToMiles(charge.RangeIdeal.StartRange) + charge.RangeIdeal.EndRange = kilometersToMiles(charge.RangeIdeal.EndRange) + charge.RangeRated.StartRange = kilometersToMiles(charge.RangeRated.StartRange) + charge.RangeRated.EndRange = kilometersToMiles(charge.RangeRated.EndRange) + } + // converting values based of settings UnitsTemperature + if UnitsTemperature == "F" { + charge.OutsideTempAvg = celsiusToFahrenheit(charge.OutsideTempAvg) + } + + // adjusting to timezone differences from UTC to be userspecific + charge.StartDate = getTimeInTimeZone(charge.StartDate) + charge.EndDate = getTimeInTimeZone(charge.EndDate) + + // checking for errors after scanning + if err != nil { + log.Fatal(err) + } + + // appending charge to ChargeData + ChargeData = charge + ValidResponse = true + + // getting detailed charge data from database + query = ` + SELECT + id AS detail_id, + date, + battery_level, + usable_battery_level, + charge_energy_added, + not_enough_power_to_heat, + COALESCE(charger_actual_current, 0) as charger_actual_current, + COALESCE(charger_phases, 0) AS charger_phases, + COALESCE(charger_pilot_current, 0) as charger_pilot_current, + COALESCE(charger_power, 0) as charger_power, + COALESCE(charger_voltage, 0) as charger_voltage, + ideal_battery_range_km AS ideal_battery_range, + rated_battery_range_km AS rated_battery_range, + battery_heater, + battery_heater_on, + battery_heater_no_power, + conn_charge_cable, + fast_charger_present, + fast_charger_brand, + fast_charger_type, + outside_temp + FROM charges + WHERE charging_process_id=$1 + ORDER BY id ASC;` + rows, err = db.Query(query, ChargeID) + + // checking for errors in query + if err != nil { + log.Fatal(err) + } + + // defer closing rows + defer rows.Close() + + // looping through all results + for rows.Next() { + + // creating chargedetails object based on struct + chargedetails := ChargeDetails{} + + // scanning row and putting values into the drive + err = rows.Scan( + &chargedetails.DetailID, + &chargedetails.Date, + &chargedetails.BatteryLevel, + &chargedetails.UsableBatteryLevel, + &chargedetails.ChargeEnergyAdded, + &chargedetails.NotEnoughPowerToHeat, + &chargedetails.ChargerDetails.ChargerActualCurrent, + &chargedetails.ChargerDetails.ChargerPhases, + &chargedetails.ChargerDetails.ChargerPilotCurrent, + &chargedetails.ChargerDetails.ChargerPower, + &chargedetails.ChargerDetails.ChargerVoltage, + &chargedetails.BatteryInfo.IdealBatteryRange, + &chargedetails.BatteryInfo.RatedBatteryRange, + &chargedetails.BatteryInfo.BatteryHeater, + &chargedetails.BatteryInfo.BatteryHeaterOn, + &chargedetails.BatteryInfo.BatteryHeaterNoPower, + &chargedetails.ConnChargeCable, + &chargedetails.FastChargerInfo.FastChargerPresent, + &chargedetails.FastChargerInfo.FastChargerBrand, + &chargedetails.FastChargerInfo.FastChargerType, + &chargedetails.OutsideTemp, + ) + + // converting values based of settings UnitsLength + if UnitsLength == "mi" { + chargedetails.BatteryInfo.IdealBatteryRange = kilometersToMiles(chargedetails.BatteryInfo.IdealBatteryRange) + chargedetails.BatteryInfo.RatedBatteryRange = kilometersToMiles(chargedetails.BatteryInfo.RatedBatteryRange) + + } + // converting values based of settings UnitsTemperature + if UnitsTemperature == "F" { + chargedetails.OutsideTemp = celsiusToFahrenheit(chargedetails.OutsideTemp) + } + // adjusting to timezone differences from UTC to be userspecific + chargedetails.Date = getTimeInTimeZone(chargedetails.Date) + + // checking for errors after scanning + if err != nil { + log.Fatal(err) + } + + // appending drive to ChargeData + ChargeDetailsData = append(ChargeDetailsData, chargedetails) + ChargeData.ChargeDetails = ChargeDetailsData + } + } + + // checking for errors in the rows result + err = rows.Err() + if err != nil { + log.Fatal(err) + } + + // + // build the data-blob + jsonData := JSONData{ + Data{ + Car: Car{ + CarID: CarID, + CarName: CarName, + }, + Charge: ChargeData, + TeslaMateUnits: TeslaMateUnits{ + UnitsLength: UnitsLength, + UnitsTemperature: UnitsTemperature, + }, + }, + } + + // print readable output to log + log.Printf("data for /cars/%d/charges/%d created:", CarID, ChargeID) + + js, _ := json.Marshal(jsonData) + fmt.Printf("%s\n", js) + return string(js), ValidResponse +} diff --git a/src/TeslaMateAPICarsDrives.go b/src/TeslaMateAPICarsDrives.go new file mode 100644 index 0000000..58b708f --- /dev/null +++ b/src/TeslaMateAPICarsDrives.go @@ -0,0 +1,246 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + + _ "github.com/lib/pq" +) + +// TeslaMateAPICarsDrives func +func TeslaMateAPICarsDrives(CarID int, ResultPage int, ResultShow int) (string, bool) { + + // creating structs for /cars//drives + // Car struct - child of Data + type Car struct { + CarID int `json:"car_id"` // smallint + CarName string `json:"car_name"` // text + } + // OdometerDetails struct - child of Drives + type OdometerDetails struct { + OdometerStart float64 `json:"odometer_start"` // float64 + OdometerEnd float64 `json:"odometer_end"` // float64 + OdometerDistance float64 `json:"odometer_distance"` // float64 + } + // BatteryDetails struct - child of Drives + type BatteryDetails struct { + StartUsableBatteryLevel int `json:"start_usable_battery_level"` // int + StartBatteryLevel int `json:"start_battery_level"` // int + EndUsableBatteryLevel int `json:"end_usable_battery_level"` // int + EndBatteryLevel int `json:"end_battery_level"` // int + ReducedRange bool `json:"reduced_range"` // bool + IsSufficientlyPrecise bool `json:"is_sufficiently_precise"` // bool + } + // PreferredRange struct - child of Drives + type PreferredRange struct { + StartRange float64 `json:"start_range"` // float64 + EndRange float64 `json:"end_range"` // float64 + RangeDiff float64 `json:"range_diff"` // float64 + } + // Drives struct - child of Data + type Drives struct { + DriveID int `json:"drive_id"` // int + StartDate string `json:"start_date"` // string + EndDate string `json:"end_date"` // string + StartAddress string `json:"start_address"` // string + EndAddress string `json:"end_address"` // string + OdometerDetails OdometerDetails `json:"odometer_details"` // OdometerDetails + DurationMin int `json:"duration_min"` // int + DurationStr string `json:"duration_str"` // string + SpeedMax int `json:"speed_max"` // int + SpeedAvg float64 `json:"speed_avg"` // float64 + PowerMax int `json:"power_max"` // int + PowerMin int `json:"power_min"` // int + BatteryDetails BatteryDetails `json:"battery_details"` // BatteryDetails + RangeIdeal PreferredRange `json:"range_ideal"` // PreferredRange + RangeRated PreferredRange `json:"range_rated"` // PreferredRange + OutsideTempAvg float64 `json:"outside_temp_avg"` // float64 + InsideTempAvg float64 `json:"inside_temp_avg"` // float64 + } + // TeslaMateUnits struct - child of Data + type TeslaMateUnits struct { + UnitsLength string `json:"unit_of_length"` // string + UnitsTemperature string `json:"unit_of_temperature"` // string + } + // Data struct - child of JSONData + type Data struct { + Car Car `json:"car"` + Drives []Drives `json:"drives"` + TeslaMateUnits TeslaMateUnits `json:"units"` + } + // JSONData struct - main + type JSONData struct { + Data Data `json:"data"` + } + + // creating required vars + var DrivesData []Drives + var UnitsLength, UnitsTemperature, CarName string + var ValidResponse bool // default is false + + // calculate offset based on page (page 0 is not possible, since first page is minimum 1) + if ResultPage > 0 { + ResultPage-- + } else { + ResultPage = 0 + } + ResultPage = (ResultPage * ResultShow) + + // getting data from database + query := ` + SELECT + drives.id AS drive_id, + start_date, + end_date, + COALESCE(start_geofence.name, CONCAT_WS(', ', COALESCE(start_address.name, nullif(CONCAT_WS(' ', start_address.road, start_address.house_number), '')), start_address.city)) AS start_address, + COALESCE(end_geofence.name, CONCAT_WS(', ', COALESCE(end_address.name, nullif(CONCAT_WS(' ', end_address.road, end_address.house_number), '')), end_address.city)) AS end_address, + start_km, + end_km, + distance, + duration_min, + TO_CHAR((duration_min * INTERVAL '1 minute'), 'HH24:MI') as duration_str, + speed_max, + COALESCE(distance / NULLIF(duration_min, 0) * 60, 0) AS speed_avg, + power_max, + power_min, + COALESCE(start_position.usable_battery_level, start_position.battery_level) as start_usable_battery_level, + start_position.battery_level as start_battery_level, + COALESCE(end_position.usable_battery_level, end_position.battery_level) as end_usable_battery_level, + end_position.battery_level as end_battery_level, + case when ( start_position.battery_level != start_position.usable_battery_level OR end_position.battery_level != end_position.usable_battery_level ) = true then true else false end as reduced_range, + duration_min > 1 AND distance > 1 AND ( start_position.usable_battery_level IS NULL OR end_position.usable_battery_level IS NULL OR ( end_position.battery_level - end_position.usable_battery_level ) = 0 ) as is_sufficiently_precise, + start_ideal_range_km, + end_ideal_range_km, + COALESCE( NULLIF ( GREATEST ( start_ideal_range_km - end_ideal_range_km, 0 ), 0 ),0 ) as range_diff_ideal_km, + start_rated_range_km, + end_rated_range_km, + COALESCE( NULLIF ( GREATEST ( start_rated_range_km - end_rated_range_km, 0 ), 0 ),0 ) as range_diff_rated_km, + outside_temp_avg, + inside_temp_avg, + (SELECT unit_of_length FROM settings LIMIT 1) as unit_of_length, + (SELECT unit_of_temperature FROM settings LIMIT 1) as unit_of_temperature, + cars.name + FROM drives + LEFT JOIN cars ON car_id = cars.id + LEFT JOIN addresses start_address ON start_address_id = start_address.id + LEFT JOIN addresses end_address ON end_address_id = end_address.id + LEFT JOIN positions start_position ON start_position_id = start_position.id + LEFT JOIN positions end_position ON end_position_id = end_position.id + LEFT JOIN geofences start_geofence ON start_geofence_id = start_geofence.id + LEFT JOIN geofences end_geofence ON end_geofence_id = end_geofence.id + WHERE drives.car_id=$1 AND end_date IS NOT NULL + ORDER BY start_date DESC + LIMIT $2 OFFSET $3;` + rows, err := db.Query(query, CarID, ResultShow, ResultPage) + + // checking for errors in query + if err != nil { + log.Fatal(err) + } + + // defer closing rows + defer rows.Close() + + // looping through all results + for rows.Next() { + + // creating drive object based on struct + drive := Drives{} + + // scanning row and putting values into the drive + err = rows.Scan( + &drive.DriveID, + &drive.StartDate, + &drive.EndDate, + &drive.StartAddress, + &drive.EndAddress, + &drive.OdometerDetails.OdometerStart, + &drive.OdometerDetails.OdometerEnd, + &drive.OdometerDetails.OdometerDistance, + &drive.DurationMin, + &drive.DurationStr, + &drive.SpeedMax, + &drive.SpeedAvg, + &drive.PowerMax, + &drive.PowerMin, + &drive.BatteryDetails.StartUsableBatteryLevel, + &drive.BatteryDetails.StartBatteryLevel, + &drive.BatteryDetails.EndUsableBatteryLevel, + &drive.BatteryDetails.EndBatteryLevel, + &drive.BatteryDetails.ReducedRange, + &drive.BatteryDetails.IsSufficientlyPrecise, + &drive.RangeIdeal.StartRange, + &drive.RangeIdeal.EndRange, + &drive.RangeIdeal.RangeDiff, + &drive.RangeRated.StartRange, + &drive.RangeRated.EndRange, + &drive.RangeRated.RangeDiff, + &drive.OutsideTempAvg, + &drive.InsideTempAvg, + &UnitsLength, + &UnitsTemperature, + &CarName, + ) + + // converting values based of settings UnitsLength + if UnitsLength == "mi" { + drive.OdometerDetails.OdometerStart = kilometersToMiles(drive.OdometerDetails.OdometerStart) + drive.OdometerDetails.OdometerEnd = kilometersToMiles(drive.OdometerDetails.OdometerEnd) + drive.OdometerDetails.OdometerDistance = kilometersToMiles(drive.OdometerDetails.OdometerDistance) + drive.RangeIdeal.StartRange = kilometersToMiles(drive.RangeIdeal.StartRange) + drive.RangeIdeal.EndRange = kilometersToMiles(drive.RangeIdeal.EndRange) + drive.RangeIdeal.RangeDiff = kilometersToMiles(drive.RangeIdeal.RangeDiff) + drive.RangeRated.StartRange = kilometersToMiles(drive.RangeRated.StartRange) + drive.RangeRated.EndRange = kilometersToMiles(drive.RangeRated.EndRange) + drive.RangeRated.RangeDiff = kilometersToMiles(drive.RangeRated.RangeDiff) + } + // converting values based of settings UnitsTemperature + if UnitsTemperature == "F" { + drive.OutsideTempAvg = celsiusToFahrenheit(drive.OutsideTempAvg) + drive.InsideTempAvg = celsiusToFahrenheit(drive.InsideTempAvg) + } + + // adjusting to timezone differences from UTC to be userspecific + drive.StartDate = getTimeInTimeZone(drive.StartDate) + drive.EndDate = getTimeInTimeZone(drive.EndDate) + + // checking for errors after scanning + if err != nil { + log.Fatal(err) + } + + // appending drive to DrivesData + DrivesData = append(DrivesData, drive) + ValidResponse = true + } + + // checking for errors in the rows result + err = rows.Err() + if err != nil { + log.Fatal(err) + } + + // + // build the data-blob + jsonData := JSONData{ + Data{ + Car: Car{ + CarID: CarID, + CarName: CarName, + }, + Drives: DrivesData, + TeslaMateUnits: TeslaMateUnits{ + UnitsLength: UnitsLength, + UnitsTemperature: UnitsTemperature, + }, + }, + } + + // print readable output to log + log.Printf("data for /cars/%d/drives created:", CarID) + + js, _ := json.Marshal(jsonData) + fmt.Printf("%s\n", js) + return string(js), ValidResponse +} diff --git a/src/TeslaMateAPICarsDrivesDetails.go b/src/TeslaMateAPICarsDrivesDetails.go new file mode 100644 index 0000000..18abbdc --- /dev/null +++ b/src/TeslaMateAPICarsDrivesDetails.go @@ -0,0 +1,383 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + + _ "github.com/lib/pq" +) + +// TeslaMateAPICarsDrivesDetails func +func TeslaMateAPICarsDrivesDetails(CarID int, DriveID int) (string, bool) { + + // creating structs for /cars//drives/ + // Car struct - child of Data + type Car struct { + CarID int `json:"car_id"` // smallint + CarName string `json:"car_name"` // text + } + // OdometerDetails struct - child of Drives + type OdometerDetails struct { + OdometerStart float64 `json:"odometer_start"` // float64 + OdometerEnd float64 `json:"odometer_end"` // float64 + OdometerDistance float64 `json:"odometer_distance"` // float64 + } + // BatteryDetails struct - child of Drives + type BatteryDetails struct { + StartUsableBatteryLevel int `json:"start_usable_battery_level"` // int + StartBatteryLevel int `json:"start_battery_level"` // int + EndUsableBatteryLevel int `json:"end_usable_battery_level"` // int + EndBatteryLevel int `json:"end_battery_level"` // int + ReducedRange bool `json:"reduced_range"` // bool + IsSufficientlyPrecise bool `json:"is_sufficiently_precise"` // bool + } + // PreferredRange struct - child of Drives + type PreferredRange struct { + StartRange float64 `json:"start_range"` // float64 + EndRange float64 `json:"end_range"` // float64 + RangeDiff float64 `json:"range_diff"` // float64 + } + // ClimateInfo struct - child of DriveDetails + type ClimateInfo struct { + InsideTemp NullFloat64 `json:"inside_temp"` // numeric(4,1) + OutsideTemp NullFloat64 `json:"outside_temp"` // numeric(4,1) + IsClimateOn NullBool `json:"is_climate_on"` // boolean + FanStatus NullInt64 `json:"fan_status"` // integer + DriverTempSetting NullFloat64 `json:"driver_temp_setting"` // numeric(4,1) + PassengerTempSetting NullFloat64 `json:"passenger_temp_setting"` // numeric(4,1) + IsRearDefrosterOn NullBool `json:"is_rear_defroster_on"` // boolean + IsFrontDefrosterOn NullBool `json:"is_front_defroster_on"` // boolean + } + // BatteryInfo struct - child of DriveDetails + type BatteryInfo struct { + EstBatteryRange NullFloat64 `json:"est_battery_range"` // numeric(6,2) + IdealBatteryRange NullFloat64 `json:"ideal_battery_range"` // numeric(6,2) + RatedBatteryRange NullFloat64 `json:"rated_battery_range"` // numeric(6,2) + BatteryHeater NullBool `json:"battery_heater"` // boolean + BatteryHeaterOn NullBool `json:"battery_heater_on"` // boolean + BatteryHeaterNoPower NullBool `json:"battery_heater_no_power"` // boolean + } + // DriveDetails struct - child of Drive + type DriveDetails struct { + DetailID int `json:"detail_id"` // integer + Date string `json:"date"` // timestamp without time zone + Latitude float64 `json:"latitude"` // numeric(8,6) + Longitude float64 `json:"longitude"` // numeric(9,6) + Speed int `json:"speed"` // smallint + Power int `json:"power"` // smallint + Odometer float64 `json:"odometer"` // double precision + BatteryLevel int `json:"battery_level"` // smallint + UsableBatteryLevel NullInt64 `json:"usable_battery_level"` // smallint + Elevation NullInt64 `json:"elevation"` // smallint + ClimateInfo ClimateInfo `json:"climate_info"` // struct + BatteryInfo BatteryInfo `json:"battery_info"` // struct + } + // Drive struct - child of Data + type Drive struct { + DriveID int `json:"drive_id"` // int + StartDate string `json:"start_date"` // string + EndDate string `json:"end_date"` // string + StartAddress string `json:"start_address"` // string + EndAddress string `json:"end_address"` // string + OdometerDetails OdometerDetails `json:"odometer_details"` // OdometerDetails + DurationMin int `json:"duration_min"` // int + DurationStr string `json:"duration_str"` // string + SpeedMax int `json:"speed_max"` // int + SpeedAvg float64 `json:"speed_avg"` // float64 + PowerMax int `json:"power_max"` // int + PowerMin int `json:"power_min"` // int + BatteryDetails BatteryDetails `json:"battery_details"` // BatteryDetails + RangeIdeal PreferredRange `json:"range_ideal"` // PreferredRange + RangeRated PreferredRange `json:"range_rated"` // PreferredRange + OutsideTempAvg float64 `json:"outside_temp_avg"` // float64 + InsideTempAvg float64 `json:"inside_temp_avg"` // float64 + DriveDetails []DriveDetails `json:"drive_details"` // struct + } + // TeslaMateUnits struct - child of Data + type TeslaMateUnits struct { + UnitsLength string `json:"unit_of_length"` // string + UnitsTemperature string `json:"unit_of_temperature"` // string + } + // Data struct - child of JSONData + type Data struct { + Car Car `json:"car"` + Drive Drive `json:"drive"` + TeslaMateUnits TeslaMateUnits `json:"units"` + } + // JSONData struct - main + type JSONData struct { + Data Data `json:"data"` + } + + // creating required vars + var DriveData Drive + var DriveDetailsData []DriveDetails + var UnitsLength, UnitsTemperature, CarName string + var ValidResponse bool // default is false + + // getting data from database + query := ` + SELECT + drives.id AS drive_id, + start_date, + end_date, + COALESCE(start_geofence.name, CONCAT_WS(', ', COALESCE(start_address.name, nullif(CONCAT_WS(' ', start_address.road, start_address.house_number), '')), start_address.city)) AS start_address, + COALESCE(end_geofence.name, CONCAT_WS(', ', COALESCE(end_address.name, nullif(CONCAT_WS(' ', end_address.road, end_address.house_number), '')), end_address.city)) AS end_address, + start_km, + end_km, + distance, + duration_min, + TO_CHAR((duration_min * INTERVAL '1 minute'), 'HH24:MI') as duration_str, + speed_max, + COALESCE(distance / NULLIF(duration_min, 0) * 60, 0) AS speed_avg, + power_max, + power_min, + COALESCE(start_position.usable_battery_level, start_position.battery_level) as start_usable_battery_level, + start_position.battery_level as start_battery_level, + COALESCE(end_position.usable_battery_level, end_position.battery_level) as end_usable_battery_level, + end_position.battery_level as end_battery_level, + case when ( start_position.battery_level != start_position.usable_battery_level OR end_position.battery_level != end_position.usable_battery_level ) = true then true else false end as reduced_range, + duration_min > 1 AND distance > 1 AND ( start_position.usable_battery_level IS NULL OR end_position.usable_battery_level IS NULL OR ( end_position.battery_level - end_position.usable_battery_level ) = 0 ) as is_sufficiently_precise, + start_ideal_range_km, + end_ideal_range_km, + COALESCE( NULLIF ( GREATEST ( start_ideal_range_km - end_ideal_range_km, 0 ), 0 ),0 ) as range_diff_ideal_km, + start_rated_range_km, + end_rated_range_km, + COALESCE( NULLIF ( GREATEST ( start_rated_range_km - end_rated_range_km, 0 ), 0 ),0 ) as range_diff_rated_km, + outside_temp_avg, + inside_temp_avg, + (SELECT unit_of_length FROM settings LIMIT 1) as unit_of_length, + (SELECT unit_of_temperature FROM settings LIMIT 1) as unit_of_temperature, + cars.name + FROM drives + LEFT JOIN cars ON car_id = cars.id + LEFT JOIN addresses start_address ON start_address_id = start_address.id + LEFT JOIN addresses end_address ON end_address_id = end_address.id + LEFT JOIN positions start_position ON start_position_id = start_position.id + LEFT JOIN positions end_position ON end_position_id = end_position.id + LEFT JOIN geofences start_geofence ON start_geofence_id = start_geofence.id + LEFT JOIN geofences end_geofence ON end_geofence_id = end_geofence.id + WHERE drives.car_id=$1 AND end_date IS NOT NULL AND drives.id = $2;` + rows, err := db.Query(query, CarID, DriveID) + + // checking for errors in query + if err != nil { + log.Fatal(err) + } + + // defer closing rows + defer rows.Close() + + // looping through all results + for rows.Next() { + + // creating drive object based on struct + drive := Drive{} + + // scanning row and putting values into the drive + err = rows.Scan( + &drive.DriveID, + &drive.StartDate, + &drive.EndDate, + &drive.StartAddress, + &drive.EndAddress, + &drive.OdometerDetails.OdometerStart, + &drive.OdometerDetails.OdometerEnd, + &drive.OdometerDetails.OdometerDistance, + &drive.DurationMin, + &drive.DurationStr, + &drive.SpeedMax, + &drive.SpeedAvg, + &drive.PowerMax, + &drive.PowerMin, + &drive.BatteryDetails.StartUsableBatteryLevel, + &drive.BatteryDetails.StartBatteryLevel, + &drive.BatteryDetails.EndUsableBatteryLevel, + &drive.BatteryDetails.EndBatteryLevel, + &drive.BatteryDetails.ReducedRange, + &drive.BatteryDetails.IsSufficientlyPrecise, + &drive.RangeIdeal.StartRange, + &drive.RangeIdeal.EndRange, + &drive.RangeIdeal.RangeDiff, + &drive.RangeRated.StartRange, + &drive.RangeRated.EndRange, + &drive.RangeRated.RangeDiff, + &drive.OutsideTempAvg, + &drive.InsideTempAvg, + &UnitsLength, + &UnitsTemperature, + &CarName, + ) + + // converting values based of settings UnitsLength + if UnitsLength == "mi" { + drive.OdometerDetails.OdometerStart = kilometersToMiles(drive.OdometerDetails.OdometerStart) + drive.OdometerDetails.OdometerEnd = kilometersToMiles(drive.OdometerDetails.OdometerEnd) + drive.OdometerDetails.OdometerDistance = kilometersToMiles(drive.OdometerDetails.OdometerDistance) + drive.SpeedMax = int(kilometersToMiles(float64(drive.SpeedMax))) + drive.SpeedAvg = kilometersToMiles(drive.SpeedAvg) + drive.RangeIdeal.StartRange = kilometersToMiles(drive.RangeIdeal.StartRange) + drive.RangeIdeal.EndRange = kilometersToMiles(drive.RangeIdeal.EndRange) + drive.RangeIdeal.RangeDiff = kilometersToMiles(drive.RangeIdeal.RangeDiff) + drive.RangeRated.StartRange = kilometersToMiles(drive.RangeRated.StartRange) + drive.RangeRated.EndRange = kilometersToMiles(drive.RangeRated.EndRange) + drive.RangeRated.RangeDiff = kilometersToMiles(drive.RangeRated.RangeDiff) + } + // converting values based of settings UnitsTemperature + if UnitsTemperature == "F" { + drive.OutsideTempAvg = celsiusToFahrenheit(drive.OutsideTempAvg) + drive.InsideTempAvg = celsiusToFahrenheit(drive.InsideTempAvg) + } + // adjusting to timezone differences from UTC to be userspecific + drive.StartDate = getTimeInTimeZone(drive.StartDate) + drive.EndDate = getTimeInTimeZone(drive.EndDate) + + // checking for errors after scanning + if err != nil { + log.Fatal(err) + } + + // appending drive to DriveData + DriveData = drive + ValidResponse = true + + // getting detailed drive data from database + query = ` + SELECT + id AS detail_id, + date, + latitude, + longitude, + COALESCE(speed, 0) AS speed, + power, + odometer, + battery_level, + usable_battery_level, + elevation, + inside_temp, + outside_temp, + is_climate_on, + fan_status, + driver_temp_setting, + passenger_temp_setting, + is_rear_defroster_on, + is_front_defroster_on, + est_battery_range_km, + ideal_battery_range_km, + rated_battery_range_km, + battery_heater, + battery_heater_on, + battery_heater_no_power + FROM positions + WHERE drive_id = $1 + ORDER BY id ASC;` + rows, err = db.Query(query, DriveID) + + // checking for errors in query + if err != nil { + log.Fatal(err) + } + + // defer closing rows + defer rows.Close() + + // looping through all results + for rows.Next() { + + // creating drivedetails object based on struct + drivedetails := DriveDetails{} + + // scanning row and putting values into the drive + err = rows.Scan( + &drivedetails.DetailID, + &drivedetails.Date, + &drivedetails.Latitude, + &drivedetails.Longitude, + &drivedetails.Speed, + &drivedetails.Power, + &drivedetails.Odometer, + &drivedetails.BatteryLevel, + &drivedetails.UsableBatteryLevel, + &drivedetails.Elevation, + &drivedetails.ClimateInfo.InsideTemp, + &drivedetails.ClimateInfo.OutsideTemp, + &drivedetails.ClimateInfo.IsClimateOn, + &drivedetails.ClimateInfo.FanStatus, + &drivedetails.ClimateInfo.DriverTempSetting, + &drivedetails.ClimateInfo.PassengerTempSetting, + &drivedetails.ClimateInfo.IsRearDefrosterOn, + &drivedetails.ClimateInfo.IsFrontDefrosterOn, + &drivedetails.BatteryInfo.EstBatteryRange, + &drivedetails.BatteryInfo.IdealBatteryRange, + &drivedetails.BatteryInfo.RatedBatteryRange, + &drivedetails.BatteryInfo.BatteryHeater, + &drivedetails.BatteryInfo.BatteryHeaterOn, + &drivedetails.BatteryInfo.BatteryHeaterNoPower, + ) + + // converting values based of settings UnitsLength + if UnitsLength == "mi" { + drivedetails.Odometer = kilometersToMiles(drivedetails.Odometer) + drivedetails.Speed = int(kilometersToMiles(float64(drivedetails.Speed))) + drivedetails.BatteryInfo.EstBatteryRange = kilometersToMilesNilSupport(drivedetails.BatteryInfo.EstBatteryRange) + drivedetails.BatteryInfo.IdealBatteryRange = kilometersToMilesNilSupport(drivedetails.BatteryInfo.IdealBatteryRange) + drivedetails.BatteryInfo.RatedBatteryRange = kilometersToMilesNilSupport(drivedetails.BatteryInfo.RatedBatteryRange) + } + // converting values based of settings UnitsTemperature + if UnitsTemperature == "F" { + drivedetails.ClimateInfo.InsideTemp = celsiusToFahrenheitNilSupport(drivedetails.ClimateInfo.InsideTemp) + drivedetails.ClimateInfo.OutsideTemp = celsiusToFahrenheitNilSupport(drivedetails.ClimateInfo.OutsideTemp) + drivedetails.ClimateInfo.DriverTempSetting = celsiusToFahrenheitNilSupport(drivedetails.ClimateInfo.DriverTempSetting) + drivedetails.ClimateInfo.PassengerTempSetting = celsiusToFahrenheitNilSupport(drivedetails.ClimateInfo.PassengerTempSetting) + } + // adjusting to timezone differences from UTC to be userspecific + drivedetails.Date = getTimeInTimeZone(drivedetails.Date) + + // checking for errors after scanning + if err != nil { + log.Fatal(err) + } + + // appending drive to DriveData + DriveDetailsData = append(DriveDetailsData, drivedetails) + DriveData.DriveDetails = DriveDetailsData + } + + // checking for errors in the rows result + err = rows.Err() + if err != nil { + log.Fatal(err) + } + + } + + // checking for errors in the rows result + err = rows.Err() + if err != nil { + log.Fatal(err) + } + + // + // build the data-blob + jsonData := JSONData{ + Data{ + Car: Car{ + CarID: CarID, + CarName: CarName, + }, + Drive: DriveData, + TeslaMateUnits: TeslaMateUnits{ + UnitsLength: UnitsLength, + UnitsTemperature: UnitsTemperature, + }, + }, + } + + // print readable output to log + log.Printf("data for /cars/%d/drives/%d created:", CarID, DriveID) + + js, _ := json.Marshal(jsonData) + fmt.Printf("%s\n", js) + return string(js), ValidResponse +} diff --git a/src/webserver.go b/src/webserver.go index 68afad1..01e649e 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -56,6 +56,74 @@ func main() { } }) + // /cars//charges endpoint + r.GET("/cars/:CarID/charges", func(c *gin.Context) { + // getting CarID param from URL + CarID := convertStringToInteger(c.Param("CarID")) + // query options to modify query when collecting data + ResultPage := convertStringToInteger(c.DefaultQuery("page", "1")) + ResultShow := convertStringToInteger(c.DefaultQuery("show", "100")) + // TeslaMateAPICarsCharges to get data + result, ValidResponse := TeslaMateAPICarsCharges(CarID, ResultPage, ResultShow) + + c.Header("Content-Type", "application/json") + if ValidResponse { + c.String(http.StatusOK, result) + } else { + c.String(http.StatusNotFound, result) + } + }) + + // /cars//charges/ endpoint + r.GET("/cars/:CarID/charges/:ChargeID", func(c *gin.Context) { + // getting CarID and ChargeID param from URL + CarID := convertStringToInteger(c.Param("CarID")) + ChargeID := convertStringToInteger(c.Param("ChargeID")) + // TeslaMateAPICarsChargesDetails to get data + result, ValidResponse := TeslaMateAPICarsChargesDetails(CarID, ChargeID) + + c.Header("Content-Type", "application/json") + if ValidResponse { + c.String(http.StatusOK, result) + } else { + c.String(http.StatusNotFound, result) + } + }) + + // /cars//drives endpoint + r.GET("/cars/:CarID/drives", func(c *gin.Context) { + // getting CarID param from URL + CarID := convertStringToInteger(c.Param("CarID")) + // query options to modify query when collecting data + ResultPage := convertStringToInteger(c.DefaultQuery("page", "1")) + ResultShow := convertStringToInteger(c.DefaultQuery("show", "100")) + // TeslaMateAPICarsDrives to get data + result, ValidResponse := TeslaMateAPICarsDrives(CarID, ResultPage, ResultShow) + + c.Header("Content-Type", "application/json") + if ValidResponse { + c.String(http.StatusOK, result) + } else { + c.String(http.StatusNotFound, result) + } + }) + + // /cars//drives/ endpoint + r.GET("/cars/:CarID/drives/:DriveID", func(c *gin.Context) { + // getting CarID and DriveID param from URL + CarID := convertStringToInteger(c.Param("CarID")) + DriveID := convertStringToInteger(c.Param("DriveID")) + // TeslaMateAPICarsDrivesDetails to get data + result, ValidResponse := TeslaMateAPICarsDrivesDetails(CarID, DriveID) + + c.Header("Content-Type", "application/json") + if ValidResponse { + c.String(http.StatusOK, result) + } else { + c.String(http.StatusNotFound, result) + } + }) + // /cars//status endpoint r.GET("/cars/:CarID/status", func(c *gin.Context) { // getting mqtt flag