Skip to content
Open
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
17 changes: 17 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,23 @@ a:hover {
--bs-btn-line-height: 2;
}

.btn-pill {
--bs-btn-bg: var(--bs-body-bg);
--bs-btn-color: var(--evcc-default-text);
--bs-btn-border-color: var(--evcc-gray-50);
--bs-btn-hover-bg: var(--bs-body-bg);
--bs-btn-hover-color: var(--evcc-default-text);
--bs-btn-hover-border-color: var(--evcc-default-text);
--bs-btn-active-bg: var(--bs-body-bg);
--bs-btn-active-color: var(--evcc-default-text);
--bs-btn-active-border-color: var(--evcc-default-text);
white-space: nowrap;
text-transform: uppercase;
font-weight: bold;
font-size: 0.75rem;
border-radius: 50rem;
}

.btn-outline-secondary {
--bs-btn-color: var(--evcc-default-text);
--bs-btn-border-color: var(--evcc-default-text);
Expand Down
39 changes: 36 additions & 3 deletions assets/js/components/Config/DeviceCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,21 @@
@edit="$emit('edit')"
/>
</div>
<div v-if="$slots.tags" ref="tagsContainer" :style="tagsStyle">
<template v-if="disabled">
<hr class="mt-3 mb-0 divide" />
<div class="disabled-region">
<button
type="button"
class="btn btn-sm btn-pill px-3"
:aria-label="$t('config.general.enable')"
data-testid="device-disabled"
@click.stop="$emit('enable')"
>
{{ $t("config.general.disabled") }}
</button>
</div>
</template>
<div v-else-if="$slots.tags" ref="tagsContainer" :style="tagsStyle">
<hr class="my-3 divide" />
<div ref="tagsContent">
<slot name="tags" />
Expand All @@ -52,8 +66,9 @@ export default {
warning: Boolean,
noEditButton: Boolean,
badge: Boolean,
disabled: Boolean,
},
emits: ["edit"],
emits: ["edit", "enable"],
data() {
return {
tagsMinHeight: null,
Expand Down Expand Up @@ -99,7 +114,8 @@ export default {

<style scoped>
.root {
display: block;
display: flex;
flex-direction: column;
list-style-type: none;
border-radius: 1rem;
padding: 1rem 1.5rem;
Expand All @@ -121,6 +137,23 @@ export default {
color: var(--evcc-gray) !important;
font-weight: normal !important;
}
.disabled-region {
flex: 1;
margin: 0 -1.5rem -1rem;
padding: 1.25rem 1.5rem;
min-height: 5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0 0 1rem 1rem;
background-image: repeating-linear-gradient(
-45deg,
transparent 0,
transparent 10px,
var(--evcc-gray-25) 10px,
var(--evcc-gray-25) 20px
);
}
.icon:empty {
display: none;
}
Expand Down
53 changes: 33 additions & 20 deletions assets/js/components/Config/DeviceModal/Actions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,36 @@
@test="$emit('test')"
/>

<div class="mt-4 d-flex justify-content-between">
<button
v-if="isDeletable"
type="button"
class="btn btn-link text-danger"
tabindex="0"
@click.prevent="$emit('remove')"
>
{{ $t("config.general.delete") }}
</button>
<button
v-else
type="button"
class="btn btn-link text-muted"
data-bs-dismiss="modal"
tabindex="0"
>
{{ $t("config.general.cancel") }}
</button>
<div class="mt-4 d-flex flex-wrap justify-content-between align-items-center gap-2">
<div class="d-flex flex-wrap align-items-center gap-1">
<button
v-if="isDeletable"
type="button"
class="btn btn-link text-danger"
tabindex="0"
@click.prevent="$emit('remove')"
>
{{ $t("config.general.delete") }}
</button>
<button
v-else
type="button"
class="btn btn-link text-muted"
data-bs-dismiss="modal"
tabindex="0"
>
{{ $t("config.general.cancel") }}
</button>
<button
v-if="isDeletable && canDisable"
type="button"
class="btn btn-link text-muted"
tabindex="0"
@click.prevent="$emit('disable', !isDisabled)"
>
{{ isDisabled ? $t("config.general.enable") : $t("config.general.disable") }}
</button>
</div>
<button
type="submit"
:class="buttonClass"
Expand Down Expand Up @@ -61,6 +72,8 @@ export default defineComponent({
},
props: {
isDeletable: Boolean as PropType<boolean>,
isDisabled: Boolean as PropType<boolean>,
canDisable: { type: Boolean as PropType<boolean>, default: true },
testState: {
type: Object as PropType<TestState>,
default: () => {},
Expand All @@ -71,7 +84,7 @@ export default defineComponent({
sponsorTokenRequired: Boolean as PropType<boolean>,
currency: String as PropType<CURRENCY>,
},
emits: ["save", "remove", "test"],
emits: ["save", "remove", "test", "disable"],
computed: {
saveButtonLabel(): string {
const { isError, isUnknown, isRunning } = this.testState;
Expand Down
20 changes: 19 additions & 1 deletion assets/js/components/Config/DeviceModal/DeviceModalBase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@
<DeviceModalActions
v-if="showActions"
:is-deletable="isDeletable"
:is-disabled="isDisabled"
:can-disable="canDisable"
:test-state="test"
:is-saving="saving"
:is-succeeded="succeeded"
Expand All @@ -165,6 +167,7 @@
@save="handleSave"
@remove="handleRemove"
@test="testManually"
@disable="handleDisable"
/>
</template>
</form>
Expand All @@ -175,7 +178,7 @@
import { defineComponent, type PropType } from "vue";
import GenericModal from "../../Helper/GenericModal.vue";
import DeviceInfoButton from "./DeviceInfoButton.vue";
import { closeModal } from "@/configModal";
import { closeModal, isNestedIn } from "@/configModal";
import ErrorMessage from "../../Helper/ErrorMessage.vue";
import PropertyEntry from "../PropertyEntry.vue";
import PropertyCollapsible from "../PropertyCollapsible.vue";
Expand Down Expand Up @@ -277,6 +280,7 @@ export default defineComponent({
"added",
"updated",
"removed",
"disable",
"close",
"template-changed",
"update:externalTemplate",
Expand Down Expand Up @@ -401,6 +405,12 @@ export default defineComponent({
isDeletable() {
return !this.isNew;
},
isDisabled() {
return Boolean(this.values.deviceDisable);
},
canDisable(): boolean {
return !isNestedIn("loadpoint");
},
showActions() {
// explicitly hide template fields (ocpp step 1)
if (this.hideTemplateFields) {
Expand Down Expand Up @@ -561,6 +571,9 @@ export default defineComponent({
if (device.deviceIcon !== undefined) {
this.values.deviceIcon = device.deviceIcon;
}
if (device.deviceDisable !== undefined) {
this.values.deviceDisable = device.deviceDisable;
}
this.applyDefaults();
this.templateName = this.values.template;

Expand Down Expand Up @@ -717,6 +730,11 @@ export default defineComponent({
handleError(e, "remove failed");
}
},
async handleDisable(disable: boolean) {
if (this.id === undefined) return;
this.$emit("disable", { id: this.id, disable });
await closeModal();
},
handleOpen() {
this.isModalVisible = true;
},
Expand Down
43 changes: 43 additions & 0 deletions assets/js/components/Config/DeviceModal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type DeviceValues = {
template: string | null;
deviceTitle?: string;
deviceIcon?: string;
deviceDisable?: boolean;
usage?: MeterTemplateUsage;
heating?: boolean;
integrateddevice?: boolean;
Expand Down Expand Up @@ -108,6 +109,39 @@ export function customChargerName(type: ConfigType, isHeating: boolean) {
return `${prefix}${type}`;
}

// flattenDeviceConfig converts a GET /config/devices/:class/:id response
// into the flat shape expected by POST/PUT/test (id and name are dropped).
//
// GET (config = device-specfic):
// {
// id: 26,
// name: "db:26",
// type: "template",
// deviceTitle: "Espresso",
// device__: "..",
// config: {
// template: "tasmota",
// host: "192.168.1.2"
// }
// }
//
// PUT|POST|test (flat):
// {
// type: "template",
// deviceTitle: "Espresso",
// device__: "..",
// template: "tasmota",
// host: "192.168.1.2"
// }
//
// TODO: align GET and PUT shapes — always nest device-specific values under
// `config` and drop the artificial `device` prefix (deviceTitle → title, ...)
// matching db structure. Once the API is symmetric this helper goes away.
export function flattenDeviceConfig(dev: any): Record<string, any> {
const { id, name, config, ...rest } = dev;
return { ...config, ...rest };
}

export async function loadServiceValues(path: string) {
try {
const response = await api.get(`/config/service/${path}`, {
Expand Down Expand Up @@ -203,6 +237,14 @@ export function createDeviceUtils(deviceType: DeviceType) {
return api.delete(`config/devices/${deviceType}/${id}`);
}

// disable flips the disable flag by re-PUTing the existing config.
// force=true so a broken device can still be toggled.
async function disable(id: number, disable: boolean) {
const dev = (await api.get(`config/devices/${deviceType}/${id}`)).data;
const body = { ...flattenDeviceConfig(dev), deviceDisable: disable };
return api.put(`config/devices/${deviceType}/${id}`, body, { params: { force: true } });
}

async function load(id: number) {
const response = await api.get(`config/devices/${deviceType}/${id}`);
return response.data;
Expand Down Expand Up @@ -268,6 +310,7 @@ export function createDeviceUtils(deviceType: DeviceType) {
test,
update,
remove,
disable,
load,
create,
loadProducts,
Expand Down
59 changes: 41 additions & 18 deletions assets/js/components/Config/LoadpointModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -544,23 +544,38 @@
</div>
</div>

<div v-if="values.charger" class="mt-5 mb-4 d-flex justify-content-between">
<button
v-if="isDeletable"
type="button"
class="btn btn-link text-danger"
@click.prevent="remove"
>
{{ $t("config.meter.delete") }}
</button>
<button
v-else
type="button"
class="btn btn-link text-muted btn-cancel"
data-bs-dismiss="modal"
>
{{ $t("config.loadpoint.cancel") }}
</button>
<div
v-if="values.charger"
class="mt-5 mb-4 d-flex flex-wrap justify-content-between align-items-center gap-2"
>
<div class="d-flex flex-wrap align-items-center gap-1">
<button
v-if="isDeletable"
type="button"
class="btn btn-link text-danger"
@click.prevent="remove"
>
{{ $t("config.meter.delete") }}
</button>
<button
v-else
type="button"
class="btn btn-link text-muted btn-cancel"
data-bs-dismiss="modal"
>
{{ $t("config.loadpoint.cancel") }}
</button>
<button
v-if="isDeletable"
type="button"
class="btn btn-link text-muted"
@click.prevent="handleDisable(!isDisabled)"
>
{{
isDisabled ? $t("config.general.enable") : $t("config.general.disable")
}}
</button>
</div>
<button type="submit" class="btn btn-primary" :disabled="saving">
<span
v-if="saving"
Expand Down Expand Up @@ -654,7 +669,7 @@ export default {
default: () => false,
},
},
emits: ["changed", "dismissed"],
emits: ["changed", "dismissed", "disable"],
data() {
return {
isModalVisible: false,
Expand Down Expand Up @@ -732,6 +747,9 @@ export default {
isDeletable() {
return !this.isNew;
},
isDisabled() {
return Boolean(this.values.disable);
},
showPriority() {
return this.isNew ? this.loadpointCount > 0 : this.loadpointCount > 1;
},
Expand Down Expand Up @@ -864,6 +882,11 @@ export default {
alert("delete failed");
}
},
async handleDisable(disable: boolean) {
if (this.id === undefined) return;
this.$emit("disable", { id: this.id, disable });
await closeModal();
},
async create() {
this.saving = true;
try {
Expand Down
Loading