forked from platypush/platypush
Migrated DateTimeWeather and Calendar widgets
This commit is contained in:
parent
ba8e5ef6a0
commit
62b651789a
14 changed files with 711 additions and 200 deletions
|
@ -4,11 +4,6 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<script type="text/javascript">
|
|
||||||
window.config = JSON.parse(`{
|
|
||||||
"websocketPort": "{{ websocket_port }}"
|
|
||||||
}`)
|
|
||||||
</script>
|
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -1,43 +1,43 @@
|
||||||
<template>
|
<template>
|
||||||
<router-view />
|
<Events ref="events" :ws-port="config['backend.http'].websocket_port" v-if="config['backend.http']" />
|
||||||
<Notifications ref="notifications" />
|
<Notifications ref="notifications" />
|
||||||
<Events ref="events" />
|
<router-view />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Notifications from "@/components/Notifications";
|
import Notifications from "@/components/Notifications";
|
||||||
import Utils from "@/Utils";
|
import Utils from "@/Utils";
|
||||||
import { bus } from '@/bus';
|
|
||||||
import Events from "@/Events";
|
import Events from "@/Events";
|
||||||
|
import { bus } from "@/bus";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {Events, Notifications},
|
components: {Events, Notifications},
|
||||||
mixins: [Utils],
|
mixins: [Utils],
|
||||||
|
|
||||||
computed: {
|
data() {
|
||||||
config() {
|
return {
|
||||||
const cfg = {
|
config: {},
|
||||||
websocketPort: 8009,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._config.websocketPort && !this._config.websocketPort.startsWith('{{')) {
|
|
||||||
cfg.websocketPort = parseInt(this._config.websocketPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onNotification(notification) {
|
onNotification(notification) {
|
||||||
this.$refs.notifications.create(notification)
|
this.$refs.notifications.create(notification)
|
||||||
|
},
|
||||||
|
|
||||||
|
async initConfig() {
|
||||||
|
this.config = await this.request('config.get')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.initConfig()
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
bus.on('notification-create', this.onNotification)
|
bus.on('notification-create', this.onNotification)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
<template>
|
|
||||||
<div id="__platypush_events"/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { bus } from "@/bus";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Events",
|
name: "Events",
|
||||||
|
props: {
|
||||||
|
wsPort: {
|
||||||
|
type: Number,
|
||||||
|
default: 8009,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
ws: null,
|
ws: null,
|
||||||
|
@ -12,108 +17,144 @@ export default {
|
||||||
opened: false,
|
opened: false,
|
||||||
timeout: null,
|
timeout: null,
|
||||||
reconnectMsecs: 30000,
|
reconnectMsecs: 30000,
|
||||||
handlers: [],
|
handlers: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
onWebsocketTimeout() {
|
||||||
|
return function() {
|
||||||
|
console.log('Websocket reconnection timed out, retrying')
|
||||||
|
this.pending = false
|
||||||
|
this.close()
|
||||||
|
this.onclose()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onMessage(event) {
|
||||||
|
const handlers = []
|
||||||
|
event = event.data
|
||||||
|
|
||||||
|
if (typeof event === 'string') {
|
||||||
|
try {
|
||||||
|
event = JSON.parse(event)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Received invalid non-JSON event')
|
||||||
|
console.warn(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(event)
|
||||||
|
if (event.type !== 'event') {
|
||||||
|
// Discard non-event messages
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null in this.handlers) {
|
||||||
|
handlers.push(this.handlers[null])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.args.type in this.handlers) {
|
||||||
|
handlers.push(...this.handlers[event.args.type])
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [handler] of handlers) {
|
||||||
|
handler(event.args)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onOpen() {
|
||||||
|
if (this.opened) {
|
||||||
|
console.log("There's already an opened websocket connection, closing the newly opened one")
|
||||||
|
this.onclose = () => {}
|
||||||
|
this.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Websocket connection successful')
|
||||||
|
this.opened = true
|
||||||
|
|
||||||
|
if (this.pending) {
|
||||||
|
this.pending = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout)
|
||||||
|
this.timeout = undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onError(error) {
|
||||||
|
console.error('Websocket error')
|
||||||
|
console.error(error)
|
||||||
|
},
|
||||||
|
|
||||||
|
onClose(event) {
|
||||||
|
if (event) {
|
||||||
|
console.log('Websocket closed - code: ' + event.code + ' - reason: ' + event.reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.opened = false
|
||||||
|
|
||||||
|
if (!this.pending) {
|
||||||
|
this.pending = true
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
try {
|
try {
|
||||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws'
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws'
|
||||||
const url = `${protocol}://${location.hostname}:${this.$root.config.websocketPort}`
|
const url = `${protocol}://${location.hostname}:${this.wsPort}`
|
||||||
this.ws = new WebSocket(url)
|
this.ws = new WebSocket(url)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Websocket initialization error')
|
console.error('Websocket initialization error')
|
||||||
console.log(err)
|
console.error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pending = true
|
this.pending = true
|
||||||
|
this.timeout = setTimeout(this.onWebsocketTimeout, this.reconnectMsecs)
|
||||||
|
this.ws.onmessage = this.onMessage
|
||||||
|
this.ws.onopen = this.onOpen
|
||||||
|
this.ws.onerror = this.onError
|
||||||
|
this.ws.onclose = this.onClose
|
||||||
|
},
|
||||||
|
|
||||||
const onWebsocketTimeout = function(self) {
|
subscribe(msg) {
|
||||||
return function() {
|
const handler = msg.handler
|
||||||
console.log('Websocket reconnection timed out, retrying')
|
const events = msg.events.length ? msg.events : [null]
|
||||||
this.pending = false
|
|
||||||
self.close()
|
for (const event of events) {
|
||||||
self.onclose()
|
if (!(event in this.handlers)) {
|
||||||
|
this.handlers[event] = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.handlers[event].push(handler)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
this.timeout = setTimeout(
|
unsubscribe(msg) {
|
||||||
onWebsocketTimeout(this.ws), this.reconnectMsecs)
|
const handler = msg.handler
|
||||||
|
const events = msg.events.length ? msg.events : [null]
|
||||||
|
|
||||||
this.ws.onmessage = (event) => {
|
for (const event of events) {
|
||||||
const handlers = []
|
if (!(event in this.handlers))
|
||||||
event = event.data
|
continue
|
||||||
|
|
||||||
if (typeof event === 'string') {
|
const idx = this.handlers[event].indexOf(handler)
|
||||||
try {
|
if (idx < 0)
|
||||||
event = JSON.parse(event)
|
continue
|
||||||
} catch (e) {
|
|
||||||
console.warn('Received invalid non-JSON event')
|
|
||||||
console.warn(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug(event)
|
this.handlers[event] = this.handlers[event].splice(idx, 1)[1]
|
||||||
if (event.type !== 'event') {
|
if (!this.handlers[event].length)
|
||||||
// Discard non-event messages
|
delete this.handlers[event]
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null in this.handlers) {
|
|
||||||
handlers.push(this.handlers[null])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.args.type in this.handlers) {
|
|
||||||
handlers.push(...this.handlers[event.args.type])
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const handler of handlers) {
|
|
||||||
handler(event.args)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
this.ws.onopen = () => {
|
|
||||||
if (this.opened) {
|
|
||||||
console.log("There's already an opened websocket connection, closing the newly opened one")
|
|
||||||
this.onclose = () => {}
|
|
||||||
this.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Websocket connection successful')
|
|
||||||
this.opened = true
|
|
||||||
|
|
||||||
if (this.pending) {
|
|
||||||
this.pending = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.timeout) {
|
|
||||||
clearTimeout(this.timeout)
|
|
||||||
this.timeout = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ws.onerror = (event) => {
|
|
||||||
console.error(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ws.onclose = (event) => {
|
|
||||||
if (event) {
|
|
||||||
console.log('Websocket closed - code: ' + event.code + ' - reason: ' + event.reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.opened = false
|
|
||||||
|
|
||||||
if (!this.pending) {
|
|
||||||
this.pending = true
|
|
||||||
this.init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
created() {
|
||||||
|
bus.on('subscribe', this.subscribe)
|
||||||
|
bus.on('unsubscribe', this.unsubscribe)
|
||||||
this.init()
|
this.init()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import Api from "@/utils/Api";
|
import Api from "@/utils/Api";
|
||||||
|
import DateTime from "@/utils/DateTime";
|
||||||
|
import Events from "@/utils/Events";
|
||||||
import Notification from "@/utils/Notification";
|
import Notification from "@/utils/Notification";
|
||||||
|
import Types from "@/utils/Types";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Utils",
|
name: "Utils",
|
||||||
mixins: [Api, Notification],
|
mixins: [Api, Notification, Events, DateTime, Types],
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
84
platypush/backend/http/webapp/src/components/Sensor.vue
Normal file
84
platypush/backend/http/webapp/src/components/Sensor.vue
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<div class="sensor">
|
||||||
|
<div class="label-container col-6" v-if="iconClass || name">
|
||||||
|
<i :class="iconClass" v-if="iconClass" />
|
||||||
|
<span v-text="name" v-else-if="name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="value-container col-6">
|
||||||
|
<span class="value" v-text="_value" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Sensor",
|
||||||
|
props: {
|
||||||
|
// The FontAwesome icon class to be used.
|
||||||
|
iconClass: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// The name of the sensor metric.
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sensor value.
|
||||||
|
value: {
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sensor unit.
|
||||||
|
unit: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Number of decimal units.
|
||||||
|
decimals: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Set to true if the sensor holds a binary value.
|
||||||
|
isBoolean: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
_value() {
|
||||||
|
if (this.value == null)
|
||||||
|
return 'N/A'
|
||||||
|
|
||||||
|
if (this.isBoolean)
|
||||||
|
return this.parseBoolean(this.value)
|
||||||
|
|
||||||
|
let value = parseFloat(this.value)
|
||||||
|
if (this.decimals != null)
|
||||||
|
value = value.toFixed(this.decimals)
|
||||||
|
if (this.unit)
|
||||||
|
value = `${value}${this.unit}`
|
||||||
|
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.label-container {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-container {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
14
platypush/backend/http/webapp/src/utils/DateTime.vue
Normal file
14
platypush/backend/http/webapp/src/utils/DateTime.vue
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "DateTime",
|
||||||
|
methods: {
|
||||||
|
formatDate(date) {
|
||||||
|
return date.toDateString().substring(0, 10)
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime(date, seconds=true) {
|
||||||
|
return date.toTimeString().substring(0, seconds ? 8 : 5)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
15
platypush/backend/http/webapp/src/utils/Events.vue
Normal file
15
platypush/backend/http/webapp/src/utils/Events.vue
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<script>
|
||||||
|
import { bus } from "@/bus";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Events",
|
||||||
|
methods: {
|
||||||
|
subscribe(handler, ...events) {
|
||||||
|
bus.emit('subscribe', {
|
||||||
|
events: events,
|
||||||
|
handler: handler,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { bus } from '@/bus';
|
import { bus } from "@/bus";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Notification",
|
name: "Notification",
|
||||||
|
|
20
platypush/backend/http/webapp/src/utils/Types.vue
Normal file
20
platypush/backend/http/webapp/src/utils/Types.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Types",
|
||||||
|
methods: {
|
||||||
|
parseBoolean(value) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
value = value.toLowerCase()
|
||||||
|
if (value === 'true')
|
||||||
|
return true
|
||||||
|
if (value === 'false')
|
||||||
|
return false
|
||||||
|
|
||||||
|
return !!parseInt(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
120
platypush/backend/http/webapp/src/widgets/Calendar/Index.vue
Normal file
120
platypush/backend/http/webapp/src/widgets/Calendar/Index.vue
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
<template>
|
||||||
|
<div class="calendar">
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
|
||||||
|
<div class="no-events" v-else-if="!events.length">
|
||||||
|
No events found
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="event upcoming-event" v-else-if="events.length > 0">
|
||||||
|
<div class="date" v-text="formatDate(events[0].start)"></div>
|
||||||
|
<div class="summary" v-text="events[0].summary"></div>
|
||||||
|
<div class="time">
|
||||||
|
{{ formatTime(events[0].start, false) }} -
|
||||||
|
{{ formatTime(events[0].end, false) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="event-list" v-if="events.length > 1">
|
||||||
|
<div class="event" v-for="event in events.slice(1, maxEvents)" :key="event.id">
|
||||||
|
<div class="date col-2" v-text="formatDate(event.start)"></div>
|
||||||
|
<div class="time col-2" v-text="formatTime(event.start, false)"></div>
|
||||||
|
<div class="summary col-8" v-text="event.summary"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Calendar",
|
||||||
|
components: {Loading},
|
||||||
|
mixins: [Utils],
|
||||||
|
props: {
|
||||||
|
// Maximum number of events to be rendered.
|
||||||
|
maxEvents: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Refresh interval in seconds.
|
||||||
|
refreshSeconds: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
refresh: async function() {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.events = (await this.request('calendar.get_upcoming_events')).map(event => {
|
||||||
|
if (event.start)
|
||||||
|
event.start = new Date(event.start.dateTime || event.start.date)
|
||||||
|
if (event.end)
|
||||||
|
event.end = new Date(event.end.dateTime || event.end.date)
|
||||||
|
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted: function() {
|
||||||
|
this.refresh()
|
||||||
|
setInterval(this.refresh, parseInt((this.refreshSeconds*1000).toFixed(0)))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.calendar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.no-events {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event {
|
||||||
|
font-size: .95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-list {
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upcoming-event {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: .15em;
|
||||||
|
font-size: 1.2em;
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
78
platypush/backend/http/webapp/src/widgets/DateTime/Index.vue
Normal file
78
platypush/backend/http/webapp/src/widgets/DateTime/Index.vue
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<div class="date-time">
|
||||||
|
<div class="date" v-text="formatDate(now)" v-if="_showDate" />
|
||||||
|
<div class="time" v-text="formatTime(now, _showSeconds)" v-if="_showTime" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
|
||||||
|
// Widget to show date and time
|
||||||
|
export default {
|
||||||
|
name: 'DateTime',
|
||||||
|
mixins: [Utils],
|
||||||
|
props: {
|
||||||
|
// If false then don't display the date.
|
||||||
|
showDate: {
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then don't display the time.
|
||||||
|
showTime: {
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then don't display the seconds.
|
||||||
|
showSeconds: {
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
_showTime() {
|
||||||
|
return this.parseBoolean(this.showTime)
|
||||||
|
},
|
||||||
|
|
||||||
|
_showDate() {
|
||||||
|
return this.parseBoolean(this.showDate)
|
||||||
|
},
|
||||||
|
|
||||||
|
_showSeconds() {
|
||||||
|
return this.parseBoolean(this.showSeconds)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
now: new Date(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
refreshTime() {
|
||||||
|
this.now = new Date()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted: function() {
|
||||||
|
this.refreshTime()
|
||||||
|
setInterval(this.refreshTime, 1000)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.date-time {
|
||||||
|
.date {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,30 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="date-time-weather">
|
<div class="date-time-weather">
|
||||||
<div class="date" v-text="formatDate(now)"></div>
|
<div class="row date-time-container">
|
||||||
<div class="time" v-text="formatTime(now)"></div>
|
<DateTime :show-date="_showDate" :show-time="_showTime" :animate="animate"
|
||||||
|
v-if="_showDate || _showTime" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 class="weather">
|
<div class="row weather-container">
|
||||||
<skycons :condition="weatherIcon" :paused="animPaused" :size="iconSize" v-if="weatherIcon" />
|
<Weather :show-summary="_showSummary" :animate="_animate" :icon-size="iconSize"
|
||||||
<span class="temperature" v-if="weather">
|
:refresh-seconds="weatherRefreshSeconds" v-if="showWeather"/>
|
||||||
{{ Math.round(parseFloat(weather.temperature)) + '°' }}
|
</div>
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="summary" v-if="weather && weather.summary" v-text="weather.summary"></div>
|
<div class="row sensors-container">
|
||||||
|
<div class="row" v-if="_showSensors && Object.keys(sensors).length">
|
||||||
|
<div class="col-3">
|
||||||
|
<Sensor icon-class="fas fa-thermometer-half" :value="sensors.temperature" unit="°"
|
||||||
|
v-if="sensors.temperature != null" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="sensors" v-if="Object.keys(sensors).length">
|
<div class="col-6"> </div>
|
||||||
<div class="sensor temperature col-6" v-if="sensors.temperature">
|
|
||||||
<i class="fas fa-thermometer-half"></i>
|
|
||||||
<span class="temperature">
|
|
||||||
{{ parseFloat(sensors.temperature).toFixed(1) + '°' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sensor humidity col-6" v-if="sensors.humidity">
|
<div class="col-3">
|
||||||
<i class="fa fa-tint"></i>
|
<Sensor icon-class="fas fa-tint" :value="sensors.humidity" unit="%"
|
||||||
<span class="humidity">
|
v-if="sensors.humidity != null" />
|
||||||
{{ parseFloat(sensors.humidity).toFixed(1) + '%' }}
|
</div>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,88 +30,128 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Utils from "@/Utils";
|
import Utils from "@/Utils";
|
||||||
import Skycons from "vue-skycons"
|
import DateTime from "@/widgets/DateTime/Index";
|
||||||
|
import Weather from "@/widgets/Weather/Index";
|
||||||
|
import Sensor from "@/components/Sensor";
|
||||||
|
|
||||||
// Widget to show date, time, weather and temperature information
|
// Widget to show date, time, weather and temperature information
|
||||||
export default {
|
export default {
|
||||||
name: 'DateTimeWeather',
|
name: 'DateTimeWeather',
|
||||||
mixins: [Utils],
|
mixins: [Utils],
|
||||||
components: {Skycons},
|
components: {Sensor, DateTime, Weather},
|
||||||
props: {
|
props: {
|
||||||
// If false then the weather icon will be animated.
|
// If false then the weather icon will be animated.
|
||||||
// Otherwise, it will be a static image.
|
// Otherwise, it will be a static image.
|
||||||
paused: {
|
animate: {
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Size of the weather icon in pixels
|
// Size of the weather icon in pixels.
|
||||||
iconSize: {
|
iconSize: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: 50,
|
default: 50,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// If false then don't display the date.
|
||||||
|
showDate: {
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then don't display the time.
|
||||||
|
showTime: {
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then don't display the weather.
|
||||||
|
showWeather: {
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then the weather summary won't be displayed.
|
||||||
|
showSummary: {
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then temperature/humidity sensor data won't be shown.
|
||||||
|
showSensors: {
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Name of the attribute on a received SensorDataChangeEvent that
|
||||||
|
// represents the temperature value to be rendered.
|
||||||
|
sensorTemperatureAttr: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'temperature',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Name of the attribute on a received SensorDataChangeEvent that
|
||||||
|
// represents the humidity value to be rendered.
|
||||||
|
sensorHumidityAttr: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'humidity',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Weather refresh interval in seconds.
|
||||||
|
weatherRefreshSeconds: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 900,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
animPaused() {
|
_showDate() {
|
||||||
return !!parseInt(this.paused)
|
return this.parseBoolean(this.showDate)
|
||||||
|
},
|
||||||
|
|
||||||
|
_showTime() {
|
||||||
|
return this.parseBoolean(this.showTime)
|
||||||
|
},
|
||||||
|
|
||||||
|
_showWeather() {
|
||||||
|
return this.parseBoolean(this.showWeather)
|
||||||
|
},
|
||||||
|
|
||||||
|
_showSummary() {
|
||||||
|
return this.parseBoolean(this.showSummary)
|
||||||
|
},
|
||||||
|
|
||||||
|
_showSensors() {
|
||||||
|
return this.parseBoolean(this.showSensors)
|
||||||
|
},
|
||||||
|
|
||||||
|
_animate() {
|
||||||
|
return this.parseBoolean(this.animate)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
data: function() {
|
||||||
return {
|
return {
|
||||||
weather: undefined,
|
|
||||||
sensors: {},
|
sensors: {},
|
||||||
now: new Date(),
|
|
||||||
weatherIcon: undefined,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async refresh() {
|
|
||||||
const weather = (await this.request('weather.darksky.get_hourly_forecast')).data[0]
|
|
||||||
this.onWeatherChange(weather)
|
|
||||||
},
|
|
||||||
|
|
||||||
refreshTime() {
|
|
||||||
this.now = new Date()
|
|
||||||
},
|
|
||||||
|
|
||||||
formatDate(date) {
|
|
||||||
return date.toDateString().substring(0, 10)
|
|
||||||
},
|
|
||||||
|
|
||||||
formatTime(date) {
|
|
||||||
return date.toTimeString().substring(0, 8)
|
|
||||||
},
|
|
||||||
|
|
||||||
onWeatherChange(event) {
|
|
||||||
if (!this.weather)
|
|
||||||
this.weather = {}
|
|
||||||
|
|
||||||
this.weather = {...this.weather, ...event}
|
|
||||||
this.weatherIcon = this.weather.icon
|
|
||||||
},
|
|
||||||
|
|
||||||
onSensorData(event) {
|
onSensorData(event) {
|
||||||
if ('temperature' in event.data)
|
if (this.sensorTemperatureAttr in event.data)
|
||||||
this.sensors.temperature = event.data.temperature
|
this.sensors.temperature = event.data.temperature
|
||||||
|
|
||||||
if ('humidity' in event.data)
|
if (this.sensorHumidityAttr in event.data)
|
||||||
this.sensors.temperature = event.data.humidity
|
this.sensors.humidity = event.data.humidity
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted: function() {
|
mounted() {
|
||||||
this.refresh()
|
this.subscribe(this.onSensorData, 'platypush.message.event.sensor.SensorDataChangeEvent');
|
||||||
setInterval(this.refresh, 900000)
|
|
||||||
setInterval(this.refreshTime, 1000)
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// registerEventHandler(this.onWeatherChange, 'platypush.message.event.weather.NewWeatherConditionEvent')
|
|
||||||
// registerEventHandler(this.onSensorData, 'platypush.message.event.sensor.SensorDataChangeEvent')
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -126,42 +164,27 @@ export default {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 0.1em;
|
padding-top: 0.1em;
|
||||||
|
|
||||||
.date {
|
.row {
|
||||||
font-size: 1.3em;
|
text-align: center;
|
||||||
height: 10%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.date-time-container {
|
||||||
font-size: 2em;
|
height: 40%;
|
||||||
height: 14%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.weather {
|
.weather-container {
|
||||||
height: 25%;
|
height: 40%;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 15%;
|
|
||||||
|
|
||||||
.temperature {
|
|
||||||
font-size: 3.1em;
|
|
||||||
margin-left: 0.4em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.sensors-container {
|
||||||
height: 28%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sensors {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 13%;
|
height: 20%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.sensor {
|
.row {
|
||||||
padding: 0 0.1em;
|
width: 100%;
|
||||||
}
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
.humidity {
|
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
114
platypush/backend/http/webapp/src/widgets/Weather/Index.vue
Normal file
114
platypush/backend/http/webapp/src/widgets/Weather/Index.vue
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
<template>
|
||||||
|
<div class="weather">
|
||||||
|
<Loading v-if="loading" />
|
||||||
|
|
||||||
|
<h1 v-else>
|
||||||
|
<skycons :condition="weatherIcon" :paused="animate" :size="iconSize" v-if="weatherIcon" />
|
||||||
|
<span class="temperature" v-if="weather">
|
||||||
|
{{ Math.round(parseFloat(weather.temperature)) + '°' }}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="summary" v-if="_showSummary && weather && weather.summary" v-text="weather.summary"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Utils from "@/Utils";
|
||||||
|
import Skycons from "vue-skycons"
|
||||||
|
import Loading from "@/components/Loading";
|
||||||
|
|
||||||
|
// Widget to show date, time, weather and temperature information
|
||||||
|
export default {
|
||||||
|
name: 'Weather',
|
||||||
|
mixins: [Utils],
|
||||||
|
components: {Loading, Skycons},
|
||||||
|
props: {
|
||||||
|
// If false then the weather icon will be animated.
|
||||||
|
// Otherwise, it will be a static image.
|
||||||
|
animate: {
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Size of the weather icon in pixels.
|
||||||
|
iconSize: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 50,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If false then the weather summary won't be displayed.
|
||||||
|
showSummary: {
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Refresh interval in seconds.
|
||||||
|
refreshSeconds: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 900,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
weather: undefined,
|
||||||
|
weatherIcon: undefined,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
_showSummary() {
|
||||||
|
return this.parseBoolean(this.showSummary)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async refresh() {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const weather = (await this.request('weather.darksky.get_hourly_forecast')).data[0]
|
||||||
|
this.onWeatherChange(weather)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onWeatherChange(event) {
|
||||||
|
if (!this.weather)
|
||||||
|
this.weather = {}
|
||||||
|
|
||||||
|
this.weather = {...this.weather, ...event}
|
||||||
|
this.weatherIcon = this.weather.icon
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted: function() {
|
||||||
|
this.refresh()
|
||||||
|
this.subscribe(this.onWeatherChange, 'platypush.message.event.weather.NewWeatherConditionEvent')
|
||||||
|
setInterval(this.refresh, parseInt((this.refreshSeconds*1000).toFixed(0)))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.weather {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature {
|
||||||
|
font-size: 3.1em;
|
||||||
|
margin-left: 0.4em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -15,7 +15,11 @@ export default {
|
||||||
.widget {
|
.widget {
|
||||||
background: $background-color;
|
background: $background-color;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-bottom: 1em;
|
margin: 0 1em 1em 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 3px 3px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08);
|
box-shadow: 0 3px 3px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue