π EnergyKit
κ°μ μ μλμ§ μ¬μ©μ μΆμ νκ³ μ΅μ ννλ νλ μμν¬
β¨ EnergyKit is?
EnergyKit is a framework introduced in iOS 18 that tracks and analyzes home energy usage in real-time. Integrated with HomeKit, it collects data from smart plugs, power meters, solar panels and other energy devices, providing users with power consumption patterns, renewable energy production, and saving tips. An energy management solution that supports both environmental protection and electricity cost reduction.
π― 1. Basic Setup
EnergyKitμ μ΄κΈ°ννκ³ κΆνμ μμ².
import SwiftUI import HomeKit // μλμ§ κ΄λ¦¬μ @Observable class EnergyManager: NSObject { var homeManager = HMHomeManager() var currentHome: HMHome? var energyDevices: [EnergyDevice] = [] var isAuthorized = false override init() { super.init() homeManager.delegate = self } // HomeKit κΆν νμΈ func checkAuthorization() async -> Bool { // HomeKitμ μλμΌλ‘ κΆν μμ² await MainActor.run { isAuthorized = homeManager.authorizationStatus == .authorized } return isAuthorized } // μλμ§ κΈ°κΈ° κ²μ func discoverEnergyDevices() { guard let home = homeManager.primaryHome else { return } currentHome = home energyDevices = [] // μ λ ₯ μΈ‘μ κΈ°λ₯μ΄ μλ κΈ°κΈ° νν°λ§ for accessory in home.accessories { for service in accessory.services { // μ λ ₯ μΈ‘μ νΉμ± νμΈ if service.characteristics.contains(where: { $0.characteristicType == HMCharacteristicTypePowerState }) { let device = EnergyDevice( id: accessory.uniqueIdentifier, name: accessory.name, room: accessory.room?.name ?? "μ μ μμ", accessory: accessory ) energyDevices.append(device) } } } } } // HomeKit λΈλ¦¬κ²μ΄νΈ extension EnergyManager: HMHomeManagerDelegate { func homeManagerDidUpdateHomes(_ manager: HMHomeManager) { discoverEnergyDevices() } } // μλμ§ κΈ°κΈ° λͺ¨λΈ struct EnergyDevice: Identifiable { let id: UUID let name: String let room: String let accessory: HMAccessory var currentPower: Double = 0 // Watts var todayEnergy: Double = 0 // kWh }
β‘ 2. μ€μκ° μ λ ₯ λͺ¨λν°λ§
νμ¬ μ λ ₯ μλΉλμ μ€μκ°μΌλ‘ νμΈ.
import SwiftUI import Charts struct PowerReading: Identifiable { let id = UUID() let timestamp: Date let power: Double // Watts } @Observable class PowerMonitor { var currentPower: Double = 0 var readings: [PowerReading] = [] private var timer: Timer? // μ€μκ° λͺ¨λν°λ§ μμ func startMonitoring() { timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in Task { @MainActor in await self.updatePowerReading() } } } // λͺ¨λν°λ§ μ€μ§ func stopMonitoring() { timer?.invalidate() timer = nil } // μ λ ₯ μΈ‘μ func updatePowerReading() async { // μ€μ λ‘λ HomeKitμμ λ°μ΄ν° μ½κΈ° let power = Double.random(in: 500...2000) currentPower = power readings.append(PowerReading(timestamp: Date(), power: power)) // μ΅κ·Ό 1μκ° λ°μ΄ν°λ§ μ μ§ let oneHourAgo = Date().addingTimeInterval(-3600) readings.removeAll { $0.timestamp < oneHourAgo } } var averagePower: Double { guard !readings.isEmpty else { return 0 } return readings.map { $0.power }.reduce(0, +) / Double(readings.count) } } struct RealTimePowerView: View { @State private var monitor = PowerMonitor() var body: some View { ScrollView { VStack(spacing: 24) { // νμ¬ μ λ ₯ VStack { Text("νμ¬ μ¬μ©λ") .font(.headline) .foregroundStyle(.secondary) HStack(alignment: .firstTextBaseline, spacing: 4) { Text("\(Int(monitor.currentPower))") .font(.system(size: 60, weight: .bold)) .contentTransition(.numericText()) Text("W") .font(.title) .foregroundStyle(.secondary) } Text("νκ· \(Int(monitor.averagePower))W") .font(.subheadline) .foregroundStyle(.secondary) } .padding() .background(Color.blue.opacity(0.1)) .cornerRadius(16) // μ€μκ° μ°¨νΈ VStack(alignment: .leading) { Text("μ΅κ·Ό 1μκ°") .font(.headline) Chart(monitor.readings) { reading in LineMark( x: .value("μκ°", reading.timestamp), y: .value("μ λ ₯", reading.power) ) .foregroundStyle(.blue) .interpolationMethod(.catmullRom) AreaMark( x: .value("μκ°", reading.timestamp), y: .value("μ λ ₯", reading.power) ) .foregroundStyle(.blue.opacity(0.2)) .interpolationMethod(.catmullRom) } .frame(height: 200) .chartYAxis { AxisMarks { value in AxisValueLabel { if let power = value.as(Double.self) { Text("\(Int(power))W") } } } } } // λΉμ© μΆμ HStack { VStack(alignment: .leading) { Text("μκ°λΉ λΉμ©") .font(.caption) .foregroundStyle(.secondary) Text("\(estimatedHourlyCost)μ") .font(.title3) .fontWeight(.semibold) } Spacer() VStack(alignment: .trailing) { Text("μ μμ λΉμ©") .font(.caption) .foregroundStyle(.secondary) Text("\(estimatedMonthlyCost)μ") .font(.title3) .fontWeight(.semibold) } } .padding() .background(Color.gray.opacity(0.1)) .cornerRadius(12) } .padding() } .navigationTitle("μ λ ₯ λͺ¨λν°") .onAppear { monitor.startMonitoring() } .onDisappear { monitor.stopMonitoring() } } // μ κΈ° μκΈ κ³μ° (kWhλΉ 300μ κ°μ ) var estimatedHourlyCost: Int { Int(monitor.currentPower / 1000 * 300) } var estimatedMonthlyCost: Int { estimatedHourlyCost * 24 * 30 } }
π 3. κΈ°κΈ°λ³ μ¬μ©λ λΆμ
κ° κΈ°κΈ°μ μλμ§ μλΉλ₯Ό λΆμνκ³ λΉκ΅.
import SwiftUI import Charts struct DeviceEnergyData: Identifiable { let id = UUID() let deviceName: String let energy: Double // kWh let percentage: Double let category: DeviceCategory } enum DeviceCategory: String, CaseIterable { case lighting = "μ‘°λͺ " case hvac = "λλλ°©" case appliance = "κ°μ μ ν" case entertainment = "μν°ν μΈλ¨ΌνΈ" case other = "κΈ°ν" var color: Color { switch self { case .lighting: return .yellow case .hvac: return .blue case .appliance: return .green case .entertainment: return .purple case .other: return .gray } } } struct DeviceAnalysisView: View { @State private var devices: [DeviceEnergyData] = [ .init(deviceName: "κ±°μ€ μμ΄μ»¨", energy: 45.2, percentage: 35, category: .hvac), .init(deviceName: "λμ₯κ³ ", energy: 28.5, percentage: 22, category: .appliance), .init(deviceName: "μ‘°λͺ ", energy: 18.7, percentage: 15, category: .lighting), .init(deviceName: "TV", energy: 15.3, percentage: 12, category: .entertainment), .init(deviceName: "μΈνκΈ°", energy: 12.8, percentage: 10, category: .appliance), .init(deviceName: "κΈ°ν", energy: 8.5, percentage: 6, category: .other) ] var totalEnergy: Double { devices.map { $0.energy }.reduce(0, +) } var body: some View { ScrollView { VStack(spacing: 24) { // μ΄ μ¬μ©λ VStack { Text("μ΄λ² λ¬ μ΄ μ¬μ©λ") .font(.headline) .foregroundStyle(.secondary) HStack(alignment: .firstTextBaseline) { Text("\(totalEnergy, specifier: "%.1f")") .font(.system(size: 48, weight: .bold)) Text("kWh") .font(.title2) .foregroundStyle(.secondary) } } // μν μ°¨νΈ Chart(devices) { device in SectorMark( angle: .value("μλμ§", device.energy), innerRadius: .ratio(0.6), angularInset: 2 ) .foregroundStyle(device.category.color) .annotation(position: .overlay) { Text("\(Int(device.percentage))%") .font(.caption) .fontWeight(.bold) } } .frame(height: 250) // κΈ°κΈ° λͺ©λ‘ VStack(alignment: .leading, spacing: 12) { Text("κΈ°κΈ°λ³ μμΈ") .font(.headline) ForEach(devices) { device in HStack { Circle() .fill(device.category.color) .frame(width: 12, height: 12) VStack(alignment: .leading, spacing: 2) { Text(device.deviceName) .font(.subheadline) Text(device.category.rawValue) .font(.caption) .foregroundStyle(.secondary) } Spacer() VStack(alignment: .trailing, spacing: 2) { Text("\(device.energy, specifier: "%.1f") kWh") .font(.subheadline) .fontWeight(.semibold) Text("\(Int(device.percentage))%") .font(.caption) .foregroundStyle(.secondary) } } .padding() .background(Color.gray.opacity(0.05)) .cornerRadius(8) } } // μ μ½ ν VStack(alignment: .leading, spacing: 12) { Label("μ μ½ ν", systemImage: "lightbulb.fill") .font(.headline) .foregroundStyle(.orange) Text("μμ΄μ»¨ μ€μ μ¨λλ₯Ό 1λ μ¬λ¦¬λ©΄ μ μ½ 6% μ μ½ν μ μμ΅λλ€.") .font(.subheadline) } .padding() .background(Color.orange.opacity(0.1)) .cornerRadius(12) } .padding() } .navigationTitle("κΈ°κΈ°λ³ μ¬μ©λ") } }
βοΈ 4. μ¬μ μλμ§ λͺ¨λν°λ§
νμκ΄ ν¨λ λ± μ¬μ μλμ§ μμ°λμ μΆμ .
import SwiftUI import Charts struct SolarData: Identifiable { let id = UUID() let hour: Int let production: Double // kWh let consumption: Double // kWh } @Observable class SolarEnergyManager { var todayProduction: Double = 18.5 var todayConsumption: Double = 22.3 var gridExport: Double = 3.2 // νλ§€λ var gridImport: Double = 7.0 // ꡬ맀λ var hourlyData: [SolarData] = [ .init(hour: 6, production: 0.2, consumption: 0.8), .init(hour: 7, production: 0.8, consumption: 1.2), .init(hour: 8, production: 1.5, consumption: 1.0), .init(hour: 9, production: 2.3, consumption: 0.9), .init(hour: 10, production: 3.0, consumption: 0.8), .init(hour: 11, production: 3.5, consumption: 1.0), .init(hour: 12, production: 3.8, consumption: 1.2), .init(hour: 13, production: 3.4, consumption: 1.5), .init(hour: 14, production: 2.8, consumption: 1.3), .init(hour: 15, production: 2.0, consumption: 1.8), .init(hour: 16, production: 1.2, consumption: 2.5), .init(hour: 17, production: 0.5, consumption: 3.2) ] var selfSufficiencyRate: Double { (todayProduction / todayConsumption) * 100 } } struct SolarEnergyView: View { @State private var manager = SolarEnergyManager() var body: some View { ScrollView { VStack(spacing: 24) { // μ€λ μμ½ HStack(spacing: 12) { VStack { Image(systemName: "sun.max.fill") .font(.title) .foregroundStyle(.orange) Text("μμ°") .font(.caption) .foregroundStyle(.secondary) Text("\(manager.todayProduction, specifier: "%.1f") kWh") .font(.headline) } .frame(maxWidth: .infinity) .padding() .background(Color.orange.opacity(0.1)) .cornerRadius(12) VStack { Image(systemName: "bolt.fill") .font(.title) .foregroundStyle(.blue) Text("μ¬μ©") .font(.caption) .foregroundStyle(.secondary) Text("\(manager.todayConsumption, specifier: "%.1f") kWh") .font(.headline) } .frame(maxWidth: .infinity) .padding() .background(Color.blue.opacity(0.1)) .cornerRadius(12) } // μκΈλ₯ VStack { Text("μλμ§ μκΈλ₯ ") .font(.headline) ZStack { Circle() .stroke(Color.gray.opacity(0.2), lineWidth: 20) Circle() .trim(from: 0, to: manager.selfSufficiencyRate / 100) .stroke( LinearGradient( colors: [.green, .yellow], startPoint: .leading, endPoint: .trailing ), style: StrokeStyle(lineWidth: 20, lineCap: .round) ) .rotationEffect(.degrees(-90)) .animation(.easeInOut, value: manager.selfSufficiencyRate) VStack { Text("\(Int(manager.selfSufficiencyRate))%") .font(.system(size: 42, weight: .bold)) Text("μκΈλ₯ ") .font(.caption) .foregroundStyle(.secondary) } } .frame(width: 200, height: 200) } // μκ°λ³ μ°¨νΈ VStack(alignment: .leading) { Text("μκ°λ³ μμ°/μλΉ") .font(.headline) Chart { ForEach(manager.hourlyData) { data in // μμ°λ BarMark( x: .value("μκ°", data.hour), y: .value("μμ°", data.production) ) .foregroundStyle(.orange) .position(by: .value("νμ ", "μμ°")) // μλΉλ BarMark( x: .value("μκ°", data.hour), y: .value("μλΉ", data.consumption) ) .foregroundStyle(.blue) .position(by: .value("νμ ", "μλΉ")) } } .frame(height: 200) .chartXAxis { AxisMarks { value in AxisValueLabel { if let hour = value.as(Int.self) { Text("\(hour)μ") } } } } } // μ λ ₯λ§ κ±°λ VStack(alignment: .leading, spacing: 12) { Text("μ λ ₯λ§ κ±°λ") .font(.headline) HStack { Label("νλ§€", systemImage: "arrow.up.circle.fill") .foregroundStyle(.green) Spacer() Text("\(manager.gridExport, specifier: "%.1f") kWh") .fontWeight(.semibold) } HStack { Label("ꡬ맀", systemImage: "arrow.down.circle.fill") .foregroundStyle(.red) Spacer() Text("\(manager.gridImport, specifier: "%.1f") kWh") .fontWeight(.semibold) } } .padding() .background(Color.gray.opacity(0.05)) .cornerRadius(12) // νκ²½ μν₯ VStack(alignment: .leading, spacing: 8) { Label("νκ²½ κΈ°μ¬λ", systemImage: "leaf.fill") .font(.headline) .foregroundStyle(.green) Text("μ€λ \(carbonReduction)kgμ COβ λ°°μΆμ μ€μμ΅λλ€") .font(.subheadline) Text("λ무 \(treesEquivalent)그루 μ¬μ ν¨κ³Ό") .font(.caption) .foregroundStyle(.secondary) } .padding() .background(Color.green.opacity(0.1)) .cornerRadius(12) } .padding() } .navigationTitle("νμκ΄ μλμ§") } // CO2 μ κ°λ (kWhλΉ μ½ 0.46kg) var carbonReduction: Double { manager.todayProduction * 0.46 } // λ무 νμ° (λ무 1그루λ μ°κ° μ½ 6kg CO2 ν‘μ) var treesEquivalent: Int { Int(carbonReduction / 6 * 365) } }
π 5. μ¬μ© ν¨ν΄ λΆμ
Analyze energy usage patterns and provide optimization suggestions.
import SwiftUI import Charts struct DailyUsage: Identifiable { let id = UUID() let date: Date let energy: Double let cost: Double } @Observable class UsagePatternAnalyzer { var weeklyData: [DailyUsage] = [] var peakHour: Int = 19 // μ€ν 7μ var recommendations: [String] = [] init() { generateWeeklyData() analyzePattern() } func generateWeeklyData() { let calendar = Calendar.current for day in 0.<7 { let date = calendar.date(byAdding: .day, value: -day, to: Date())! let energy = Double.random(in: 15...30) let cost = energy * 300 // kWhλΉ 300μ weeklyData.insert( DailyUsage(date: date, energy: energy, cost: cost), at: 0 ) } } func analyzePattern() { // ν¨ν΄ λΆμ λ° μΆμ² recommendations = [ "μ€ν 7-9μμ μ¬μ©λμ΄ κ°μ₯ λμ΅λλ€. μΈνκΈ°λ μ¬μΌ μκ°μ μ¬μ©νμΈμ.", "νκ· λ³΄λ€ 20% λ§μ΄ μ¬μ©νκ³ μμ΅λλ€. λκΈ°μ λ ₯μ μ°¨λ¨νμΈμ.", "λλλ°© μ€μ μ¨λλ₯Ό μ‘°μ νλ©΄ μ 15,000μ μ μ½ν μ μμ΅λλ€." ] } var averageDaily: Double { weeklyData.map { $0.energy }.reduce(0, +) / Double(weeklyData.count) } var estimatedMonthlyCost: Double { averageDaily * 30 * 300 } } struct UsagePatternView: View { @State private var analyzer = UsagePatternAnalyzer() var body: some View { ScrollView { VStack(spacing: 24) { // μ£Όκ° μΆμ΄ VStack(alignment: .leading) { Text("μ£Όκ° μ¬μ© μΆμ΄") .font(.headline) Chart(analyzer.weeklyData) { usage in BarMark( x: .value("λ μ§", usage.date, unit: .day), y: .value("μλμ§", usage.energy) ) .foregroundStyle( LinearGradient( colors: [.blue, .cyan], startPoint: .top, endPoint: .bottom ) ) RuleMark(y: .value("νκ· ", analyzer.averageDaily)) .foregroundStyle(.red) .lineStyle(StrokeStyle(lineWidth: 2, dash: [5])) .annotation(position: .top, alignment: .trailing) { Text("νκ· ") .font(.caption) .foregroundStyle(.red) } } .frame(height: 200) .chartXAxis { AxisMarks { value in AxisValueLabel(format: .dateTime.weekday(.narrow)) } } } // ν΅κ³ HStack(spacing: 12) { VStack { Text("μΌνκ· ") .font(.caption) .foregroundStyle(.secondary) Text("\(analyzer.averageDaily, specifier: "%.1f")") .font(.title2) .fontWeight(.bold) Text("kWh") .font(.caption) } .frame(maxWidth: .infinity) .padding() .background(Color.blue.opacity(0.1)) .cornerRadius(12) VStack { Text("μμμ") .font(.caption) .foregroundStyle(.secondary) Text("\(Int(analyzer.estimatedMonthlyCost))") .font(.title2) .fontWeight(.bold) Text("μ") .font(.caption) } .frame(maxWidth: .infinity) .padding() .background(Color.green.opacity(0.1)) .cornerRadius(12) } // νΌν¬ νμ VStack(alignment: .leading, spacing: 8) { Label("μ΅λ μ¬μ© μκ°", systemImage: "clock.fill") .font(.headline) Text("μ€ν \(analyzer.peakHour - 12)μ") .font(.title3) .fontWeight(.semibold) .foregroundStyle(.blue) Text("μ κΈ° μκΈμ΄ μ λ ΄ν μ¬μΌ μκ°(μ€ν 11μ~μ€μ 9μ)μ κ³ μ λ ₯ κΈ°κΈ°λ₯Ό μ¬μ©νμΈμ.") .font(.caption) .foregroundStyle(.secondary) } .padding() .background(Color.gray.opacity(0.05)) .cornerRadius(12) // μΆμ² μ¬ν VStack(alignment: .leading, spacing: 16) { Label("λ§μΆ€ μΆμ²", systemImage: "sparkles") .font(.headline) ForEach(Array(analyzer.recommendations.enumerated()), id: \.0) { index, tip in HStack(alignment: .top, spacing: 12) { Text("\(index + 1)") .font(.caption) .fontWeight(.bold) .foregroundStyle(.white) .frame(width: 24, height: 24) .background(Color.blue) .clipShape(Circle()) Text(tip) .font(.subheadline) } } } .padding() .background(Color.blue.opacity(0.05)) .cornerRadius(12) } .padding() } .navigationTitle("μ¬μ© ν¨ν΄ λΆμ") } }
π‘ HIG Guidelines
- Privacy: μλμ§ λ°μ΄ν°λ λ―Όκ° μ 보μ΄λ―λ‘ μ¨λλ°μ΄μ€μμ μ²λ¦¬
- μ€μκ° μ λ°μ΄νΈ: νμ¬ μ λ ₯ μλΉλ₯Ό 5-10μ΄ κ°κ²©μΌλ‘ μ λ°μ΄νΈ
- λͺ νν μκ°ν: μ°¨νΈμ κ·Έλνλ‘ μ΄ν΄νκΈ° μ½κ² νμ
- μ€μ©μ μΈ ν: μ€μ λ‘ μ€μ² κ°λ₯ν μ μ½ λ°©λ² μ μ
- HomeKit ν΅ν©: κΈ°μ‘΄ HomeKit κΈ°κΈ°μ μννκ² μ°λ
π― Practical Usage
- μ€λ§νΈν μ±: μ 체 κ°μ μ μλμ§ λμ보λ
- νμκ΄ λͺ¨λν°λ§: μ¬μ μλμ§ μμ°λ μΆμ
- μλμ§ μ μ½ μ±λ¦°μ§: κ²μ΄λ―ΈνΌμΌμ΄μ μΌλ‘ μ μ½ μ λ
- λΆλμ° κ΄λ¦¬: 건물 μλμ§ ν¨μ¨ λͺ¨λν°λ§
- νκ²½ μ±: νμ λ°μκ΅ κ³μ° λ° κ°μ