From 61aad2ca78379ace6aa99830ba6e3ea45fe34cba Mon Sep 17 00:00:00 2001 From: "Merza, Leonardo (lm240n)" Date: Thu, 13 Dec 2018 10:52:40 -0500 Subject: [PATCH] first commit --- README.md | 71 +++++++ calendar-card.js | 440 ++++++++++++++++++++++++++++++++++++++++++++ custom_updater.json | 9 + 3 files changed, 520 insertions(+) create mode 100644 README.md create mode 100644 calendar-card.js create mode 100644 custom_updater.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..8563a03 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# Calendar Card for Home Assistant +![](https://i.imgur.com/egQofZM.png "Layout") +() + +### Features +* show the next 5 events on your Google Calendar (default set by home assistant) +* Set custom time format for each event +* click on event to open in your Google calendar app +* Integrate multiple calendars +* Update notifications via custom_updater + +### Track Updates +This custom card can be tracked with the help of [custom-updater](https://github.com/custom-components/custom_updater). + +In your configuration.yaml + +``` +custom_updater: + card_urls: + - https://raw.githubusercontent.com/ljmerza/homeassistant-lovelace-google-calendar-card/master/custom_updater.json +``` + +## Usage +### Prerequisites +You should have setup Google calendar integration or Caldav integration in HomeAssistant. + +## Options + +| Name | Type | Requirement | Description +| ---- | ---- | ------- | ----------- +| type | string | **Required** | `custom:calendar-card` +| showProgressBar | boolean | **Optional** | `true` Option to show the progress bar +| numberOfDays | number | **Optional** | `7` Number of days to display from calendars +| entities | object | **Required** | List of calendars to display +| timeFormat | object | **Optional** | `HH:mm` Format to show event time (see [here](https://momentjs.com/docs/#/displaying/format/) for options) + +### Configuration +Go to your config directory and create a www folder. Inside the www run + +``` +git clone https://github.com/ljmerza/homeassistant-lovelace-google-calendar-card.git +``` + +In your ui-lovelace.yaml + +``` +resources: + - url: https://unpkg.com/moment@2.23.0/moment.js + type: js + - url: /local/homeassistant-lovelace-google-calendar-card/calendar-card.js?v=1.1.0 + type: module +``` + +Add the custom card to views: + +``` +views: + - type: custom:calendar-card + name: "My Calendar" + numberOfDays: 14 + entities: + - calendar.ljmerzagmailcom +``` + +### You want more than 5 Google events? +``` +mkdir /config/custom_components/calendar +cd /config/custom_components/calendar +wget https://raw.githubusercontent.com/home-assistant/home-assistant/dev/homeassistant/components/calendar/google.py +``` +Use a text editor to change the `'maxResults': 5` in `google.py` to a number of your liking. diff --git a/calendar-card.js b/calendar-card.js new file mode 100644 index 0000000..a9902d4 --- /dev/null +++ b/calendar-card.js @@ -0,0 +1,440 @@ +/** + * + */ +class CalendarCard extends HTMLElement { + + + + /** + * called by hass - creates card, sets up any conmfig settings, and generates card + * @param {[type]} hass [description] + * @return {[type]} [description] + */ + set hass(hass) { + + // if we don't have the card yet then create it + if (!this.content) { + const card = document.createElement('ha-card'); + card.header = this.config.name; + this.content = document.createElement('div'); + this.content.style.padding = '0 16px 10px'; + card.appendChild(this.content); + this.appendChild(card); + moment.locale(hass.language); + } + + // save an instance of hass for later + this._hass = hass; + + // save css rules + this.cssRules = ` + + `; + + // update card with calendars + this + .getAllEvents(this.config.entities) + .then(events => this.updateHtmlIfNecessary(events)) + .catch(error => console.log('error', error)); + } + + /** + * [getAllEvents description] + * @param {[type]} entities [description] + * @return {[type]} [description] + */ + async getAllEvents(entities) { + + // don't update if it's only been 15 min + if(this.lastUpdate && moment().diff(this.lastUpdate, 'minutes') <= 15) { + return this.events; + } + + // create url params + const dateFormat = "YYYY-MM-DDTHH:mm:ss"; + const today = moment().startOf('day'); + const start = today.format(dateFormat); + const end = today.add(this.config.numberOfDays, 'days').format(dateFormat); + + // generate urls for calendars and get each calendar data + const urls = entities.map(entity => `calendars/${entity}?start=${start}Z&end=${end}Z`); + let allResults = await this.getAllUrls(urls); + + // convert each calendar object to a UI event + let events = [].concat.apply([], allResults).map(event => new CalendarEvent(event)); + + // show progress bar if turned on + if (this.config.showProgressBar && events.length > 0 && moment().format('DD') === moment(events[0].startDateTime).format('DD')) { + let now = {startDateTime: moment().format(), type: 'now'} + events.push(now); + } + + // sort events by date starting with soonest + events.sort((a, b) => new Date(a.startDateTime) - new Date(b.startDateTime)); + + // see if anything changed since last time, save events, and update last time we updated + const isSomethingChanged = this.isSomethingChanged(events); + this.events = events; + this.lastUpdate = moment(); + return { events, isSomethingChanged }; + + } + + /** + * given a list of urls get the data from them + * @param {Array} urls + * @return {Array} + */ + async getAllUrls(urls) { + try { + return await Promise.all(urls.map(url => this._hass.callApi('get', url))); + } catch (error) { + throw error; + } + } + + /** + * updates the entire card if we need to + * @param {[type]} eventList [description] + * @return {[type]} [description] + */ + updateHtmlIfNecessary(eventList) { + if(!eventList.isSomethingChanged) return; + + // save CSS rules then group events by day + this.content.innerHTML = this.cssRules; + const events = eventList.events; + const groupedEventsPerDay = this.groupBy(events, event => moment(event.startDateTime).format('YYYY-MM-DD')); + + // for each group event create a UI 'day' + groupedEventsPerDay.forEach((events, day) => { + const eventStateCardContentElement = document.createElement('div'); + eventStateCardContentElement.classList.add('day-wrapper'); + eventStateCardContentElement.innerHTML = this.getDayHtml(day, events); + this.content.append(eventStateCardContentElement); + }); + } + + /** + * generates the HTML for a single day + * @param {[type]} day [description] + * @param {[type]} events [description] + * @return {[type]} [description] + */ + getDayHtml(day, events) { + const className = moment().format('DD') === moment(day).format('DD') ? 'date now' : 'date'; + let momentDay = moment(day); + + return ` +
+
+
${momentDay.format('DD')}
+
${momentDay.format('ddd')}
+
+
+ ${events.map(event => this.getEventHtml(event)).join('')} +
+
`; + } + + /** + * generate HTML for a single event + * @param {[type]} event [description] + * @return {[type]} [description] + */ + getEventHtml(event) { + if(event.type) return ''; + + return ` +
+
+
+
${event.title}
+ ${this.getLocationHtml(event)} +
+
${this.getTimeHtml(event)}
+
+
`; + } + + /** + * generates HTML for opening an event + * @param {*} event + */ + getLinkHtml(event){ + return event.htmlLink ? `onClick="(function(){window.open('${event.htmlLink}');return false;})();return false;"` : ''; + } + + /** + * generates HTML for showing an event times + * @param {*} event + */ + getTimeHtml(event){ + if (event.isFullDayEvent) return 'All day' + + const start = moment(event.startDateTime).format(this.config.timeFormat); + const end = moment(event.endDateTime).format(this.config.timeFormat); + return `${start} - ${end}`; + } + + /** + * generate the html for showing an event location + * @param {*} event + */ + getLocationHtml(event){ + let locationHtml = ``; + + if (event.location) { + locationHtml += ` +
+  ` + } + + if (event.location && locationAddress) { + locationHtml += ` + + ${event.location} + +
`; + + } else if (event.location) { + locationHtml += `` + } + + return locationHtml; + } + + /** + * merge the user configuration with default configuration + * @param {[type]} config [description] + */ + setConfig(config) { + if (!config.entities) { + throw new Error('You need to define at least one calendar entity via entities'); + } + + this.config = { + name: 'Calendar', + showProgressBar: true, + numberOfDays: 7, + timeFormat: 'HH:mm', + ...config + }; + } + + /** + * get the size of the card + * @return {[type]} [description] + */ + getCardSize() { + return 3; + } + + /** + * did any event change since the last time we checked? + * @param {[type]} events [description] + * @return {Boolean} [description] + */ + isSomethingChanged(events) { + let isSomethingChanged = JSON.stringify(events) !== JSON.stringify(this.events); + return isSomethingChanged; + } + + /** + * ddep clone a js object + * @param {[type]} obj [description] + * @return {[type]} [description] + */ + deepClone(obj) { + return JSON.parse(JSON.stringify(obj)); + } + + /** + * group evbents by a givenkey + * @param {[type]} list [description] + * @param {[type]} keyGetter [description] + * @return {[type]} [description] + */ + groupBy(list, keyGetter) { + const map = new Map(); + + list.forEach(item => { + const key = keyGetter(item); + const collection = map.get(key); + + if (!collection) { + map.set(key, [item]); + } else { + collection.push(item); + } + }); + + return map; + } +} + +/** + * Creaates an generalized Calendar Event to use when creating the calendar card + * There can be Google Events and CalDav Events. This class normalizes those + */ +class CalendarEvent { + + /** + * [constructor description] + * @param {[type]} calendarEvent [description] + * @return {[type]} [description] + */ + constructor(calendarEvent) { + this.calendarEvent = calendarEvent; + } + + /** + * get the start time for an event + * @return {[type]} [description] + */ + get startDateTime() { + if (this.calendarEvent.start.date) { + let dateTime = moment(this.calendarEvent.start.date); + return dateTime.toISOString(); + } + + return this.calendarEvent.start && this.calendarEvent.start.dateTime || this.calendarEvent.start || ''; + } + + /** + * get the end time for an event + * @return {[type]} [description] + */ + get endDateTime() { + return this.calendarEvent.end && this.calendarEvent.end.dateTime || this.calendarEvent.end || ''; + } + + /** + * get the URL for an event + * @return {[type]} [description] + */ + get htmlLink(){ + return this.calendarEvent.htmlLink; + } + + /** + * get the title for an event + * @return {[type]} [description] + */ + get title() { + return this.calendarEvent.summary || this.calendarEvent.title; + } + + /** + * get the description for an event + * @return {[type]} [description] + */ + get description() { + return this.calendarEvent.description; + } + + /** + * parse location for an event + * @return {[type]} [description] + */ + get location() { + if(this.calendarEvent.location) { + return this.calendarEvent.location.split(',')[0] + } + + return undefined; + } + + /** + * get location address for an event + * @return {[type]} [description] + */ + get locationAddress() { + if(this.calendarEvent.location) { + let address = this.calendarEvent.location.substring(this.calendarEvent.location.indexOf(',') + 1); + return address.split(' ').join('+'); + } + return undefined; + } + + /** + * is the event a full day event? + * @return {Boolean} [description] + */ + get isFullDayEvent() { + if (this.calendarEvent.start && this.calendarEvent.start.date){ + return this.calendarEvent.start.date; + } + + let start = moment(this.startDateTime); + let end = moment(this.endDateTime); + let diffInHours = end.diff(start, 'hours'); + return diffInHours >= 24; + } +} + +/** + * add card definition to hass + */ +customElements.define('calendar-card', CalendarCard); \ No newline at end of file diff --git a/custom_updater.json b/custom_updater.json new file mode 100644 index 0000000..4f7e3f4 --- /dev/null +++ b/custom_updater.json @@ -0,0 +1,9 @@ +{ + "calendar-card": { + "updated_at": "2018-12-13", + "version": "1.1.0", + "remote_location": "https://raw.githubusercontent.com/ljmerza/homeassistant-lovelace-google-calendar-card/master/calendar-card.js", + "visit_repo": "https://github.com/ljmerza/homeassistant-lovelace-google-calendar-card", + "changelog": "https://github.com/ljmerza/homeassistant-lovelace-google-calendar-card/releases/latest" + } +} \ No newline at end of file