Repository: evcc-io/evcc
Pull request #8371 (2023-06-09 09:57:13)

automatically switch session log energy unit
I use evcc to charge devices with small batteries (pedelecs, vacuum cleaner). Showing the charged energy only in kWh is too coarse for these devices, whose battery is usually smaller than 1 kWh.

This commit changes the session log and log detail views to show the charged energy in Wh for values below 1 kWh and in kWh for larger values.


Patch:
diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml
index cde0ccd7..5b6c099d 100644
--- a/.github/workflows/default.yml
+++ b/.github/workflows/default.yml
@@ -44,31 +44,15 @@ jobs:
     steps:
       - uses: actions/checkout@v3
 
+      # - uses: actions/setup-go@v4
       - uses: erezrokah/setup-go@feat/add_cache_prefix
         with:
           go-version: ^1.20
           cache-key-prefix: build-cache-
         id: go
 
-      - uses: actions/setup-node@v3
-        with:
-          node-version: "18"
-          cache: "npm"
-
-      - name: Install UI
-        run: make install-ui
-
-      - name: Build UI
-        run: make ui
-
       - name: Build
-        run: make build
-
-      - name: Upload
-        uses: actions/upload-artifact@v3
-        with:
-          name: evcc-binary
-          path: ./evcc
+        run: mkdir dist && touch dist/empty && make build
 
   test:
     name: Test
@@ -108,7 +92,7 @@ jobs:
           version: latest
           args: --timeout 5m
 
-  ui:
+  build-ui:
     name: UI
     runs-on: ubuntu-latest
 
@@ -120,7 +104,7 @@ jobs:
           node-version: "18"
           cache: "npm"
 
-      - name: Install
+      - name: Install tools
         run: make install-ui
 
       - name: Lint
@@ -129,48 +113,12 @@ jobs:
       - name: Test
         run: make test-ui
 
-      - name: Porcelain
-        run: |
-          test -z "$(git status --porcelain)" || (git status; git diff; false)
-
-  integration:
-    name: Integration
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v3
-
-      - uses: actions/setup-node@v3
-        with:
-          node-version: "18"
-          cache: "npm"
-
-      - name: Install npm
-        run: npm ci
-
-      - run: npx playwright install --with-deps chromium
-
-      - name: Wait for Build
-        uses: lewagon/wait-on-check-action@v1.3.1
-        with:
-          ref: ${{ github.event.pull_request.head.sha || github.sha }}
-          check-name: Build
-          repo-token: ${{ secrets.GITHUB_TOKEN }}
-          wait-interval: 10
-
-      - uses: actions/download-artifact@v3
-        with:
-          name: evcc-binary
-
-      - name: Make executable
-        run: chmod +x evcc
-
-      - name: Run tests
-        run: npx playwright test
+      - name: Build
+        run: make ui
 
-      - uses: actions/upload-artifact@v3
-        if: always()
+      - name: Cache dist
+        uses: actions/cache/save@v3
+        id: cache-dist
         with:
-          name: playwright-report
-          path: playwright-report/
-          retention-days: 30
+          path: dist
+          key: ${{ runner.os }}-${{ github.sha }}-dist
diff --git a/.gitignore b/.gitignore
index 5d5fc691..9da0cd4c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,7 +12,6 @@ __debug_bin
 !util/templates/**/*.yaml
 !package*.json
 !evcc.dist.yaml
-!tests/**/*.evcc.yaml
 evcc
 evcc.exe
 evcc_*
@@ -33,6 +32,3 @@ fly.toml
 vendor/*
 QEMU_EFI.fd
 asset-stats.html
-/test-results/
-/playwright-report/
-/playwright/.cache/
diff --git a/api/error.go b/api/error.go
index 817ab0a0..ce4d78ac 100644
--- a/api/error.go
+++ b/api/error.go
@@ -26,3 +26,11 @@ type errTimeoutError struct{}
 func (errTimeoutError) Error() string   { return "timeout" }
 func (errTimeoutError) Timeout() bool   { return true }
 func (errTimeoutError) Temporary() bool { return true }
+
+// ErrAsleep indicates that vehicle is asleep. Caller may chose to wake up the vehicle and retry.
+var ErrAsleep error = errAsleep{}
+
+type errAsleep struct{}
+
+func (errAsleep) Error() string { return "asleep" }
+func (errAsleep) Unwrap() error { return ErrTimeout }
diff --git a/assets/js/components/ChargingSessionModal.vue b/assets/js/components/ChargingSessionModal.vue
index 9b6215d7..e2e9d96c 100644
--- a/assets/js/components/ChargingSessionModal.vue
+++ b/assets/js/components/ChargingSessionModal.vue
@@ -67,7 +67,12 @@
 										{{ $t("sessions.energy") }}
 									</th>
 									<td>
-										{{ fmtKWh(session.chargedEnergy * 1e3) }}
+										{{
+											fmtKWh(
+												session.chargedEnergy * 1e3,
+												session.chargedEnergy >= 1
+											)
+										}}
 									</td>
 								</tr>
 								<tr v-if="session.solarPercentage != null">
diff --git a/assets/js/components/Energyflow/Visualization.vue b/assets/js/components/Energyflow/Visualization.vue
index 53aa89d1..57745dd6 100644
--- a/assets/js/components/Energyflow/Visualization.vue
+++ b/assets/js/components/Energyflow/Visualization.vue
@@ -1,9 +1,5 @@
 <template>
-	<div
-		data-testid="visualization"
-		class="visualization"
-		:class="{ 'visualization--ready': visualizationReady }"
-	>
+	<div class="visualization" :class="{ 'visualization--ready': visualizationReady }">
 		<div class="label-scale d-flex">
 			<div class="d-flex justify-content-start flex-grow-1">
 				<LabelBar v-bind="labelBarProps('top', 'pvProduction')">
diff --git a/assets/js/components/Loadpoints.vue b/assets/js/components/Loadpoints.vue
index 0a03afb2..dbc0fc07 100644
--- a/assets/js/components/Loadpoints.vue
+++ b/assets/js/components/Loadpoints.vue
@@ -11,7 +11,6 @@
 				<Loadpoint
 					v-bind="loadpoint"
 					:id="index + 1"
-					data-testid="loadpoint"
 					:vehicles="vehicles"
 					:smartCostLimit="smartCostLimit"
 					:smartCostType="smartCostType"
diff --git a/assets/js/views/ChargingSessions.vue b/assets/js/views/ChargingSessions.vue
index 227c763d..85b5518e 100644
--- a/assets/js/views/ChargingSessions.vue
+++ b/assets/js/views/ChargingSessions.vue
@@ -90,7 +90,12 @@
 											{{ session.vehicle }}
 										</td>
 										<td class="text-end">
-											{{ fmtKWh(session.chargedEnergy * 1e3) }}
+											{{
+												fmtKWh(
+													session.chargedEnergy * 1e3,
+													session.chargedEnergy >= 1
+												)
+											}}
 										</td>
 										<td class="text-end">
 											<span v-if="session.solarPercentage != null">
diff --git a/charger/easee.go b/charger/easee.go
index b53b03f2..3953b4dd 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -559,6 +559,7 @@ func (c *Easee) updateSmartCharging() {
 		}
 		if err != nil {
 			c.log.WARN.Printf("smart charging: %v", err)
+			return
 		}
 
 		c.mux.Lock()
diff --git a/charger/easee/observationid_enumer.go b/charger/easee/observationid_enumer.go
index 7d49f43a..15c0eb97 100644
--- a/charger/easee/observationid_enumer.go
+++ b/charger/easee/observationid_enumer.go
@@ -7,8 +7,8 @@ import (
 	"strings"
 )
 
-const _ObservationIDName = "SELF_TEST_RESULTSELF_TEST_DETAILSWIFI_EVENTCHARGER_OFFLINE_REASONEASEE_LINK_COMMAND_RESPONSEEASEE_LINK_DATA_RECEIVEDLOCAL_PRE_AUTHORIZE_ENABLEDLOCAL_AUTHORIZE_OFFLINE_ENABLEDALLOW_OFFLINE_TX_FOR_UNKNOWN_IDERRATIC_EVMAX_TOGGLESBACKPLATE_TYPESITE_STRUCTUREDETECTED_POWER_GRID_TYPECIRCUIT_MAX_CURRENT_P1CIRCUIT_MAX_CURRENT_P2CIRCUIT_MAX_CURRENT_P3LOCATIONSITE_IDSTRINGSITE_IDNUMERICLOCK_CABLE_PERMANENTLYIS_ENABLEDCIRCUIT_SEQUENCE_NUMBERSINGLE_PHASE_NUMBERENABLE3_PHASES_DEPRECATEDWI_FI_SSIDENABLE_IDLE_CURRENTPHASE_MODEFORCED_THREE_PHASE_ON_ITWITH_GND_FAULTLED_STRIP_BRIGHTNESSLOCAL_AUTHORIZATION_REQUIREDAUTHORIZATION_REQUIREDREMOTE_START_REQUIREDSMART_BUTTON_ENABLEDOFFLINE_CHARGING_MODELEDMODEMAX_CHARGER_CURRENTDYNAMIC_CHARGER_CURRENTMAX_CURRENT_OFFLINE_FALLBACK_P1MAX_CURRENT_OFFLINE_FALLBACK_P2MAX_CURRENT_OFFLINE_FALLBACK_P3LISTEN_TO_CONTROL_PULSECONTROL_PULSE_RTTCHARGING_SCHEDULEPAIRED_EQUALIZERWI_FI_APENABLEDPAIRED_USER_IDTOKENCIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L1CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L2CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L3CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L1CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L2CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L3NUMBER_OF_CARS_CONNECTEDNUMBER_OF_CARS_CHARGINGNUMBER_OF_CARS_IN_QUEUENUMBER_OF_CARS_FULLY_CHARGEDSOFTWARE_RELEASEICCIDMODEM_FW_IDOTAERROR_CODEMOBILE_NETWORK_OPERATORREBOOT_REASONPOWER_PCBVERSIONCOM_PCBVERSIONREASON_FOR_NO_CURRENTLOAD_BALANCING_NUMBER_OF_CONNECTED_CHARGERSUDPNUM_OF_CONNECTED_NODESLOCAL_CONNECTIONPILOT_MODECAR_CONNECTED_DEPRECATEDSMART_CHARGINGCABLE_LOCKEDCABLE_RATINGPILOT_HIGHPILOT_LOWBACK_PLATE_IDUSER_IDTOKEN_REVERSEDCHARGER_OP_MODEOUTPUT_PHASEDYNAMIC_CIRCUIT_CURRENT_P1DYNAMIC_CIRCUIT_CURRENT_P2DYNAMIC_CIRCUIT_CURRENT_P3OUTPUT_CURRENTDERATED_CURRENTDERATING_ACTIVEDEBUG_STRINGERROR_STRINGERROR_CODETOTAL_POWERSESSION_ENERGYENERGY_PER_HOURLEGACY_EV_STATUSLIFETIME_ENERGYLIFETIME_RELAY_SWITCHESLIFETIME_HOURSDYNAMIC_CURRENT_OFFLINE_FALLBACK_DEPRICATEDUSER_IDTOKENCHARGING_SESSIONCELL_RSSICELL_RATWI_FI_RSSICELL_ADDRESSWI_FI_ADDRESSWI_FI_TYPELOCAL_RSSIMASTER_BACK_PLATE_IDLOCAL_TX_POWERLOCAL_STATEFOUND_WI_FICHARGER_RATCELLULAR_INTERFACE_ERROR_COUNTCELLULAR_INTERFACE_RESET_COUNTWIFI_INTERFACE_ERROR_COUNTWIFI_INTERFACE_RESET_COUNTLOCAL_NODE_TYPELOCAL_RADIO_CHANNELLOCAL_SHORT_ADDRESSLOCAL_PARENT_ADDR_OR_NUM_OF_NODESTEMP_MAXTEMP_AMBIENT_POWER_BOARDTEMP_INPUT_T2TEMP_INPUT_T3TEMP_INPUT_T4TEMP_INPUT_T5TEMP_OUTPUT_NTEMP_OUTPUT_L1TEMP_OUTPUT_L2TEMP_OUTPUT_L3TEMP_AMBIENTLIGHT_AMBIENTINT_REL_HUMIDITYBACK_PLATE_LOCKEDCURRENT_MOTORBACK_PLATE_HALL_SENSORIN_CURRENT_T2IN_CURRENT_T3IN_CURRENT_T4IN_CURRENT_T5IN_VOLT_T1_T2IN_VOLT_T1_T3IN_VOLT_T1_T4IN_VOLT_T1_T5IN_VOLT_T2_T3IN_VOLT_T2_T4IN_VOLT_T2_T5IN_VOLT_T3_T4IN_VOLT_T3_T5IN_VOLT_T4_T5OUT_VOLT_PIN1_2OUT_VOLT_PIN1_3OUT_VOLT_PIN1_4OUT_VOLT_PIN1_5VOLT_LEVEL33VOLT_LEVEL5VOLT_LEVEL12LTE_RSRPLTE_SINRLTE_RSRQEQ_AVAILABLE_CURRENT_P1EQ_AVAILABLE_CURRENT_P2EQ_AVAILABLE_CURRENT_P3"
-const _ObservationIDLowerName = "self_test_resultself_test_detailswifi_eventcharger_offline_reasoneasee_link_command_responseeasee_link_data_receivedlocal_pre_authorize_enabledlocal_authorize_offline_enabledallow_offline_tx_for_unknown_iderratic_evmax_togglesbackplate_typesite_structuredetected_power_grid_typecircuit_max_current_p1circuit_max_current_p2circuit_max_current_p3locationsite_idstringsite_idnumericlock_cable_permanentlyis_enabledcircuit_sequence_numbersingle_phase_numberenable3_phases_deprecatedwi_fi_ssidenable_idle_currentphase_modeforced_three_phase_on_itwith_gnd_faultled_strip_brightnesslocal_authorization_requiredauthorization_requiredremote_start_requiredsmart_button_enabledoffline_charging_modeledmodemax_charger_currentdynamic_charger_currentmax_current_offline_fallback_p1max_current_offline_fallback_p2max_current_offline_fallback_p3listen_to_control_pulsecontrol_pulse_rttcharging_schedulepaired_equalizerwi_fi_apenabledpaired_user_idtokencircuit_total_allocated_phase_conductor_current_l1circuit_total_allocated_phase_conductor_current_l2circuit_total_allocated_phase_conductor_current_l3circuit_total_phase_conductor_current_l1circuit_total_phase_conductor_current_l2circuit_total_phase_conductor_current_l3number_of_cars_connectednumber_of_cars_chargingnumber_of_cars_in_queuenumber_of_cars_fully_chargedsoftware_releaseiccidmodem_fw_idotaerror_codemobile_network_operatorreboot_reasonpower_pcbversioncom_pcbversionreason_for_no_currentload_balancing_number_of_connected_chargersudpnum_of_connected_nodeslocal_connectionpilot_modecar_connected_deprecatedsmart_chargingcable_lockedcable_ratingpilot_highpilot_lowback_plate_iduser_idtoken_reversedcharger_op_modeoutput_phasedynamic_circuit_current_p1dynamic_circuit_current_p2dynamic_circuit_current_p3output_currentderated_currentderating_activedebug_stringerror_stringerror_codetotal_powersession_energyenergy_per_hourlegacy_ev_statuslifetime_energylifetime_relay_switcheslifetime_hoursdynamic_current_offline_fallback_depricateduser_idtokencharging_sessioncell_rssicell_ratwi_fi_rssicell_addresswi_fi_addresswi_fi_typelocal_rssimaster_back_plate_idlocal_tx_powerlocal_statefound_wi_ficharger_ratcellular_interface_error_countcellular_interface_reset_countwifi_interface_error_countwifi_interface_reset_countlocal_node_typelocal_radio_channellocal_short_addresslocal_parent_addr_or_num_of_nodestemp_maxtemp_ambient_power_boardtemp_input_t2temp_input_t3temp_input_t4temp_input_t5temp_output_ntemp_output_l1temp_output_l2temp_output_l3temp_ambientlight_ambientint_rel_humidityback_plate_lockedcurrent_motorback_plate_hall_sensorin_current_t2in_current_t3in_current_t4in_current_t5in_volt_t1_t2in_volt_t1_t3in_volt_t1_t4in_volt_t1_t5in_volt_t2_t3in_volt_t2_t4in_volt_t2_t5in_volt_t3_t4in_volt_t3_t5in_volt_t4_t5out_volt_pin1_2out_volt_pin1_3out_volt_pin1_4out_volt_pin1_5volt_level33volt_level5volt_level12lte_rsrplte_sinrlte_rsrqeq_available_current_p1eq_available_current_p2eq_available_current_p3"
+const _ObservationIDName = "SELF_TEST_RESULTSELF_TEST_DETAILSWIFI_EVENTCHARGER_OFFLINE_REASONEASEE_LINK_COMMAND_RESPONSEEASEE_LINK_DATA_RECEIVEDLOCAL_PRE_AUTHORIZE_ENABLEDLOCAL_AUTHORIZE_OFFLINE_ENABLEDALLOW_OFFLINE_TX_FOR_UNKNOWN_IDERRATIC_EVMAX_TOGGLESBACKPLATE_TYPESITE_STRUCTUREDETECTED_POWER_GRID_TYPECIRCUIT_MAX_CURRENT_P1CIRCUIT_MAX_CURRENT_P2CIRCUIT_MAX_CURRENT_P3LOCATIONSITE_IDSTRINGSITE_IDNUMERICLOCK_CABLE_PERMANENTLYIS_ENABLEDCIRCUIT_SEQUENCE_NUMBERSINGLE_PHASE_NUMBERENABLE3_PHASES_DEPRECATEDWI_FI_SSIDENABLE_IDLE_CURRENTPHASE_MODEFORCED_THREE_PHASE_ON_ITWITH_GND_FAULTLED_STRIP_BRIGHTNESSLOCAL_AUTHORIZATION_REQUIREDAUTHORIZATION_REQUIREDREMOTE_START_REQUIREDSMART_BUTTON_ENABLEDOFFLINE_CHARGING_MODELEDMODEMAX_CHARGER_CURRENTDYNAMIC_CHARGER_CURRENTMAX_CURRENT_OFFLINE_FALLBACK_P1MAX_CURRENT_OFFLINE_FALLBACK_P2MAX_CURRENT_OFFLINE_FALLBACK_P3LISTEN_TO_CONTROL_PULSECONTROL_PULSE_RTTCHARGING_SCHEDULEPAIRED_EQUALIZERWI_FI_APENABLEDPAIRED_USER_IDTOKENCIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L1CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L2CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L3CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L1CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L2CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L3NUMBER_OF_CARS_CONNECTEDNUMBER_OF_CARS_CHARGINGNUMBER_OF_CARS_IN_QUEUENUMBER_OF_CARS_FULLY_CHARGEDSOFTWARE_RELEASEICCIDMODEM_FW_IDOTAERROR_CODEMOBILE_NETWORK_OPERATORREBOOT_REASONPOWER_PCBVERSIONCOM_PCBVERSIONREASON_FOR_NO_CURRENTLOAD_BALANCING_NUMBER_OF_CONNECTED_CHARGERSUDPNUM_OF_CONNECTED_NODESLOCAL_CONNECTIONPILOT_MODECAR_CONNECTED_DEPRECATEDSMART_CHARGINGCABLE_LOCKEDCABLE_RATINGPILOT_HIGHPILOT_LOWBACK_PLATE_IDUSER_IDTOKEN_REVERSEDCHARGER_OP_MODEOUTPUT_PHASEDYNAMIC_CIRCUIT_CURRENT_P1DYNAMIC_CIRCUIT_CURRENT_P2DYNAMIC_CIRCUIT_CURRENT_P3OUTPUT_CURRENTDERATED_CURRENTDERATING_ACTIVEDEBUG_STRINGERROR_STRINGERROR_CODETOTAL_POWERSESSION_ENERGYENERGY_PER_HOURLEGACY_EV_STATUSLIFETIME_ENERGYLIFETIME_RELAY_SWITCHESLIFETIME_HOURSDYNAMIC_CURRENT_OFFLINE_FALLBACK_DEPRICATEDUSER_IDTOKENCHARGING_SESSIONCELL_RSSICELL_RATWI_FI_RSSICELL_ADDRESSWI_FI_ADDRESSWI_FI_TYPELOCAL_RSSIMASTER_BACK_PLATE_IDLOCAL_TX_POWERLOCAL_STATEFOUND_WI_FICHARGER_RATCELLULAR_INTERFACE_ERROR_COUNTCELLULAR_INTERFACE_RESET_COUNTWIFI_INTERFACE_ERROR_COUNTWIFI_INTERFACE_RESET_COUNTLOCAL_NODE_TYPELOCAL_RADIO_CHANNELLOCAL_SHORT_ADDRESSLOCAL_PARENT_ADDR_OR_NUM_OF_NODESTEMP_MAXTEMP_AMBIENT_POWER_BOARDTEMP_INPUT_T2TEMP_INPUT_T3TEMP_INPUT_T4TEMP_INPUT_T5TEMP_OUTPUT_NTEMP_OUTPUT_L1TEMP_OUTPUT_L2TEMP_OUTPUT_L3TEMP_AMBIENTLIGHT_AMBIENTINT_REL_HUMIDITYBACK_PLATE_LOCKEDCURRENT_MOTORBACK_PLATE_HALL_SENSORIN_CURRENT_T2IN_CURRENT_T3IN_CURRENT_T4IN_CURRENT_T5IN_VOLT_T1_T2IN_VOLT_T1_T3IN_VOLT_T1_T4IN_VOLT_T1_T5IN_VOLT_T2_T3IN_VOLT_T2_T4IN_VOLT_T2_T5IN_VOLT_T3_T4IN_VOLT_T3_T5IN_VOLT_T4_T5OUT_VOLT_PIN1_2OUT_VOLT_PIN1_3OUT_VOLT_PIN1_4OUT_VOLT_PIN1_5VOLT_LEVEL33VOLT_LEVEL5VOLT_LEVEL12LTE_RSRPLTE_SINRLTE_RSRQEQ_AVAILABLE_CURRENT_P1EQ_AVAILABLE_CURRENT_P2EQ_AVAILABLE_CURRENT_P3CONNECTED_TO_CLOUDCLOUD_DISCONNECT_REASON"
+const _ObservationIDLowerName = "self_test_resultself_test_detailswifi_eventcharger_offline_reasoneasee_link_command_responseeasee_link_data_receivedlocal_pre_authorize_enabledlocal_authorize_offline_enabledallow_offline_tx_for_unknown_iderratic_evmax_togglesbackplate_typesite_structuredetected_power_grid_typecircuit_max_current_p1circuit_max_current_p2circuit_max_current_p3locationsite_idstringsite_idnumericlock_cable_permanentlyis_enabledcircuit_sequence_numbersingle_phase_numberenable3_phases_deprecatedwi_fi_ssidenable_idle_currentphase_modeforced_three_phase_on_itwith_gnd_faultled_strip_brightnesslocal_authorization_requiredauthorization_requiredremote_start_requiredsmart_button_enabledoffline_charging_modeledmodemax_charger_currentdynamic_charger_currentmax_current_offline_fallback_p1max_current_offline_fallback_p2max_current_offline_fallback_p3listen_to_control_pulsecontrol_pulse_rttcharging_schedulepaired_equalizerwi_fi_apenabledpaired_user_idtokencircuit_total_allocated_phase_conductor_current_l1circuit_total_allocated_phase_conductor_current_l2circuit_total_allocated_phase_conductor_current_l3circuit_total_phase_conductor_current_l1circuit_total_phase_conductor_current_l2circuit_total_phase_conductor_current_l3number_of_cars_connectednumber_of_cars_chargingnumber_of_cars_in_queuenumber_of_cars_fully_chargedsoftware_releaseiccidmodem_fw_idotaerror_codemobile_network_operatorreboot_reasonpower_pcbversioncom_pcbversionreason_for_no_currentload_balancing_number_of_connected_chargersudpnum_of_connected_nodeslocal_connectionpilot_modecar_connected_deprecatedsmart_chargingcable_lockedcable_ratingpilot_highpilot_lowback_plate_iduser_idtoken_reversedcharger_op_modeoutput_phasedynamic_circuit_current_p1dynamic_circuit_current_p2dynamic_circuit_current_p3output_currentderated_currentderating_activedebug_stringerror_stringerror_codetotal_powersession_energyenergy_per_hourlegacy_ev_statuslifetime_energylifetime_relay_switcheslifetime_hoursdynamic_current_offline_fallback_depricateduser_idtokencharging_sessioncell_rssicell_ratwi_fi_rssicell_addresswi_fi_addresswi_fi_typelocal_rssimaster_back_plate_idlocal_tx_powerlocal_statefound_wi_ficharger_ratcellular_interface_error_countcellular_interface_reset_countwifi_interface_error_countwifi_interface_reset_countlocal_node_typelocal_radio_channellocal_short_addresslocal_parent_addr_or_num_of_nodestemp_maxtemp_ambient_power_boardtemp_input_t2temp_input_t3temp_input_t4temp_input_t5temp_output_ntemp_output_l1temp_output_l2temp_output_l3temp_ambientlight_ambientint_rel_humidityback_plate_lockedcurrent_motorback_plate_hall_sensorin_current_t2in_current_t3in_current_t4in_current_t5in_volt_t1_t2in_volt_t1_t3in_volt_t1_t4in_volt_t1_t5in_volt_t2_t3in_volt_t2_t4in_volt_t2_t5in_volt_t3_t4in_volt_t3_t5in_volt_t4_t5out_volt_pin1_2out_volt_pin1_3out_volt_pin1_4out_volt_pin1_5volt_level33volt_level5volt_level12lte_rsrplte_sinrlte_rsrqeq_available_current_p1eq_available_current_p2eq_available_current_p3connected_to_cloudcloud_disconnect_reason"
 
 var _ObservationIDMap = map[ObservationID]string{
 	1:   _ObservationIDName[0:16],
@@ -172,6 +172,8 @@ var _ObservationIDMap = map[ObservationID]string{
 	230: _ObservationIDName[2877:2900],
 	231: _ObservationIDName[2900:2923],
 	232: _ObservationIDName[2923:2946],
+	250: _ObservationIDName[2946:2964],
+	251: _ObservationIDName[2964:2987],
 }
 
 func (i ObservationID) String() string {
@@ -346,9 +348,11 @@ func _ObservationIDNoOp() {
 	_ = x[EQ_AVAILABLE_CURRENT_P1-(230)]
 	_ = x[EQ_AVAILABLE_CURRENT_P2-(231)]
 	_ = x[EQ_AVAILABLE_CURRENT_P3-(232)]
+	_ = x[CONNECTED_TO_CLOUD-(250)]
+	_ = x[CLOUD_DISCONNECT_REASON-(251)]
 }
 
-var _ObservationIDValues = []ObservationID{SELF_TEST_RESULT, SELF_TEST_DETAILS, WIFI_EVENT, CHARGER_OFFLINE_REASON, EASEE_LINK_COMMAND_RESPONSE, EASEE_LINK_DATA_RECEIVED, LOCAL_PRE_AUTHORIZE_ENABLED, LOCAL_AUTHORIZE_OFFLINE_ENABLED, ALLOW_OFFLINE_TX_FOR_UNKNOWN_ID, ERRATIC_EVMAX_TOGGLES, BACKPLATE_TYPE, SITE_STRUCTURE, DETECTED_POWER_GRID_TYPE, CIRCUIT_MAX_CURRENT_P1, CIRCUIT_MAX_CURRENT_P2, CIRCUIT_MAX_CURRENT_P3, LOCATION, SITE_IDSTRING, SITE_IDNUMERIC, LOCK_CABLE_PERMANENTLY, IS_ENABLED, CIRCUIT_SEQUENCE_NUMBER, SINGLE_PHASE_NUMBER, ENABLE3_PHASES_DEPRECATED, WI_FI_SSID, ENABLE_IDLE_CURRENT, PHASE_MODE, FORCED_THREE_PHASE_ON_ITWITH_GND_FAULT, LED_STRIP_BRIGHTNESS, LOCAL_AUTHORIZATION_REQUIRED, AUTHORIZATION_REQUIRED, REMOTE_START_REQUIRED, SMART_BUTTON_ENABLED, OFFLINE_CHARGING_MODE, LEDMODE, MAX_CHARGER_CURRENT, DYNAMIC_CHARGER_CURRENT, MAX_CURRENT_OFFLINE_FALLBACK_P1, MAX_CURRENT_OFFLINE_FALLBACK_P2, MAX_CURRENT_OFFLINE_FALLBACK_P3, LISTEN_TO_CONTROL_PULSE, CONTROL_PULSE_RTT, CHARGING_SCHEDULE, PAIRED_EQUALIZER, WI_FI_APENABLED, PAIRED_USER_IDTOKEN, CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L1, CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L2, CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L3, CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L1, CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L2, CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L3, NUMBER_OF_CARS_CONNECTED, NUMBER_OF_CARS_CHARGING, NUMBER_OF_CARS_IN_QUEUE, NUMBER_OF_CARS_FULLY_CHARGED, SOFTWARE_RELEASE, ICCID, MODEM_FW_ID, OTAERROR_CODE, MOBILE_NETWORK_OPERATOR, REBOOT_REASON, POWER_PCBVERSION, COM_PCBVERSION, REASON_FOR_NO_CURRENT, LOAD_BALANCING_NUMBER_OF_CONNECTED_CHARGERS, UDPNUM_OF_CONNECTED_NODES, LOCAL_CONNECTION, PILOT_MODE, CAR_CONNECTED_DEPRECATED, SMART_CHARGING, CABLE_LOCKED, CABLE_RATING, PILOT_HIGH, PILOT_LOW, BACK_PLATE_ID, USER_IDTOKEN_REVERSED, CHARGER_OP_MODE, OUTPUT_PHASE, DYNAMIC_CIRCUIT_CURRENT_P1, DYNAMIC_CIRCUIT_CURRENT_P2, DYNAMIC_CIRCUIT_CURRENT_P3, OUTPUT_CURRENT, DERATED_CURRENT, DERATING_ACTIVE, DEBUG_STRING, ERROR_STRING, ERROR_CODE, TOTAL_POWER, SESSION_ENERGY, ENERGY_PER_HOUR, LEGACY_EV_STATUS, LIFETIME_ENERGY, LIFETIME_RELAY_SWITCHES, LIFETIME_HOURS, DYNAMIC_CURRENT_OFFLINE_FALLBACK_DEPRICATED, USER_IDTOKEN, CHARGING_SESSION, CELL_RSSI, CELL_RAT, WI_FI_RSSI, CELL_ADDRESS, WI_FI_ADDRESS, WI_FI_TYPE, LOCAL_RSSI, MASTER_BACK_PLATE_ID, LOCAL_TX_POWER, LOCAL_STATE, FOUND_WI_FI, CHARGER_RAT, CELLULAR_INTERFACE_ERROR_COUNT, CELLULAR_INTERFACE_RESET_COUNT, WIFI_INTERFACE_ERROR_COUNT, WIFI_INTERFACE_RESET_COUNT, LOCAL_NODE_TYPE, LOCAL_RADIO_CHANNEL, LOCAL_SHORT_ADDRESS, LOCAL_PARENT_ADDR_OR_NUM_OF_NODES, TEMP_MAX, TEMP_AMBIENT_POWER_BOARD, TEMP_INPUT_T2, TEMP_INPUT_T3, TEMP_INPUT_T4, TEMP_INPUT_T5, TEMP_OUTPUT_N, TEMP_OUTPUT_L1, TEMP_OUTPUT_L2, TEMP_OUTPUT_L3, TEMP_AMBIENT, LIGHT_AMBIENT, INT_REL_HUMIDITY, BACK_PLATE_LOCKED, CURRENT_MOTOR, BACK_PLATE_HALL_SENSOR, IN_CURRENT_T2, IN_CURRENT_T3, IN_CURRENT_T4, IN_CURRENT_T5, IN_VOLT_T1_T2, IN_VOLT_T1_T3, IN_VOLT_T1_T4, IN_VOLT_T1_T5, IN_VOLT_T2_T3, IN_VOLT_T2_T4, IN_VOLT_T2_T5, IN_VOLT_T3_T4, IN_VOLT_T3_T5, IN_VOLT_T4_T5, OUT_VOLT_PIN1_2, OUT_VOLT_PIN1_3, OUT_VOLT_PIN1_4, OUT_VOLT_PIN1_5, VOLT_LEVEL33, VOLT_LEVEL5, VOLT_LEVEL12, LTE_RSRP, LTE_SINR, LTE_RSRQ, EQ_AVAILABLE_CURRENT_P1, EQ_AVAILABLE_CURRENT_P2, EQ_AVAILABLE_CURRENT_P3}
+var _ObservationIDValues = []ObservationID{SELF_TEST_RESULT, SELF_TEST_DETAILS, WIFI_EVENT, CHARGER_OFFLINE_REASON, EASEE_LINK_COMMAND_RESPONSE, EASEE_LINK_DATA_RECEIVED, LOCAL_PRE_AUTHORIZE_ENABLED, LOCAL_AUTHORIZE_OFFLINE_ENABLED, ALLOW_OFFLINE_TX_FOR_UNKNOWN_ID, ERRATIC_EVMAX_TOGGLES, BACKPLATE_TYPE, SITE_STRUCTURE, DETECTED_POWER_GRID_TYPE, CIRCUIT_MAX_CURRENT_P1, CIRCUIT_MAX_CURRENT_P2, CIRCUIT_MAX_CURRENT_P3, LOCATION, SITE_IDSTRING, SITE_IDNUMERIC, LOCK_CABLE_PERMANENTLY, IS_ENABLED, CIRCUIT_SEQUENCE_NUMBER, SINGLE_PHASE_NUMBER, ENABLE3_PHASES_DEPRECATED, WI_FI_SSID, ENABLE_IDLE_CURRENT, PHASE_MODE, FORCED_THREE_PHASE_ON_ITWITH_GND_FAULT, LED_STRIP_BRIGHTNESS, LOCAL_AUTHORIZATION_REQUIRED, AUTHORIZATION_REQUIRED, REMOTE_START_REQUIRED, SMART_BUTTON_ENABLED, OFFLINE_CHARGING_MODE, LEDMODE, MAX_CHARGER_CURRENT, DYNAMIC_CHARGER_CURRENT, MAX_CURRENT_OFFLINE_FALLBACK_P1, MAX_CURRENT_OFFLINE_FALLBACK_P2, MAX_CURRENT_OFFLINE_FALLBACK_P3, LISTEN_TO_CONTROL_PULSE, CONTROL_PULSE_RTT, CHARGING_SCHEDULE, PAIRED_EQUALIZER, WI_FI_APENABLED, PAIRED_USER_IDTOKEN, CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L1, CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L2, CIRCUIT_TOTAL_ALLOCATED_PHASE_CONDUCTOR_CURRENT_L3, CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L1, CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L2, CIRCUIT_TOTAL_PHASE_CONDUCTOR_CURRENT_L3, NUMBER_OF_CARS_CONNECTED, NUMBER_OF_CARS_CHARGING, NUMBER_OF_CARS_IN_QUEUE, NUMBER_OF_CARS_FULLY_CHARGED, SOFTWARE_RELEASE, ICCID, MODEM_FW_ID, OTAERROR_CODE, MOBILE_NETWORK_OPERATOR, REBOOT_REASON, POWER_PCBVERSION, COM_PCBVERSION, REASON_FOR_NO_CURRENT, LOAD_BALANCING_NUMBER_OF_CONNECTED_CHARGERS, UDPNUM_OF_CONNECTED_NODES, LOCAL_CONNECTION, PILOT_MODE, CAR_CONNECTED_DEPRECATED, SMART_CHARGING, CABLE_LOCKED, CABLE_RATING, PILOT_HIGH, PILOT_LOW, BACK_PLATE_ID, USER_IDTOKEN_REVERSED, CHARGER_OP_MODE, OUTPUT_PHASE, DYNAMIC_CIRCUIT_CURRENT_P1, DYNAMIC_CIRCUIT_CURRENT_P2, DYNAMIC_CIRCUIT_CURRENT_P3, OUTPUT_CURRENT, DERATED_CURRENT, DERATING_ACTIVE, DEBUG_STRING, ERROR_STRING, ERROR_CODE, TOTAL_POWER, SESSION_ENERGY, ENERGY_PER_HOUR, LEGACY_EV_STATUS, LIFETIME_ENERGY, LIFETIME_RELAY_SWITCHES, LIFETIME_HOURS, DYNAMIC_CURRENT_OFFLINE_FALLBACK_DEPRICATED, USER_IDTOKEN, CHARGING_SESSION, CELL_RSSI, CELL_RAT, WI_FI_RSSI, CELL_ADDRESS, WI_FI_ADDRESS, WI_FI_TYPE, LOCAL_RSSI, MASTER_BACK_PLATE_ID, LOCAL_TX_POWER, LOCAL_STATE, FOUND_WI_FI, CHARGER_RAT, CELLULAR_INTERFACE_ERROR_COUNT, CELLULAR_INTERFACE_RESET_COUNT, WIFI_INTERFACE_ERROR_COUNT, WIFI_INTERFACE_RESET_COUNT, LOCAL_NODE_TYPE, LOCAL_RADIO_CHANNEL, LOCAL_SHORT_ADDRESS, LOCAL_PARENT_ADDR_OR_NUM_OF_NODES, TEMP_MAX, TEMP_AMBIENT_POWER_BOARD, TEMP_INPUT_T2, TEMP_INPUT_T3, TEMP_INPUT_T4, TEMP_INPUT_T5, TEMP_OUTPUT_N, TEMP_OUTPUT_L1, TEMP_OUTPUT_L2, TEMP_OUTPUT_L3, TEMP_AMBIENT, LIGHT_AMBIENT, INT_REL_HUMIDITY, BACK_PLATE_LOCKED, CURRENT_MOTOR, BACK_PLATE_HALL_SENSOR, IN_CURRENT_T2, IN_CURRENT_T3, IN_CURRENT_T4, IN_CURRENT_T5, IN_VOLT_T1_T2, IN_VOLT_T1_T3, IN_VOLT_T1_T4, IN_VOLT_T1_T5, IN_VOLT_T2_T3, IN_VOLT_T2_T4, IN_VOLT_T2_T5, IN_VOLT_T3_T4, IN_VOLT_T3_T5, IN_VOLT_T4_T5, OUT_VOLT_PIN1_2, OUT_VOLT_PIN1_3, OUT_VOLT_PIN1_4, OUT_VOLT_PIN1_5, VOLT_LEVEL33, VOLT_LEVEL5, VOLT_LEVEL12, LTE_RSRP, LTE_SINR, LTE_RSRQ, EQ_AVAILABLE_CURRENT_P1, EQ_AVAILABLE_CURRENT_P2, EQ_AVAILABLE_CURRENT_P3, CONNECTED_TO_CLOUD, CLOUD_DISCONNECT_REASON}
 
 var _ObservationIDNameToValueMap = map[string]ObservationID{
 	_ObservationIDName[0:16]:           SELF_TEST_RESULT,
@@ -673,6 +677,10 @@ var _ObservationIDNameToValueMap = map[string]ObservationID{
 	_ObservationIDLowerName[2900:2923]: EQ_AVAILABLE_CURRENT_P2,
 	_ObservationIDName[2923:2946]:      EQ_AVAILABLE_CURRENT_P3,
 	_ObservationIDLowerName[2923:2946]: EQ_AVAILABLE_CURRENT_P3,
+	_ObservationIDName[2946:2964]:      CONNECTED_TO_CLOUD,
+	_ObservationIDLowerName[2946:2964]: CONNECTED_TO_CLOUD,
+	_ObservationIDName[2964:2987]:      CLOUD_DISCONNECT_REASON,
+	_ObservationIDLowerName[2964:2987]: CLOUD_DISCONNECT_REASON,
 }
 
 var _ObservationIDNames = []string{
@@ -837,6 +845,8 @@ var _ObservationIDNames = []string{
 	_ObservationIDName[2877:2900],
 	_ObservationIDName[2900:2923],
 	_ObservationIDName[2923:2946],
+	_ObservationIDName[2946:2964],
+	_ObservationIDName[2964:2987],
 }
 
 // ObservationIDString retrieves an enum value from the enum constants string name.
diff --git a/charger/easee/signalr.go b/charger/easee/signalr.go
index db454fa9..cb8d825f 100644
--- a/charger/easee/signalr.go
+++ b/charger/easee/signalr.go
@@ -205,6 +205,8 @@ const (
 	EQ_AVAILABLE_CURRENT_P1                            ObservationID = 230 // Available current for charging on P1 according to Equalizer [Double]
 	EQ_AVAILABLE_CURRENT_P2                            ObservationID = 231 // Available current for charging on P2 according to Equalizer [Double]
 	EQ_AVAILABLE_CURRENT_P3                            ObservationID = 232 // Available current for charging on P3 according to Equalizer [Double]
+	CONNECTED_TO_CLOUD                                 ObservationID = 250 // If charger connected to cloud or not
+	CLOUD_DISCONNECT_REASON                            ObservationID = 251 // Reason why charger disconnected from cloud
 	LISTEN_TO_CONTROL_PULSE                            ObservationID = 56  // True = charger needs control pulse to consider itself online. Readback on charger setting [event] [Boolean]
 	CONTROL_PULSE_RTT                                  ObservationID = 57  // Control pulse round-trip time in milliseconds [Integer]
 )
diff --git a/charger/openevse/api.go b/charger/openevse/api.go
index f8b2aaf5..0c10fe7f 100644
--- a/charger/openevse/api.go
+++ b/charger/openevse/api.go
@@ -2106,7 +2106,7 @@ type GetStatusResponse struct {
 		Voltage *float32 `json:"voltage,omitempty"`
 
 		// The total amount of energy transfered, in W/hour
-		Watthour *int `json:"watthour,omitempty"`
+		Watthour *float32 `json:"watthour,omitempty"`
 
 		// The amount of energy transfered in this charge session, in W/sec
 		Wattsec *float32 `json:"wattsec,omitempty"`
@@ -2285,7 +2285,7 @@ type StatusUpdatesResponse struct {
 		Voltage *float32 `json:"voltage,omitempty"`
 
 		// The total amount of energy transfered, in W/hour
-		Watthour *int `json:"watthour,omitempty"`
+		Watthour *float32 `json:"watthour,omitempty"`
 
 		// The amount of energy transfered in this charge session, in W/sec
 		Wattsec *float32 `json:"wattsec,omitempty"`
@@ -3284,7 +3284,7 @@ func ParseGetStatusResponse(rsp *http.Response) (*GetStatusResponse, error) {
 			Voltage *float32 `json:"voltage,omitempty"`
 
 			// The total amount of energy transfered, in W/hour
-			Watthour *int `json:"watthour,omitempty"`
+			Watthour *float32 `json:"watthour,omitempty"`
 
 			// The amount of energy transfered in this charge session, in W/sec
 			Wattsec *float32 `json:"wattsec,omitempty"`
@@ -3471,7 +3471,7 @@ func ParseStatusUpdatesResponse(rsp *http.Response) (*StatusUpdatesResponse, err
 			Voltage *float32 `json:"voltage,omitempty"`
 
 			// The total amount of energy transfered, in W/hour
-			Watthour *int `json:"watthour,omitempty"`
+			Watthour *float32 `json:"watthour,omitempty"`
 
 			// The amount of energy transfered in this charge session, in W/sec
 			Wattsec *float32 `json:"wattsec,omitempty"`
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 9b178998..ca2232ab 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -665,6 +665,14 @@ func (lp *Loadpoint) setLimit(chargeCurrent float64, force bool) error {
 		}
 
 		if err != nil {
+			v := lp.GetVehicle()
+			if vv, ok := v.(api.Resurrector); ok && errors.Is(err, api.ErrAsleep) {
+				// https://github.com/evcc-io/evcc/issues/8254
+				// wakeup vehicle
+				lp.log.DEBUG.Printf("max charge current: waking up vehicle")
+				return vv.WakeUp()
+			}
+
 			return fmt.Errorf("max charge current %.3gA: %w", chargeCurrent, err)
 		}
 
diff --git a/detect/tasks/modbus.go b/detect/tasks/modbus.go
index 796145ca..68880de9 100644
--- a/detect/tasks/modbus.go
+++ b/detect/tasks/modbus.go
@@ -79,9 +79,9 @@ func (h *ModbusHandler) testRegister(log *util.Logger, conn gridx.Client) bool {
 	var err error
 
 	switch h.op.FuncCode {
-	case rs485.ReadHoldingReg:
+	case gridx.FuncCodeReadHoldingRegisters:
 		bytes, err = conn.ReadHoldingRegisters(h.op.OpCode, h.op.ReadLen)
-	case rs485.ReadInputReg:
+	case gridx.FuncCodeReadInputRegisters:
 		bytes, err = conn.ReadInputRegisters(h.op.OpCode, h.op.ReadLen)
 	}
 
diff --git a/package-lock.json b/package-lock.json
index c0797a59..e0df2122 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,6 @@
         "@babel/core": "^7.17.2",
         "@h2d2/shopicons": "^1.2.0",
         "@histoire/plugin-vue": "^0.16.1",
-        "@playwright/test": "^1.34.3",
         "@popperjs/core": "^2.11.5",
         "@vitejs/plugin-legacy": "^4.0.2",
         "@vitejs/plugin-vue": "^4.1.0",
@@ -2357,24 +2356,6 @@
         "node": ">= 8"
       }
     },
-    "node_modules/@playwright/test": {
-      "version": "1.34.3",
-      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.3.tgz",
-      "integrity": "sha512-zPLef6w9P6T/iT6XDYG3mvGOqOyb6eHaV9XtkunYs0+OzxBtrPAAaHotc0X+PJ00WPPnLfFBTl7mf45Mn8DBmw==",
-      "dependencies": {
-        "@types/node": "*",
-        "playwright-core": "1.34.3"
-      },
-      "bin": {
-        "playwright": "cli.js"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "optionalDependencies": {
-        "fsevents": "2.3.2"
-      }
-    },
     "node_modules/@polka/url": {
       "version": "1.0.0-next.21",
       "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz",
@@ -6143,17 +6124,6 @@
       "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz",
       "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w=="
     },
-    "node_modules/playwright-core": {
-      "version": "1.34.3",
-      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.3.tgz",
-      "integrity": "sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==",
-      "bin": {
-        "playwright-core": "cli.js"
-      },
-      "engines": {
-        "node": ">=14"
-      }
-    },
     "node_modules/postcss": {
       "version": "8.4.23",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
diff --git a/package.json b/package.json
index 376006a4..8f9c2698 100644
--- a/package.json
+++ b/package.json
@@ -7,15 +7,13 @@
     "test": "TZ=Europe/Berlin vitest",
     "lint": "prettier assets './**/*.{yml,yaml,toml}' --write && eslint assets/**/*.js assets/**/*.vue --fix",
     "dev": "vite",
-    "histoire": "histoire dev",
-    "playwright": "playwright test --ui"
+    "histoire": "histoire dev"
   },
   "main": "index.js",
   "dependencies": {
     "@babel/core": "^7.17.2",
     "@h2d2/shopicons": "^1.2.0",
     "@histoire/plugin-vue": "^0.16.1",
-    "@playwright/test": "^1.34.3",
     "@popperjs/core": "^2.11.5",
     "@vitejs/plugin-legacy": "^4.0.2",
     "@vitejs/plugin-vue": "^4.1.0",
diff --git a/packaging/toml.go b/packaging/toml.go
index 8f8a96d8..0b1e19c9 100644
--- a/packaging/toml.go
+++ b/packaging/toml.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"bufio"
+	"bytes"
 	"fmt"
 	"io/fs"
 	"os"
@@ -11,44 +12,85 @@ import (
 	"github.com/BurntSushi/toml"
 )
 
-func main() {
-	err := filepath.WalkDir("./i18n", func(filepath string, d fs.DirEntry, err error) error {
-		if err != nil {
-			return fmt.Errorf("%s: %v", filepath, err)
-		}
+func process(filepath string) error {
+	in, err := os.ReadFile(filepath)
+	if err != nil {
+		return err
+	}
 
-		if d.IsDir() {
-			return nil
-		}
+	pre := new(strings.Builder)
+	{
+		sc := bufio.NewScanner(bytes.NewReader(in))
+		for sc.Scan() {
+			col := strings.SplitN(sc.Text(), "=", 2)
+			if len(col) < 2 {
+				fmt.Fprintln(pre, sc.Text())
+				continue
+			}
 
-		var config map[string]any
-		if _, err := toml.DecodeFile(filepath, &config); err != nil {
-			return fmt.Errorf("%s: %v", filepath, err)
+			key := strings.TrimSpace(col[0])
+			val := strings.TrimSpace(col[1])
+
+			if strings.HasPrefix(val, "\"") {
+				fmt.Fprintln(pre, sc.Text())
+				continue
+			}
+
+			quote := `"`
+			if strings.Contains(val, quote) {
+				quote = `'`
+			}
+
+			fmt.Fprintf(pre, "%s = %s%s%s\n", key, quote, val, quote)
 		}
+	}
+
+	var config map[string]any
+	if _, err := toml.Decode(pre.String(), &config); err != nil {
+		return fmt.Errorf("%s: %v", filepath, err)
+	}
+
+	s := new(strings.Builder)
+	enc := toml.NewEncoder(s)
+	enc.Indent = ""
 
-		s := new(strings.Builder)
-		enc := toml.NewEncoder(s)
-		enc.Indent = ""
+	if err := enc.Encode(config); err != nil {
+		return fmt.Errorf("%s: %v", filepath, err)
+	}
 
-		if err := enc.Encode(config); err != nil {
-			return fmt.Errorf("%s: %v", filepath, err)
+	out := new(strings.Builder)
+	sc := bufio.NewScanner(strings.NewReader(s.String()))
+	for sc.Scan() {
+		if strings.HasPrefix(sc.Text(), "[") && strings.Contains(sc.Text(), ".") && out.Len() > 0 {
+			fmt.Fprintln(out, "")
 		}
+		fmt.Fprintln(out, sc.Text())
+	}
 
-		out := new(strings.Builder)
-		sc := bufio.NewScanner(strings.NewReader(s.String()))
-		for sc.Scan() {
-			if strings.HasPrefix(sc.Text(), "[") && strings.Contains(sc.Text(), ".") && out.Len() > 0 {
-				fmt.Fprintln(out, "")
+	if err := os.WriteFile(filepath, []byte(out.String()), 0644); err != nil {
+		return fmt.Errorf("%s: %v", filepath, err)
+	}
+
+	return nil
+}
+
+func main() {
+	var err error
+	if len(os.Args) > 1 {
+		err = process(os.Args[1])
+	} else {
+		err = filepath.WalkDir("./i18n", func(filepath string, d fs.DirEntry, err error) error {
+			if err != nil {
+				return fmt.Errorf("%s: %v", filepath, err)
 			}
-			fmt.Fprintln(out, sc.Text())
-		}
 
-		if err := os.WriteFile(filepath, []byte(out.String()), 0644); err != nil {
-			return fmt.Errorf("%s: %v", filepath, err)
-		}
+			if d.IsDir() {
+				return nil
+			}
 
-		return nil
-	})
+			return process(filepath)
+		})
+	}
 
 	if err != nil {
 		panic(err)
diff --git a/playwright.config.js b/playwright.config.js
deleted file mode 100644
index 443a018c..00000000
--- a/playwright.config.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const { defineConfig, devices } = require("@playwright/test");
-
-/**
- * @see https://playwright.dev/docs/test-configuration
- */
-module.exports = defineConfig({
-  testDir: "./tests",
-  /* Run tests in files in parallel */
-  fullyParallel: false,
-  /* Fail the build on CI if you accidentally left test.only in the source code. */
-  forbidOnly: !!process.env.CI,
-  /* Retry on CI only */
-  retries: process.env.CI ? 2 : 0,
-  /* Opt out of parallel tests on CI. */
-  workers: process.env.CI ? 1 : undefined,
-  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
-  reporter: "html",
-  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
-  use: {
-    /* Base URL to use in actions like `await page.goto('/')`. */
-    baseURL: "http://127.0.0.1:7070",
-
-    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
-    trace: "on-first-retry",
-  },
-  projects: [
-    {
-      name: "chromium",
-      use: { ...devices["Desktop Chrome"] },
-    },
-  ],
-});
diff --git a/provider/modbus.go b/provider/modbus.go
index 56aa7ce9..6d423019 100644
--- a/provider/modbus.go
+++ b/provider/modbus.go
@@ -13,7 +13,6 @@ import (
 	"github.com/evcc-io/evcc/util/modbus"
 	gridx "github.com/grid-x/modbus"
 	"github.com/volkszaehler/mbmd/meters"
-	"github.com/volkszaehler/mbmd/meters/rs485"
 	"github.com/volkszaehler/mbmd/meters/sunspec"
 )
 
@@ -140,10 +139,12 @@ func NewModbusFromConfig(other map[string]interface{}) (Provider, error) {
 func (m *Modbus) bytesGetter() ([]byte, error) {
 	if op := m.op.MBMD; op.FuncCode != 0 {
 		switch op.FuncCode {
-		case rs485.ReadHoldingReg:
+		case gridx.FuncCodeReadHoldingRegisters:
 			return m.conn.ReadHoldingRegisters(op.OpCode, op.ReadLen)
-		case rs485.ReadInputReg:
+		case gridx.FuncCodeReadInputRegisters:
 			return m.conn.ReadInputRegisters(op.OpCode, op.ReadLen)
+		case gridx.FuncCodeReadCoils:
+			return m.conn.ReadCoils(op.OpCode, op.ReadLen)
 		default:
 			return nil, fmt.Errorf("unknown function code %d", op.FuncCode)
 		}
@@ -234,6 +235,8 @@ func (m *Modbus) StringGetter() func() (string, error) {
 // UintFromBytes converts byte slice to bigendian uint value
 func UintFromBytes(bytes []byte) (u uint64, err error) {
 	switch l := len(bytes); l {
+	case 1:
+		u = uint64(bytes[0])
 	case 2:
 		u = uint64(binary.BigEndian.Uint16(bytes))
 	case 4:
@@ -272,6 +275,13 @@ func (m *Modbus) IntSetter(param string) func(int64) error {
 			switch op.FuncCode {
 			case gridx.FuncCodeWriteSingleRegister:
 				_, err = m.conn.WriteSingleRegister(op.OpCode, uval)
+			case gridx.FuncCodeWriteSingleCoil:
+				if uval != 0 {
+					// Modbus protocol requires 0xFF00 for ON
+					// and 0x0000 for OFF
+					uval = 0xFF00
+				}
+				_, err = m.conn.WriteSingleCoil(op.OpCode, uval)
 			default:
 				err = fmt.Errorf("unknown function code %d", op.FuncCode)
 			}
diff --git a/templates/definition/charger/twc3.yaml b/templates/definition/charger/twc3.yaml
index 9c39380f..73065347 100644
--- a/templates/definition/charger/twc3.yaml
+++ b/templates/definition/charger/twc3.yaml
@@ -5,8 +5,8 @@ products:
       generic: TWC3
 requirements:
   description:
-    en: The Tesla vehicle must be associated with the TWC3 in the evcc ui.
-    de: Das Teslafahrzeug muss dem TWC3 im evcc UI zugewiesen sein.
+    en: The TWC wallbox cannot be controlled directly. Control is via the vehicle. The vehicle must be associated with the TWC3 loadpoint. At this time only Tesla vehicles are supported.
+    de: Die TWC Wallbox ist nicht direkt regelbar. Die Regelung erfolgt über das Fahrzeug. Das Fahrzeug muss dem TWC3 Ladepunkt zugewiesen sein. Aktuell ausschließlich mit Tesla Fahrzeugen nutzbar.
 params:
   - name: host
 render: |
diff --git a/templates/definition/meter/sunspec-hybrid.yaml b/templates/definition/meter/sunspec-hybrid.yaml
index d8de484b..7958441f 100644
--- a/templates/definition/meter/sunspec-hybrid.yaml
+++ b/templates/definition/meter/sunspec-hybrid.yaml
@@ -10,7 +10,7 @@ params:
     choice: ["grid", "pv", "battery"]
     allinone: true
   - name: modbus
-    choice: ["tcpip"]
+    choice: ["tcpip", "rs485"]
   - name: integer
     description:
       de: Integer Registerzugriff (nur für Netzzähler)
@@ -73,6 +73,17 @@ render: |
       {{- include "modbus" . | indent 4 }}
       model: sunspec
       value: 160:2:DCW # mppt 2
+  energy:
+    source: calc
+    add:
+    - source: modbus
+      {{- include "modbus" . | indent 4 }}
+      model: sunspec
+      value: 160:1:DCWH # mppt 1
+    - source: modbus
+      {{- include "modbus" . | indent 4 }}
+      model: sunspec
+      value: 160:2:DCWH # mppt 2
   {{- end }}
   {{- if eq .usage "battery" }}
   type: custom
@@ -82,12 +93,17 @@ render: |
     - source: modbus
       {{- include "modbus" . | indent 4 }}
       model: sunspec
-      value: 160:3:DCW # mppt 3 charge
+      value: 160:3:DCW # mppt 3 (charge)
       scale: -1
     - source: modbus
       {{- include "modbus" . | indent 4 }}
       model: sunspec
-      value: 160:4:DCW # mppt 4 discharge
+      value: 160:4:DCW # mppt 4 (discharge)
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    model: sunspec
+    value: 160:4:DCWH # mppt 4 (discharge)
   soc:
     source: modbus
     {{- include "modbus" . | indent 2 }}
diff --git a/templates/definition/meter/sunspec-inverter.yaml b/templates/definition/meter/sunspec-inverter.yaml
index c86d31fd..58d206b7 100644
--- a/templates/definition/meter/sunspec-inverter.yaml
+++ b/templates/definition/meter/sunspec-inverter.yaml
@@ -9,7 +9,7 @@ params:
     choice: ["grid", "pv"]
     allinone: true
   - name: modbus
-    choice: ["tcpip"]
+    choice: ["tcpip", "rs485"]
   - name: integer
     description:
       de: Integer Registerzugriff (nur für Netzzähler)
@@ -60,4 +60,5 @@ render: |
   {{- else }}
   model: sunspec
   power: Power
+  energy: Export
   {{- end }}
diff --git a/templates/definition/vehicle/audi.yaml b/templates/definition/vehicle/audi.yaml
index 1d1392c8..ca8895d7 100644
--- a/templates/definition/vehicle/audi.yaml
+++ b/templates/definition/vehicle/audi.yaml
@@ -9,6 +9,6 @@ params:
   - name: vin
     example: WAUZZZ...
 render: |
-  type: audi
+  type: etron
   {{ include "vehicle-base" . }}
   {{ include "vehicle-identify" . }}
diff --git a/templates/definition/vehicle/iso15118.yaml b/templates/definition/vehicle/iso15118.yaml
index 16cddf1c..fcb2c289 100644
--- a/templates/definition/vehicle/iso15118.yaml
+++ b/templates/definition/vehicle/iso15118.yaml
@@ -2,6 +2,7 @@ template: iso15118
 products:
   - description:
       generic: ISO15118
+capabilities: ["iso151182"]
 group: generic
 requirements:
   description:
@@ -29,4 +30,7 @@ render: |
   {{- if .phases }}
   phases: {{ .phases }}
   {{- end }}
+  soc:
+    source: const
+    value: 0
   {{ include "vehicle-identify" . }}
diff --git a/templates/definition/vehicle/offline.yaml b/templates/definition/vehicle/offline.yaml
index 135fe4d4..31ae4e9a 100644
--- a/templates/definition/vehicle/offline.yaml
+++ b/templates/definition/vehicle/offline.yaml
@@ -20,6 +20,7 @@ params:
   - name: identifiers
     advanced: true
     type: stringlist
+  - preset: vehicle-identify
 render: |
   type: custom
   features: ["offline"]
@@ -33,15 +34,15 @@ render: |
   {{- if .phases }}
   phases: {{ .phases }}
   {{- end }}
-  soc: # fixed - no Soc available
-    source: js
-    script: 0;
+  soc:
+    source: const
+    value: 0
   onIdentify:
   {{- if .mode }}
     mode: {{ .mode }}
   {{- end }}
-    minSoc: 0 # fixed - no Soc available
-    targetSoc: 100 # fixed - no Soc available
+    minSoc: 0 # fixed - no soc available
+    targetSoc: 100 # fixed - no soc available
   {{- if .minCurrent }}
     minCurrent: {{ .minCurrent }}
   {{- end }}
diff --git a/templates/docs/charger/twc3_0.yaml b/templates/docs/charger/twc3_0.yaml
index a14da3d7..9fbb3879 100644
--- a/templates/docs/charger/twc3_0.yaml
+++ b/templates/docs/charger/twc3_0.yaml
@@ -2,7 +2,7 @@ product:
   brand: Tesla
   description: TWC3
 description: |
-  Das Teslafahrzeug muss dem TWC3 im evcc UI zugewiesen sein.
+  Die TWC Wallbox ist nicht direkt regelbar. Die Regelung erfolgt über das Fahrzeug. Das Fahrzeug muss dem TWC3 Ladepunkt zugewiesen sein. Aktuell ausschließlich mit Tesla Fahrzeugen nutzbar.
 render:
   - default: |
       type: template
diff --git a/templates/docs/meter/sunspec-hybrid_0.yaml b/templates/docs/meter/sunspec-hybrid_0.yaml
index 2a482ba8..99a427b5 100644
--- a/templates/docs/meter/sunspec-hybrid_0.yaml
+++ b/templates/docs/meter/sunspec-hybrid_0.yaml
@@ -8,6 +8,19 @@ render:
       template: sunspec-hybrid
       usage: grid
 
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+
       # Modbus TCP
       modbus: tcpip
       id: 1
@@ -18,6 +31,19 @@ render:
       template: sunspec-hybrid
       usage: grid
 
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+
       # Modbus TCP
       modbus: tcpip
       id: 1
@@ -30,6 +56,19 @@ render:
       template: sunspec-hybrid
       usage: pv
 
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+
       # Modbus TCP
       modbus: tcpip
       id: 1
@@ -40,6 +79,19 @@ render:
       template: sunspec-hybrid
       usage: pv
 
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+
       # Modbus TCP
       modbus: tcpip
       id: 1
@@ -52,6 +104,19 @@ render:
       template: sunspec-hybrid
       usage: battery
 
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+
       # Modbus TCP
       modbus: tcpip
       id: 1
@@ -62,6 +127,19 @@ render:
       template: sunspec-hybrid
       usage: battery
 
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+
       # Modbus TCP
       modbus: tcpip
       id: 1
diff --git a/templates/docs/meter/sunspec-inverter_0.yaml b/templates/docs/meter/sunspec-inverter_0.yaml
index 834cbdfc..f6ef5ec0 100644
--- a/templates/docs/meter/sunspec-inverter_0.yaml
+++ b/templates/docs/meter/sunspec-inverter_0.yaml
@@ -8,6 +8,19 @@ render:
       template: sunspec-inverter
       usage: grid
 
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+
       # Modbus TCP
       modbus: tcpip
       id: 1
@@ -18,6 +31,19 @@ render:
       template: sunspec-inverter
       usage: grid
 
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+
       # Modbus TCP
       modbus: tcpip
       id: 1
@@ -30,6 +56,19 @@ render:
       template: sunspec-inverter
       usage: pv
 
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+
       # Modbus TCP
       modbus: tcpip
       id: 1
@@ -40,6 +79,19 @@ render:
       template: sunspec-inverter
       usage: pv
 
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+
       # Modbus TCP
       modbus: tcpip
       id: 1
diff --git a/templates/docs/vehicle/iso15118_0.yaml b/templates/docs/vehicle/iso15118_0.yaml
index 8a3be6f4..db05912e 100644
--- a/templates/docs/vehicle/iso15118_0.yaml
+++ b/templates/docs/vehicle/iso15118_0.yaml
@@ -1,6 +1,7 @@
 product:
   description: ISO15118
   group: Generische Unterstützung
+capabilities: ["iso151182"]
 description: |
   Nur unterstützt wenn das Fahrzeug den Ladestand (Soc) an die verbundene Wallbox übermitteln kann.
 render:
diff --git a/templates/docs/vehicle/offline_0.yaml b/templates/docs/vehicle/offline_0.yaml
index 714964f2..d97bed53 100644
--- a/templates/docs/vehicle/offline_0.yaml
+++ b/templates/docs/vehicle/offline_0.yaml
@@ -18,3 +18,10 @@ render:
       minCurrent: 6 # Definiert die minimale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll (Optional)
       maxCurrent: 16 # Definiert die maximale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll (Optional)
       identifiers: # Kann meist erst später eingetragen werden, siehe: https://docs.evcc.io/docs/guides/vehicles/#erkennung-des-fahrzeugs-an-der-wallbox (Optional)
+      mode: # Möglich sind Off, Now, MinPV und PV, oder leer wenn keiner definiert werden soll (Optional)
+      minSoc: 25 # Ladung mit maximaler Geschwindigkeit bis zu dem angegeben Ladestand unabhängig PV-Erzeugung, wenn der Lademodus nicht auf 'Aus' steht (Optional)
+      targetSoc: 80 # Bis zu welchem Ladestand (Soc) soll das Fahrzeug geladen werden (Optional)
+      minCurrent: 6 # Definiert die minimale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll (Optional)
+      maxCurrent: 16 # Definiert die maximale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll (Optional)
+      identifiers: # Kann meist erst später eingetragen werden, siehe: https://docs.evcc.io/docs/guides/vehicles/#erkennung-des-fahrzeugs-an-der-wallbox (Optional)
+      priority: # Priorität des Ladepunktes oder Fahrzeugs in Relation zu anderen Ladepunkten oder Fahrzeugen für die Zuweisung von PV-Energie (Optional)
diff --git a/util/modbus/functions.go b/util/modbus/functions.go
index bcb98e89..7a733009 100644
--- a/util/modbus/functions.go
+++ b/util/modbus/functions.go
@@ -39,6 +39,15 @@ func decodeMask(mask string) (uint64, error) {
 	return strconv.ParseUint(mask, 10, 64)
 }
 
+// decodeBool8 converts a masked uint1 to a bool
+func decodeBool8(b []byte) float64 {
+	u := b[0]
+	if u > 0 {
+		return 1
+	}
+	return 0
+}
+
 // decodeBool16 converts a masked uint16 to a bool
 func decodeBool16(mask uint64) func(b []byte) float64 {
 	return func(b []byte) float64 {
diff --git a/util/modbus/modbus.go b/util/modbus/modbus.go
index 382803ac..acc91e3f 100644
--- a/util/modbus/modbus.go
+++ b/util/modbus/modbus.go
@@ -375,14 +375,25 @@ func RegisterOperation(r Register) (rs485.Operation, error) {
 		op.FuncCode = modbus.FuncCodeReadHoldingRegisters
 	case "input":
 		op.FuncCode = modbus.FuncCodeReadInputRegisters
-	case "writesingle":
+	case "coil":
+		op.FuncCode = modbus.FuncCodeReadCoils
+		r.Decode = "bool8"
+	case "writesingle", "writeholding":
 		op.FuncCode = modbus.FuncCodeWriteSingleRegister
+	case "writecoil":
+		op.FuncCode = modbus.FuncCodeWriteSingleCoil
+		r.Decode = "bool8"
 	default:
 		return rs485.Operation{}, fmt.Errorf("invalid register type: %s", r.Type)
 	}
 
 	switch strings.ToLower(r.Decode) {
 
+	// 8 bit (coil)
+	case "bool8":
+		op.Transform = decodeBool8
+		op.ReadLen = 1
+
 	// 16 bit
 	case "int16":
 		op.Transform = asFloat64(encoding.Int16)
diff --git a/vehicle/tesla.go b/vehicle/tesla.go
index 2e2adff4..acec7991 100644
--- a/vehicle/tesla.go
+++ b/vehicle/tesla.go
@@ -2,6 +2,7 @@ package vehicle
 
 import (
 	"context"
+	"errors"
 	"time"
 
 	"github.com/bogosj/tesla"
@@ -76,20 +77,29 @@ func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 		v.Title_ = v.vehicle.DisplayName
 	}
 
-	v.dataG = provider.Cached(v.vehicle.Data, cc.Cache)
+	v.dataG = provider.Cached(func() (*tesla.VehicleData, error) {
+		res, err := v.vehicle.Data()
+		return res, v.apiError(err)
+	}, cc.Cache)
 
 	return v, nil
 }
 
+// apiError converts HTTP 408 error to ErrTimeout
+func (v *Tesla) apiError(err error) error {
+	if err != nil && err.Error() == "408 Request Timeout" {
+		err = api.ErrAsleep
+	}
+	return err
+}
+
 // Soc implements the api.Vehicle interface
 func (v *Tesla) Soc() (float64, error) {
 	res, err := v.dataG()
-
-	if err == nil {
-		return float64(res.Response.ChargeState.UsableBatteryLevel), nil
+	if err != nil {
+		return 0, err
 	}
-
-	return 0, err
+	return float64(res.Response.ChargeState.UsableBatteryLevel), nil
 }
 
 var _ api.ChargeState = (*Tesla)(nil)
@@ -188,51 +198,29 @@ var _ api.CurrentLimiter = (*Tesla)(nil)
 
 // StartCharge implements the api.VehicleChargeController interface
 func (v *Tesla) MaxCurrent(current int64) error {
-	return v.vehicle.SetChargingAmps(int(current))
+	return v.apiError(v.vehicle.SetChargingAmps(int(current)))
 }
 
 var _ api.Resurrector = (*Tesla)(nil)
 
 func (v *Tesla) WakeUp() error {
 	_, err := v.vehicle.Wakeup()
-	return err
+	return v.apiError(err)
 }
 
 var _ api.VehicleChargeController = (*Tesla)(nil)
 
 // StartCharge implements the api.VehicleChargeController interface
 func (v *Tesla) StartCharge() error {
-	err := v.vehicle.StartCharging()
-
-	if err != nil && err.Error() == "408 Request Timeout" {
-		if _, err := v.vehicle.Wakeup(); err != nil {
-			return err
-		}
-
-		timer := time.NewTimer(90 * time.Second)
-
-		for {
-			select {
-			case <-timer.C:
-				return api.ErrTimeout
-			default:
-				time.Sleep(2 * time.Second)
-				if err := v.vehicle.StartCharging(); err == nil || err.Error() != "408 Request Timeout" {
-					return err
-				}
-			}
-		}
-	}
-
-	return err
+	return v.apiError(v.vehicle.StartCharging())
 }
 
 // StopCharge implements the api.VehicleChargeController interface
 func (v *Tesla) StopCharge() error {
-	err := v.vehicle.StopCharging()
+	err := v.apiError(v.vehicle.StopCharging())
 
 	// ignore sleeping vehicle
-	if err != nil && err.Error() == "408 Request Timeout" {
+	if errors.Is(err, api.ErrAsleep) {
 		err = nil
 	}
 


Test patch:
diff --git a/tests/basics.evcc.yaml b/tests/basics.evcc.yaml
deleted file mode 100755
index 99832e63..00000000
--- a/tests/basics.evcc.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-site:
-  title: Hello World
-  meters:
-    grid: grid
-
-meters:
-  - name: grid
-    type: custom
-    power:
-      source: js
-      script: |
-        1000
-
-loadpoints:
-  - title: Carport
-    charger: charger
-
-chargers:
-  - name: charger
-    type: custom
-    enable:
-      source: js
-      script:
-    enabled:
-      source: js
-      script: |
-        false
-    status:
-      source: js
-      script: |
-        "B"
-    maxcurrent:
-      source: js
-      script:
diff --git a/tests/basics.spec.js b/tests/basics.spec.js
deleted file mode 100644
index a2a6cf44..00000000
--- a/tests/basics.spec.js
+++ /dev/null
@@ -1,40 +0,0 @@
-const { test, expect } = require("@playwright/test");
-const { execEvcc } = require("./utils");
-
-let server;
-
-test.beforeAll(() => {
-  server = execEvcc("basics.evcc.yaml");
-});
-
-test.afterAll(() => {
-  server.kill();
-});
-
-test.beforeEach(async ({ page }) => {
-  await page.goto("/");
-});
-
-test.describe("main screen", async () => {
-  test("site title", async ({ page }) => {
-    await expect(page.getByRole("heading", { name: "Hello World" })).toBeVisible();
-  });
-
-  test("visualization", async ({ page }) => {
-    const locator = page.getByTestId("visualization");
-    await expect(locator).toBeVisible();
-    await expect(locator).toContainText("1,000 W");
-  });
-
-  test("one loadpoint", async ({ page }) => {
-    await expect(page.getByTestId("loadpoint")).toHaveCount(1);
-  });
-
-  test("loadpoint title", async ({ page }) => {
-    await expect(page.getByRole("heading", { name: "Carport" })).toBeVisible();
-  });
-
-  test("guest vehicle", async ({ page }) => {
-    await expect(page.getByRole("heading", { name: "Guest vehicle" })).toBeVisible();
-  });
-});
diff --git a/tests/utils.js b/tests/utils.js
deleted file mode 100644
index 04a3b926..00000000
--- a/tests/utils.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { exec } from "child_process";
-
-export function execEvcc(config) {
-  const server = exec(`./evcc --config tests/${config}`);
-  server.stdout.pipe(process.stdout);
-  return server;
-}