From 39671546b68d3190ef842954f313ef4a2f81da79 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 17 Aug 2015 23:33:32 +0200 Subject: [PATCH] add some initial support for scenes and alarms. Not fully completed yet --- CMakeLists.txt | 1 + apps/ubuntu/CMakeLists.txt | 16 ++ apps/ubuntu/qml/BigColorPicker.qml | 7 +- apps/ubuntu/qml/CreateDialog.qml | 77 ++++++ apps/ubuntu/qml/EditAlarmDialog.qml | 258 ++++++++++++++++++++ apps/ubuntu/qml/EditSceneDialog.qml | 84 +++++++ apps/ubuntu/qml/GroupsPage.qml | 26 --- apps/ubuntu/qml/LightDelegate.qml | 98 ++++++-- apps/ubuntu/qml/LightsPage.qml | 207 +++++++---------- apps/ubuntu/qml/MainTabs.qml | 2 +- apps/ubuntu/qml/ScenesPage.qml | 115 +++++++++ apps/ubuntu/qml/SchedulesPage.qml | 88 +++++++ apps/ubuntu/qml/Shine.qml | 79 ++++--- apps/ubuntu/qml/UbuntuColorPicker.qml | 4 +- apps/ubuntu/qml/UbuntuColorPickerCt.qml | 4 +- libhue/CMakeLists.txt | 4 + libhue/group.cpp | 293 ++++++++++++++--------- libhue/group.h | 22 ++ libhue/groups.cpp | 4 +- libhue/huebridgeconnection.cpp | 5 + libhue/light.cpp | 8 +- libhue/lightinterface.h | 2 +- libhue/lights.cpp | 10 + libhue/lights.h | 1 + libhue/lightsfiltermodel.cpp | 5 + libhue/scene.cpp | 130 +++++++++++ libhue/scene.h | 67 ++++++ libhue/scenes.cpp | 258 ++++++++++++++++++++ libhue/scenes.h | 72 ++++++ libhue/schedule.cpp | 172 ++++++++++++++ libhue/schedule.h | 89 +++++++ libhue/schedules.cpp | 297 ++++++++++++++++++++++++ libhue/schedules.h | 79 +++++++ plugin/Hue/hueplugin.cpp | 8 + 34 files changed, 2284 insertions(+), 308 deletions(-) create mode 100644 apps/ubuntu/qml/CreateDialog.qml create mode 100644 apps/ubuntu/qml/EditAlarmDialog.qml create mode 100644 apps/ubuntu/qml/EditSceneDialog.qml delete mode 100644 apps/ubuntu/qml/GroupsPage.qml create mode 100644 apps/ubuntu/qml/ScenesPage.qml create mode 100644 apps/ubuntu/qml/SchedulesPage.qml create mode 100644 libhue/scene.cpp create mode 100644 libhue/scene.h create mode 100644 libhue/scenes.cpp create mode 100644 libhue/scenes.h create mode 100644 libhue/schedule.cpp create mode 100644 libhue/schedule.h create mode 100644 libhue/schedules.cpp create mode 100644 libhue/schedules.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6252782..adac620 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,5 @@ cmake_minimum_required(VERSION 2.8.9) +set(UBUNTU_MANIFEST_PATH "apps/ubuntu/manifest.json" CACHE INTERNAL "Tells QtCreator location and name of the manifest file") project(shine C CXX) diff --git a/apps/ubuntu/CMakeLists.txt b/apps/ubuntu/CMakeLists.txt index 472785a..94715a7 100644 --- a/apps/ubuntu/CMakeLists.txt +++ b/apps/ubuntu/CMakeLists.txt @@ -14,3 +14,19 @@ add_custom_target(shine_ubuntu-qmlfiles ALL COMMAND cp -r ${CMAKE_CURRENT_SOURCE_DIR}/qml ${CMAKE_CURRENT_BINARY_DIR} DEPENDS ${QMLFILES} ) + +file(GLOB QML_FILES qml/*.qml) + +set(PLUGIN_FILES + ${CMAKE_BINARY_DIR}/plugin/Hue/libhueplugin.so + ${CMAKE_BINARY_DIR}/plugin/Hue/ColorPicker.qml + ${CMAKE_BINARY_DIR}/plugin/Hue/ColorPickerCt.qml + ${CMAKE_BINARY_DIR}/plugin/Hue/qmldir +) + +install(FILES ${PLUGIN_FILES} DESTINATION /Hue/) +install(FILES "shine.json" DESTINATION /) +install(FILES "manifest.json" DESTINATION /) +install(FILES "shine.desktop" DESTINATION /) +install(FILES ${QML_FILES} DESTINATION /qml/) +install(TARGETS shine_ubuntu DESTINATION /) diff --git a/apps/ubuntu/qml/BigColorPicker.qml b/apps/ubuntu/qml/BigColorPicker.qml index 1ff4ee1..27a7631 100644 --- a/apps/ubuntu/qml/BigColorPicker.qml +++ b/apps/ubuntu/qml/BigColorPicker.qml @@ -18,21 +18,20 @@ */ import QtQuick 2.3 -import Ubuntu.Components 1.1 +import Ubuntu.Components 1.3 import Hue 0.1 -Page { +Item { property alias lights: bigColorPicker.lights UbuntuColorPicker { id: bigColorPicker anchors.fill: parent anchors.margins: units.gu(2) - visible: root.orientation == "landscape" touchDelegate: UbuntuShape { id: lightDelegate - color: light && light.reachable ? (light.on ? "blue" : "gray") : "red" + backgroundColor: light && light.reachable ? (light.on ? "blue" : "gray") : "red" height: units.gu(5) width: units.gu(5) property var point: light ? bigColorPicker.calculateXy(light.color) : undefined diff --git a/apps/ubuntu/qml/CreateDialog.qml b/apps/ubuntu/qml/CreateDialog.qml new file mode 100644 index 0000000..f36a9f7 --- /dev/null +++ b/apps/ubuntu/qml/CreateDialog.qml @@ -0,0 +1,77 @@ +import QtQuick 2.3 +import Ubuntu.Components 1.3 +import Ubuntu.Components.Popups 1.3 +import Ubuntu.Components.ListItems 1.3 + +Dialog { + id: root + title: mode == "group" ? "Add group" : "Add scene" + text: mode == "group" ? "Please enter a name for the new group:" : "Please enter a name for the scene:" + + property string mode: "group" // or "scene" + property var lights: null + property var checkedLights: null + property alias name: nameTextField.text + + signal accepted(string name, var ligths); + signal rejected(); + + Component.onCompleted: { + print("completed", root.checkedLights) + if (root.checkedLights) { + for (var i = 0; i < root.checkedLights.length; i++) { + print("checked light:", root.checkedLights[i]) + lightsCheckboxes.itemAt(root.checkedLights[i] - 1).checked = true + } + } + } + + TextField { + id: nameTextField + width: parent.width - x + } + ThinDivider {} + + Repeater { + id: lightsCheckboxes + model: root.lights + delegate: Row { + width: parent.width + spacing: units.gu(1) + property alias checked: checkBox.checked + CheckBox { + id: checkBox + checked: false + } + Label { + text: name + anchors.verticalCenter: parent.verticalCenter + } + } + } + Button { + text: "OK" + color: UbuntuColors.green + onClicked: { + var lightsList = new Array; + for (var i = 0; i < lightsCheckboxes.count; ++i) { + if (lightsCheckboxes.itemAt(i).checked) { + print("adding light", i) + lightsList.push(lights.get(i).id); + print("list is now", lightsList.length) + } + } + + root.accepted(nameTextField.text, lightsList) + PopupUtils.close(root) + } + } + Button { + text: "Cancel" + color: UbuntuColors.red + onClicked: { + root.rejected(); + PopupUtils.close(root) + } + } +} diff --git a/apps/ubuntu/qml/EditAlarmDialog.qml b/apps/ubuntu/qml/EditAlarmDialog.qml new file mode 100644 index 0000000..7303f96 --- /dev/null +++ b/apps/ubuntu/qml/EditAlarmDialog.qml @@ -0,0 +1,258 @@ +import QtQuick 2.3 +import QtQuick.Layouts 1.1 +import Ubuntu.Components 1.3 +import Ubuntu.Components.Popups 1.3 +import Ubuntu.Components.ListItems 1.3 +import Ubuntu.Components.Pickers 1.3 + +Page { + id: root + title: "Create Alarm" + + property var lights: null + property var scenes: null + property var schedules: null + + readonly property bool recurring: recurringSelector.selectedIndex == 1 + readonly property string mode: modeSelector.selectedIndex == 0 ? "lights" : "scene" + + head { + actions: [ + Action { + iconName: "tick" + enabled: nameTextField.text + onTriggered: { + if (root.recurring) { + if (root.mode == "scene") { + + root.schedules.createRecurringAlarmForScene(nameTextField.text, scenes.get(sceneSelector.selectedIndex).id, dateTime.date, recurrenceSelector.weekdays) + } + } else { + if (root.mode == "scene") { + root.schedules.createSingleAlarmForScene(nameTextField.text, scenes.get(sceneSelector.selectedIndex).id, dateTime.date) + } + } + pageStack.pop() + } + } + ] + } + + Column { + anchors { + fill: parent + topMargin: units.gu(1) + } +// anchors.margins: units.gu(1) + spacing: units.gu(1) + + RowLayout { + anchors { left: parent.left; right: parent.right; margins: units.gu(2) } + spacing: units.gu(1) + + Label { + text: i18n.tr("Name") + } + TextField { + id: nameTextField + Layout.fillWidth: true + } + } + + Column { + width: parent.width + ItemSelector { + id: recurringSelector + width: parent.width + model: ["Single alarm", "Recurring alarm"] + } + + Subtitled { + id: dateTime + text: root.recurring ? i18n.tr("Time") : i18n.tr("Date & Time") + subText: root.recurring ? date.toString("hh:mm") : date.toString(i18n.tr("YYYY-MM-dd hh:mm")) + property var date: new Date() + onClicked: { + var popup = PopupUtils.open(dateTimePicker, root, {date: date}) + popup.closed.connect(function(date) { + dateTime.date = date + }) + } + } + + Component { + id: dateTimePicker + Dialog { + id: dtp + title: root.recurring ? i18n.tr("Time") : i18n.tr("Date & Time") + signal closed(var date) + + property var date: new Date() + + Component.onCompleted: { +// timePicker.date.setHours(hour) +// timePicker.date.setMinutes(minute) +// print("**************", timePicker.hours, hour) + } + + DatePicker { + id: datePicker + date: dtp.date + visible: !root.recurring + } + + DatePicker { + id: timePicker + mode: "Hours|Minutes" + date: dtp.date + } + + Button { + text: i18n.tr("OK") + onClicked: { + var mixedDate = datePicker.date; + mixedDate.setHours(timePicker.date.getHours()) + mixedDate.setMinutes(timePicker.date.getMinutes()) + dtp.closed(mixedDate) + PopupUtils.close(dtp) + } + } + } + } + + Subtitled { + id: recurrenceSelector + text: "Recurrence" + subText: { + var strings = new Array() + if (weekdays[1] == "1") strings.push("Mon") + if (weekdays[2] == "1") strings.push("Tue") + if (weekdays[3] == "1") strings.push("Wed") + if (weekdays[4] == "1") strings.push("Thu") + if (weekdays[5] == "1") strings.push("Fri") + if (weekdays[6] == "1") strings.push("Sat") + if (weekdays[7] == "1") strings.push("Sun") + return strings.join(", ") + } + visible: root.recurring + property string weekdays: "00000000" + onClicked: { + var popup = PopupUtils.open(recurrancePicker, root) + popup.closed.connect(function(weekdays) { + recurrenceSelector.weekdays = weekdays; + }) + } + } + Component { + id: recurrancePicker + Dialog { + id: rp + title: i18n.tr("Days") + signal closed(string weekdays) + + RowLayout { CheckBox { id: cbMonday; checked: recurrenceSelector.weekdays[1] == "1" } Label { Layout.fillWidth: true; text: "Monday"} } + RowLayout { CheckBox { id: cbTuesday; checked: recurrenceSelector.weekdays[2] == "1" } Label { Layout.fillWidth: true; text: "Tuesday"} } + RowLayout { CheckBox { id: cbWednesday; checked: recurrenceSelector.weekdays[3] == "1" } Label { Layout.fillWidth: true; text: "Wednesday"} } + RowLayout { CheckBox { id: cbThursday; checked: recurrenceSelector.weekdays[4] == "1" } Label { Layout.fillWidth: true; text: "Thursday"} } + RowLayout { CheckBox { id: cbFriday; checked: recurrenceSelector.weekdays[5] == "1" } Label { Layout.fillWidth: true; text: "Friday"} } + RowLayout { CheckBox { id: cbSaturday; checked: recurrenceSelector.weekdays[6] == "1" } Label { Layout.fillWidth: true; text: "Saturday"} } + RowLayout { CheckBox { id: cbSunday; checked: recurrenceSelector.weekdays[7] == "1" } Label { Layout.fillWidth: true; text: "Sunday"} } + + Button { + text: i18n.tr("OK") + onClicked: { + var weekdays = "0"; + weekdays += cbMonday.checked ? "1" : "0" + weekdays += cbTuesday.checked ? "1" : "0" + weekdays += cbWednesday.checked ? "1" : "0" + weekdays += cbThursday.checked ? "1" : "0" + weekdays += cbFriday.checked ? "1" : "0" + weekdays += cbSaturday.checked ? "1" : "0" + weekdays += cbSunday.checked ? "1" : "0" + rp.closed(weekdays) + PopupUtils.close(rp); + } + } + } + } + + ItemSelector { + id: modeSelector + width: parent.width + model: ["Lights", "Scene"] + } + } + + Label { + width: parent.width + text: root.mode == "lights" ? + i18n.tr("Check all the lights that should be controlled by this alarm. The current brightness and color values will be used.") + : i18n.tr("Select the scene to be enabled by the alarm") + wrapMode: Text.WordWrap + } + Repeater { + id: lightsCheckboxes + model: root.lights + delegate: Row { + visible: root.mode == "lights" + width: parent.width + spacing: units.gu(1) + property alias checked: checkBox.checked + CheckBox { + id: checkBox + checked: false + } + Label { + text: name + anchors.verticalCenter: parent.verticalCenter + } + } + } + ItemSelector { + id: sceneSelector + width: parent.width + visible: root.mode == "scene" + model: root.scenes + delegate: OptionSelectorDelegate { + text: model.name + } + } + } + + + + + + +// Button { +// text: "OK" +// color: UbuntuColors.green +// enabled: nameTextField.text.length > 0 || nameTextField.inputMethodComposing +// onClicked: { +// var lightsOrScene; +// if (root.mode == "lights") { +// var lightsList = new Array; +// for (var i = 0; i < lightsCheckboxes.count; ++i) { +// if (lightsCheckboxes.itemAt(i).checked) { +// print("adding light", i) +// lightsList.push(lights.get(i).id); +// print("list is now", lightsList.length) +// } +// } +// lightsOrScene = lightsList; +// } else { +// lightsOrScene = scenes.get(sceneSelector.selectedIndex).id +// } +// root.accepted(nameTextField.text, lightsOrScene) +// PopupUtils.close(root) +// } +// } +// Button { +// text: "Cancel" +// color: UbuntuColors.red +// onClicked: { +// root.rejected(); +// PopupUtils.close(root) +// } +// } +} diff --git a/apps/ubuntu/qml/EditSceneDialog.qml b/apps/ubuntu/qml/EditSceneDialog.qml new file mode 100644 index 0000000..4bfd706 --- /dev/null +++ b/apps/ubuntu/qml/EditSceneDialog.qml @@ -0,0 +1,84 @@ +import QtQuick 2.3 +import Ubuntu.Components 1.3 +import Ubuntu.Components.Popups 1.3 +import Ubuntu.Components.ListItems 1.3 + +Dialog { + id: root + + property var lights: null + property var checkedLights: null + property alias name: nameTextField.text + + signal accepted(string name, var ligths); + signal rejected(); + + Component.onCompleted: { + print("completed", root.checkedLights) + if (root.checkedLights) { + for (var i = 0; i < root.checkedLights.length; i++) { + print("checked light:", root.checkedLights[i]) + lightsCheckboxes.itemAt(root.checkedLights[i] - 1).checked = true + } + } + } + + Label { + text: i18n.tr("Scene name") + } + + TextField { + id: nameTextField + width: parent.width - x + } + ThinDivider {} + + Label { + text: i18n.tr("Check all the lights that should be controlled by this scene. The current brightness and color values will be used.") + wrapMode: Text.WordWrap + } + + Repeater { + id: lightsCheckboxes + model: root.lights + delegate: Row { + width: parent.width + spacing: units.gu(1) + property alias checked: checkBox.checked + CheckBox { + id: checkBox + checked: false + } + Label { + text: name + anchors.verticalCenter: parent.verticalCenter + } + } + } + Button { + text: "OK" + color: UbuntuColors.green + enabled: nameTextField.text.length > 0 || nameTextField.inputMethodComposing + onClicked: { + var lightsList = new Array; + for (var i = 0; i < lightsCheckboxes.count; ++i) { + if (lightsCheckboxes.itemAt(i).checked) { + print("adding light", i) + lightsList.push(lights.get(i).id); + print("list is now", lightsList.length) + } + } + + root.accepted(nameTextField.text, lightsList) + PopupUtils.close(root) + } + } + Button { + text: "Cancel" + color: UbuntuColors.red + onClicked: { + root.rejected(); + PopupUtils.close(root) + } + } +} diff --git a/apps/ubuntu/qml/GroupsPage.qml b/apps/ubuntu/qml/GroupsPage.qml deleted file mode 100644 index 63ec15a..0000000 --- a/apps/ubuntu/qml/GroupsPage.qml +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2013 Michael Zanetti - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation; version 2. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - * - * Authors: - * Michael Zanetti - */ - -import QtQuick 2.3 -import Ubuntu.Components 1.1 -import Ubuntu.Components.ListItems 1.0 -import Hue 0.1 - -Page { -} diff --git a/apps/ubuntu/qml/LightDelegate.qml b/apps/ubuntu/qml/LightDelegate.qml index d639dbd..20bd5f8 100644 --- a/apps/ubuntu/qml/LightDelegate.qml +++ b/apps/ubuntu/qml/LightDelegate.qml @@ -18,8 +18,8 @@ */ import QtQuick 2.3 -import Ubuntu.Components 1.1 -import Ubuntu.Components.ListItems 1.0 +import Ubuntu.Components 1.3 +import Ubuntu.Components.ListItems 1.3 import Hue 0.1 Empty { @@ -41,6 +41,7 @@ Empty { name: "rename" PropertyChanges { target: mainRow; opacity: 0 } PropertyChanges { target: renameRow; opacity: 1 } + PropertyChanges { target: root.light; alert: "lselect" } } ] @@ -64,16 +65,23 @@ Empty { } } onPressAndHold: { - if (delegateItem.state == "rename") { - delegateItem.state = "" + if (root.state == "rename") { + root.state = "" } else { - delegateItem.state = "rename" + root.state = "rename" } } + MouseArea { + anchors.fill: delegateColumn + anchors.topMargin: units.gu(6) + enabled: root.__isExpanded + preventStealing: true + } + Column { id: delegateColumn - anchors { left: parent.left; right: parent.right; leftMargin: units.gu(2); rightMargin: units.gu(2) } + anchors { left: parent.left; right: parent.right; } spacing: units.gu(2) height: childrenRect.height @@ -97,15 +105,48 @@ Empty { } } - Item { + ListItem { anchors { left: parent.left right: parent.right } - height: units.gu(6) + height: units.gu(6) + units.dp(2) + divider { colorFrom: "transparent"; colorTo: "transparent" } + onClicked: root.clicked() + onPressAndHold: root.pressAndHold() + leadingActions: groups.get(index).id == 0 ? null : deleteAction + ListItemActions { + id: deleteAction + actions: [ + Action { + iconName: "delete" + onTriggered: groups.deleteGroup(groups.get(index).id) + } + ] + } + trailingActions: ListItemActions { + actions: [ + Action { + iconName: "alarm-clock" + }, + Action { + iconName: "camera-self-timer" + }, + Action { + iconName: "edit" + onTriggered: root.state = "rename" + } + + ] + } + Row { id: mainRow - anchors.fill: parent + anchors { + fill: parent + leftMargin: units.gu(2); + rightMargin: units.gu(2) + } spacing: units.gu(1) visible: opacity > 0 @@ -135,7 +176,11 @@ Empty { } Row { id: renameRow - anchors.fill: parent + anchors { + fill: parent + leftMargin: units.gu(2); + rightMargin: units.gu(2) + } spacing: units.gu(2) height: units.gu(6) visible: opacity > 0 @@ -153,14 +198,19 @@ Empty { anchors.verticalCenter: parent.verticalCenter onClicked: { light.name = renameTextField.text - delegateItem.state = "" + root.state = "" } } } } Row { - anchors { left: parent.left; right: parent.right } + anchors { + left: parent.left; + right: parent.right + leftMargin: units.gu(2); + rightMargin: units.gu(2) + } spacing: units.gu(1) Icon { height: brightnessSlider.height @@ -187,7 +237,12 @@ Empty { UbuntuColorPicker { id: colorPicker - anchors { left: parent.left; right: parent.right } + anchors { + left: parent.left; + right: parent.right + leftMargin: units.gu(2); + rightMargin: units.gu(2) + } height: width / 3 color: light ? light.color : "black" active: light ? (light.colormode == LightInterface.ColorModeHS || light.colormode == LightInterface.ColorModeXY) : false @@ -195,7 +250,7 @@ Empty { touchDelegate: UbuntuShape { height: units.gu(3) width: units.gu(3) - color: "black" + backgroundColor: "black" } onColorChanged: { @@ -207,7 +262,12 @@ Empty { UbuntuColorPickerCt { id: colorPickerCt - anchors { left: parent.left; right: parent.right } + anchors { + left: parent.left; + right: parent.right + leftMargin: units.gu(2); + rightMargin: units.gu(2) + } height: width / 6 ct: light ? light.ct : minCt active: light && light.colormode == LightInterface.ColorModeCT @@ -227,8 +287,14 @@ Empty { } } - OptionSelector { + ItemSelector { id: effectSelector + anchors { + left: parent.left; + right: parent.right + leftMargin: units.gu(2); + rightMargin: units.gu(2) + } model: ListModel { id: effectModel ListElement { name: "No effect"; value: "none" } diff --git a/apps/ubuntu/qml/LightsPage.qml b/apps/ubuntu/qml/LightsPage.qml index df5882c..a403766 100644 --- a/apps/ubuntu/qml/LightsPage.qml +++ b/apps/ubuntu/qml/LightsPage.qml @@ -18,9 +18,9 @@ */ import QtQuick 2.3 -import Ubuntu.Components 1.1 -import Ubuntu.Components.ListItems 1.0 -import Ubuntu.Components.Popups 1.0 +import Ubuntu.Components 1.3 +import Ubuntu.Components.ListItems 1.3 +import Ubuntu.Components.Popups 1.3 import Hue 0.1 Page { @@ -33,17 +33,13 @@ Page { actions: [ Action { text: "group" - iconName: "delete" - enabled: groupSelector.selectedIndex > 0 + iconName: "add" onTriggered: { - groups.deleteGroup(groups.get(groupSelector.selectedIndex).id) - groupSelector.selectedIndex = 0; + var popup = PopupUtils.open(Qt.resolvedUrl("CreateDialog.qml"), root, {mode: "group", lights: root.lights}) + popup.accepted.connect(function(name, lightsList) { + groups.createGroup(name, lightsList); + }) } - }, - Action { - text: "group" - iconName: "add" - onTriggered: PopupUtils.open(addGroupComponent, root) } ] } @@ -52,138 +48,109 @@ Page { id: groups } - Column { + Item { // wrap flickable to disable header fancyness anchors.fill: parent + clip: true - Row { - anchors { left: parent.left; right: parent.right; margins: units.gu(2) } - height: groupSelector.height + units.gu(2) - spacing: units.gu(2) - - OptionSelector { - id: groupSelector - anchors { top: parent.top; margins: units.gu(1) } - width: parent.width - groupSwitch.width - parent.spacing - model: groups - - delegate: OptionSelectorDelegate { - text: name - height: units.gu(5) - } - - onSelectedIndexChanged: { - lightsFilterModel.groupId = groups.get(selectedIndex).id - groupSwitch.reload(); - } - } - Switch { - id: groupSwitch - anchors { top: parent.top; margins: units.gu(1) } - iconSource: "image://theme/torch-off" - checked: groups.get(groupSelector.selectedIndex).on - visible: groups.count > 0 - onClicked: { - groups.get(groupSelector.selectedIndex).on = checked; - } + Flickable { + id: mainFlickable + anchors.fill: parent + contentHeight: mainColumn.height + interactive: contentHeight > height && groupsListView.expandedItem == null && lightsListView.expandedItem == null - Connections { - target: groups.get(groupSelector.selectedIndex) - onStateChanged: groupSwitch.reload(); - } - function reload() { - groupSwitch.checked = groups.get(groupSelector.selectedIndex).on; + Column { + id: mainColumn + anchors { left: parent.left; right: parent.right } + + Label { + text: "Groups" + anchors {left: parent.left; right: parent.right; margins: units.gu(2) } + height: units.gu(3) + verticalAlignment: Text.AlignBottom } - } - } - - Divider {} - - ListView { - id: lightsListView - anchors {left: parent.left; right: parent.right } - height: parent.height - y - clip: true - model: LightsFilterModel { - id: lightsFilterModel - } - - property var expandedItem: null + ThinDivider {} - delegate: LightDelegate { - id: delegateItem - light: lightsFilterModel.get(index) - } + ListView { + id: groupsListView + anchors {left: parent.left; right: parent.right } + height: contentHeight + model: groups + interactive: false + property var expandedItem: null + onExpandedItemChanged: { + if (expandedItem) { + lightsListView.expandedItem = null + } + } - add: Transition { - UbuntuNumberAnimation { properties: "opacity"; from: 0; to: 1 } - } - displaced: Transition { - UbuntuNumberAnimation { properties: "x,y" } - } - } - } + delegate: LightDelegate { + id: delegateItem + light: groups.get(index) - Component { - id: addGroupComponent - ComposerSheet { - title: "Add group" - - onConfirmClicked: { - var lightsList = new Array; - for (var i = 0; i < lightsCheckboxes.count; ++i) { - if (lightsCheckboxes.itemAt(i).checked) { - print("adding light", i) - lightsList.push(lights.get(i).id); - print("list is now", lightsList.length) + onHeightChanged: { + var y = delegateItem.mapToItem(mainFlickable).y; + if (y + delegateItem.height > mainFlickable.height) { + mainFlickable.contentY += y + delegateItem.height - mainFlickable.height; + } + } } } - groups.createGroup(nameTextField.text, lightsList); - } + Label { + anchors {left: parent.left; right: parent.right; margins: units.gu(2) } + text: "Lights" + height: units.gu(3) + verticalAlignment: Text.AlignBottom + } - Column { - anchors { fill: parent; margins: units.gu(2) } - spacing: units.gu(1) + ThinDivider {} - Row { - anchors { left: parent.left; right: parent.right } - spacing: units.gu(1) + ListView { + id: lightsListView + anchors {left: parent.left; right: parent.right } + height: contentHeight + interactive: false - Label { - text: "Name" - anchors.verticalCenter: parent.verticalCenter + model: LightsFilterModel { + id: lightsFilterModel } - TextField { - id: nameTextField - width: parent.width - x + property Item expandedItem: null + onExpandedItemChanged: { + if (expandedItem) { + groupsListView.expandedItem = null + } } - } - ThinDivider {} - Column { - anchors { left: parent.left; right: parent.right } - spacing: units.gu(1) - - Repeater { - id: lightsCheckboxes - model: root.lights - delegate: Row { - width: parent.width - spacing: units.gu(1) - property alias checked: checkBox.checked - CheckBox { - id: checkBox + + delegate: LightDelegate { + id: delegateItem + light: lightsFilterModel.get(index) + + onHeightChanged: { + var y = delegateItem.mapToItem(mainFlickable).y; + print("at:", y) + if (y + delegateItem.height > mainFlickable.height) { + mainFlickable.contentY += y + delegateItem.height - mainFlickable.height } - Label { - text: name - anchors.verticalCenter: parent.verticalCenter + if (y < 0) { + mainFlickable.contentY += y + mainFlickable.returnToBounds() } } } + + add: Transition { + UbuntuNumberAnimation { properties: "opacity"; from: 0; to: 1 } + } + displaced: Transition { + UbuntuNumberAnimation { properties: "x,y" } + } } } } } + + } diff --git a/apps/ubuntu/qml/MainTabs.qml b/apps/ubuntu/qml/MainTabs.qml index 6ac3790..e3fb1f8 100644 --- a/apps/ubuntu/qml/MainTabs.qml +++ b/apps/ubuntu/qml/MainTabs.qml @@ -18,7 +18,7 @@ */ import QtQuick 2.3 -import Ubuntu.Components 1.1 +import Ubuntu.Components 1.3 Tabs { diff --git a/apps/ubuntu/qml/ScenesPage.qml b/apps/ubuntu/qml/ScenesPage.qml new file mode 100644 index 0000000..323b28b --- /dev/null +++ b/apps/ubuntu/qml/ScenesPage.qml @@ -0,0 +1,115 @@ +/* + * Copyright 2013-2015 Michael Zanetti + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authors: + * Michael Zanetti + */ + +import QtQuick 2.3 +import QtQuick.Layouts 1.1 +import Ubuntu.Components 1.3 +import Ubuntu.Components.ListItems 1.3 +import Ubuntu.Components.Popups 1.3 +import Hue 0.1 + +Page { + id: root + title: "Scenes" + + property var lights: null + property var scenes: null + + head { + actions: [ + Action { + iconName: "add" + onTriggered: { + var popup = PopupUtils.open(Qt.resolvedUrl("EditSceneDialog.qml"), root, {title: i18n.tr("Create Scene"), lights: root.lights}) + popup.accepted.connect(function(name, lightsList) { + var colors; + for (var i = 0; i < lightsList.length; i++) { + colors += root.lights.get(lightsList[i]).color + } + + scenes.createScene(name, lightsList, colors); + }) + } + } + + ] + } + + Lights { + id: allLights + } + + ListView { + anchors.fill: parent + model: scenes + + delegate: ListItem { + property var scene: scenes.get(index) + trailingActions: ListItemActions { + actions: [ + Action { + iconName: "edit" + onTriggered: { + var checkedLights = []; + for (var i = 0; i < scene.lightsCount; i++) { + checkedLights.push(scene.light(i)) + } + + print("editing scene", scene.id) + var popup = PopupUtils.open(Qt.resolvedUrl("EditSceneDialog.qml"), root, {title: i18n.tr("Edit scene"), lights: root.lights, checkedLights: checkedLights, name: scene.name}) + popup.accepted.connect(function(name, lightsList) { + print("updating scene", scene.id) + scenes.updateScene(scene.id, name, lightsList); + }) + } + } + ] + } + + RowLayout { + anchors.fill: parent + anchors.margins: units.gu(1) + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + Label { + Layout.fillWidth: true + text: model.name + } + + RowLayout { + Layout.fillWidth: true + + Repeater { + model: scene.lightsCount + Label { + text: allLights.findLight(scene.light(index)).name + } + } + } + } + } + + onClicked: { + scenes.recallScene(index) + } + } + } +} diff --git a/apps/ubuntu/qml/SchedulesPage.qml b/apps/ubuntu/qml/SchedulesPage.qml new file mode 100644 index 0000000..86655cc --- /dev/null +++ b/apps/ubuntu/qml/SchedulesPage.qml @@ -0,0 +1,88 @@ +/* + * Copyright 2013-2015 Michael Zanetti + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authors: + * Michael Zanetti + */ + +import QtQuick 2.3 +import QtQuick.Layouts 1.1 +import Ubuntu.Components 1.3 +import Ubuntu.Components.ListItems 1.3 +import Ubuntu.Components.Popups 1.3 +import Hue 0.1 + +Page { + id: root + title: "Alarms" + + property var lights: null + property var schedules: null + property var scenes: null + + head { + actions: [ + Action { + iconName: "add" + onTriggered: { + pageStack.push(Qt.resolvedUrl("EditAlarmDialog.qml"), {lights: root.lights, scenes: root.scenes, schedules: root.schedules }) + } + } + ] + } + + ListView { + anchors.fill: parent + model: schedules + + delegate: ListItem { + leadingActions: ListItemActions { + actions: [ + Action { + iconName: "delete" + } + ] + } + + + ColumnLayout { + anchors.fill: parent + Label { + text: model.name + } + Label { + property string weekdays: model.weekdays + property string weekdaysString: { + var strings = new Array() + if (weekdays[1] == "1") strings.push("Mon") + if (weekdays[2] == "1") strings.push("Tue") + if (weekdays[3] == "1") strings.push("Wed") + if (weekdays[4] == "1") strings.push("Thu") + if (weekdays[5] == "1") strings.push("Fri") + if (weekdays[6] == "1") strings.push("Sat") + if (weekdays[7] == "1") strings.push("Sun") + return strings.join(", ") + } + + text: model.recurring ? + Qt.formatTime(model.dateTime) + " " + weekdaysString + : Qt.formatDateTime(model.dateTime) + + + } + } + } + } +} diff --git a/apps/ubuntu/qml/Shine.qml b/apps/ubuntu/qml/Shine.qml index dcb3fee..6bc5eac 100644 --- a/apps/ubuntu/qml/Shine.qml +++ b/apps/ubuntu/qml/Shine.qml @@ -19,32 +19,20 @@ import QtQuick 2.3 import QtQuick.Window 2.0 -import Ubuntu.Components 1.1 -import Ubuntu.Components.ListItems 1.0 -import Ubuntu.Components.Popups 1.0 +import Ubuntu.Components 1.3 +import Ubuntu.Components.ListItems 1.3 +import Ubuntu.Components.Popups 1.3 import Hue 0.1 MainView { id: root - width: units.gu(50) - height: units.gu(75) + width: units.gu(40) + height: units.gu(70) - applicationName: "com.ubuntu.developer.mzanetti.shine" + property string orientation: width > height ? "landscape" : "portrait" - useDeprecatedToolbar: false - automaticOrientation: true - property string orientation: pageStack.width > pageStack.height ? "landscape" : "portrait" - - onOrientationChanged: { - if (orientation == "portrait") { - pageStack.pop(); -// pageStack.push(Qt.resolvedUrl("MainTabs.qml")) - pageStack.push(lightsPage) - } else { - pageStack.pop(); - pageStack.push(bigColorPicker) - } - } + applicationName: "com.ubuntu.developer.mzanetti.shine" + theme.name: "Ubuntu.Components.Themes.SuruDark" Component.onCompleted: { HueBridge.apiKey = keystore.apiKey; @@ -73,19 +61,46 @@ MainView { } } - PageStack { - id: pageStack - } - LightsPage { - id: lightsPage - visible: false - lights: lights + Tabs { + anchors.fill: parent + visible: root.orientation == "portrait" + + Tab { + title: "Lights" + page: LightsPage { + id: lightsPage + lights: lights + } + } + Tab { + title: "Scenes" + page: ScenesPage { + id: scenesPage + lights: lights + scenes: scenes + } + } + Tab { + title: "Schedules" + page: PageStack { + id: pageStack + Component.onCompleted: push(schedulesPage) + SchedulesPage { + id: schedulesPage + lights: lights + schedules: schedules + scenes: scenes + } + } + } } + BigColorPicker { id: bigColorPicker - visible: false + anchors.fill: parent + visible: root.orientation == "landscape" lights: lights } @@ -93,6 +108,14 @@ MainView { id: lights } + Scenes { + id: scenes + } + + Schedules { + id: schedules + } + Component { id: loginComponent Dialog { diff --git a/apps/ubuntu/qml/UbuntuColorPicker.qml b/apps/ubuntu/qml/UbuntuColorPicker.qml index 81286c5..01b9701 100644 --- a/apps/ubuntu/qml/UbuntuColorPicker.qml +++ b/apps/ubuntu/qml/UbuntuColorPicker.qml @@ -18,7 +18,7 @@ */ import QtQuick 2.3 -import Ubuntu.Components 1.1 +import Ubuntu.Components 1.3 import Hue 0.1 Item { @@ -48,7 +48,7 @@ Item { Shape { id: shape - image: source + source: source anchors.fill: parent } diff --git a/apps/ubuntu/qml/UbuntuColorPickerCt.qml b/apps/ubuntu/qml/UbuntuColorPickerCt.qml index 7ed298e..42452c2 100644 --- a/apps/ubuntu/qml/UbuntuColorPickerCt.qml +++ b/apps/ubuntu/qml/UbuntuColorPickerCt.qml @@ -18,7 +18,7 @@ */ import QtQuick 2.3 -import Ubuntu.Components 1.1 +import Ubuntu.Components 1.3 import Hue 0.1 Item { @@ -41,7 +41,7 @@ Item { Shape { id: shape - image: source + source: source anchors.fill: parent } diff --git a/libhue/CMakeLists.txt b/libhue/CMakeLists.txt index 5fc547d..82a4b74 100644 --- a/libhue/CMakeLists.txt +++ b/libhue/CMakeLists.txt @@ -12,6 +12,10 @@ set(libhue_SRCS lightsfiltermodel.cpp light.cpp lightinterface.h + scenes.cpp + scene.cpp + schedules.cpp + schedule.cpp ) add_library(hue ${libhue_SRCS}) diff --git a/libhue/group.cpp b/libhue/group.cpp index 2e28f4c..72d7c56 100644 --- a/libhue/group.cpp +++ b/libhue/group.cpp @@ -23,11 +23,19 @@ #include #include #include +#include Group::Group(int id, const QString &name, QObject *parent) : LightInterface(parent) , m_id(id) , m_name(name) + , m_bri(0), + m_busyStateChangeId(-1), + m_hueDirty(false), + m_satDirty(false), + m_briDirty(false), + m_ctDirty(false), + m_xyDirty(false) { refresh(); } @@ -65,65 +73,47 @@ void Group::setOn(bool on) quint8 Group::bri() const { - quint8 bri = 0; - foreach (int lightId, m_lightIds) { -// if (bri > 0 && Lights::get(lightId)->bri() != bri) -// return 0; -// bri = Lights::get(lightId)->bri(); - } - - return bri; + return m_bri; } void Group::setBri(quint8 bri) { - foreach (int lightId, m_lightIds) { - // Lights::get(lightId)->setBri(bri); + if (bri != m_bri) { + QVariantMap params; + params.insert("on", true); + params.insert("bri", bri); + HueBridgeConnection::instance()->put("groups/" + QString::number(m_id) + "/action", params, this, "setStateFinished"); } - - emit stateChanged(); } quint16 Group::hue() const { - quint16 hue = 0; - foreach (int lightId, m_lightIds) { - // if (hue > 0 && Lights::get(lightId)->hue() != hue) - // return 0; - // hue = Lights::get(lightId)->hue(); - } - - return hue; + return m_hue; } void Group::setHue(quint16 hue) { - foreach (int lightId, m_lightIds) { - // Lights::get(lightId)->setHue(hue); + if (hue != m_hue) { + QVariantMap params; + params.insert("on", true); + params.insert("hue", hue); + HueBridgeConnection::instance()->put("groups/" + QString::number(m_id) + "/action", params, this, "setStateFinished"); } - - emit stateChanged(); } quint8 Group::sat() const { - quint8 sat = 0; - foreach (int lightId, m_lightIds) { - // if (sat > 0 && Lights::get(lightId)->sat() != sat) - // return 0; - // sat = Lights::get(lightId)->sat(); - } - - return sat; + return m_sat; } void Group::setSat(quint8 sat) { - foreach (int lightId, m_lightIds) { - // Lights::get(lightId)->setSat(sat); + if (sat != m_sat) { + QVariantMap params; + params.insert("on", true); + params.insert("sat", sat); + HueBridgeConnection::instance()->put("groups/" + QString::number(m_id) + "/action", params, this, "setStateFinished"); } - - emit stateChanged(); } QColor Group::color() const @@ -133,117 +123,131 @@ QColor Group::color() const void Group::setColor(const QColor &color) { - foreach (int lightId, m_lightIds) { - // Lights::get(lightId)->setColor(color); - } + // Transform from RGB to Hue/Sat + quint16 hue = color.hue() * 65535 / 360; + quint8 sat = color.saturation(); - emit stateChanged(); + // Transform from RGB to XYZ + QGenericMatrix<3, 3, qreal> rgb2xyzMatrix; + rgb2xyzMatrix(0, 0) = 0.412453; rgb2xyzMatrix(0, 1) = 0.357580; rgb2xyzMatrix(0, 2) = 0.180423; + rgb2xyzMatrix(1, 0) = 0.212671; rgb2xyzMatrix(1, 1) = 0.715160; rgb2xyzMatrix(1, 2) = 0.072169; + rgb2xyzMatrix(2, 0) = 0.019334; rgb2xyzMatrix(2, 1) = 0.119193; rgb2xyzMatrix(2, 2) = 0.950227; + + QGenericMatrix<1, 3, qreal> rgbMatrix; + rgbMatrix(0, 0) = 1.0 * color.red() / 255; + rgbMatrix(1, 0) = 1.0 * color.green() / 255; + rgbMatrix(2, 0) = 1.0 * color.blue() / 255; + + QGenericMatrix<1, 3, qreal> xyzMatrix = rgb2xyzMatrix * rgbMatrix; + + // transform from XYZ to CIELUV u' and v' + qreal u = 4*xyzMatrix(0, 0) / (xyzMatrix(0, 0) + 15*xyzMatrix(1, 0) + 3*xyzMatrix(2, 0)); + qreal v = 9*xyzMatrix(1, 0) / (xyzMatrix(0, 0) + 15*xyzMatrix(1, 0) + 3*xyzMatrix(2, 0)); + + // Transform from CIELUV to (x,y) + qreal x = 27*u / (18*u - 48*v + 36); + qreal y = 12*v / (18*u - 48*v + 36); + + qDebug() << "setting color" << color << x << y; + if (m_busyStateChangeId == -1) { + QVariantMap params; + + params.insert("hue", hue); + params.insert("sat", sat); + // FIXME: There is a bug in the API that it doesn't report back the set state of "sat" + // Lets just assume it always succeeds + m_sat = sat; + +// QVariantList xyList; +// xyList << x << y; +// params.insert("xy", xyList); + + + params.insert("on", true); + m_busyStateChangeId = HueBridgeConnection::instance()->put("groups/" + QString::number(m_id) + "/action", params, this, "setStateFinished"); + } else { + m_dirtyHue = hue; + m_hueDirty = true; + m_dirtySat = sat; + m_satDirty = true; +// m_xyDirty = true; +// m_dirtyXy = QPointF(x, y); + } } QPointF Group::xy() const { - QPointF p; - foreach (int lightId, m_lightIds) { - // if ((p.x() > 0 || p.y() > 0) && Lights::get(lightId)->xy() != p) - // return QPointF(); - // p = Lights::get(lightId)->xy(); - } - - return p; + return m_xy; } void Group::setXy(const QPointF &xy) { - foreach (int lightId, m_lightIds) { - // Lights::get(lightId)->setXy(xy); + if (m_xy != xy) { + m_xy = xy; + emit stateChanged(); } - - emit stateChanged(); } quint16 Group::ct() const { - quint16 ct = 0; - foreach (int lightId, m_lightIds) { - // if (ct > 0 && Lights::get(lightId)->ct() != ct) - // return 0; - // ct = Lights::get(lightId)->ct(); - } - - return ct; + return m_ct; } void Group::setCt(quint16 ct) { - foreach (int lightId, m_lightIds) { - // Lights::get(lightId)->setCt(ct); + if (m_busyStateChangeId == -1) { + QVariantMap params; + params.insert("ct", ct); + params.insert("on", true); + m_busyStateChangeId = HueBridgeConnection::instance()->put("groups/" + QString::number(m_id) + "/action", params, this, "setStateFinished"); + } else { + m_dirtyCt = ct; + m_ctDirty = true; } - - emit stateChanged(); } QString Group::alert() const { - QString alert; - foreach (int lightId, m_lightIds) { - // if (!alert.isEmpty() && Lights::get(lightId)->alert() != alert) - // return QString(); - // alert = Lights::get(lightId)->alert(); - } - - return alert; + return m_alert; } void Group::setAlert(const QString &alert) { - foreach (int lightId, m_lightIds) { - // Lights::get(lightId)->setAlert(alert); + if (m_alert != alert) { + QVariantMap params; + params.insert("alert", alert); + if (alert != "none") { + params.insert("on", true); + } + HueBridgeConnection::instance()->put("groups/" + QString::number(m_id) + "/action", params, this, "setStateFinished"); } - - emit stateChanged(); } QString Group::effect() const { - QString effect; - foreach (int lightId, m_lightIds) { - // if (!effect.isEmpty() && Lights::get(lightId)->effect() != effect) - // return QString(); - // effect = Lights::get(lightId)->effect(); - } - - return effect; + return m_effect; } void Group::setEffect(const QString &effect) { - foreach (int lightId, m_lightIds) { - // Lights::get(lightId)->setEffect(effect); + if (m_effect != effect) { + QVariantMap params; + params.insert("effect", effect); + if (effect != "none") { + params.insert("on", true); + } + HueBridgeConnection::instance()->put("groups/" + QString::number(m_id) + "/action", params, this, "setStateFinished"); } - - emit stateChanged(); } LightInterface::ColorMode Group::colorMode() const { - ColorMode colormode = ColorModeHS; - foreach (int lightId, m_lightIds) { - // if (!colormode.isEmpty() && Lights::get(lightId)->colormode() != colormode) - // return QString(); - // colormode = Lights::get(lightId)->colormode(); - } - - return colormode; + return m_colormode; } bool Group::reachable() const { - foreach (int lightId, m_lightIds) { - // if (Lights::get(lightId)->reachable()) - // return true; - } - - return false; + return m_reachable; } QList Group::lightIds() const @@ -272,6 +276,23 @@ void Group::responseReceived(int id, const QVariant &response) QVariantMap action = attributes.value("action").toMap(); m_on = action.value("on").toBool(); + m_bri = action.value("bri").toUInt(); + m_hue = action.value("hue").toUInt(); + m_sat = action.value("sat").toUInt(); + + m_xy = action.value("xy").toPointF(); + m_ct = action.value("ct").toInt(); + m_alert = action.value("alert").toString(); + m_effect = action.value("effect").toString(); + QString colorModeString = action.value("colormode").toString(); + if (colorModeString == "hs") { + m_colormode = ColorModeHS; + } else if (colorModeString == "xy") { + m_colormode = ColorModeXY; + } else if (colorModeString == "ct") { + m_colormode = ColorModeCT; + } + m_reachable = true;//action.value("reachable").toBool(); emit stateChanged(); } @@ -292,14 +313,78 @@ void Group::setDescriptionFinished(int id, const QVariant &response) void Group::setStateFinished(int id, const QVariant &response) { + qDebug() << "set state finished" << response; foreach (const QVariant &resultVariant, response.toList()) { QVariantMap result = resultVariant.toMap(); if (result.contains("success")) { QVariantMap successMap = result.value("success").toMap(); - if (successMap.contains("/groups/" + QString::number(m_id) + "/state/on")) { - m_on = successMap.value("/groups/" + QString::number(m_id) + "/state/on").toBool(); + if (successMap.contains("/groups/" + QString::number(m_id) + "/action/on")) { + m_on = successMap.value("/groups/" + QString::number(m_id) + "/action/on").toBool(); + } + if (successMap.contains("/groups/" + QString::number(m_id) + "/action/hue")) { + m_hue = successMap.value("/groups/" + QString::number(m_id) + "/action/hue").toInt(); + m_colormode = ColorModeHS; + } + if (successMap.contains("/groups/" + QString::number(m_id) + "/action/bri")) { + m_bri = successMap.value("/groups/" + QString::number(m_id) + "/action/bri").toInt(); + } + if (successMap.contains("/groups/" + QString::number(m_id) + "/action/sat")) { + m_sat = successMap.value("/groups/" + QString::number(m_id) + "/action/sat").toInt(); + m_colormode = ColorModeHS; + } + if (successMap.contains("/groups/" + QString::number(m_id) + "/action/xy")) { + m_xy = successMap.value("/groups/" + QString::number(m_id) + "/action/xy").toPoint(); + m_colormode = ColorModeXY; + } + if (successMap.contains("/groups/" + QString::number(m_id) + "/action/ct")) { + m_ct = successMap.value("/groups/" + QString::number(m_id) + "/action/ct").toInt(); + m_colormode = ColorModeCT; + } + if (successMap.contains("/groups/" + QString::number(m_id) + "/action/effect")) { + m_effect = successMap.value("/groups/" + QString::number(m_id) + "/action/effect").toString(); } } } + emit stateChanged(); + + if (m_busyStateChangeId == id) { + m_busyStateChangeId = -1; + if (m_hueDirty || m_satDirty || m_briDirty) { + QVariantMap params; + if (m_hueDirty) { + params.insert("hue", m_dirtyHue); + m_hueDirty = false; + } + if (m_satDirty) { + params.insert("sat", m_dirtySat); + m_satDirty = false; + } + if (m_briDirty) { + params.insert("bri", m_dirtyBri); + m_briDirty = false; + } + + // FIXME: There is a bug in the API that it doesn't report back the set state of "sat" + // Lets just assume it always succeeds + m_sat = m_dirtySat; + + m_busyStateChangeId = HueBridgeConnection::instance()->put("groups/" + QString::number(m_id) + "/action", params, this, "setStateFinished"); + } else if(m_ctDirty) { + QVariantMap params; + params.insert("ct", m_dirtyCt); + m_ctDirty = false; + + m_busyStateChangeId = HueBridgeConnection::instance()->put("groups/" + QString::number(m_id) + "/action", params, this, "setStateFinished"); + } else if (m_xyDirty) { + QVariantMap params; + QVariantList xyList; + xyList << m_dirtyXy.x() << m_dirtyXy.y(); + params.insert("xy", xyList); + m_xyDirty = false; + + m_busyStateChangeId = HueBridgeConnection::instance()->put("lights/" + QString::number(m_id) + "/state", params, this, "setStateFinished"); + } + } + } diff --git a/libhue/group.h b/libhue/group.h index fa5d89a..78fb4d2 100644 --- a/libhue/group.h +++ b/libhue/group.h @@ -83,6 +83,28 @@ private slots: QList m_lightIds; bool m_on; + quint8 m_bri; + quint16 m_hue; + quint8 m_sat; + QPointF m_xy; + quint16 m_ct; + QString m_alert; + QString m_effect; + ColorMode m_colormode; + bool m_reachable; + + int m_busyStateChangeId; + bool m_hueDirty; + quint16 m_dirtyHue; + bool m_satDirty; + quint8 m_dirtySat; + bool m_briDirty; + quint8 m_dirtyBri; + bool m_ctDirty; + quint16 m_dirtyCt; + bool m_xyDirty; + QPointF m_dirtyXy; + }; #endif diff --git a/libhue/groups.cpp b/libhue/groups.cpp index 43fbe5d..f0a8883 100644 --- a/libhue/groups.cpp +++ b/libhue/groups.cpp @@ -136,10 +136,10 @@ void Groups::groupsReceived(int id, const QVariant &variant) endRemoveRows(); } - Group *group = createGroupInternal(0, "All"); + createGroupInternal(0, "All"); foreach (const QString &groupId, groups.keys()) { if (findGroup(groupId.toInt()) == 0) { - Group *group = createGroupInternal(groupId.toInt(), groups.value(groupId).toMap().value("name").toString()); + createGroupInternal(groupId.toInt(), groups.value(groupId).toMap().value("name").toString()); } } emit countChanged(); diff --git a/libhue/huebridgeconnection.cpp b/libhue/huebridgeconnection.cpp index ab3ddab..d645ea7 100644 --- a/libhue/huebridgeconnection.cpp +++ b/libhue/huebridgeconnection.cpp @@ -178,6 +178,7 @@ int HueBridgeConnection::post(const QString &path, const QVariantMap ¶ms, QO QUrl url(m_baseApiUrl + path); QNetworkRequest request; + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setUrl(url); #if QT_VERSION >= 0x050000 @@ -188,6 +189,7 @@ int HueBridgeConnection::post(const QString &path, const QVariantMap ¶ms, QO QByteArray data = serializer.serialize(params); #endif + qDebug() << "posting" << data << "\nto" << request.url(); QNetworkReply *reply = m_nam->post(request, data); connect(reply, SIGNAL(finished()), this, SLOT(slotOpFinished())); m_requestIdMap.insert(reply, m_requestCounter); @@ -229,6 +231,8 @@ int HueBridgeConnection::put(const QString &path, const QVariantMap ¶ms, QOb void HueBridgeConnection::createUserFinished() { QNetworkReply *reply = static_cast(sender()); + reply->deleteLater(); + QByteArray response = reply->readAll(); qDebug() << "create user finished" << response; @@ -288,6 +292,7 @@ void HueBridgeConnection::slotOpFinished() return; } QVariant rsp = jsonDoc.toVariant(); +// qDebug() << "got:" << jsonDoc.toJson(); #else QJson::Parser parser; bool ok; diff --git a/libhue/light.cpp b/libhue/light.cpp index 547c6e4..9372200 100644 --- a/libhue/light.cpp +++ b/libhue/light.cpp @@ -256,8 +256,12 @@ QString Light::alert() const void Light::setAlert(const QString &alert) { if (m_alert != alert) { - m_alert = alert; - emit stateChanged(); + QVariantMap params; + params.insert("alert", alert); + if (alert != "none") { + params.insert("on", true); + } + HueBridgeConnection::instance()->put("lights/" + QString::number(m_id) + "/state", params, this, "setStateFinished"); } } diff --git a/libhue/lightinterface.h b/libhue/lightinterface.h index 36ad35d..ff1cf09 100644 --- a/libhue/lightinterface.h +++ b/libhue/lightinterface.h @@ -44,7 +44,7 @@ class LightInterface: public QObject Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY stateChanged) Q_PROPERTY(QPointF xy READ xy NOTIFY stateChanged) Q_PROPERTY(quint16 ct READ ct WRITE setCt NOTIFY stateChanged) - Q_PROPERTY(QString alert READ alert NOTIFY stateChanged) + Q_PROPERTY(QString alert READ alert WRITE setAlert NOTIFY stateChanged) Q_PROPERTY(QString effect READ effect WRITE setEffect NOTIFY stateChanged) Q_PROPERTY(ColorMode colormode READ colorMode NOTIFY stateChanged) Q_PROPERTY(bool reachable READ reachable NOTIFY stateChanged) diff --git a/libhue/lights.cpp b/libhue/lights.cpp index 17c4858..5e12754 100644 --- a/libhue/lights.cpp +++ b/libhue/lights.cpp @@ -112,6 +112,16 @@ Light *Lights::get(int index) const return 0; } +Light *Lights::findLight(int lightId) const +{ + foreach (Light *light, m_list) { + if (light->id() == lightId) { + return light; + } + } + return 0; +} + void Lights::refresh() { HueBridgeConnection::instance()->get("lights", this, "lightsReceived"); diff --git a/libhue/lights.h b/libhue/lights.h index eea39cd..55c7445 100644 --- a/libhue/lights.h +++ b/libhue/lights.h @@ -53,6 +53,7 @@ class Lights : public QAbstractListModel QVariant data(const QModelIndex &index, int role) const; QHash roleNames() const; Q_INVOKABLE Light* get(int index) const; + Q_INVOKABLE Light* findLight(int lightId) const; public slots: void refresh(); diff --git a/libhue/lightsfiltermodel.cpp b/libhue/lightsfiltermodel.cpp index 75f0b49..d6aec20 100644 --- a/libhue/lightsfiltermodel.cpp +++ b/libhue/lightsfiltermodel.cpp @@ -65,6 +65,7 @@ Light *LightsFilterModel::get(int row) const bool LightsFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { + Q_UNUSED(sourceParent) if (m_groupId == 0) { return true; } @@ -92,6 +93,10 @@ void LightsFilterModel::groupChanged(const QModelIndex &first, const QModelIndex void LightsFilterModel::groupsAdded(const QModelIndex &parent, int first, int last) { + Q_UNUSED(parent) + Q_UNUSED(first) + Q_UNUSED(last) + if (!m_group) { m_group = findGroup(); } diff --git a/libhue/scene.cpp b/libhue/scene.cpp new file mode 100644 index 0000000..82e9d79 --- /dev/null +++ b/libhue/scene.cpp @@ -0,0 +1,130 @@ +/* + * Copyright 2015 Michael Zanetti + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authors: + * Michael Zanetti + */ + +#include "scene.h" +#include "huebridgeconnection.h" + +#include +#include +#include + +Scene::Scene(const QString &id, const QString &name, QObject *parent) + : QObject(parent) + , m_id(id) + , m_name(name) +{ +// refresh(); +} + +QString Scene::id() const +{ + return m_id; +} + +QString Scene::name() const +{ + return m_name; +} + +void Scene::setName(const QString &name) +{ + if (m_name != name) { + m_name = name; + emit nameChanged(); + } +} + +QList Scene::lights() const +{ + return m_lightIds; +} + +void Scene::setLights(const QList lights) +{ + if (lights != m_lightIds) { + m_lightIds = lights; + emit lightsChanged(); + } +} + +int Scene::light(int index) const +{ + if (index < 0 || index > m_lightIds.count() > 0) { + return -1; + } + return m_lightIds.at(index); +} + +int Scene::lightsCount() const +{ + return m_lightIds.count(); +} + +void Scene::refresh() +{ +// HueBridgeConnection::instance()->get("groups/" + QString::number(m_id), this, "responseReceived"); +} + +//void Scene::responseReceived(int id, const QVariant &response) +//{ +// Q_UNUSED(id) + +// m_lightIds.clear(); + +// QVariantMap attributes = response.toMap(); +// QVariantList lightsMap = attributes.value("lights").toList(); +// foreach (const QVariant &lightId, lightsMap) { +// m_lightIds << lightId.toUInt(); +// } + +// emit lightsChanged(); + +// QVariantMap action = attributes.value("action").toMap(); +// m_on = action.value("on").toBool(); +// emit stateChanged(); +//} + +//void Group::setDescriptionFinished(int id, const QVariant &response) +//{ +// Q_UNUSED(id) +// qDebug() << "setDescription finished" << response; +// QVariantMap result = response.toList().first().toMap(); + +// if (result.contains("success")) { +// QVariantMap successMap = result.value("success").toMap(); +// if (successMap.contains("/groups/" + QString::number(m_id) + "/name")) { +// m_name = successMap.value("/groups/" + QString::number(m_id) + "/name").toString(); +// emit nameChanged(); +// } +// } +//} + +//void Group::setStateFinished(int id, const QVariant &response) +//{ +// foreach (const QVariant &resultVariant, response.toList()) { +// QVariantMap result = resultVariant.toMap(); +// if (result.contains("success")) { +// QVariantMap successMap = result.value("success").toMap(); +// if (successMap.contains("/groups/" + QString::number(m_id) + "/state/on")) { +// m_on = successMap.value("/groups/" + QString::number(m_id) + "/state/on").toBool(); +// } +// } +// } +// emit stateChanged(); +//} diff --git a/libhue/scene.h b/libhue/scene.h new file mode 100644 index 0000000..45330e6 --- /dev/null +++ b/libhue/scene.h @@ -0,0 +1,67 @@ +/* + * Copyright 2015 Michael Zanetti + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authors: + * Michael Zanetti + */ + +#ifndef SCENE_H +#define SCENE_H + +#include +#include + +class Scene: public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) + Q_PROPERTY(int lightsCount READ lightsCount NOTIFY lightsChanged) + +public: + Scene(const QString &id, const QString &name, QObject *parent = 0); + + QString id() const; + + QString name() const; + void setName(const QString &name); + + QList lights() const; + void setLights(const QList lights); + + int lightsCount() const; + Q_INVOKABLE int light(int index) const; + +public slots: + void refresh(); + +signals: + void nameChanged(); + void lightsChanged(); + +private slots: +// void responseReceived(int id, const QVariant &response); +// void setDescriptionFinished(int id, const QVariant &response); + +// void setStateFinished(int id, const QVariant &response); + +private: + QString m_id; + QString m_name; + QList m_lightIds; +}; + +#endif diff --git a/libhue/scenes.cpp b/libhue/scenes.cpp new file mode 100644 index 0000000..494ee19 --- /dev/null +++ b/libhue/scenes.cpp @@ -0,0 +1,258 @@ +/* + * Copyright 2015 Michael Zanetti + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authors: + * Michael Zanetti + */ + +#include "scenes.h" +#include "scene.h" + +#include "huebridgeconnection.h" + +#include +#include + +Scenes::Scenes(QObject *parent) + : QAbstractListModel(parent) +{ + connect(HueBridgeConnection::instance(), SIGNAL(connectedBridgeChanged()), this, SLOT(refresh())); + refresh(); + +#if QT_VERSION < 0x050000 + setRoleNames(roleNames()); +#endif +} + +int Scenes::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} + +int Scenes::count() const +{ + return m_list.count(); +} + +QVariant Scenes::data(const QModelIndex &index, int role) const +{ + Scene *scene= m_list.at(index.row()); + switch (role) { + case RoleId: + return scene->id(); + case RoleName: + return scene->name(); + } + + return QVariant(); +} + +QHash Scenes::roleNames() const +{ + QHash roles; + roles.insert(RoleId, "id"); + roles.insert(RoleName, "name"); + return roles; +} + +Scene *Scenes::get(int index) const +{ + if (index > -1 && index < m_list.count()) { + return m_list.at(index); + } + return 0; +} + +Scene *Scenes::findScene(const QString &id) const +{ + foreach (Scene *scene, m_list) { + if (scene->id() == id) { + return scene; + } + } + return 0; +} + +void Scenes::recallScene(int index) +{ + QVariantMap params; + params.insert("scene", m_list.at(index)->id()); + HueBridgeConnection::instance()->put("groups/0/action", params, this, "recallSceneFinished"); +} + +void Scenes::refresh() +{ + HueBridgeConnection::instance()->get("scenes", this, "scenesReceived"); +} + +void Scenes::scenesReceived(int id, const QVariant &variant) +{ + qDebug() << "**** scenes received" << variant; + Q_UNUSED(id) + QVariantMap scenes = variant.toMap(); + QList removedScenes; + foreach (Scene *scene, m_list) { + if (!scenes.contains(scene->id())) { + qDebug() << "removing scene" << scene->id(); + removedScenes.append(scene); + } else { + qDebug() << "updating scene" << scene->id(); + QVariantMap sceneMap = scenes.value(scene->id()).toMap(); + scene->setName(sceneMap.value("name").toString()); + QList lights; + foreach (const QVariant &light, sceneMap.value("lights").toList()) { + lights << light.toInt(); + } + scene->setLights(lights); + } + } + + qDebug() << removedScenes.count() << "scenes removed"; + foreach (Scene *scene, removedScenes) { + int index = m_list.indexOf(scene); + beginRemoveRows(QModelIndex(), index, index); + m_list.takeAt(index)->deleteLater(); + endRemoveRows(); + } + + foreach (const QString &sceneId, scenes.keys()) { + if (findScene(sceneId) == 0) { + QVariantMap sceneMap = scenes.value(sceneId).toMap(); + QList lights; + foreach (const QVariant &light, sceneMap.value("lights").toList()) { + lights << light.toInt(); + } + Scene *scene = createSceneInternal(sceneId, sceneMap.value("name").toString(), lights); + } + } + emit countChanged(); +} + +void Scenes::sceneNameChanged() +{ + Scene *scene = static_cast(sender()); + int idx = m_list.indexOf(scene); + QModelIndex modelIndex = index(idx); + +#if QT_VERSION >= 0x050000 + QVector roles = QVector() + << RoleName; + + emit dataChanged(modelIndex, modelIndex, roles); +#else + emit dataChanged(modelIndex, modelIndex); +#endif +} + +//void Groups::groupLightsChanged() +//{ +// Group *group = static_cast(sender()); +// int idx = m_list.indexOf(group); +// QModelIndex modelIndex = index(idx); + +//#if QT_VERSION >= 0x050000 +// QVector roles = QVector() << RoleLightIds; +// emit dataChanged(modelIndex, modelIndex, roles); +//#else +// emit dataChanged(modelIndex, modelIndex); +//#endif +//} + +void Scenes::createScene(const QString &name, const QList &lights, const QString &userData) +{ + const QString possibleCharacters("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); + + QString randomString; + for(int i=0; i < 10; ++i) { + int index = qrand() % possibleCharacters.length(); + QChar nextChar = possibleCharacters.at(index); + randomString.append(nextChar); + } + updateScene("s" + randomString, name, lights); +} + +void Scenes::updateScene(const QString &id, const QString &name, const QList &lights) +{ + qDebug() << "create scene" << name << lights; + QVariantMap params; + QVariantList lightsList; + foreach (int lightId, lights) { + qDebug() << "got light" << lightId; + lightsList.append(QString::number(lightId)); + } + qDebug() << "lightslist" << lightsList; + params.insert("name", name); + params.insert("lights", lightsList); + HueBridgeConnection::instance()->put("scenes/" + id, params, this, "createSceneFinished"); +} + +Scene *Scenes::createSceneInternal(const QString &id, const QString &name, const QList lights) +{ + Scene *scene = new Scene(id, name, this); + scene->setLights(lights); + + connect(scene, SIGNAL(nameChanged()), this, SLOT(sceneNameChanged())); + connect(scene, SIGNAL(activeChanged()), this, SLOT(sceneActiveChanged())); +// connect(scene, SIGNAL(lightsChanged()), this, SLOT(groupLightsChanged())); + + beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + m_list.append(scene); + endInsertRows(); + return scene; +} + +void Scenes::createSceneFinished(int id, const QVariant &response) +{ + Q_UNUSED(id) + qDebug() << "got createScene result" << response; + + QVariantMap result = response.toList().first().toMap(); + + if (result.contains("success")) { + //TODO: could be added without refrshing, but we don't know the name at this point. + //TODO: might be best to ctor groups/lights with id only and make them fetch their own info. + refresh(); + } +} + +void Scenes::recallSceneFinished(int id, const QVariant &variant) +{ + qDebug() << "scene recalled" << variant; +} + +//void Groups::deleteGroup(int id) +//{ +// HueBridgeConnection::instance()->deleteResource("groups/" + QString::number(id), this, "deleteGroupFinished"); +//} + +//void Groups::deleteGroupFinished(int id, const QVariant &response) +//{ +// Q_UNUSED(id) +// qDebug() << "got deleteGroup result" << response; + +// QVariantMap result = response.toList().first().toMap(); + +// if (result.contains("success")) { +// QString success = result.value("success").toString(); +// if (success.contains("/groups/")) { +// QString groupId = success.mid(success.indexOf("/groups/") + 8); +// groupId = groupId.left(groupId.indexOf(" deleted")); + +// //TODO: could be deleted without refrshing +// refresh(); +// } +// } +//} diff --git a/libhue/scenes.h b/libhue/scenes.h new file mode 100644 index 0000000..ee9c75d --- /dev/null +++ b/libhue/scenes.h @@ -0,0 +1,72 @@ +/* + * Copyright 2015 Michael Zanetti + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authors: + * Michael Zanetti + */ + +#ifndef SCENES_H +#define SCENES_H + +#include + +class Scene; + +class Scenes: public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + enum Roles { + RoleId, + RoleName + }; + + explicit Scenes(QObject *parent = 0); + + int rowCount(const QModelIndex &parent) const; + int count() const; + QVariant data(const QModelIndex &index, int role) const; + QHash roleNames() const; + Q_INVOKABLE Scene* get(int index) const; + Q_INVOKABLE Scene *findScene(const QString &id) const; + + Q_INVOKABLE void recallScene(int index); + +public slots: + Q_INVOKABLE void createScene(const QString &name, const QList &lights, const QString &userData = QString()); + Q_INVOKABLE void updateScene(const QString &id, const QString &name, const QList &lights); + + void refresh(); + +signals: + void countChanged(); + +private slots: + void createSceneFinished(int id, const QVariant &variant); + void recallSceneFinished(int id, const QVariant &variant); +// void deleteGroupFinished(int id, const QVariant &variant); + void scenesReceived(int id, const QVariant &variant); + void sceneNameChanged(); +// void groupLightsChanged(); + +private: + Scene* createSceneInternal(const QString &id, const QString &name, const QList lights); + + QList m_list; +}; + +#endif // SCENES_H diff --git a/libhue/schedule.cpp b/libhue/schedule.cpp new file mode 100644 index 0000000..e30b097 --- /dev/null +++ b/libhue/schedule.cpp @@ -0,0 +1,172 @@ +/* + * Copyright 2015 Michael Zanetti + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authors: + * Michael Zanetti + */ + +#include "schedule.h" +#include "huebridgeconnection.h" + +#include +#include +#include + +Schedule::Schedule(const QString &id, const QString &name, QObject *parent) + : QObject(parent) + , m_id(id) + , m_name(name) + , m_enabled(true) + , m_autodelete(true) + , m_recurring(false) +{ +// refresh(); +} + +QString Schedule::id() const +{ + return m_id; +} + +QString Schedule::name() const +{ + return m_name; +} + +void Schedule::setName(const QString &name) +{ + if (m_name != name) { + m_name = name; + emit nameChanged(); + } +} + +QDateTime Schedule::dateTime() const +{ + return m_dateTime; +} + +void Schedule::setDateTime(const QDateTime &dateTime) +{ + if (m_dateTime != dateTime) { + m_dateTime = dateTime; + emit dateTimeChanged(); + } +} + +QString Schedule::weekdays() const +{ + return m_weekdays; +} + +void Schedule::setWeekdays(const QString &weekdays) +{ + if (m_weekdays != weekdays) { + m_weekdays = weekdays; + emit weekdaysChanged(); + } +} + +bool Schedule::enabled() const +{ + return m_enabled; +} + +void Schedule::setEnabled(bool enabled) +{ + if (m_enabled != enabled) { + m_enabled = enabled; + emit enabledChanged(); + } +} + +bool Schedule::autodelete() const +{ + return m_autodelete; +} + +void Schedule::setAutoDelete(bool autodelete) +{ + if (m_autodelete != autodelete) { + m_autodelete = autodelete; + emit autodeleteChanged(); + } +} + +bool Schedule::recurring() const +{ + return m_recurring; +} + +void Schedule::setRecurring(bool recurring) +{ + if (m_recurring != recurring) { + m_recurring = recurring; + emit recurringChanged(); + } +} + +void Schedule::refresh() +{ +// HueBridgeConnection::instance()->get("groups/" + QString::number(m_id), this, "responseReceived"); +} + +//void Scene::responseReceived(int id, const QVariant &response) +//{ +// Q_UNUSED(id) + +// m_lightIds.clear(); + +// QVariantMap attributes = response.toMap(); +// QVariantList lightsMap = attributes.value("lights").toList(); +// foreach (const QVariant &lightId, lightsMap) { +// m_lightIds << lightId.toUInt(); +// } + +// emit lightsChanged(); + +// QVariantMap action = attributes.value("action").toMap(); +// m_on = action.value("on").toBool(); +// emit stateChanged(); +//} + +//void Group::setDescriptionFinished(int id, const QVariant &response) +//{ +// Q_UNUSED(id) +// qDebug() << "setDescription finished" << response; +// QVariantMap result = response.toList().first().toMap(); + +// if (result.contains("success")) { +// QVariantMap successMap = result.value("success").toMap(); +// if (successMap.contains("/groups/" + QString::number(m_id) + "/name")) { +// m_name = successMap.value("/groups/" + QString::number(m_id) + "/name").toString(); +// emit nameChanged(); +// } +// } +//} + +//void Group::setStateFinished(int id, const QVariant &response) +//{ +// foreach (const QVariant &resultVariant, response.toList()) { +// QVariantMap result = resultVariant.toMap(); +// if (result.contains("success")) { +// QVariantMap successMap = result.value("success").toMap(); +// if (successMap.contains("/groups/" + QString::number(m_id) + "/state/on")) { +// m_on = successMap.value("/groups/" + QString::number(m_id) + "/state/on").toBool(); +// } +// } +// } +// emit stateChanged(); +//} diff --git a/libhue/schedule.h b/libhue/schedule.h new file mode 100644 index 0000000..dc0aa47 --- /dev/null +++ b/libhue/schedule.h @@ -0,0 +1,89 @@ +/* + * Copyright 2015 Michael Zanetti + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authors: + * Michael Zanetti + */ + +#ifndef SCHEDULE_H +#define SCHEDULE_H + +#include +#include +#include + +class Schedule: public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) + Q_PROPERTY(QDateTime dateTime READ dateTime NOTIFY dateTimeChanged) + Q_PROPERTY(QString weekdays READ weekdays NOTIFY weekdaysChanged) + Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged) + Q_PROPERTY(bool autodelete READ autodelete NOTIFY autodeleteChanged) + Q_PROPERTY(bool recurring READ recurring NOTIFY recurringChanged) + +public: + Schedule(const QString &id, const QString &name, QObject *parent = 0); + + QString id() const; + + QString name() const; + void setName(const QString &name); + + QDateTime dateTime() const; + void setDateTime(const QDateTime &dateTime); + + QString weekdays() const; + void setWeekdays(const QString &weekdays); + + bool enabled() const; + void setEnabled(bool enabled); + + bool autodelete() const; + void setAutoDelete(bool autodelete); + + bool recurring() const; + void setRecurring(bool recurring); + +public slots: + void refresh(); + +signals: + void nameChanged(); + void dateTimeChanged(); + void weekdaysChanged(); + void enabledChanged(); + void autodeleteChanged(); + void recurringChanged(); + +private slots: +// void responseReceived(int id, const QVariant &response); +// void setDescriptionFinished(int id, const QVariant &response); + +// void setStateFinished(int id, const QVariant &response); + +private: + QString m_id; + QString m_name; + QDateTime m_dateTime; + QString m_weekdays; + bool m_enabled; + bool m_autodelete; + bool m_recurring; +}; + +#endif diff --git a/libhue/schedules.cpp b/libhue/schedules.cpp new file mode 100644 index 0000000..b5dcfbd --- /dev/null +++ b/libhue/schedules.cpp @@ -0,0 +1,297 @@ +/* + * Copyright 2015 Michael Zanetti + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authors: + * Michael Zanetti + */ + +#include "schedules.h" +#include "schedule.h" + +#include "huebridgeconnection.h" + +#include +#include + +Schedules::Schedules(QObject *parent) + : QAbstractListModel(parent) +{ + connect(HueBridgeConnection::instance(), SIGNAL(connectedBridgeChanged()), this, SLOT(refresh())); + refresh(); + +#if QT_VERSION < 0x050000 + setRoleNames(roleNames()); +#endif +} + +int Schedules::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} + +int Schedules::count() const +{ + return m_list.count(); +} + +QVariant Schedules::data(const QModelIndex &index, int role) const +{ + Schedule *schedule = m_list.at(index.row()); + switch (role) { + case RoleId: + return schedule->id(); + case RoleName: + return schedule->name(); + case RoleDateTime: + return schedule->dateTime(); + case RoleRecurring: + return schedule->recurring(); + case RoleWeekdays: + return schedule->weekdays(); + } + + return QVariant(); +} + +QHash Schedules::roleNames() const +{ + QHash roles; + roles.insert(RoleId, "id"); + roles.insert(RoleName, "name"); + roles.insert(RoleDateTime, "dateTime"); + roles.insert(RoleRecurring, "recurring"); + roles.insert(RoleWeekdays, "weekdays"); + return roles; +} + +Schedule *Schedules::get(int index) const +{ + if (index > -1 && index < m_list.count()) { + return m_list.at(index); + } + return 0; +} + +Schedule *Schedules::findSchedule(const QString &id) const +{ + foreach (Schedule *scene, m_list) { + if (scene->id() == id) { + return scene; + } + } + return 0; +} + +void Schedules::createSingleAlarmForScene(const QString &name, const QString &sceneId, const QDateTime &dateTime) +{ + createAlarmForScene(name, sceneId, dateTime.toString(Qt::ISODate)); +} + +void Schedules::createRecurringAlarmForScene(const QString &name, const QString &sceneId, const QDateTime &time, const QString &weekdays) +{ + QString timeString = "W" + QString::number(weekdays.toInt(0, 2)) + "/T" + time.time().toString(); + createAlarmForScene(name, sceneId, timeString); +} + +//void Scenes::recallScene(int index) +//{ +// QVariantMap params; +// params.insert("scene", m_list.at(index)->id()); +// HueBridgeConnection::instance()->put("groups/0/action", params, this, "recallSceneFinished"); +//} + +void Schedules::refresh() +{ + HueBridgeConnection::instance()->get("schedules", this, "schedulesReceived"); +} + +void Schedules::createAlarmForScene(const QString &name, const QString &sceneId, const QString &timeString) +{ + QVariantMap commandParams; + commandParams.insert("scene", sceneId); + + QVariantMap command; + command.insert("address", "/api/" + HueBridgeConnection::instance()->apiKey() + "/groups/0/action"); + command.insert("method", "PUT"); + command.insert("body", commandParams); + + QVariantMap params; + params.insert("name", name); + params.insert("command", command); + params.insert("localtime", timeString); + HueBridgeConnection::instance()->post("schedules", params, this, "createScheduleFinished"); +} + +void Schedules::schedulesReceived(int id, const QVariant &variant) +{ + qDebug() << "**** schedules received" << variant; + Q_UNUSED(id) + QVariantMap schedules = variant.toMap(); + QList removedSchedules; + foreach (Schedule *schedule, m_list) { + if (!schedules.contains(schedule->id())) { + qDebug() << "removing schedule" << schedule->id(); + removedSchedules.append(schedule); + } else { + qDebug() << "updating schedule" << schedule->id(); + QVariantMap scheduleMap = schedules.value(schedule->id()).toMap(); + schedule->setName(scheduleMap.value("name").toString()); + schedule->setEnabled(scheduleMap.value("status").toString() == "enabled"); + schedule->setAutoDelete(scheduleMap.value("autodelete").toBool()); + schedule->setDateTime(scheduleMap.value("time").toDateTime()); + } + } + + qDebug() << removedSchedules.count() << "schedules removed"; + foreach (Schedule *schedule, removedSchedules) { + int index = m_list.indexOf(schedule); + beginRemoveRows(QModelIndex(), index, index); + m_list.takeAt(index)->deleteLater(); + endRemoveRows(); + } + + foreach (const QString &scheduleId, schedules.keys()) { + if (findSchedule(scheduleId) == 0) { + QVariantMap scheduleMap = schedules.value(scheduleId).toMap(); + Schedule *schedule = createScheduleInternal(scheduleId, scheduleMap.value("name").toString()); + schedule->setEnabled(scheduleMap.value("status").toString() == "enabled"); + schedule->setAutoDelete(scheduleMap.value("autodelete").toBool()); + QString timeString = scheduleMap.value("time").toString(); + if (timeString.startsWith("W")) { + schedule->setRecurring(true); + timeString = timeString.right(timeString.length() - 1); + schedule->setWeekdays(QString("0%1").arg(timeString.left(3).toInt(), 7, 2, QChar('0'))); + qDebug() << "fooo" << schedule->weekdays(); + timeString = timeString.right(timeString.length() - 5); + QDateTime dateTime = QDateTime::fromMSecsSinceEpoch(0); + dateTime.setTime(QTime::fromString(timeString)); + schedule->setDateTime(dateTime); + } else { + schedule->setDateTime(scheduleMap.value("time").toDateTime()); + } + qDebug() << "timeString" << timeString << QDateTime::fromString(timeString) << schedule->weekdays(); + } + } + emit countChanged(); +} + +//void Schedules::sceneNameChanged() +//{ +// Scene *scene = static_cast(sender()); +// int idx = m_list.indexOf(scene); +// QModelIndex modelIndex = index(idx); + +//#if QT_VERSION >= 0x050000 +// QVector roles = QVector() +// << RoleName; + +// emit dataChanged(modelIndex, modelIndex, roles); +//#else +// emit dataChanged(modelIndex, modelIndex); +//#endif +//} + +//void Groups::groupLightsChanged() +//{ +// Group *group = static_cast(sender()); +// int idx = m_list.indexOf(group); +// QModelIndex modelIndex = index(idx); + +//#if QT_VERSION >= 0x050000 +// QVector roles = QVector() << RoleLightIds; +// emit dataChanged(modelIndex, modelIndex, roles); +//#else +// emit dataChanged(modelIndex, modelIndex); +//#endif +//} + +void Schedules::createSchedule(const QString &name, const QList &lights) +{ + const QString possibleCharacters("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); + + QString randomString; + for(int i=0; i < 12; ++i) { + int index = qrand() % possibleCharacters.length(); + QChar nextChar = possibleCharacters.at(index); + randomString.append(nextChar); + } + updateSchedule("schedule" + randomString, name, lights); +} + +void Schedules::updateSchedule(const QString &id, const QString &name, const QList &lights) +{ + qDebug() << "create schedule" << name << lights; + QVariantMap params; + QVariantList lightsList; + foreach (int lightId, lights) { + qDebug() << "got light" << lightId; + lightsList.append(QString::number(lightId)); + } + qDebug() << "lightslist" << lightsList; + params.insert("name", name); + params.insert("lights", lightsList); + HueBridgeConnection::instance()->put("scenes/" + id, params, this, "createSceneFinished"); +} + +void Schedules::deleteSchedule(const QString &id) +{ + HueBridgeConnection::instance()->deleteResource("schedules/" + id, this, "deleteScheduleFinished"); +} + +Schedule *Schedules::createScheduleInternal(const QString &id, const QString &name) +{ + Schedule *schedule = new Schedule(id, name, this); + + connect(schedule, SIGNAL(nameChanged()), this, SLOT(sceneNameChanged())); + connect(schedule, SIGNAL(activeChanged()), this, SLOT(sceneActiveChanged())); + + beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + m_list.append(schedule); + endInsertRows(); + return schedule; +} + +void Schedules::createScheduleFinished(int id, const QVariant &response) +{ + Q_UNUSED(id) + qDebug() << "got createScene result" << response; + + QVariantMap result = response.toList().first().toMap(); + + if (result.contains("success")) { + //TODO: could be added without refrshing, but we don't know the name at this point. + //TODO: might be best to ctor groups/lights with id only and make them fetch their own info. + refresh(); + } +} + +//void Scenes::recallSceneFinished(int id, const QVariant &variant) +//{ +// qDebug() << "scene recalled" << variant; +//} + +void Schedules::deleteScheduleFinished(int id, const QVariant &response) +{ + Q_UNUSED(id) + qDebug() << "got deleteSchedule result" << response; + + QVariantMap result = response.toList().first().toMap(); + + if (result.contains("success")) { + //TODO: could be deleted without refrshing + refresh(); + } +} diff --git a/libhue/schedules.h b/libhue/schedules.h new file mode 100644 index 0000000..511dec1 --- /dev/null +++ b/libhue/schedules.h @@ -0,0 +1,79 @@ +/* + * Copyright 2015 Michael Zanetti + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; version 2. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Authors: + * Michael Zanetti + */ + +#ifndef SCHEDULES_H +#define SCHEDULES_H + +#include + +class Schedule; + +class Schedules: public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + enum Roles { + RoleId, + RoleName, + RoleDateTime, + RoleRecurring, + RoleWeekdays + }; + + explicit Schedules(QObject *parent = 0); + + int rowCount(const QModelIndex &parent) const; + int count() const; + QVariant data(const QModelIndex &index, int role) const; + QHash roleNames() const; + Q_INVOKABLE Schedule* get(int index) const; + Q_INVOKABLE Schedule* findSchedule(const QString &id) const; + +// Q_INVOKABLE void recallScene(int index); + +public slots: + Q_INVOKABLE void createSingleAlarmForScene(const QString &name, const QString &sceneId, const QDateTime &dateTime); + Q_INVOKABLE void createRecurringAlarmForScene(const QString &name, const QString &sceneId, const QDateTime &time, const QString &weekdays); + Q_INVOKABLE void createSchedule(const QString &name, const QList &lights); + Q_INVOKABLE void updateSchedule(const QString &id, const QString &name, const QList &lights); + Q_INVOKABLE void deleteSchedule(const QString &id); + + void refresh(); + +signals: + void countChanged(); + +private slots: + void createAlarmForScene(const QString &name, const QString &sceneId, const QString &timeString); + void createScheduleFinished(int id, const QVariant &variant); +// void recallSceneFinished(int id, const QVariant &variant); + void deleteScheduleFinished(int id, const QVariant &variant); + void schedulesReceived(int id, const QVariant &variant); +// void sceneNameChanged(); +//// void groupLightsChanged(); + +private: + Schedule* createScheduleInternal(const QString &id, const QString &name); + + QList m_list; +}; + +#endif // SCENES_H diff --git a/plugin/Hue/hueplugin.cpp b/plugin/Hue/hueplugin.cpp index 52e0788..f6bdfd9 100644 --- a/plugin/Hue/hueplugin.cpp +++ b/plugin/Hue/hueplugin.cpp @@ -25,6 +25,10 @@ #include "../../libhue/light.h" #include "../../libhue/groups.h" #include "../../libhue/group.h" +#include "../../libhue/scenes.h" +#include "../../libhue/scene.h" +#include "../../libhue/schedules.h" +#include "../../libhue/schedule.h" #include "../../libhue/lightsfiltermodel.h" #if QT_VERSION >= 0x050000 @@ -53,6 +57,10 @@ void HuePlugin::registerTypes(const char *uri) qmlRegisterUncreatableType(uri, 0, 1, "Light", "Cannot create lights. Get them from the Lights model."); qmlRegisterUncreatableType(uri, 0, 1, "LightInterface", "Abstract interface."); qmlRegisterType(uri, 0, 1, "Groups"); + qmlRegisterType(uri, 0, 1, "Scenes"); + qmlRegisterUncreatableType(uri, 0, 1, "Scene", "Get create Scene objects. Get them from the Scenes model."); + qmlRegisterType(uri, 0, 1, "Schedules"); + qmlRegisterUncreatableType(uri, 0, 1, "Schedule", "Get create Schedule objects. Get them from the Schedules model."); //FIXME: eventually creatable qmlRegisterUncreatableType(uri, 0, 1, "Group", "Cannot create groups. Get them from the Groups model."); qmlRegisterType(uri, 0, 1, "LightsFilterModel");