Skip to content

Commit

Permalink
Refactored for cleaner code
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewpetro committed Feb 8, 2024
1 parent b063d88 commit b3650e8
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 59 deletions.
45 changes: 45 additions & 0 deletions src/irrigation-events/device-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { compareAsc, parseISO } from 'date-fns'
import { IrrigationEventDocument } from './interfaces/irrigation-event-document.interface'
import { DeviceState } from './enums/device-state.interface'

const sortFn = (a: IrrigationEventDocument, b: IrrigationEventDocument) => compareAsc(parseISO(a._id), parseISO(b._id))

export class DeviceEvents {
private deviceId: number
private events: IrrigationEventDocument[] = []
private currentDeviceState?: DeviceState

constructor(deviceId: number, events: IrrigationEventDocument[]) {
this.deviceId = deviceId
this.events = [...events].sort(sortFn)
}

public getDeviceId(): number {
return this.deviceId
}

public getEvents(): IrrigationEventDocument[] {
return [...this.events]
}

public getFirstEvent(): IrrigationEventDocument {
return this.events[0]
}

public getLastEvent(): IrrigationEventDocument {
return this.events[this.events.length - 1]
}

public setCurrentDeviceState(state: DeviceState | undefined) {
this.currentDeviceState = state
}

public getCurrentDeviceState(): DeviceState | undefined {
return this.currentDeviceState
}

public addEvent(event: IrrigationEventDocument) {
this.events.push(event)
this.events.sort(sortFn)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DeviceState } from '../enums/device-state.interface'

export interface DeviceStates {
[deviceId: string]: DeviceState | undefined
[deviceId: number]: DeviceState | undefined
}
85 changes: 40 additions & 45 deletions src/irrigation-events/irrigation-events.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { IsISO8601 } from 'class-validator'
import { isWithinInterval, parseISO } from 'date-fns'
import { IrrigationEventsService } from './irrigation-events.service'
import { IrrigationEventDocument } from './interfaces/irrigation-event-document.interface'
import { MakerApiEventDto } from './dto/maker-api-event-dto'
import { MakerApiEventDto } from './dto/maker-api-event.dto'
import { DeviceState } from './enums/device-state.interface'
import { MakerApiService } from './maker-api.service'
import { ViewmodelTransformService } from './viewmodel-transform.service'
import { DeviceEvents } from './device-events'

class QueryParameters {
@IsISO8601()
Expand All @@ -15,14 +16,6 @@ class QueryParameters {
endTimestamp: string
}

type DeviceEventLists = { [deviceId: string]: IrrigationEventDocument[] }

const shouldPrependAdditionalOnEvent = (events: IrrigationEventDocument[]) =>
events && events.length >= 1 && events[0].state === DeviceState.ON

const shouldAppendAdditionalOffEvent = (events: IrrigationEventDocument[]) =>
events && events.length >= 1 && events[0].state === DeviceState.OFF

const isCurrentTimeWithinInterval = (startTimestamp: string, endTimestamp: string) =>
isWithinInterval(Date.now(), { start: parseISO(startTimestamp), end: parseISO(endTimestamp) })

Expand All @@ -34,21 +27,18 @@ const makerEventToIrrigationEvent = ({ displayName, value, deviceId }: MakerApiE
deviceId,
}) as IrrigationEventDocument

// Split the list of events into lists by deviceId
const createDeviceEventLists = (dbDocuments: IrrigationEventDocument[]): DeviceEventLists =>
dbDocuments.reduce(
(accumulator, event) => {
const deviceId = event.deviceId.toString()
if (!accumulator[deviceId]) {
accumulator[deviceId] = []
}
accumulator[deviceId].push(event)
return accumulator
},
// I'd like to leave deviceId as a number, but Object.entries()
// would convert it to a string.
{} as { [deviceId: string]: IrrigationEventDocument[] }
)
const createDeviceEvents = (dbDocuments: IrrigationEventDocument[]): DeviceEvents[] => {
const deviceEvents: DeviceEvents[] = []
const deviceIds = new Set<number>()
dbDocuments.forEach((event) => {
deviceIds.add(event.deviceId)
})
deviceIds.forEach((deviceId) => {
const events = dbDocuments.filter((event) => event.deviceId === deviceId)
deviceEvents.push(new DeviceEvents(deviceId, events))
})
return deviceEvents
}

@Controller('irrigation-events')
export class IrrigationEventsController {
Expand All @@ -70,46 +60,51 @@ export class IrrigationEventsController {
@UsePipes(new ValidationPipe())
async get(@Query() { startTimestamp, endTimestamp }: QueryParameters) {
const irrigationEvents = await this.irrigationEventsService.getIrrigationEvents(startTimestamp, endTimestamp)
const deviceEventLists = createDeviceEventLists(irrigationEvents)
const eventLists = await this.addMissingEvents(deviceEventLists, startTimestamp, endTimestamp)
const currentDeviceStates = isCurrentTimeWithinInterval(startTimestamp, endTimestamp)
? await this.makerApiService.getAllDeviceStates()
: {}
return this.viewmodelTransformService.transform(eventLists, currentDeviceStates)
const deviceEventsList = createDeviceEvents(irrigationEvents)
await this.addEventsOutsideTimeRange(deviceEventsList, startTimestamp, endTimestamp)
if (isCurrentTimeWithinInterval(startTimestamp, endTimestamp)) {
this.addCurrentDeviceStates(deviceEventsList)
}
return this.viewmodelTransformService.transform(deviceEventsList)
}

private readonly addMissingEvents = async (
deviceEventLists: DeviceEventLists,
private async addEventsOutsideTimeRange(
deviceEventLists: DeviceEvents[],
startTimestamp: string,
endTimestamp: string
): Promise<IrrigationEventDocument[][]> => {
const eventListEntries = Object.entries(deviceEventLists)
const appendOnEventPromises = eventListEntries.map(async ([deviceId, deviceEvents]) => {
if (deviceEvents[0].state !== DeviceState.ON) {
): Promise<void> {
const appendOnEventPromises = deviceEventLists.map(async (deviceEvents) => {
if (deviceEvents.getFirstEvent().state !== DeviceState.ON) {
const eventsBeforeStart = await this.irrigationEventsService.getEventsBeforeStart(
startTimestamp,
parseInt(deviceId, 10)
deviceEvents.getDeviceId()
)
if (shouldPrependAdditionalOnEvent(eventsBeforeStart)) {
deviceEvents.unshift(eventsBeforeStart[0])
if (eventsBeforeStart[0]?.state === DeviceState.ON) {
deviceEvents.addEvent(eventsBeforeStart[0])
}
}
})
await Promise.allSettled(appendOnEventPromises)

const appendOffEventPromises = eventListEntries.map(async ([deviceId, deviceEvents]) => {
if (deviceEvents[deviceEvents.length - 1].state !== DeviceState.OFF) {
const appendOffEventPromises = deviceEventLists.map(async (deviceEvents) => {
if (deviceEvents.getLastEvent().state !== DeviceState.OFF) {
const eventsAfterEnd = await this.irrigationEventsService.getEventsAfterEnd(
endTimestamp,
parseInt(deviceId, 10)
deviceEvents.getDeviceId()
)
if (shouldAppendAdditionalOffEvent(eventsAfterEnd)) {
deviceEvents.push(eventsAfterEnd[0])
if (eventsAfterEnd[0]?.state === DeviceState.OFF) {
deviceEvents.addEvent(eventsAfterEnd[0])
}
}
})
await Promise.allSettled(appendOffEventPromises)
}

return Object.values(deviceEventLists)
private async addCurrentDeviceStates(deviceEventsList: DeviceEvents[]): Promise<void> {
const makerEvents = await this.makerApiService.getAllDeviceStates()
deviceEventsList.forEach((device) => {
const currentDeviceState = makerEvents[device.getDeviceId()]
device.setCurrentDeviceState(currentDeviceState)
})
}
}
2 changes: 1 addition & 1 deletion src/irrigation-events/maker-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ export class MakerApiService {
}
public async getAllDeviceStates() {
const data = await this.axiosInstance.get<MakerDeviceDetails[]>('/all').then((response) => response.data)
return Object.fromEntries(data.map((device) => [device.id, device.attributes.switch])) as DeviceStates
return Object.fromEntries(data.map((device) => [parseInt(device.id, 10), device.attributes.switch])) as DeviceStates
}
}
21 changes: 9 additions & 12 deletions src/irrigation-events/viewmodel-transform.service.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import { Injectable } from '@nestjs/common'
import { roundToNearestMinutes } from 'date-fns'
import { IrrigationEventDocument } from './interfaces/irrigation-event-document.interface'
import { DeviceStates } from './interfaces/device-states.interface'
import { IrrigationEventViewmodel } from './interfaces/irrigation-event-viewmodel.interface'
import { IrrigationEventViewmodel } from './dto/irrigation-event-viewmodel.dto'
import { DeviceState } from './enums/device-state.interface'
import { Warning } from './enums/warning.interface'
import { DeviceEvents } from './device-events'

const roundTimestampToMinute = (timestamp: string): string =>
roundToNearestMinutes(new Date(timestamp), {
nearestTo: 1,
roundingMethod: 'trunc',
}).toISOString()

function createViewmodelsFromDeviceEvents(
deviceEvents: IrrigationEventDocument[],
deviceStates: DeviceStates
): IrrigationEventViewmodel[] {
function createViewmodelsFromDeviceEvents(deviceEvents: DeviceEvents): IrrigationEventViewmodel[] {
const viewmodels: IrrigationEventViewmodel[] = []
const events = deviceEvents.getEvents()
let i = 0
while (i < deviceEvents.length) {
const [event, nextEvent] = deviceEvents.slice(i, i + 2)
while (i < events.length) {
const [event, nextEvent] = events.slice(i, i + 2)
if (event.state === DeviceState.ON && nextEvent?.state === DeviceState.OFF) {
// Happy path: ON followed by OFF
viewmodels.push({
Expand All @@ -46,7 +43,7 @@ function createViewmodelsFromDeviceEvents(
// the final OFF event is missing. Check the current device
// states to determine which is the case.
viewmodels.push(
deviceStates[event.deviceId.toString()] === DeviceState.ON
deviceEvents.getCurrentDeviceState() === DeviceState.ON
? {
// eslint-disable-next-line no-underscore-dangle
startDate: roundTimestampToMinute(event._id),
Expand Down Expand Up @@ -81,7 +78,7 @@ function createViewmodelsFromDeviceEvents(

@Injectable()
export class ViewmodelTransformService {
public transform(eventLists: IrrigationEventDocument[][], deviceStates: DeviceStates): IrrigationEventViewmodel[] {
return eventLists.flatMap((deviceEvents) => createViewmodelsFromDeviceEvents(deviceEvents, deviceStates))
public transform(deviceEventsList: DeviceEvents[]): IrrigationEventViewmodel[] {
return deviceEventsList.flatMap((deviceEvents) => createViewmodelsFromDeviceEvents(deviceEvents))
}
}

0 comments on commit b3650e8

Please sign in to comment.