diff --git a/platypush/backend/http/static/css/source/common/elements.scss b/platypush/backend/http/static/css/source/common/elements.scss
index f07a9d04..d78c2be4 100644
--- a/platypush/backend/http/static/css/source/common/elements.scss
+++ b/platypush/backend/http/static/css/source/common/elements.scss
@@ -11,6 +11,10 @@
     text-align: right !important;
 }
 
+.clickable {
+    cursor: pointer;
+}
+
 a:focus {
     outline: none;
 }
diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/zwave/index.scss b/platypush/backend/http/static/css/source/webpanel/plugins/zwave/index.scss
new file mode 100644
index 00000000..872ab07c
--- /dev/null
+++ b/platypush/backend/http/static/css/source/webpanel/plugins/zwave/index.scss
@@ -0,0 +1,195 @@
+@import 'common/vars';
+@import 'webpanel/plugins/zwave/vars';
+
+.fa.fa-zwave:before {
+    content: ' ';
+    background: url('/static/img/icons/z-wave-logo.png');
+    background-size: 1em 1em;
+    width: 1em;
+    height: 1em;
+    display: inline-block;
+}
+
+.zwave-container {
+    height: 100%;
+    padding: 0 .5em;
+    background: $container-bg;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+
+    .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;
+        }
+    }
+
+    .modal {
+        .section {
+            .header {
+                background: none;
+                padding: .5em 0;
+            }
+
+            .body {
+                padding: 0;
+            }
+        }
+
+        .network-info {
+            min-width: 600pt;
+        }
+    }
+
+    .error {
+        color: $error-color;
+    }
+
+    .node {
+        .actions {
+            .row {
+                cursor: pointer;
+            }
+        }
+
+        form {
+            margin-bottom: 0;
+        }
+    }
+}
+
diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/zwave/vars.scss b/platypush/backend/http/static/css/source/webpanel/plugins/zwave/vars.scss
new file mode 100644
index 00000000..798334a2
--- /dev/null
+++ b/platypush/backend/http/static/css/source/webpanel/plugins/zwave/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/z-wave-logo.png b/platypush/backend/http/static/img/icons/z-wave-logo.png
new file mode 100644
index 00000000..df9c9550
Binary files /dev/null and b/platypush/backend/http/static/img/icons/z-wave-logo.png differ
diff --git a/platypush/backend/http/static/js/plugins/light.hue/index.js b/platypush/backend/http/static/js/plugins/light.hue/index.js
index 52fc3e6c..d903a07f 100644
--- a/platypush/backend/http/static/js/plugins/light.hue/index.js
+++ b/platypush/backend/http/static/js/plugins/light.hue/index.js
@@ -132,7 +132,7 @@ Vue.component('light-hue', {
             );
 
             this.selectedScene = event.id;
-            groups = {}
+            const groups = {};
 
             for (const lightId of Object.values(this.scenes[this.selectedScene].lights)) {
                 this.lights[lightId].state.on = true;
@@ -148,7 +148,7 @@ Vue.component('light-hue', {
 
                     groups[groupId].lights.push(lightId);
 
-                    if (groups[groupId].lights.length == Object.values(group.lights).length) {
+                    if (groups[groupId].lights.length === Object.values(group.lights).length) {
                         groups[groupId].all_on = true;
                     }
                 }
@@ -164,8 +164,8 @@ Vue.component('light-hue', {
         },
 
         collapsedToggled: function(event) {
-            if (event.type == this.selectedProperties.type
-                    && event.id == this.selectedProperties.id) {
+            if (event.type === this.selectedProperties.type
+                    && event.id === this.selectedProperties.id) {
                 this.selectedProperties = {
                     type: undefined,
                     id: undefined,
diff --git a/platypush/backend/http/static/js/plugins/zwave/group.js b/platypush/backend/http/static/js/plugins/zwave/group.js
new file mode 100644
index 00000000..0410c1af
--- /dev/null
+++ b/platypush/backend/http/static/js/plugins/zwave/group.js
@@ -0,0 +1,24 @@
+Vue.component('zwave-group', {
+    template: '#tmpl-zwave-group',
+    props: ['group','nodes','bus','selected'],
+
+    methods: {
+        onGroupClicked: function() {
+            this.bus.$emit('groupClicked', {
+                groupId: this.group.index,
+            });
+        },
+
+        removeFromGroup: async function(nodeId) {
+            if (!confirm('Are you sure that you want to remove this node from ' + this.group.label + '?')) {
+                return;
+            }
+
+            await request('zwave.remove_node_from_group', {
+                node_id: nodeId,
+                group_index: this.group.index,
+            });
+        },
+    },
+});
+
diff --git a/platypush/backend/http/static/js/plugins/zwave/index.js b/platypush/backend/http/static/js/plugins/zwave/index.js
new file mode 100644
index 00000000..48aed6dc
--- /dev/null
+++ b/platypush/backend/http/static/js/plugins/zwave/index.js
@@ -0,0 +1,457 @@
+Vue.component('zwave', {
+    template: '#tmpl-zwave',
+    props: ['config'],
+
+    data: function() {
+        return {
+            bus: new Vue({}),
+            status: {},
+            views: {},
+            nodes: {},
+            groups: {},
+            scenes: {},
+            values: {},
+            switches: new Set(),
+            dimmers: new Set(),
+            sensors: new Set(),
+            batteryLevels: new Set(),
+            powerLevels: new Set(),
+            bulbs: new Set(),
+            doorlocks: new Set(),
+            usercodes: new Set(),
+            thermostats: new Set(),
+            protections: new Set(),
+            commandRunning: false,
+            selected: {
+                view: 'nodes',
+                nodeId: undefined,
+                groupId: undefined,
+                sceneId: undefined,
+                valueId: undefined,
+            },
+            loading: {
+                status: false,
+                nodes: false,
+                groups: false,
+                scenes: false,
+                values: false,
+            },
+            modal: {
+                networkInfo: {
+                    visible: false,
+                },
+                group: {
+                    visible: false,
+                },
+            },
+        };
+    },
+
+    computed: {
+        networkDropdownItems: function() {
+            const self = this;
+            return [
+                {
+                    text: 'Start Network',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        self.commandRunning = true;
+                        await request('zwave.start_network');
+                        self.commandRunning = false;
+                    },
+                },
+
+                {
+                    text: 'Stop Network',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        self.commandRunning = true;
+                        await request('zwave.start_network');
+                        self.commandRunning = false;
+                    },
+                },
+
+                {
+                    text: 'Switch All On',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        self.commandRunning = true;
+                        await request('zwave.switch_all', {state: true});
+                        self.commandRunning = false;
+                        self.refresh();
+                    },
+                },
+
+                {
+                    text: 'Switch All Off',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        self.commandRunning = true;
+                        await request('zwave.switch_all', {state: false});
+                        self.commandRunning = false;
+                        self.refresh();
+                    },
+                },
+
+                {
+                    text: 'Cancel Command',
+                    click: async function() {
+                        await request('zwave.cancel_command');
+                    },
+                },
+
+                {
+                    text: 'Kill Command',
+                    click: async function() {
+                        await request('zwave.kill_command');
+                    },
+                },
+
+                {
+                    text: 'Set Controller Name',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        const name = prompt('Controller name');
+                        if (!name) {
+                            return;
+                        }
+
+                        self.commandRunning = true;
+                        await request('zwave.set_controller_name', {name: name});
+                        self.commandRunning = false;
+                        self.refresh();
+                    },
+                },
+
+
+                {
+                    text: 'Receive Configuration From Primary',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        self.commandRunning = true;
+                        await request('zwave.receive_configuration');
+                        self.commandRunning = false;
+                        self.refresh();
+                    },
+                },
+
+                {
+                    text: 'Create New Primary',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        self.commandRunning = true;
+                        await request('zwave.create_new_primary');
+                        self.commandRunning = false;
+                        self.refresh();
+                    },
+                },
+
+                {
+                    text: 'Transfer Primary Role',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        self.commandRunning = true;
+                        await request('zwave.transfer_primary_role');
+                        self.commandRunning = false;
+                        self.refresh();
+                    },
+                },
+
+                {
+                    text: 'Heal Network',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        self.commandRunning = true;
+                        await request('zwave.heal');
+                        self.commandRunning = false;
+                        self.refresh();
+                    },
+                },
+
+                {
+                    text: 'Soft Reset',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        if (!confirm('Are you sure that you want to do a device soft reset? Network information will not be lost')) {
+                            return;
+                        }
+
+                        await request('zwave.soft_reset');
+                    },
+                },
+
+                {
+                    text: 'Hard Reset',
+                    disabled: this.commandRunning,
+                    click: async function() {
+                        if (!confirm('Are you sure that you want to do a device soft reset? ALL network information will be lost!')) {
+                            return;
+                        }
+
+                        await request('zwave.hard_reset');
+                    },
+                },
+            ]
+        },
+    },
+
+    methods: {
+        refreshNodes: async function () {
+            this.loading.nodes = true;
+            this.loading.values = true;
+
+            this.nodes = await request('zwave.get_nodes');
+            this.loading.nodes = false;
+
+            this.values = Object.values(this.nodes).reduce((values, node) => {
+                values = {
+                    ...Object.values(node.values).reduce((values, value) => {
+                        values[value.value_id] = {
+                            node_id: node.node_id,
+                            ...value,
+                        };
+
+                        return values;
+                    }, {}),
+                    ...values
+                };
+
+                return values;
+            }, {});
+
+            this.loading.values = false;
+        },
+
+        refreshGroups: async function () {
+            this.loading.groups = true;
+            this.groups = Object.values(await request('zwave.get_groups'))
+                .filter((group) => group.index)
+                .reduce((groups, group) => {
+                    groups[group.index] = group;
+                    return groups;
+                }, {});
+
+            if (Object.keys(this.groups).length) {
+                Vue.set(this.views, 'groups', true);
+            }
+
+            this.loading.groups = false;
+        },
+
+        refreshScenes: async function () {
+            this.loading.scenes = true;
+            this.scenes = Object.values(await request('zwave.get_scenes'))
+                .filter((scene) => scene.scene_id)
+                .reduce((scenes, scene) => {
+                    scenes[scene.scene_id] = scene;
+                    return scenes;
+                }, {});
+
+            this.loading.scenes = false;
+        },
+
+        refreshSwitches: async function () {
+            this.switches = new Set(Object.values(await request('zwave.get_switches'))
+                .filter((sw) => sw.id_on_network).map((sw) => sw.value_id));
+
+            if (this.switches.size) {
+                Vue.set(this.views, 'switches', true);
+            }
+        },
+
+        refreshDimmers: async function () {
+            this.dimmers = new Set(Object.values(await request('zwave.get_dimmers'))
+                .filter((dimmer) => dimmer.id_on_network).map((dimmer) => dimmer.value_id));
+
+            if (this.dimmers.size) {
+                Vue.set(this.views, 'dimmers', true);
+            }
+        },
+
+        refreshSensors: async function () {
+            this.sensors = new Set(Object.values(await request('zwave.get_sensors'))
+                .filter((sensor) => sensor.id_on_network).map((sensor) => sensor.value_id));
+
+            if (this.sensors.size) {
+                Vue.set(this.views, 'sensors', true);
+            }
+        },
+
+        refreshBatteryLevels: async function () {
+            this.batteryLevels = new Set(Object.values(await request('zwave.get_battery_levels'))
+                .filter((battery) => battery.id_on_network).map((battery) => battery.value_id));
+
+            if (this.batteryLevels.size) {
+                Vue.set(this.views, 'batteryLevels', true);
+            }
+        },
+
+        refreshPowerLevels: async function () {
+            this.powerLevels = new Set(Object.values(await request('zwave.get_power_levels'))
+                .filter((power) => power.id_on_network).map((power) => power.value_id));
+
+            if (this.powerLevels.size) {
+                Vue.set(this.views, 'powerLevels', true);
+            }
+        },
+
+        refreshBulbs: async function () {
+            this.bulbs = new Set(Object.values(await request('zwave.get_bulbs'))
+                .filter((bulb) => bulb.id_on_network).map((bulb) => bulb.value_id));
+
+            if (this.bulbs.size) {
+                Vue.set(this.views, 'bulbs', true);
+            }
+        },
+
+        refreshDoorlocks: async function () {
+            this.doorlocks = new Set(Object.values(await request('zwave.get_doorlocks'))
+                .filter((lock) => lock.id_on_network).map((lock) => lock.value_id));
+
+            if (this.doorlocks.size) {
+                Vue.set(this.views, 'doorlocks', true);
+            }
+        },
+
+        refreshUsercodes: async function () {
+            this.doorlocks = new Set(Object.values(await request('zwave.get_usercodes'))
+                .filter((code) => code.id_on_network).map((code) => code.value_id));
+
+            if (this.usercodes.size) {
+                Vue.set(this.views, 'usercodes', true);
+            }
+        },
+
+        refreshThermostats: async function () {
+            this.thermostats = new Set(Object.values(await request('zwave.get_thermostats'))
+                .filter((th) => th.id_on_network).map((th) => th.value_id));
+
+            if (this.thermostats.size) {
+                Vue.set(this.views, 'thermostats', true);
+            }
+        },
+
+        refreshProtections: async function () {
+            this.protections = new Set(Object.values(await request('zwave.get_protections'))
+                .filter((p) => p.id_on_network).map((p) => p.value_id));
+
+            if (this.protections.size) {
+                Vue.set(this.views, 'protections', true);
+            }
+        },
+
+        refreshStatus: async function() {
+            this.loading.status = true;
+            this.status = await request('zwave.status');
+            this.loading.status = false;
+        },
+
+        refresh: function () {
+            this.views = {
+                nodes: true,
+                scenes: true,
+            };
+
+            this.refreshNodes();
+            this.refreshGroups();
+            this.refreshScenes();
+            this.refreshSwitches();
+            this.refreshDimmers();
+            this.refreshSensors();
+            this.refreshBulbs();
+            this.refreshDoorlocks();
+            this.refreshUsercodes();
+            this.refreshThermostats();
+            this.refreshProtections();
+            this.refreshBatteryLevels();
+            this.refreshPowerLevels();
+            this.refreshStatus();
+        },
+
+        onNodeUpdate: function(event) {
+            Vue.set(this.nodes, event.node.node_id, event.node);
+        },
+
+        onViewChange: function(event) {
+            Vue.set(this.selected, 'view', event.target.value);
+        },
+
+        onNodeClicked: function(event) {
+            Vue.set(this.selected, 'nodeId', event.nodeId === this.selected.nodeId ? undefined : event.nodeId);
+        },
+
+        onGroupClicked: function(event) {
+            Vue.set(this.selected, 'groupId', event.groupId === this.selected.groupId ? undefined : event.groupId);
+        },
+
+        onNetworkInfoModalOpen: function() {
+            this.refreshStatus();
+            this.modal.networkInfo.visible = true;
+        },
+
+        onCommandEvent: function(event) {
+            if (event.error && event.error.length) {
+                createNotification({
+                    text: event.state_description + ': ' + event.error_description,
+                    error: true,
+                });
+            }
+        },
+
+        openNetworkCommandsDropdown: function() {
+            openDropdown(this.$refs.networkCommandsDropdown);
+        },
+
+        addNode: async function() {
+            this.commandRunning = true;
+            await request('zwave.add_node');
+            this.commandRunning = false;
+        },
+
+        addToGroup: async function(nodeId, groupId) {
+            this.commandRunning = true;
+            await request('zwave.add_node_to_group', {
+                node_id: nodeId,
+                group_index: groupId,
+            });
+
+            this.commandRunning = false;
+            this.refreshGroups();
+        },
+
+        removeNode: async function() {
+            this.commandRunning = true;
+            await request('zwave.remove_node');
+            this.commandRunning = false;
+        },
+    },
+
+    created: function() {
+        const self = this;
+        this.bus.$on('nodeClicked', this.onNodeClicked);
+        this.bus.$on('groupClicked', this.onGroupClicked);
+        this.bus.$on('openAddToGroupModal', () => {self.modal.group.visible = true});
+
+        registerEventHandler(this.refreshGroups, 'platypush.message.event.zwave.ZwaveNodeGroupEvent');
+        registerEventHandler(this.refreshScenes, 'platypush.message.event.zwave.ZwaveNodeSceneEvent');
+        registerEventHandler(this.refreshNodes, 'platypush.message.event.zwave.ZwaveNodeRemovedEvent');
+        registerEventHandler(this.onCommandEvent, 'platypush.message.event.zwave.ZwaveCommandEvent');
+
+        registerEventHandler(this.refreshStatus,
+            'platypush.message.event.zwave.ZwaveNetworkReadyEvent',
+            'platypush.message.event.zwave.ZwaveNetworkStoppedEvent',
+            'platypush.message.event.zwave.ZwaveNetworkErrorEvent',
+            'platypush.message.event.zwave.ZwaveNetworkResetEvent');
+
+        registerEventHandler(this.onNodeUpdate,
+            'platypush.message.event.zwave.ZwaveNodeEvent',
+            'platypush.message.event.zwave.ZwaveNodeAddedEvent',
+            'platypush.message.event.zwave.ZwaveNodeRenamedEvent',
+            'platypush.message.event.zwave.ZwaveNodeReadyEvent');
+    },
+
+    mounted: function() {
+        this.refresh();
+    },
+});
+
diff --git a/platypush/backend/http/static/js/plugins/zwave/node.js b/platypush/backend/http/static/js/plugins/zwave/node.js
new file mode 100644
index 00000000..9dc88a44
--- /dev/null
+++ b/platypush/backend/http/static/js/plugins/zwave/node.js
@@ -0,0 +1,96 @@
+Vue.component('zwave-node', {
+    template: '#tmpl-zwave-node',
+    props: ['node','bus','selected'],
+    data: function() {
+        return {
+            editMode: {
+                name: false,
+            },
+        };
+    },
+
+    methods: {
+        onNodeClicked: function() {
+            this.bus.$emit('nodeClicked', {
+                nodeId: this.node.node_id,
+            });
+        },
+
+        removeFailedNode: async function() {
+            if (!confirm('Are you sure that you want to remove this node?')) {
+                return;
+            }
+
+            await request('zwave.remove_node', {
+                node_id: this.node.node_id,
+            });
+        },
+
+        replaceFailedNode: async function() {
+            if (!confirm('Are you sure that you want to replace this node?')) {
+                return;
+            }
+
+            await request('zwave.replace_node', {
+                node_id: this.node.node_id,
+            });
+        },
+
+        replicationSend: async function() {
+            await request('zwave.replication_send', {
+                node_id: this.node.node_id,
+            });
+        },
+
+        requestNetworkUpdate: async function() {
+            await request('zwave.request_network_update', {
+                node_id: this.node.node_id,
+            });
+        },
+
+        requestNeighbourUpdate: async function() {
+            await request('zwave.request_node_neighbour_update', {
+                node_id: this.node.node_id,
+            });
+        },
+
+        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('zwave.set_node_name', {
+                node_id: this.node.node_id,
+                new_name: name,
+            });
+
+            this.editMode.name = false;
+            this.enableForm(event.target);
+        },
+
+        heal: async function(event) {
+            await request('zwave.node_heal', {
+                node_id: this.node.node_id,
+            });
+        },
+    },
+});
+
diff --git a/platypush/backend/http/templates/nav.html b/platypush/backend/http/templates/nav.html
index 9c105527..72a80ff1 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',
+        'zwave': 'fa fa-zwave',
     }
 %}
 
diff --git a/platypush/backend/http/templates/plugins/zwave/group.html b/platypush/backend/http/templates/plugins/zwave/group.html
new file mode 100644
index 00000000..f2ae849d
--- /dev/null
+++ b/platypush/backend/http/templates/plugins/zwave/group.html
@@ -0,0 +1,54 @@
+<script type="text/x-template" id="tmpl-zwave-group">
+    <div class="item group" :class="{selected: selected}">
+        <div class="row name vertical-center" :class="{selected: selected}"
+             v-text="group.label" @click="onGroupClicked"></div>
+
+        <div class="params" v-if="selected">
+            <div class="section nodes">
+                <div class="header">
+                    <div class="title col-10">Nodes</div>
+                    <div class="buttons col-2">
+                        <button class="btn btn-default" title="Add to group" @click="bus.$emit('openAddToGroupModal')"
+                                v-if="!group.max_associations || Object.keys(nodes).length < group.max_associations">
+                            <i class="fa fa-plus"></i>
+                        </button>
+                    </div>
+                </div>
+
+                <div class="body">
+                    <div class="row"
+                         v-for="node in nodes">
+                        <div class="col-10"
+                             v-text="node.name && node.name.length ? node.name : '<Node ' + node.node_id + '>'"></div>
+                        <div class="buttons col-2">
+                            <button class="btn btn-default" title="Remove from group" @click="removeFromGroup(node.node_id)">
+                                <i class="fa fa-trash"></i>
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="section config">
+                <div class="header">
+                    <div class="title">Parameters</div>
+                </div>
+
+                <div class="body">
+                    <div class="row">
+                        <div class="param-name">Index</div>
+                        <div class="param-value" v-text="group.index"></div>
+                    </div>
+
+                    <div class="row">
+                        <div class="param-name">Max associations</div>
+                        <div class="param-value" v-text="group.max_associations"></div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</script>
+
+<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/zwave/group.js') }}"></script>
+
diff --git a/platypush/backend/http/templates/plugins/zwave/index.html b/platypush/backend/http/templates/plugins/zwave/index.html
new file mode 100644
index 00000000..5f0478c2
--- /dev/null
+++ b/platypush/backend/http/templates/plugins/zwave/index.html
@@ -0,0 +1,99 @@
+{% include 'plugins/zwave/node.html' %}
+{% include 'plugins/zwave/group.html' %}
+
+<script type="text/x-template" id="tmpl-zwave">
+    <div class="zwave-container">
+        {% include 'plugins/zwave/modals/network.html' %}
+        {% include 'plugins/zwave/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)"
+                            :key="view"
+                            :selected="view == selected.view"
+                            :value="view">
+                    </option>
+                </select>
+            </div>
+
+            <div class="buttons">
+                <button class="btn btn-default" title="Add node" v-if="selected.view === 'nodes'"
+                        @click="addNode" :disabled="commandRunning">
+                    <i class="fa fa-plus"></i>
+                </button>
+
+                <button class="btn btn-default" title="Remove node" v-if="selected.view === 'nodes'"
+                        @click="removeNode" :disabled="commandRunning">
+                    <i class="fa fa-minus"></i>
+                </button>
+
+                <button class="btn btn-default" title="Add scene" v-if="selected.view === 'scenes'"
+                        :disabled="commandRunning">
+                    <i class="fa fa-plus"></i>
+                </button>
+
+                <button class="btn btn-default" title="Network info" @click="onNetworkInfoModalOpen">
+                    <i class="fa fa-info"></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 nodes" v-if="selected.view == 'nodes'">
+            <div class="no-items" v-if="Object.keys(nodes).length == 0">
+                <div class="loading" v-if="loading.nodes">Loading nodes...</div>
+                <div class="empty" v-else>No nodes available on the network</div>
+            </div>
+
+            <zwave-node
+                    v-for="node, nodeId in nodes"
+                    :key="nodeId"
+                    :node="node"
+                    :bus="bus"
+                    :selected="selected.nodeId == nodeId">
+            </zwave-node>
+        </div>
+
+        <div class="view groups" v-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>
+
+            <zwave-group
+                    v-for="group, groupId in groups"
+                    :key="groupId"
+                    :group="group"
+                    :nodes="groupId in groups ? groups[groupId].associations.map((node) => nodes[node]).reduce((nodes, node) => {nodes[node.node_id] = node; return nodes}, {}) : {}"
+                    :selected="selected.groupId == groupId"
+                    :bus="bus">
+            </zwave-group>
+        </div>
+
+        <div class="view scenes" v-if="selected.view == 'scenes'">
+            <div class="no-items" v-if="Object.keys(scenes).length == 0">
+                <div class="loading" v-if="loading.scenes">Loading scenes...</div>
+                <div class="empty" v-else>No scenes configured on the network</div>
+            </div>
+
+            <!--            <zwave-scenes-->
+            <!--                    v-for="scene, sceneId in scenes"-->
+            <!--                    :key="sceneId"-->
+            <!--                    :name="scene.label"-->
+            <!--                    :bus="bus">-->
+            <!--            </zwave-scene>-->
+        </div>
+    </div>
+</script>
+
diff --git a/platypush/backend/http/templates/plugins/zwave/modals/group.html b/platypush/backend/http/templates/plugins/zwave/modals/group.html
new file mode 100644
index 00000000..668bb309
--- /dev/null
+++ b/platypush/backend/http/templates/plugins/zwave/modals/group.html
@@ -0,0 +1,19 @@
+<modal id="zwave-add-to-group" title="Add nodes 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 nodes to add</div>
+                </div>
+
+                <div class="body">
+                    <div class="row clickable" @click="addToGroup(node.node_id, selected.groupId)" :key="node.node_id"
+                         v-for="node in Object.values(nodes).filter((node) => groups[selected.groupId].associations.indexOf(node.node_id) < 0)">
+                        <div class="param-name" v-text="node.name"></div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</modal>
+
diff --git a/platypush/backend/http/templates/plugins/zwave/modals/network.html b/platypush/backend/http/templates/plugins/zwave/modals/network.html
new file mode 100644
index 00000000..1cc7fce9
--- /dev/null
+++ b/platypush/backend/http/templates/plugins/zwave/modals/network.html
@@ -0,0 +1,35 @@
+<modal id="zwave-network-info" title="Network info" v-model="modal.networkInfo.visible" v-if="modal.networkInfo.visible">
+    <div class="network-info">
+        <div class="no-items" v-if="loading.status">
+            Loading status...
+        </div>
+
+        <div class="params" v-else>
+            <div class="row">
+                <div class="param-name">State</div>
+                <div class="param-value" v-text="status.state"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Device</div>
+                <div class="param-value" v-text="status.device"></div>
+            </div>
+
+            <div class="section">
+                <div class="header">
+                    <div class="title">Statistics</div>
+                </div>
+
+                <div class="body">
+                    <div class="row"
+                         v-for="value, name in status.stats"
+                         :key="name">
+                        <div class="param-name" v-text="name"></div>
+                        <div class="param-value" v-text="value"></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
new file mode 100644
index 00000000..ea59ba14
--- /dev/null
+++ b/platypush/backend/http/templates/plugins/zwave/node.html
@@ -0,0 +1,186 @@
+<script type="text/x-template" id="tmpl-zwave-node">
+    <div class="item node" :class="{selected: selected}">
+        <div class="row name vertical-center" :class="{selected: selected}"
+             v-text="node.name" @click="onNodeClicked"></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="node.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="node.name && node.name.length ? node.name : '<Node ' + node.node_id + '>'"></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" v-if="node.location && node.location.length">
+                <div class="param-name">Location</div>
+                <div class="param-value" v-text="node.location"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Type</div>
+                <div class="param-value" v-text="node.type"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Role</div>
+                <div class="param-value" v-text="node.role"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Node ID</div>
+                <div class="param-value" v-text="node.node_id"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Is Ready</div>
+                <div class="param-value" v-text="node.is_ready"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Is Failed</div>
+                <div class="param-value" v-text="node.is_failed"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Product ID</div>
+                <div class="param-value" v-text="node.manufacturer_id"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Product Type</div>
+                <div class="param-value" v-text="node.product_type"></div>
+            </div>
+
+            <div class="row" v-if="node.product_name && node.product_name.length">
+                <div class="param-name">Product Name</div>
+                <div class="param-value" v-text="node.product_name"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Manufacturer ID</div>
+                <div class="param-value" v-text="node.manufacturer_id"></div>
+            </div>
+
+            <div class="row" v-if="node.manufacturer_name && node.manufacturer_name.length">
+                <div class="param-name">Manufacturer Name</div>
+                <div class="param-value" v-text="node.manufacturer_name"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Capabilities</div>
+                <div class="param-value" v-text="node.capabilities.join(', ')"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Command Classes</div>
+                <div class="param-value" v-text="node.command_classes.join(', ')"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Groups</div>
+                <div class="param-value" v-text="Object.values(node.groups).map((g) => g.label || '').join(', ')"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Home ID</div>
+                <div class="param-value" v-text="node.home_id.toString(16)"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Is Awake</div>
+                <div class="param-value" v-text="node.is_awake"></div>
+            </div>
+
+            <div class="row">
+                <div class="param-name">Is Locked</div>
+                <div class="param-value" v-text="node.is_locked"></div>
+            </div>
+
+            <div class="row" v-if="node.last_update">
+                <div class="param-name">Last Update</div>
+                <div class="param-value" v-text="node.last_update"></div>
+            </div>
+
+            <div class="row" v-if="node.last_update">
+                <div class="param-name">Max Baud Rate</div>
+                <div class="param-value" v-text="node.max_baud_rate"></div>
+            </div>
+
+            <div class="section actions">
+                <div class="header">
+                    <div class="title">Actions</div>
+                </div>
+
+                <div class="body">
+                    <div class="row error" v-if="node.is_failed" @click="removeFailedNode">
+                        <div class="param-name">Remove Failed Node</div>
+                        <div class="param-value">
+                            <i class="fa fa-trash"></i>
+                        </div>
+                    </div>
+
+                    <div class="row error" v-if="node.is_failed" @click="replaceFailedNode">
+                        <div class="param-name">Replace Failed Node</div>
+                        <div class="param-value">
+                            <i class="fa fa-sync-alt"></i>
+                        </div>
+                    </div>
+
+                    <div class="row" @click="heal">
+                        <div class="param-name">Heal Node</div>
+                        <div class="param-value">
+                            <i class="fas fa-wrench"></i>
+                        </div>
+                    </div>
+
+                    <div class="row" @click="replicationSend">
+                        <div class="param-name">Replicate info to secondary controller</div>
+                        <div class="param-value">
+                            <i class="fa fa-clone"></i>
+                        </div>
+                    </div>
+
+                    <div class="row" @click="requestNetworkUpdate">
+                        <div class="param-name">Request network update</div>
+                        <div class="param-value">
+                            <i class="fas fa-wifi"></i>
+                        </div>
+                    </div>
+
+                    <div class="row" @click="requestNeighbourUpdate">
+                        <div class="param-name">Request neighbours update</div>
+                        <div class="param-value">
+                            <i class="fas fa-network-wired"></i>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</script>
+
+<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/zwave/node.js') }}"></script>
+
diff --git a/platypush/plugins/zwave/__init__.py b/platypush/plugins/zwave/__init__.py
index 2b88c357..a08606fe 100644
--- a/platypush/plugins/zwave/__init__.py
+++ b/platypush/plugins/zwave/__init__.py
@@ -52,6 +52,22 @@ class ZwavePlugin(Plugin):
         backend = self._get_backend()
         backend.stop_network()
 
+    @action
+    def status(self) -> Dict[str, Any]:
+        """
+        Get the status of the controller.
+        :return: dict
+        """
+        backend = self._get_backend()
+        network = self._get_network()
+        controller = self._get_controller()
+
+        return {
+            'device': backend.device,
+            'state': network.state_str,
+            'stats': controller.stats,
+        }
+
     @action
     def add_node(self, do_security=False):
         """
@@ -189,7 +205,7 @@ class ZwavePlugin(Plugin):
             'node_id': node.node_id,
             'home_id': node.home_id,
             'capabilities': list(node.capabilities),
-            'command_classes': list(node.command_classes),
+            'command_classes': [node.get_command_class_as_string(cc) for cc in node.command_classes],
             'device_type': node.device_type,
             'groups': {
                 group_id: cls.group_to_dict(group)
@@ -367,13 +383,13 @@ class ZwavePlugin(Plugin):
         self._get_controller().kill_command()
 
     @action
-    def set_controller_name(self, node_name: str):
+    def set_controller_name(self, name: str):
         """
         Set the name of the controller on the network.
 
-        :param node_name: New controller name.
+        :param name: New controller name.
         """
-        self._get_controller().name = node_name
+        self._get_controller().name = name
         self.write_config()
 
     @action
@@ -613,9 +629,9 @@ class ZwavePlugin(Plugin):
         return self._get_values('power_levels', node_id=node_id, node_name=node_name)
 
     @action
-    def get_rgb_bulbs(self, node_id: Optional[int] = None, node_name: Optional[str] = None) -> Dict[int, Any]:
+    def get_bulbs(self, node_id: Optional[int] = None, node_name: Optional[str] = None) -> Dict[int, Any]:
         """
-        Get the RGB bulbs/LEDs on the network or associated to a node.
+        Get the bulbs/LEDs on the network or associated to a node.
 
         :param node_id: Select node by node_id.
         :param node_name: Select node by name.