netatmo-api-go/weather.go
2021-01-14 13:18:36 +01:00

313 lines
9.1 KiB
Go

package netatmo
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"golang.org/x/oauth2"
)
const (
// DefaultBaseURL is netatmo api url
baseURL = "https://api.netatmo.net/"
// DefaultAuthURL is netatmo auth url
authURL = baseURL + "oauth2/token"
// DefaultDeviceURL is netatmo device url
stationURL = baseURL + "/api/getstationsdata"
homecoachURL = baseURL + "/api/gethomecoachsdata"
)
// Config is used to specify credential to Netatmo API
// ClientID : Client ID from netatmo app registration at http://dev.netatmo.com/dev/listapps
// ClientSecret : Client app secret
// Username : Your netatmo account username
// Password : Your netatmo account password
type Config struct {
ClientID string
ClientSecret string
Username string
Password string
}
// Client use to make request to Netatmo API
type Client struct {
oauth *oauth2.Config
httpClient *http.Client
httpResponse *http.Response
Dc *DeviceCollection
}
// DeviceCollection hold all devices from netatmo account
type DeviceCollection struct {
Body struct {
Devices []*Device `json:"devices"`
}
}
// Device is a station or a module
// ID : Mac address
// StationName : Station name (only for station)
// ModuleName : Module name
// BatteryPercent : Percentage of battery remaining
// WifiStatus : Wifi status per Base station
// RFStatus : Current radio status per module
// Type : Module type :
// "NAMain" : for the base station
// "NAModule1" : for the outdoor module
// "NAModule4" : for the additional indoor module
// "NAModule3" : for the rain gauge module
// "NAModule2" : for the wind gauge module
// DashboardData : Data collection from device sensors
// DataType : List of available datas
// LinkedModules : Associated modules (only for station)
type Device struct {
ID string `json:"_id"`
StationName string `json:"station_name"`
ModuleName string `json:"module_name"`
BatteryPercent *int32 `json:"battery_percent,omitempty"`
WifiStatus *int32 `json:"wifi_status,omitempty"`
RFStatus *int32 `json:"rf_status,omitempty"`
Type string
DashboardData DashboardData `json:"dashboard_data"`
//DataType []string `json:"data_type"`
LinkedModules []*Device `json:"modules"`
}
// DashboardData is used to store sensor values
// Temperature : Last temperature measure @ LastMeasure (in °C)
// Humidity : Last humidity measured @ LastMeasure (in %)
// CO2 : Last Co2 measured @ time_utc (in ppm)
// Noise : Last noise measured @ LastMeasure (in db)
// Pressure : Last Sea level pressure measured @ LastMeasure (in mb)
// AbsolutePressure : Real measured pressure @ LastMeasure (in mb)
// Rain : Last rain measured (in mm)
// Rain1Hour : Amount of rain in last hour
// Rain1Day : Amount of rain today
// WindAngle : Current 5 min average wind direction @ LastMeasure (in °)
// WindStrength : Current 5 min average wind speed @ LastMeasure (in km/h)
// GustAngle : Direction of the last 5 min highest gust wind @ LastMeasure (in °)
// GustStrength : Speed of the last 5 min highest gust wind @ LastMeasure (in km/h)
// FIXME health_idx
// LastMeasure : Contains timestamp of last data received
type DashboardData struct {
Temperature *float32 `json:"Temperature,omitempty"` // use pointer to detect omitted field by json mapping
Humidity *int32 `json:"Humidity,omitempty"`
CO2 *int32 `json:"CO2,omitempty"`
Noise *int32 `json:"Noise,omitempty"`
Pressure *float32 `json:"Pressure,omitempty"`
AbsolutePressure *float32 `json:"AbsolutePressure,omitempty"`
Rain *float32 `json:"Rain,omitempty"`
Rain1Hour *float32 `json:"sum_rain_1,omitempty"`
Rain1Day *float32 `json:"sum_rain_24,omitempty"`
WindAngle *int32 `json:"WindAngle,omitempty"`
WindStrength *int32 `json:"WindStrength,omitempty"`
GustAngle *int32 `json:"GustAngle,omitempty"`
GustStrength *int32 `json:"GustStrength,omitempty"`
HealthIndex *int32 `json:"health_idx,omitempty"`
LastMeasure *int64 `json:"time_utc"`
}
// NewClient create a handle authentication to Netamo API
func NewClient(config Config) (*Client, error) {
oauth := &oauth2.Config{
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Scopes: []string{"read_station", "read_homecoach"},
Endpoint: oauth2.Endpoint{
AuthURL: baseURL,
TokenURL: authURL,
},
}
token, err := oauth.PasswordCredentialsToken(oauth2.NoContext, config.Username, config.Password)
return &Client{
oauth: oauth,
httpClient: oauth.Client(oauth2.NoContext, token),
Dc: &DeviceCollection{},
}, err
}
// do a url encoded HTTP POST request
func (c *Client) doHTTPPostForm(url string, data url.Values) (*http.Response, error) {
req, err := http.NewRequest("POST", url, strings.NewReader(data.Encode()))
if err != nil {
return nil, err
}
//req.ContentLength = int64(reader.Len())
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return c.doHTTP(req)
}
// send http GET request
func (c *Client) doHTTPGet(url string, data url.Values) (*http.Response, error) {
if data != nil {
url = url + "?" + data.Encode()
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return c.doHTTP(req)
}
// do a generic HTTP request
func (c *Client) doHTTP(req *http.Request) (*http.Response, error) {
// debug
//debug, _ := httputil.DumpRequestOut(req, true)
//fmt.Printf("%s\n\n", debug)
var err error
c.httpResponse, err = c.httpClient.Do(req)
if err != nil {
return nil, err
}
return c.httpResponse, nil
}
// process HTTP response
// Unmarshall received data into holder struct
func processHTTPResponse(resp *http.Response, err error, holder interface{}) error {
defer resp.Body.Close()
if err != nil {
return err
}
// debug
//debug, _ := httputil.DumpResponse(resp, true)
//fmt.Printf("%s\n\n", debug)
// check http return code
if resp.StatusCode != 200 {
//bytes, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("Bad HTTP return code %d", resp.StatusCode)
}
// Unmarshall response into given struct
if err = json.NewDecoder(resp.Body).Decode(holder); err != nil {
return err
}
return nil
}
// GetStations returns the list of stations owned by the user, and their modules
func (c *Client) Read() (*DeviceCollection, error) {
// resp, err := c.doHTTPGet(stationURL, url.Values{"app_type": {"app_station"}})
//dc := &DeviceCollection{}
/*
if err = processHTTPResponse(resp, err, c.Dc); err != nil {
return nil, err
}
*/
resp2, err := c.doHTTPGet(homecoachURL, url.Values{"app_type": {"app_station"}})
// dc := &DeviceCollection{}
if err = processHTTPResponse(resp2, err, c.Dc); err != nil {
return nil, err
}
return c.Dc, nil
}
// Devices returns the list of devices
func (dc *DeviceCollection) Devices() []*Device {
return dc.Body.Devices
}
// Stations is an alias of Devices
func (dc *DeviceCollection) Stations() []*Device {
return dc.Devices()
}
// Modules returns associated device module
func (d *Device) Modules() []*Device {
modules := d.LinkedModules
modules = append(modules, d)
return modules
}
// Data returns timestamp and the list of sensor value for this module
func (d *Device) Data() (int64, map[string]interface{}) {
// return only populate field of DashboardData
m := make(map[string]interface{})
if d.DashboardData.Temperature != nil {
m["Temperature"] = *d.DashboardData.Temperature
}
if d.DashboardData.Humidity != nil {
m["Humidity"] = *d.DashboardData.Humidity
}
if d.DashboardData.CO2 != nil {
m["CO2"] = *d.DashboardData.CO2
}
if d.DashboardData.Noise != nil {
m["Noise"] = *d.DashboardData.Noise
}
if d.DashboardData.Pressure != nil {
m["Pressure"] = *d.DashboardData.Pressure
}
if d.DashboardData.AbsolutePressure != nil {
m["AbsolutePressure"] = *d.DashboardData.AbsolutePressure
}
if d.DashboardData.Rain != nil {
m["Rain"] = *d.DashboardData.Rain
}
if d.DashboardData.Rain1Hour != nil {
m["Rain1Hour"] = *d.DashboardData.Rain1Hour
}
if d.DashboardData.Rain1Day != nil {
m["Rain1Day"] = *d.DashboardData.Rain1Day
}
if d.DashboardData.WindAngle != nil {
m["WindAngle"] = *d.DashboardData.WindAngle
}
if d.DashboardData.WindStrength != nil {
m["WindStrength"] = *d.DashboardData.WindStrength
}
if d.DashboardData.GustAngle != nil {
m["GustAngle"] = *d.DashboardData.GustAngle
}
if d.DashboardData.GustAngle != nil {
m["GustAngle"] = *d.DashboardData.GustAngle
}
if d.DashboardData.GustStrength != nil {
m["GustStrength"] = *d.DashboardData.GustStrength
}
if d.DashboardData.HealthIndex != nil {
m["HealthIndex"] = *d.DashboardData.HealthIndex
}
return *d.DashboardData.LastMeasure, m
}
// Info returns timestamp and the list of info value for this module
func (d *Device) Info() (int64, map[string]interface{}) {
// return only populate field of DashboardData
m := make(map[string]interface{})
// Return data from module level
if d.BatteryPercent != nil {
m["BatteryPercent"] = *d.BatteryPercent
}
if d.WifiStatus != nil {
m["WifiStatus"] = *d.WifiStatus
}
if d.RFStatus != nil {
m["RFStatus"] = *d.RFStatus
}
return *d.DashboardData.LastMeasure, m
}