diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/zigbee.mqtt/index.scss b/platypush/backend/http/static/css/source/webpanel/plugins/zigbee.mqtt/index.scss
new file mode 100644
index 000000000..28293469d
--- /dev/null
+++ b/platypush/backend/http/static/css/source/webpanel/plugins/zigbee.mqtt/index.scss
@@ -0,0 +1,264 @@
+@import 'common/vars';
+@import 'webpanel/plugins/zigbee.mqtt/vars';
+
+.fa.fa-zigbee:before {
+    content: ' ';
+    background: url('/static/img/icons/zigbee-logo.svg');
+    background-size: 1em 1em;
+    width: 1em;
+    height: 1em;
+    display: inline-block;
+}
+
+.zigbee-container {
+    height: 100%;
+    padding: 0 .5em;
+    background: $container-bg;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    overflow: auto;
+
+    .no-items {
+        padding: 2em;
+        font-size: 1.5em;
+        color: $no-items-color;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+    }
+
+    .view-options {
+        display: flex;
+        width: 100%;
+        justify-content: space-between;
+        padding: 1em 0;
+
+        .view-selector {
+            display: inline-flex;
+        }
+
+        .buttons {
+            display: inline-flex;
+        }
+
+        select {
+            width: 100%;
+            border-radius: 1em;
+        }
+    }
+
+    .btn-default {
+        border: 0;
+        padding: 0 1em;
+
+        &:hover {
+            border: $default-border-2;
+            border-radius: 1em;
+        }
+    }
+
+
+    .buttons {
+        text-align: right;
+    }
+
+    .view {
+        min-width: 400pt;
+        max-width: 750pt;
+        background: $view-bg;
+        border: $view-border;
+        border-radius: 1.5em;
+        box-shadow: $view-box-shadow;
+    }
+
+    .item {
+        &.selected {
+            box-shadow: $selected-item-box-shadow;
+        }
+
+        .name {
+            padding: 1em;
+            cursor: pointer;
+            text-transform: uppercase;
+            letter-spacing: .06em;
+
+            &.selected {
+                border-radius: 1.5em;
+            }
+        }
+
+        &:hover {
+            background: $hover-bg;
+        }
+
+        &:not(:last-child) {
+            border-bottom: $item-border;
+        }
+
+        &:first-child {
+            border-radius: 1.5em 1.5em 0 0;
+        }
+
+        &:last-child {
+            border-radius: 0 0 1.5em 1.5em;
+        }
+    }
+
+    .params {
+        background: $params-bg;
+        padding-bottom: 1em;
+
+        .section {
+            display: flex;
+            flex-direction: column;
+            padding: 0 1em;
+
+            &:not(:first-child) {
+                padding-top: 1em;
+            }
+
+            .header {
+                display: flex;
+                align-items: center;
+                font-weight: bold;
+                border-bottom: $param-section-header-border;
+            }
+        }
+
+        .row {
+            display: flex;
+            align-items: center;
+            border-radius: 1em;
+            padding: .3em;
+
+            &:nth-child(even) {
+                background: $param-even-row-bg;
+            }
+
+            &:nth-child(odd) {
+                background: $param-odd-row-bg;
+            }
+
+            &:hover {
+                background: $hover-bg;
+            }
+        }
+
+        .param-name {
+            display: inline-flex;
+            width: 40%;
+            margin-left: 1%;
+            vertical-align: top;
+            letter-spacing: .04em;
+        }
+
+        .param-value {
+            display: inline-block;
+            width: 58%;
+            text-align: right;
+
+            .value-edit {
+                display: flex;
+                align-items: center;
+            }
+
+            .value-data {
+                display: inline-block;
+                font-weight: bold;
+            }
+
+            .slider-container {
+                display: flex;
+                align-items: center;
+            }
+
+            .unit {
+                font-size: .8em;
+                margin-left: 1em;
+                display: inline;
+            }
+
+            select {
+                width: 100%;
+                border-radius: 2em;
+            }
+
+            .numeric {
+                input.slider {
+                    text-align: left;
+                }
+
+                input[type=text] {
+                    text-align: right;
+                    width: 100%;
+                }
+
+                .row {
+                    background: none;
+                    &:hover {
+                        background: none;
+                    }
+                }
+
+                .value-min, .value-max {
+                    width: 50%;
+                    font-size: .85em;
+                    opacity: .75;
+                }
+
+                .value-min {
+                    text-align: left;
+                }
+
+                .value-max {
+                    text-align: right;
+                }
+            }
+        }
+    }
+
+    .btn-value-name-edit {
+        padding: 0;
+    }
+
+    .modal {
+        .section {
+            .header {
+                background: none;
+                padding: .5em 0;
+            }
+
+            .body {
+                padding: 0;
+            }
+        }
+
+        .network-info {
+            min-width: 600pt;
+        }
+    }
+
+    .error {
+        color: $error-color;
+    }
+
+    .device, .group {
+        .actions {
+            .row {
+                cursor: pointer;
+            }
+        }
+
+        form {
+            margin-bottom: 0;
+        }
+
+        .param-value {
+            input[type=text] {
+                text-align: right;
+            }
+        }
+    }
+}
+
diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/zigbee.mqtt/vars.scss b/platypush/backend/http/static/css/source/webpanel/plugins/zigbee.mqtt/vars.scss
new file mode 100644
index 000000000..798334a20
--- /dev/null
+++ b/platypush/backend/http/static/css/source/webpanel/plugins/zigbee.mqtt/vars.scss
@@ -0,0 +1,13 @@
+$container-bg: #f1f1f1;
+$view-bg: white;
+$view-border: 1px solid #d8d8d8;
+$view-box-shadow: 1px 2px 2px #ccc;
+$item-border: 1px solid #dddddd;
+$no-items-color: #555555;
+$params-bg: white;
+$param-even-row-bg: #ededed;
+$param-odd-row-bg: white;
+$param-section-header-border: 1px solid #e8e8e8;
+$selected-item-box-shadow: 0 2px 4px 0 #bbb;
+$error-color: #aa0000;
+
diff --git a/platypush/backend/http/static/img/icons/zigbee-logo.svg b/platypush/backend/http/static/img/icons/zigbee-logo.svg
new file mode 100644
index 000000000..b7cf73a1b
--- /dev/null
+++ b/platypush/backend/http/static/img/icons/zigbee-logo.svg
@@ -0,0 +1,5 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+    <style>.color {fill: #5f7869}</style>
+    <title>Zigbee icon</title>
+    <path class="color" d="M11.988 0a11.85 11.85 0 00-8.617 3.696c7.02-.875 11.401-.583 13.289-.34 3.752.583 3.558 3.404 3.558 3.404L8.237 19.112c2.299.22 6.897.366 13.796-.631a11.86 11.86 0 001.912-6.469C23.945 5.374 18.595 0 11.988 0zm.232 4.31c-2.451-.014-5.772.146-9.963.723C.854 7.003.055 9.41.055 12.012.055 18.626 5.38 24 11.988 24c3.63 0 6.85-1.63 9.053-4.182-7.286.948-11.813.631-13.75.388-3.775-.56-3.557-3.404-3.557-3.404L15.691 4.474a38.635 38.635 0 00-3.471-.163Z"/>
+</svg>
diff --git a/platypush/backend/http/static/js/elements/dropdown.js b/platypush/backend/http/static/js/elements/dropdown.js
index c1d98766e..292ef325e 100644
--- a/platypush/backend/http/static/js/elements/dropdown.js
+++ b/platypush/backend/http/static/js/elements/dropdown.js
@@ -14,6 +14,11 @@ Vue.component('dropdown', {
             type: Array,
             default: [],
         },
+
+        classes: {
+            type: Array,
+            default: () => [],
+        },
     },
 
     methods: {
@@ -38,7 +43,7 @@ let clickHndl = function(event) {
 
     var element = event.target;
     while (element) {
-        if (element == openedDropdown) {
+        if (element === openedDropdown) {
             return;
         }
 
diff --git a/platypush/backend/http/static/js/plugins/zigbee.mqtt/device.js b/platypush/backend/http/static/js/plugins/zigbee.mqtt/device.js
new file mode 100644
index 000000000..102bfae8f
--- /dev/null
+++ b/platypush/backend/http/static/js/plugins/zigbee.mqtt/device.js
@@ -0,0 +1,127 @@
+Vue.component('zigbee-device', {
+    template: '#tmpl-zigbee-device',
+    props: ['device','bus','selected'],
+    data: function() {
+        return {
+            newPropertyName: '',
+            editMode: {
+                name: false,
+            },
+        };
+    },
+
+    methods: {
+        onDeviceClicked: function() {
+            this.bus.$emit('deviceClicked', {
+                deviceId: this.device.friendly_name,
+            });
+        },
+
+        setValue: async function(event) {
+            let name = undefined;
+            if (this.newPropertyName && this.newPropertyName.length) {
+                name = this.newPropertyName;
+            } else {
+                name = event.event
+                    ? event.event.target.parentElement.dataset.name
+                    : event.target.dataset.name;
+            }
+
+            if (!name || !name.length) {
+                return;
+            }
+
+            const target = event.event
+                ? event.event.target.parentElement.querySelector('input')
+                : event.target;
+
+            const value = target.getAttribute('type') === 'checkbox'
+                ? (target.checked ? 'OFF' : 'ON')
+                : target.value;
+
+            await request('zigbee.mqtt.device_set', {
+                device: this.device.friendly_name,
+                property: name,
+                value: value,
+            });
+
+            if (this.newPropertyName && this.newPropertyName.length) {
+                this.newPropertyName = '';
+            }
+
+            this.bus.$emit('refreshDevices');
+        },
+
+        removeDevice: async function(force=false) {
+            if (!confirm('Are you sure that you want to remove this device?')) {
+                return;
+            }
+
+            await request('zigbee.mqtt.device_remove', {
+                device: this.device.friendly_name,
+                force: force,
+            });
+
+            this.bus.$emit('refreshDevices');
+        },
+
+        banDevice: async function() {
+            if (!confirm('Are you sure that you want to ban this device?')) {
+                return;
+            }
+
+            await request('zigbee.mqtt.device_ban', {
+                device: this.device.friendly_name,
+            });
+
+            this.bus.$emit('refreshDevices');
+        },
+
+        whitelistDevice: async function() {
+            if (!confirm('Are you sure that you want to whitelist this device? Note: ALL the other non-whitelisted ' +
+                    'devices will be removed from the network')) {
+                return;
+            }
+
+            await request('zigbee.mqtt.device_whitelist', {
+                device: this.device.friendly_name,
+            });
+
+            this.bus.$emit('refreshDevices');
+        },
+
+        disableForm: function(form) {
+            form.querySelector('input,button').readOnly = true;
+        },
+
+        enableForm: function(form) {
+            form.querySelector('input,button').readOnly = false;
+        },
+
+        onEditMode: function(mode) {
+            Vue.set(this.editMode, mode, true);
+            const form = this.$refs[mode + 'Form'];
+            const input = form.querySelector('input[type=text]');
+
+            setTimeout(() => {
+                input.focus();
+                input.select();
+            }, 10);
+        },
+
+        editName: async function(event) {
+            this.disableForm(event.target);
+            const name = event.target.querySelector('input[name=name]').value;
+
+            await request('zigbee.mqtt.device_rename', {
+                device: this.device.friendly_name,
+                name: name,
+            });
+
+            this.editMode.name = false;
+            this.enableForm(event.target);
+            this.bus.$emit('refreshDevices');
+        },
+    },
+});
+
diff --git a/platypush/backend/http/static/js/plugins/zigbee.mqtt/group.js b/platypush/backend/http/static/js/plugins/zigbee.mqtt/group.js
new file mode 100644
index 000000000..26f902574
--- /dev/null
+++ b/platypush/backend/http/static/js/plugins/zigbee.mqtt/group.js
@@ -0,0 +1,149 @@
+Vue.component('zigbee-group', {
+    template: '#tmpl-zigbee-group',
+    props: ['group','bus','selected'],
+    data: function() {
+        return {
+            properties: {},
+        };
+    },
+
+    methods: {
+        anyOn: async function() {
+            for (const dev of this.group.devices) {
+                const params = await request('zigbee.mqtt.device_get', {device: dev.friendly_name});
+                if (params.state === 'ON') {
+                    return true;
+                }
+            }
+
+            return false;
+        },
+
+        allOn: async function() {
+            for (const dev of this.group.devices) {
+                const params = await request('zigbee.mqtt.device_get', {device: dev.friendly_name});
+                if (params.state === 'OFF') {
+                    return false;
+                }
+            }
+
+            return true;
+        },
+
+        refreshProperties: async function() {
+            const props = {};
+
+            for (const dev of this.group.devices) {
+                const params = await request('zigbee.mqtt.device_get', {device: dev.friendly_name});
+                for (const [name, value] of Object.entries(params)) {
+                    if (name === 'linkquality') {
+                        continue;
+                    }
+
+                    if (name in props) {
+                        props[name].push(value);
+                    } else {
+                        props[name] = [value];
+                    }
+                }
+            }
+
+            for (const [name, values] of Object.entries(props)) {
+                if (name === 'state') {
+                    props[name] = values.filter((value) => value === 'ON').length > 0;
+                } else if (!isNaN(values[0])) {
+                    props[name] = values.reduce((sum, value) => sum + value, 0) / values.length;
+                } else {
+                    props[name] = values[0];
+                }
+            }
+
+            this.properties = props;
+        },
+
+        onGroupClicked: function() {
+            this.bus.$emit('groupClicked', {
+                groupId: this.group.id,
+            });
+        },
+
+        setValue: async function(event) {
+            const name = event.target.dataset.name;
+            if (!name || !name.length) {
+                return;
+            }
+
+            await request('zigbee.mqtt.group_set', {
+                group: this.group.friendly_name,
+                property: name,
+                value: event.target.value,
+            });
+
+            this.bus.$emit('refreshDevices');
+        },
+
+        toggleState: async function() {
+            const state = (await this.anyOn()) ? 'OFF' : 'ON';
+            await request('zigbee.mqtt.group_set', {
+                group: this.group.friendly_name,
+                property: 'state',
+                value: state,
+            });
+
+            this.bus.$emit('refreshDevices');
+        },
+
+        renameGroup: async function() {
+            const name = prompt('New name', this.group.friendly_name);
+            if (!name || !name.length || name === this.group.friendly_name) {
+                return;
+            }
+
+            this.commandRunning = true;
+            await request('zigbee.mqtt.group_rename', {
+                name: name,
+                group: this.group.friendly_name,
+            });
+
+            this.commandRunning = false;
+            const self = this;
+
+            setTimeout(() => {
+                self.bus.$emit('refreshGroups');
+            }, 100);
+        },
+
+        removeGroup: async function() {
+            if (!confirm('Are you sure that you want to delete this group?')) {
+                return;
+            }
+
+            this.commandRunning = true;
+            await request('zigbee.mqtt.group_remove', {name: this.group.friendly_name});
+            this.commandRunning = false;
+            this.bus.$emit('refreshGroups');
+        },
+
+        removeFromGroup: async function(device) {
+            if (!confirm('Are you sure that you want to remove this node from ' + this.group.label + '?')) {
+                return;
+            }
+
+            await request('zigbee.mqtt.group_remove_device', {
+                device: device,
+                group: this.group.friendly_name,
+            });
+
+            this.bus.$emit('refreshGroups');
+        },
+    },
+
+    created: function() {
+        this.refreshProperties();
+        this.bus.$on('refresh', this.refreshProperties);
+        this.bus.$on('refreshDevices', this.refreshProperties);
+        this.bus.$on('refreshGroups', this.refreshProperties);
+        this.bus.$on('refreshProperties', this.refreshProperties);
+    },
+});
+
diff --git a/platypush/backend/http/static/js/plugins/zigbee.mqtt/index.js b/platypush/backend/http/static/js/plugins/zigbee.mqtt/index.js
new file mode 100644
index 000000000..024728a7c
--- /dev/null
+++ b/platypush/backend/http/static/js/plugins/zigbee.mqtt/index.js
@@ -0,0 +1,311 @@
+Vue.component('zigbee-mqtt', {
+    template: '#tmpl-zigbee-mqtt',
+    props: ['config'],
+
+    data: function() {
+        return {
+            bus: new Vue({}),
+            status: {},
+            devices: {},
+            groups: {},
+            commandRunning: false,
+            selected: {
+                view: 'devices',
+                deviceId: undefined,
+                groupId: undefined,
+            },
+            loading: {
+                status: false,
+                devices: false,
+                groups: false,
+            },
+            views: {
+                devices: true,
+                groups: true,
+            },
+            modal: {
+                group: {
+                    visible: false,
+                },
+            },
+        };
+    },
+
+    computed: {
+        networkDropdownItems: function() {
+            const self = this;
+            return [
+                {
+                    text: 'Start Network',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        self.commandRunning = true;
+                        await request('zigbee.mqtt.start_network');
+                        self.commandRunning = false;
+                    },
+                },
+
+                {
+                    text: 'Stop Network',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        self.commandRunning = true;
+                        await request('zigbee.mqtt.stop_network');
+                        self.commandRunning = false;
+                    },
+                },
+
+                {
+                    text: 'Permit Join',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        let seconds = prompt('Join allow period in seconds (type 0 for no time limits)', '60');
+                        if (!seconds) {
+                            return;
+                        }
+
+                        seconds = parseInt(seconds);
+                        self.commandRunning = true;
+                        await request('zigbee.mqtt.permit_join', {permit: true, timeout: seconds || null});
+                        self.commandRunning = false;
+                    },
+                },
+
+                {
+                    text: 'Reset',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        if (!confirm('Are you sure that you want to reset the device?')) {
+                            return;
+                        }
+
+                        await request('zigbee.mqtt.reset');
+                    },
+                },
+
+                {
+                    text: 'Factory Reset',
+                    disabled: this.commandRunning,
+                    classes: ['error'],
+                    click: async function() {
+                        if (!confirm('Are you sure that you want to do a device soft reset? ALL network information and custom firmware will be lost!!')) {
+                            return;
+                        }
+
+                        await request('zigbee.mqtt.factory_reset');
+                    },
+                },
+            ]
+        },
+
+        addToGroupDropdownItems: function() {
+            const self = this;
+            return Object.values(this.groups).filter((group) => {
+                return !group.values || !group.values.length || !(this.selected.valueId in this.scene.values);
+            }).map((group) => {
+                return {
+                    text: group.name,
+                    disabled: this.commandRunning,
+                    click: async function () {
+                        if (!self.selected.valueId) {
+                            return;
+                        }
+
+                        self.commandRunning = true;
+                        await request('zwave.scene_add_value', {
+                            id_on_network: self.selected.valueId,
+                            scene_id: group.scene_id,
+                        });
+
+                        self.commandRunning = false;
+                        self.refresh();
+                    },
+                };
+            });
+        },
+    },
+
+    methods: {
+        refreshDevices: async function () {
+            const self = this;
+            this.loading.devices = true;
+            this.devices = (await request('zigbee.mqtt.devices')).reduce((devices, device) => {
+                if (device.friendly_name in self.devices) {
+                    device = {
+                        values: self.devices[device.friendly_name].values || {},
+                        ...self.devices[device.friendly_name],
+                    }
+                }
+
+                devices[device.friendly_name] = device;
+                return devices;
+            }, {});
+
+            Object.values(this.devices).forEach((device) => {
+                if (device.type === 'Coordinator') {
+                    return;
+                }
+
+                request('zigbee.mqtt.device_get', {device: device.friendly_name}).then((response) => {
+                    Vue.set(self.devices[device.friendly_name], 'values', response || {});
+                });
+            });
+
+            this.loading.devices = false;
+        },
+
+        refreshGroups: async function () {
+            this.loading.groups = true;
+            this.groups = (await request('zigbee.mqtt.groups')).reduce((groups, group) => {
+                groups[group.id] = group;
+                return groups;
+            }, {});
+
+            this.loading.groups = false;
+        },
+
+        refresh: function () {
+            this.refreshDevices();
+            this.refreshGroups();
+            this.bus.$emit('refreshProperties');
+        },
+
+        addGroup: async function() {
+            const name = prompt('Group name');
+            if (!name) {
+                return;
+            }
+
+            this.commandRunning = true;
+            await request('zigbee.mqtt.group_add', {name: name});
+            this.commandRunning = false;
+            this.refreshGroups();
+        },
+
+        onViewChange: function(event) {
+            Vue.set(this.selected, 'view', event.target.value);
+        },
+
+        onDeviceClicked: function(event) {
+            Vue.set(this.selected, 'deviceId', event.deviceId === this.selected.deviceId ? undefined : event.deviceId);
+        },
+
+        onGroupClicked: function(event) {
+            Vue.set(this.selected, 'groupId', event.groupId === this.selected.groupId ? undefined : event.groupId);
+        },
+
+        openNetworkCommandsDropdown: function() {
+            openDropdown(this.$refs.networkCommandsDropdown);
+        },
+
+        openAddToGroupDropdown: function(event) {
+            this.selected.valueId = event.valueId;
+            openDropdown(this.$refs.addToGroupDropdown);
+        },
+
+        addToGroup: async function(device, group) {
+            this.commandRunning = true;
+            await request('zigbee.mqtt.group_add_device', {
+                device: device,
+                group: group,
+            });
+
+            this.commandRunning = false;
+            const self = this;
+
+            setTimeout(() => {
+                self.refresh();
+                self.bus.$emit('refreshProperties');
+            }, 100)
+        },
+
+        removeNodeFromGroup: async function(event) {
+            if (!confirm('Are you sure that you want to remove this value from the group?')) {
+                return;
+            }
+
+            this.commandRunning = true;
+            await request('zigbee.mqtt.group_remove_device', {
+                group: event.group,
+                device: event.device,
+            });
+
+            this.commandRunning = false;
+        },
+    },
+
+    created: function() {
+        const self = this;
+        this.bus.$on('refresh', this.refresh);
+        this.bus.$on('refreshDevices', this.refreshDevices);
+        this.bus.$on('refreshGroups', this.refreshGroups);
+        this.bus.$on('deviceClicked', this.onDeviceClicked);
+        this.bus.$on('groupClicked', this.onGroupClicked);
+        this.bus.$on('openAddToGroupModal', () => {self.modal.group.visible = true});
+        this.bus.$on('openAddToGroupDropdown', this.openAddToGroupDropdown);
+        this.bus.$on('removeFromGroup', this.removeNodeFromGroup);
+
+        registerEventHandler(() => {
+            createNotification({
+                text: 'WARNING: The controller is now offline',
+                error: true,
+            });
+        }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttOnlineEvent');
+
+        registerEventHandler(() => {
+            createNotification({
+                text: 'Failed to remove the device',
+                error: true,
+            });
+        }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceRemovedFailedEvent');
+
+        registerEventHandler(() => {
+            createNotification({
+                text: 'Failed to add the group',
+                error: true,
+            });
+        }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupAddedFailedEvent');
+
+        registerEventHandler(() => {
+            createNotification({
+                text: 'Failed to remove the group',
+                error: true,
+            });
+        }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemovedFailedEvent');
+
+        registerEventHandler(() => {
+            createNotification({
+                text: 'Failed to remove the devices from the group',
+                error: true,
+            });
+        }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemoveAllFailedEvent');
+
+        registerEventHandler((event) => {
+            createNotification({
+                text: 'Unhandled Zigbee error: ' + (event.error || '[Unknown error]'),
+                error: true,
+            });
+        }, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttErrorEvent');
+
+        registerEventHandler(this.refresh,
+            'platypush.message.event.zigbee.mqtt.ZigbeeMqttOnlineEvent',
+            'platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePropertySetEvent',
+            'platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePairingEvent',
+            'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceConnectedEvent',
+            'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceBannedEvent',
+            'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceRemovedEvent',
+            'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceWhitelistedEvent',
+            'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceRenamedEvent',
+            'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceBindEvent',
+            'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceUnbindEvent',
+            'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupAddedEvent',
+            'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemovedEvent',
+            'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemoveAllEvent',
+        );
+    },
+
+    mounted: function() {
+        this.refresh();
+    },
+});
+
diff --git a/platypush/backend/http/static/js/plugins/zwave/index.js b/platypush/backend/http/static/js/plugins/zwave/index.js
index 2f27231be..90c2b0749 100644
--- a/platypush/backend/http/static/js/plugins/zwave/index.js
+++ b/platypush/backend/http/static/js/plugins/zwave/index.js
@@ -77,7 +77,7 @@ Vue.component('zwave', {
                     disabled: this.commandRunning,
                     click: async function() {
                         self.commandRunning = true;
-                        await request('zwave.start_network');
+                        await request('zwave.stop_network');
                         self.commandRunning = false;
                     },
                 },
diff --git a/platypush/backend/http/templates/elements/dropdown.html b/platypush/backend/http/templates/elements/dropdown.html
index 0b22fd5b9..82e4e9b46 100644
--- a/platypush/backend/http/templates/elements/dropdown.html
+++ b/platypush/backend/http/templates/elements/dropdown.html
@@ -1,6 +1,8 @@
 <script type="text/x-template" id="tmpl-dropdown">
     <div class="dropdown" :id="id" :class="{hidden: !visible}">
-        <div class="row item" :class="{disabled: item.disabled}" v-for="item in items" @click="clicked(item)">
+        <div class="row item"
+             :class="{disabled: item.disabled, ...classes.reduce((classes, c) => {classes[c] = true; return classes}, {})}"
+             v-for="item in items" @click="clicked(item)">
             <div class="col-1 icon">
                 <i class="fa" :class="['fa-' + (item.icon || '')]" v-if="item.icon"></i>
                 <i :class="item.iconClass" v-else-if="item.iconClass"></i>
diff --git a/platypush/backend/http/templates/nav.html b/platypush/backend/http/templates/nav.html
index 72a80ff1a..bd342acac 100644
--- a/platypush/backend/http/templates/nav.html
+++ b/platypush/backend/http/templates/nav.html
@@ -19,6 +19,7 @@
         'switches': 'fa fa-toggle-on',
         'tts': 'fa fa-comment',
         'tts.google': 'fa fa-comment',
+        'zigbee.mqtt': 'fa fa-zigbee',
         'zwave': 'fa fa-zwave',
     }
 %}
diff --git a/platypush/backend/http/templates/plugins/zigbee.mqtt/device.html b/platypush/backend/http/templates/plugins/zigbee.mqtt/device.html
new file mode 100644
index 000000000..a481399a4
--- /dev/null
+++ b/platypush/backend/http/templates/plugins/zigbee.mqtt/device.html
@@ -0,0 +1,163 @@
+<script type="text/x-template" id="tmpl-zigbee-device">
+    <div class="item device" :class="{selected: selected}">
+        <div class="row name vertical-center" :class="{selected: selected}"
+             v-text="device.friendly_name" @click="onDeviceClicked"></div>
+
+        <div class="params" v-if="selected">
+            <div class="row">
+                <div class="param-name">Name</div>
+                <div class="param-value">
+                    <div :class="{hidden: !editMode.name}">
+                        <form ref="nameForm" @submit.prevent="editName">
+                            <input type="text" name="name" :value="device.friendly_name">
+
+                            <span class="buttons">
+                                <button type="button" class="btn btn-default" @click="editMode.name = false">
+                                    <i class="fas fa-times"></i>
+                                </button>
+
+                                <button type="submit" class="btn btn-default">
+                                    <i class="fa fa-check"></i>
+                                </button>
+                            </span>
+                        </form>
+                    </div>
+
+                    <div :class="{hidden: editMode.name}">
+                        <span v-text="device.friendly_name"></span>
+                        <span class="buttons">
+                            <button type="button" class="btn btn-default" @click="onEditMode('name')">
+                                <i class="fa fa-edit"></i>
+                            </button>
+                        </span>
+                    </div>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">IEEE Address</div>
+                <div class="param-value" v-text="device.ieeeAddr"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Network Address</div>
+                <div class="param-value" v-text="'0x' + parseInt(device.networkAddress).toString(16)"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Type</div>
+                <div class="param-value" v-text="device.type"></div>
+            </div>
+
+            <div class="row" v-if="device.manufacturerID">
+                <div class="param-name">Manufacturer</div>
+                <div class="param-value">
+                    {% raw %}{{ device.manufacturerName }} ({{ device.manufacturerID }}){% endraw %}
+                </div>
+            </div>
+
+            <div class="row" v-if="device.modelID">
+                <div class="param-name">Model</div>
+                <div class="param-value">
+                    {% raw %}{{ device.model }} ({{ device.modelID }}){% endraw %}
+                </div>
+            </div>
+
+            <div class="row" v-if="device.softwareBuildID && device.softwareBuildID">
+                <div class="param-name">Software Build ID</div>
+                <div class="param-value" v-text="device.softwareBuildID"></div>
+            </div>
+
+            <div class="row" v-if="device.dateCode && device.dateCode.length">
+                <div class="param-name">Date Code</div>
+                <div class="param-value" v-text="device.dateCode"></div>
+            </div>
+
+            <div class="row" v-if="device.powerSource">
+                <div class="param-name">Power Source</div>
+                <div class="param-value" v-text="device.powerSource"></div>
+            </div>
+
+            <div class="row" v-if="device.lastSeen">
+                <div class="param-name">Last Seen</div>
+                <div class="param-value" v-text="(new Date(device.lastSeen)).toLocaleString()"></div>
+            </div>
+
+            <div class="section values" v-if="device.values && Object.keys(device.values).length">
+                <div class="header">
+                    <div class="title">Values</div>
+                </div>
+
+                <div class="body">
+                    <div class="row"
+                         v-for="value, name in device.values"
+                         :key="name">
+                        <div class="param-name" v-text="name"></div>
+                        <div class="param-value">
+                            <toggle-switch :value="value === 'ON' ? true : false"
+                                           :data-name="name"
+                                           @toggled="setValue"
+                                           v-if="name === 'state'">
+                            </toggle-switch>
+
+                            <span v-text="value.toString() + '%'" v-else-if="name === 'linkquality'"></span>
+
+                            <div v-else>
+                                <input type="text" :value="value" :data-name="name" @change="setValue">
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="row">
+                        <div class="param-name">
+                            <input type="text" placeholder="New property name" v-model="newPropertyName">
+                        </div>
+
+                        <div class="param-value">
+                            <input type="text" placeholder="New property value" @change="setValue">
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="section actions">
+                <div class="header">
+                    <div class="title">Actions</div>
+                </div>
+
+                <div class="body">
+                    <div class="row" @click="removeDevice(false)">
+                        <div class="param-name">Remove Device</div>
+                        <div class="param-value">
+                            <i class="fa fa-trash"></i>
+                        </div>
+                    </div>
+
+                    <div class="row error" @click="removeDevice(true)">
+                        <div class="param-name">Force Remove Device</div>
+                        <div class="param-value">
+                            <i class="fa fa-trash"></i>
+                        </div>
+                    </div>
+
+                    <div class="row" @click="banDevice">
+                        <div class="param-name">Ban Device</div>
+                        <div class="param-value">
+                            <i class="fa fa-ban"></i>
+                        </div>
+                    </div>
+
+                    <div class="row" @click="whitelistDevice">
+                        <div class="param-name">Whitelist Device</div>
+                        <div class="param-value">
+                            <i class="fa fa-list"></i>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</script>
+
+<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/zigbee.mqtt/device.js') }}"></script>
+
diff --git a/platypush/backend/http/templates/plugins/zigbee.mqtt/group.html b/platypush/backend/http/templates/plugins/zigbee.mqtt/group.html
new file mode 100644
index 000000000..9ba2e44eb
--- /dev/null
+++ b/platypush/backend/http/templates/plugins/zigbee.mqtt/group.html
@@ -0,0 +1,75 @@
+<script type="text/x-template" id="tmpl-zigbee-group">
+    <div class="item group" :class="{selected: selected}">
+        <div class="row name vertical-center" :class="{selected: selected}"
+             v-text="group.friendly_name" @click="onGroupClicked"></div>
+
+        <div class="params" v-if="selected">
+            <div class="section values">
+                <div class="header">
+                    <div class="title">Values</div>
+                </div>
+
+                <div class="body">
+                    <div class="row" v-for="value, name in properties" :key="name">
+                        <div class="param-name" v-text="name"></div>
+                        <div class="param-value">
+                            <div v-if="name === 'state'">
+                                <toggle-switch :value="value" @toggled="toggleState"></toggle-switch>
+                            </div>
+                            <div v-else>
+                                <input type="text" :value="value" :data-name="name" @change="setValue">
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="section devices">
+                <div class="header">
+                    <div class="title col-10">Devices</div>
+                    <div class="buttons col-2">
+                        <button class="btn btn-default" title="Add Devices" @click="bus.$emit('openAddToGroupModal')">
+                            <i class="fa fa-plus"></i>
+                        </button>
+                    </div>
+                </div>
+
+                <div class="body">
+                    <div class="row" v-for="device in group.devices">
+                        <div class="col-10" v-text="device.friendly_name"></div>
+                        <div class="buttons col-2">
+                            <button class="btn btn-default" title="Remove from group" @click="removeFromGroup(device.friendly_name)">
+                                <i class="fa fa-trash"></i>
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="section actions">
+                <div class="header">
+                    <div class="title">Actions</div>
+                </div>
+
+                <div class="body">
+                    <div class="row" @click="renameGroup">
+                        <div class="col-10">Rename Group</div>
+                        <div class="buttons col-2">
+                            <i class="fa fa-edit"></i>
+                        </div>
+                    </div>
+
+                    <div class="row" @click="removeGroup">
+                        <div class="col-10">Remove Group</div>
+                        <div class="buttons col-2">
+                            <i class="fa fa-trash"></i>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</script>
+
+<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/zigbee.mqtt/group.js') }}"></script>
+
diff --git a/platypush/backend/http/templates/plugins/zigbee.mqtt/index.html b/platypush/backend/http/templates/plugins/zigbee.mqtt/index.html
new file mode 100644
index 000000000..e059679f2
--- /dev/null
+++ b/platypush/backend/http/templates/plugins/zigbee.mqtt/index.html
@@ -0,0 +1,69 @@
+{% include 'plugins/zigbee.mqtt/device.html' %}
+{% include 'plugins/zigbee.mqtt/group.html' %}
+
+<script type="text/x-template" id="tmpl-zigbee-mqtt">
+    <div class="zigbee-container">
+        {% include 'plugins/zigbee.mqtt/modals/group.html' %}
+
+        <div class="view-options">
+            <div class="view-selector col-s-9 col-m-10 col-l-11">
+                <select @change="onViewChange">
+                    <option v-for="_, view in views"
+                            v-text="(view[0].toUpperCase() + view.slice(1)).replace('_', ' ')"
+                            :key="view" :selected="view == selected.view" :value="view">
+                    </option>
+                </select>
+            </div>
+
+            <div class="buttons">
+                <button class="btn btn-default" title="Add Group" v-if="selected.view === 'groups'"
+                        :disabled="commandRunning" @click="addGroup">
+                    <i class="fa fa-plus"></i>
+                </button>
+
+                <button class="btn btn-default" title="Network commands" @click="openNetworkCommandsDropdown">
+                    <i class="fa fa-cog"></i>
+                </button>
+
+                <button class="btn btn-default" title="Refresh network" @click="refresh">
+                    <i class="fa fa-sync-alt"></i>
+                </button>
+            </div>
+
+            <dropdown ref="networkCommandsDropdown" :items="networkDropdownItems"></dropdown>
+        </div>
+
+        <div class="view devices" v-if="selected.view == 'devices'">
+            <div class="no-items" v-if="Object.keys(devices).length == 0">
+                <div class="loading" v-if="loading.nodes">Loading devices...</div>
+                <div class="empty" v-else>No devices found on the network</div>
+            </div>
+
+            <zigbee-device
+                    v-for="device, deviceId in devices"
+                    :key="deviceId"
+                    :device="device"
+                    :bus="bus"
+                    :selected="selected.deviceId == deviceId">
+            </zigbee-device>
+
+            <dropdown ref="addToGroupDropdown" :items="addToGroupDropdownItems"></dropdown>
+        </div>
+
+        <div class="view groups" v-else-if="selected.view == 'groups'">
+            <div class="no-items" v-if="Object.keys(groups).length == 0">
+                <div class="loading" v-if="loading.groups">Loading groups...</div>
+                <div class="empty" v-else>No groups available on the network</div>
+            </div>
+
+            <zigbee-group
+                    v-for="group, groupId in groups"
+                    :key="groupId"
+                    :group="group"
+                    :selected="selected.groupId == groupId"
+                    :bus="bus">
+            </zigbee-group>
+        </div>
+    </div>
+</script>
+
diff --git a/platypush/backend/http/templates/plugins/zigbee.mqtt/modals/group.html b/platypush/backend/http/templates/plugins/zigbee.mqtt/modals/group.html
new file mode 100644
index 000000000..0f7f39e5d
--- /dev/null
+++ b/platypush/backend/http/templates/plugins/zigbee.mqtt/modals/group.html
@@ -0,0 +1,20 @@
+<modal id="zigbee-add-to-group" title="Add devices to group" v-model="modal.group.visible" v-if="modal.group.visible">
+    <div class="group-add">
+        <div class="params">
+            <div class="section">
+                <div class="header">
+                    <div class="title">Select devices to add</div>
+                </div>
+
+                <div class="body">
+                    <div class="row clickable" @click="addToGroup(device.friendly_name, groups[selected.groupId].friendly_name)"
+                         :key="device.friendly_name"
+                         v-for="device in Object.values(devices).filter((dev) => Object.values(groups[selected.groupId].devices).map((d) => d.friendly_name).indexOf(dev.friendly_name) < 0)">
+                        <div class="param-name" v-text="device.friendly_name"></div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</modal>
+
diff --git a/platypush/backend/http/templates/plugins/zwave/node.html b/platypush/backend/http/templates/plugins/zwave/node.html
index 666c71eaf..27cce6d9d 100644
--- a/platypush/backend/http/templates/plugins/zwave/node.html
+++ b/platypush/backend/http/templates/plugins/zwave/node.html
@@ -57,7 +57,7 @@
             <div class="row" v-if="node.neighbours.length">
                 <div class="param-name">Neighbours</div>
                 <div class="param-value">
-                    <div class="row" v-for="neighbour in node.neighbours" v-text="neighbour"></div>
+                    <div class="row pull-right" v-for="neighbour in node.neighbours" v-text="neighbour"></div>
                 </div>
             </div>
 
diff --git a/platypush/message/event/zigbee/mqtt.py b/platypush/message/event/zigbee/mqtt.py
index 8a473ab9a..e3179407f 100644
--- a/platypush/message/event/zigbee/mqtt.py
+++ b/platypush/message/event/zigbee/mqtt.py
@@ -1,4 +1,4 @@
-from typing import Optional, Dict, Any
+from typing import Dict, Any
 
 from platypush.message.event import Event
 
@@ -108,7 +108,7 @@ class ZigbeeMqttGroupAddedEvent(ZigbeeMqttEvent):
     Triggered when a group is added.
     """
     def __init__(self, host: str, port: int, group=None, *args, **kwargs):
-        super().__init__(*args, host=host, port=port, device=device, **kwargs)
+        super().__init__(*args, host=host, port=port, group=group, **kwargs)
 
 
 class ZigbeeMqttGroupAddedFailedEvent(ZigbeeMqttEvent):
@@ -116,7 +116,7 @@ class ZigbeeMqttGroupAddedFailedEvent(ZigbeeMqttEvent):
     Triggered when a request to add a group fails.
     """
     def __init__(self, host: str, port: int, group=None, *args, **kwargs):
-        super().__init__(*args, host=host, port=port, device=device, **kwargs)
+        super().__init__(*args, host=host, port=port, group=group, **kwargs)
 
 
 class ZigbeeMqttGroupRemovedEvent(ZigbeeMqttEvent):
@@ -124,7 +124,7 @@ class ZigbeeMqttGroupRemovedEvent(ZigbeeMqttEvent):
     Triggered when a group is removed.
     """
     def __init__(self, host: str, port: int, group=None, *args, **kwargs):
-        super().__init__(*args, host=host, port=port, device=device, **kwargs)
+        super().__init__(*args, host=host, port=port, group=group, **kwargs)
 
 
 class ZigbeeMqttGroupRemovedFailedEvent(ZigbeeMqttEvent):
@@ -132,7 +132,7 @@ class ZigbeeMqttGroupRemovedFailedEvent(ZigbeeMqttEvent):
     Triggered when a request to remove a group fails.
     """
     def __init__(self, host: str, port: int, group=None, *args, **kwargs):
-        super().__init__(*args, host=host, port=port, device=device, **kwargs)
+        super().__init__(*args, host=host, port=port, group=group, **kwargs)
 
 
 class ZigbeeMqttGroupRemoveAllEvent(ZigbeeMqttEvent):
@@ -140,7 +140,7 @@ class ZigbeeMqttGroupRemoveAllEvent(ZigbeeMqttEvent):
     Triggered when all the devices are removed from a group.
     """
     def __init__(self, host: str, port: int, group=None, *args, **kwargs):
-        super().__init__(*args, host=host, port=port, device=device, **kwargs)
+        super().__init__(*args, host=host, port=port, group=group, **kwargs)
 
 
 class ZigbeeMqttGroupRemoveAllFailedEvent(ZigbeeMqttEvent):
@@ -148,7 +148,7 @@ class ZigbeeMqttGroupRemoveAllFailedEvent(ZigbeeMqttEvent):
     Triggered when a request to remove all the devices from a group fails.
     """
     def __init__(self, host: str, port: int, group=None, *args, **kwargs):
-        super().__init__(*args, host=host, port=port, device=device, **kwargs)
+        super().__init__(*args, host=host, port=port, group=group, **kwargs)
 
 
 class ZigbeeMqttErrorEvent(ZigbeeMqttEvent):
diff --git a/platypush/message/response/__init__.py b/platypush/message/response/__init__.py
index 67629e0d8..7488dc4b7 100644
--- a/platypush/message/response/__init__.py
+++ b/platypush/message/response/__init__.py
@@ -66,7 +66,7 @@ class Response(Message):
         Overrides the str() operator and converts
         the message into a UTF-8 JSON string
         """
-        output = self.output if self.output is not None and self.output != {} else {
+        output = self.output if self.output is not None else {
             'success': True if not self.errors else False
         }
 
diff --git a/platypush/plugins/zigbee/mqtt.py b/platypush/plugins/zigbee/mqtt.py
index 180a413a1..6df4ebf87 100644
--- a/platypush/plugins/zigbee/mqtt.py
+++ b/platypush/plugins/zigbee/mqtt.py
@@ -92,12 +92,12 @@ class ZigbeeMqttPlugin(MqttPlugin):
 
     """
 
-    def __init__(self, host: str, port: int = 1883, base_topic: str = 'zigbee2mqtt', timeout: int = 60,
+    def __init__(self, host: str = 'localhost', port: int = 1883, base_topic: str = 'zigbee2mqtt', timeout: int = 60,
                  tls_certfile: Optional[str] = None, tls_keyfile: Optional[str] = None,
                  tls_version: Optional[str] = None, tls_ciphers: Optional[str] = None,
                  username: Optional[str] = None, password: Optional[str] = None, **kwargs):
         """
-        :param host: Default MQTT broker where ``zigbee2mqtt`` publishes its messages.
+        :param host: Default MQTT broker where ``zigbee2mqtt`` publishes its messages (default: ``localhost``).
         :param port: Broker listen port (default: 1883).
         :param base_topic: Topic prefix, as specified in ``/opt/zigbee2mqtt/data/configuration.yaml``
             (default: '``base_topic``').
@@ -305,6 +305,15 @@ class ZigbeeMqttPlugin(MqttPlugin):
         :param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
             (default: query the default configured device).
         """
+        if name == device:
+            self.logger.info('Old and new name are the same: nothing to do')
+            return
+
+        # noinspection PyUnresolvedReferences
+        devices = self.devices().output
+        assert not [dev for dev in devices if dev.get('friendly_name') == name], \
+            'A device named {} already exists on the network'.format(name)
+
         self.publish(
             topic=self._topic('bridge/config/rename{}'.format('_last' if not device else '')),
             msg={'old': device, 'new': name} if device else name,
@@ -333,7 +342,7 @@ class ZigbeeMqttPlugin(MqttPlugin):
 
         return properties
 
-    # noinspection PyShadowingBuiltins
+    # noinspection PyShadowingBuiltins,DuplicatedCode
     @action
     def device_set(self, device: str, property: str, value: Any, **kwargs):
         """
@@ -372,6 +381,37 @@ class ZigbeeMqttPlugin(MqttPlugin):
                             reply_topic=self._topic(device), msg=device, **self._mqtt_args(**kwargs)).\
             output.get('group_list', [])
 
+    @action
+    def groups(self, **kwargs):
+        """
+        Get the groups registered on the device.
+
+        :param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
+            (default: query the default configured device).
+        """
+        groups = self.publish(topic=self._topic('bridge/config/groups'), msg={},
+                              reply_topic=self._topic('bridge/log'),
+                              **self._mqtt_args(**kwargs)).output.get('message', [])
+
+        # noinspection PyUnresolvedReferences
+        devices = {
+            device['ieeeAddr']: device
+            for device in self.devices(**kwargs).output
+        }
+
+        return [
+            {
+                'id': group['ID'],
+                'friendly_name': group['friendly_name'],
+                'optimistic': group.get('optimistic', False),
+                'devices': [
+                    devices[device.split('/')[0]]
+                    for device in group.get('devices', [])
+                ]
+            }
+            for group in groups
+        ]
+
     @action
     def group_add(self, name: str, id: Optional[int] = None, **kwargs):
         """
@@ -388,6 +428,53 @@ class ZigbeeMqttPlugin(MqttPlugin):
 
         self.publish(topic=self._topic('bridge/config/add_group'), msg=args, **self._mqtt_args(**kwargs))
 
+    # noinspection PyShadowingBuiltins,DuplicatedCode
+    @action
+    def group_set(self, group: str, property: str, value: Any, **kwargs):
+        """
+        Set a properties on a group. The compatible properties vary depending on the devices on the group.
+        For example, a light bulb may have the "``state``" (with values ``"ON"`` and ``"OFF"``) and "``brightness``"
+        properties, while an environment sensor may have the "``temperature``" and "``humidity``" properties, and so on.
+
+        :param group: Display name of the group.
+        :param property: Name of the property that should be set.
+        :param value: New value of the property.
+        :param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
+            (default: query the default configured device).
+        """
+        properties = self.publish(topic=self._topic(group + '/set'),
+                                  reply_topic=self._topic(group),
+                                  msg={property: value}, **self._mqtt_args(**kwargs)).output
+
+        if property:
+            assert property in properties, 'No such property: ' + property
+            return {property: properties[property]}
+
+        return properties
+
+    @action
+    def group_rename(self, name: str, group: str, **kwargs):
+        """
+        Rename a group.
+
+        :param name: New name.
+        :param group: Current name of the group to rename.
+        :param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
+            (default: query the default configured device).
+        """
+        if name == group:
+            self.logger.info('Old and new name are the same: nothing to do')
+            return
+
+        # noinspection PyUnresolvedReferences
+        groups = {group.get('friendly_name'): group for group in self.groups().output}
+        assert name not in groups, 'A group named {} already exists on the network'.format(name)
+
+        self.publish(
+            topic=self._topic('bridge/config/rename'),
+            msg={'old': group, 'new': name} if group else name,
+            **self._mqtt_args(**kwargs))
+
     @action
     def group_remove(self, name: str, **kwargs):
         """