Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions wled/Model/DeviceApi/Response/Info.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ struct Info: Decodable {
var product : String?
var mac : String?
var ipAddress : String?
// Missing: u - UserMods

var userMods: UserMods?

enum CodingKeys: String, CodingKey {
case leds
Expand Down Expand Up @@ -80,5 +79,6 @@ struct Info: Decodable {
case product
case mac
case ipAddress = "ip"
case userMods = "u"
}
}
27 changes: 27 additions & 0 deletions wled/Model/DeviceApi/Response/UserMods.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation

struct UserMods: Decodable {
var batteryLevel: Int?
var batteryVoltage: Double?

enum CodingKeys: String, CodingKey {
case batteryLevel = "Battery level"
case batteryVoltage = "Battery voltage"
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

// The API returns arrays [85], [3.9]. We want the first element.
// Battery level might be Int or Double.
if let levelArray = try? container.decode([Int].self, forKey: .batteryLevel), let first = levelArray.first {
batteryLevel = first
} else if let levelArray = try? container.decode([Double].self, forKey: .batteryLevel), let first = levelArray.first {
batteryLevel = Int(first)
}

if let voltageArray = try? container.decode([Double].self, forKey: .batteryVoltage), let first = voltageArray.first {
batteryVoltage = first
}
}
}
49 changes: 49 additions & 0 deletions wled/View/DeviceBatteryView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import SwiftUI

struct DeviceBatteryView: View {
let batteryLevel: Int

var body: some View {
HStack(spacing: 3) {
Image(systemName: getBatteryIconName(level: batteryLevel))
Text("\(batteryLevel)%")
}
.font(.caption2)
.foregroundStyle(getBatteryColor(level: batteryLevel))
}

private func getBatteryIconName(level: Int) -> String {
switch level {
case 0...10:
return "battery.0"
case 11...35:
return "battery.25"
case 36...60:
return "battery.50"
case 61...85:
return "battery.75"
default:
return "battery.100"
}
}

private func getBatteryColor(level: Int) -> Color {
if level <= 20 {
return .red
} else {
return .primary
}
}
}

struct DeviceBatteryView_Previews: PreviewProvider {
static var previews: some View {
VStack {
DeviceBatteryView(batteryLevel: 5)
DeviceBatteryView(batteryLevel: 25)
DeviceBatteryView(batteryLevel: 50)
DeviceBatteryView(batteryLevel: 75)
DeviceBatteryView(batteryLevel: 100)
}
}
}
5 changes: 5 additions & 0 deletions wled/View/DeviceInfoTwoRows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ struct DeviceInfoTwoRows: View {
.lineLimit(1)
.fixedSize()
.lineSpacing(0)

if let batteryLevel = device.stateInfo?.info.userMods?.batteryLevel {
DeviceBatteryView(batteryLevel: batteryLevel)
}

let signalStrength = Int(device.stateInfo?.info.wifi.signal ?? 0)
Label {
Text(
Expand Down
20 changes: 16 additions & 4 deletions wled/View/Preview/PreviewData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct PreviewData {
// MARK: - Devices

static var onlineDevice: DeviceWithState {
createDevice(name: "WLED Beam", ip: "10.0.1.12")
createDevice(name: "WLED Beam", ip: "10.0.1.12", batteryLevel: 85)
}

static var offlineDevice: DeviceWithState {
Expand Down Expand Up @@ -50,7 +50,8 @@ struct PreviewData {
ip: String,
version: String = "0.14.0",
isHidden: Bool = false,
color: [Int] = [255, 160, 0]
color: [Int] = [255, 160, 0],
batteryLevel: Int? = nil
) -> DeviceWithState {
let macAddress = "mock:mac:\(ip)"
let request: NSFetchRequest<Device> = Device.fetchRequest()
Expand All @@ -73,7 +74,7 @@ struct PreviewData {

let deviceWithState = DeviceWithState(initialDevice: device)
deviceWithState.websocketStatus = .connected
deviceWithState.stateInfo = .mock(name: name, version: version, color: color)
deviceWithState.stateInfo = .mock(name: name, version: version, color: color, batteryLevel: batteryLevel)

// Save to ensure ID is stable
if viewContext.hasChanges {
Expand Down Expand Up @@ -104,11 +105,21 @@ struct PreviewData {
// MARK: - Mock Data Extensions

extension DeviceStateInfo {
static func mock(name: String, version: String, color: [Int]) -> DeviceStateInfo {
static func mock(name: String, version: String, color: [Int], batteryLevel: Int? = nil) -> DeviceStateInfo {
let r = color.indices.contains(0) ? color[0] : 255
let g = color.indices.contains(1) ? color[1] : 160
let b = color.indices.contains(2) ? color[2] : 0

var userModsJson = ""
if let batteryLevel = batteryLevel {
userModsJson = """
, "u": {
"Battery level": [\(batteryLevel)],
"Battery voltage": [3.9]
}
"""
}

let json = """
{
"state": {
Expand All @@ -123,6 +134,7 @@ extension DeviceStateInfo {
"ver": "\(version)",
"leds": { "count": 30, "pwr": 0, "fps": 0, "maxpwr": 0, "maxseg": 0 },
"wifi": { "signal": -60 }
\(userModsJson)
}
}
"""
Expand Down
74 changes: 74 additions & 0 deletions wledTests/UserModsDecodingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@

import Testing
import Foundation
@testable import WLED

struct UserModsDecodingTests {

@Test func testDecodeUserMods() throws {
let json = """
{
"u": {
"Battery level": [85],
"Battery voltage": [3.9]
}
}
""".data(using: .utf8)!

// We can't decode UserMods directly easily because it's part of Info which has many required fields.
// However, we can create a wrapper struct just for testing decoding of UserMods,
// or we can test decoding of UserMods itself if we conform it to Decodable (which it is).

let decoder = JSONDecoder()

struct TestContainer: Decodable {
let u: UserMods
}

let result = try decoder.decode(TestContainer.self, from: json)

#expect(result.u.batteryLevel == 85)
#expect(result.u.batteryVoltage == 3.9)
}

@Test func testDecodeUserModsWithDoubleLevel() throws {
let json = """
{
"u": {
"Battery level": [85.5],
"Battery voltage": [3.9]
}
}
""".data(using: .utf8)!

let decoder = JSONDecoder()

struct TestContainer: Decodable {
let u: UserMods
}

let result = try decoder.decode(TestContainer.self, from: json)

#expect(result.u.batteryLevel == 85) // casted to Int
#expect(result.u.batteryVoltage == 3.9)
}

@Test func testDecodeUserModsMissingData() throws {
let json = """
{
"u": {}
}
""".data(using: .utf8)!

let decoder = JSONDecoder()

struct TestContainer: Decodable {
let u: UserMods
}

let result = try decoder.decode(TestContainer.self, from: json)

#expect(result.u.batteryLevel == nil)
#expect(result.u.batteryVoltage == nil)
}
}