joshuar-go-hass-agent/internal/linux/device.go

281 lines
6.3 KiB
Go

// Copyright (c) 2024 Joshua Rich <joshua.rich@gmail.com>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
// revive:disable:unused-receiver
package linux
import (
"errors"
"os"
"strings"
"syscall"
"github.com/gofrs/uuid/v5"
"github.com/iancoleman/strcase"
"github.com/jaypipes/ghw"
mqtthass "github.com/joshuar/go-hass-anything/v9/pkg/hass"
"github.com/rs/zerolog/log"
"github.com/joshuar/go-hass-agent/internal/preferences"
"github.com/joshuar/go-hass-agent/pkg/linux/whichdistro"
)
const (
unknownVendor = "Unknown Vendor"
unknownModel = "Unknown Model"
unknownDistro = "Unknown Distro"
unknownDistroVersion = "Unknown Version"
)
var (
ErrDesktopPortalMissing = errors.New("no portal present")
ErrUnsupportedHardware = errors.New("unsupported hardware")
)
type Device struct {
appName string
appVersion string
hostname string
deviceID string
hwVendor string
hwModel string
distro string
distroVersion string
}
func (l *Device) AppName() string {
return l.appName
}
func (l *Device) AppVersion() string {
return l.appVersion
}
func (l *Device) AppID() string {
return strcase.ToSnake(l.appName)
}
func (l *Device) DeviceName() string {
shortHostname, _, _ := strings.Cut(l.hostname, ".")
return shortHostname
}
func (l *Device) DeviceID() string {
return l.deviceID
}
func (l *Device) Manufacturer() string {
return l.hwVendor
}
func (l *Device) Model() string {
return l.hwModel
}
func (l *Device) OsName() string {
return l.distro
}
func (l *Device) OsVersion() string {
return l.distroVersion
}
func (l *Device) SupportsEncryption() bool {
return false
}
func (l *Device) AppData() any {
return &struct {
PushWebsocket bool `json:"push_websocket_channel"`
}{
PushWebsocket: true,
}
}
//nolint:exhaustruct
func NewDevice(name, version string) *Device {
dev := &Device{
appName: name,
appVersion: version,
deviceID: getDeviceID(),
hostname: getHostname(),
}
dev.distro, dev.distroVersion = GetDistroID()
dev.hwModel, dev.hwVendor = getHWProductInfo()
return dev
}
//nolint:exhaustruct
func MQTTDevice() *mqtthass.Device {
dev := NewDevice(preferences.AppName, preferences.AppVersion)
prefs, err := preferences.Load()
if err != nil {
log.Warn().Err(err).Msg("Could not retrieve preferences.")
}
return &mqtthass.Device{
Name: dev.DeviceName(),
URL: preferences.AppURL,
SWVersion: dev.OsVersion(),
Manufacturer: dev.Manufacturer(),
Model: dev.Model(),
Identifiers: []string{prefs.DeviceID},
}
}
// getDeviceID create a new device ID. It will be a randomly generated UUIDv4.
func getDeviceID() string {
deviceID, err := uuid.NewV4()
if err != nil {
log.Warn().Err(err).
Msg("Could not retrieve a machine ID")
return "unknown"
}
return deviceID.String()
}
// getHostname retrieves the hostname of the device running the agent, or
// localhost if that doesn't work.
func getHostname() string {
hostname, err := os.Hostname()
if err != nil {
log.Warn().Err(err).Msg("Could not retrieve hostname. Using 'localhost'.")
return "localhost"
}
return hostname
}
// getHWProductInfo retrieves the model and vendor of the machine. If these
// cannot be retrieved or cannot be found, they will be set to default unknown
// strings.
func getHWProductInfo() (model, vendor string) {
product, err := ghw.Product(ghw.WithDisableWarnings())
if err != nil {
log.Warn().Err(err).Msg("Could not retrieve hardware information.")
return unknownModel, unknownVendor
}
return product.Name, product.Vendor
}
// Chassis will return the chassis type of the machine, such as "desktop" or
// "laptop". If this cannot be retrieved, it will return "unknown".
func Chassis() string {
chassisInfo, err := ghw.Chassis(ghw.WithDisableWarnings())
if err != nil {
log.Warn().Err(err).Msg("Could not determine chassis type.")
return "unknown"
}
return chassisInfo.Type
}
// FindPortal is a helper function to work out which portal interface should be
// used for getting information on running apps.
func FindPortal() (string, error) {
desktop := os.Getenv("XDG_CURRENT_DESKTOP")
switch {
case strings.Contains(desktop, "KDE"):
return "org.freedesktop.impl.portal.desktop.kde", nil
case strings.Contains(desktop, "GNOME"):
return "org.freedesktop.impl.portal.desktop.gtk", nil
default:
return "", ErrDesktopPortalMissing
}
}
// GetDistroID will retrieve the distribution ID and version ID. These are
// suitable for usage as part of identifiers and variables. See also
// GetDistroDetails.
func GetDistroID() (id, versionid string) {
var distroName, distroVersion string
osReleaseInfo, err := whichdistro.GetOSRelease()
if err != nil {
log.Warn().Err(err).Msg("Could not read /etc/os-release. Contact your distro vendor to implement this file.")
return unknownDistro, unknownDistroVersion
}
if v, ok := osReleaseInfo.GetValue("ID"); !ok {
distroName = unknownDistro
} else {
distroName = v
}
if v, ok := osReleaseInfo.GetValue("VERSION_ID"); !ok {
distroVersion = unknownDistroVersion
} else {
distroVersion = v
}
return distroName, distroVersion
}
// GetDistroDetails will retrieve the distribution name and version. The values
// are pretty-printed and may not be suitable for usage as identifiers and
// variables. See also GetDistroID.
func GetDistroDetails() (name, version string) {
var distroName, distroVersion string
osReleaseInfo, err := whichdistro.GetOSRelease()
if err != nil {
log.Warn().Err(err).Msg("Could not read /etc/os-release. Contact your distro vendor to implement this file.")
return unknownDistro, unknownDistroVersion
}
if v, ok := osReleaseInfo.GetValue("NAME"); !ok {
distroName = unknownDistro
} else {
distroName = v
}
if v, ok := osReleaseInfo.GetValue("VERSION"); !ok {
distroVersion = unknownDistroVersion
} else {
distroVersion = v
}
return distroName, distroVersion
}
// GetKernelVersion will retrieve the kernel version.
//
//nolint:prealloc
func GetKernelVersion() string {
var utsname syscall.Utsname
var versionBytes []byte
err := syscall.Uname(&utsname)
if err != nil {
log.Warn().Err(err).Msg("Could not retrieve kernel version.")
return "Unknown"
}
for _, v := range utsname.Release {
if v == 0 {
continue
}
versionBytes = append(versionBytes, uint8(v))
}
return string(versionBytes)
}