AHS FINALLY Gets It Right

So, we’ve gotten to the first week of 2019, and I can finally lay to rest the American Home Shield service request that started last June.

Seems that any form of contact with AHS other than social media is useless. After getting two more bills telling me I owed them for an extra service call they never made, and getting no response from the person that had finally gotten the HVAC fixed, I went after them on Twitter and Facebook, and got responses on both within a couple of hours.

I was told the exact same thing, pretty much word for word (must have been reading the same script), about it being fixed and I’d see my account corrected online in a couple of days… but this time they actually did it!

So, fingers crossed, we’re actually done. System is working. And if I go back and look at the amount of time I’ve invested getting to this point, I would have come out way ahead had I worked those hours and paid for it myself.

Easy On The Hand…

So a couple months ago, I beat the heck out of my right hand two days in a row. First, spending several hours driving an Formula 1 car in a racing sim, where you’re up shifting with the paddle shifter almost as fast as you can hit it coming out of every turn, and followed it up with hours on end working on a CAD drawing using a mouse the next day.

Day after that, I woke up and I could hardly do anything with my right hand. Couldn’t even pick up a cup of coffee. Now, this is not something with which I am unfamiliar. I’d done this to myself before back in the late ’90s after too many 12+ hour days on the computers at work without taking any breaks. RSI, repetitive strain injury, for which there is little help but to let it heal. Slowly.

Since I really don’t like using a mouse left-handed (which I did for several months last time) I decided to improve the ergonomics of my workstation a bit. First improvement was a vertical mouse, which lets you keep your wrist in a natural position, rather than twisting it flat, though it still hurt to press the buttons, and the scroll wheel, which gets used more than anything else, was even worse.

These vertical misc have gotten quite affordable.

So, I decided to use both hands. Moving the mouse didn’t strain anything that was injured, only manipulating the controls, so, enter the left hand scroll wheel / button box.

Phase One, rotary encoder and a coupe of buttons on the breadboard.

An Arduino Pro Micro, a couple of switches, and a KY-040 rotary encoder. The proof of concept version was functional, but only the push button in the KY-040 was easy to use.

Phase Two, not pictured, added a couple of cheap buttons to the plastic case with the rotary encoder. This kind of worked, but it wasn’t great having to grab and twist the encoder with two fingers.

Phase Three, a nice big plastic knob for the rotary encoder so you can spin it with one finger.

Phase Three added a nice large knob for the rotary encoder allowing it to be spun with one finger, but of course, I’d positioned the buttons too close and it blocks access to them. Not such a big deal, as this isn’t anything but a prototype, and there are nicer buttons on order. I’ve been holding off on the layout for the final version until I have the hardware.

FYI, that Hall Effect switch on the right front corner has nothing to do with the wheel / button box. Just added it as a button to play with it.

The software has gone through a multiple iterations as I’ve added things. It’s quite the hack, but it works.

Originally, it just used the encoder as a mouse wheel, but after spending several hours on a video editing project, I decided it would be nice to be able to switch it to transmit the left-arrow right-arrow keyboard keys the editing software uses to move on the timeline, so modes were added.

While holding down the left and right buttons, press the encoder, and the green LED starts flashing to display the current mode. Turning the the encoder either direction scrolls through the modes (currently only two), and pressing the encoder one more time exits mode selection and returns to normal operation.

I’ve included the code below, warts and all. It’s not pretty, but it works. Please excuse the mess – I have no need to clean it up for my own use. You’ll see there is provision for additional buttons, but the standard mouse library doesn’t seem to support more than five buttons. Still a work in progress.

/*
	Modified to support multiple modes of operation
		- Buttons + Mouse wheel
		- Buttons + Left & Right arrow keys (useful for video editing)
*/

#include <Keyboard.h>
#include <Mouse.h>

// #define	DEBUGGING
// #define DEBUG_MODES
//#define DEBUG_ENCODER
//#define DEBUG_BUTTONS
// #define DEBUG_BLINK

#define ENCODER_CLOCK		2		// Rotary encoder clock
#define	HE_SWITCH			3		// hall effect switch
#define ENCODER_DATA		A0		// Rotary encoder data 
#define ENCODER_SWITCH 		A1		// rotary encoder switch
#define LEFT_SWITCH			5		// left mouse button
#define	RIGHT_SWITCH		6		// right mouse button
#define SIX_SWITCH			7		// pin for mouse button 5
#define	SEVEN_SWITCH		8		// pin for mouse button 6

#define EXTERN_LED			16		// external LED

#define	MOUSE_SIX			(1<<5)	// six button
#define	MOUSE_SEVEN			(1<<6)	// seven button

#define	LED 				13		// power led pin

// #define DEBOUNCE_TIME		100		// delay 100 milliseconds for switch transitions
#define DEBOUNCE_TIME		20		// delay 20 milliseconds for switch transitions

#define LED_PULSETIME		250
#define DEADTIME			1000	// delay between LED pulse streams

/*
	Setup mode
		Toggle setup mode by holding down buttons 1 & 2, then pressing button 3
*/
// what mode are we in? allows functionality changes
bool setupMode = false;		// changing modes
// available modes
enum hidMode {
	mouse,			// encoder sends mouse wheel events
	arrows			// encoder sends left and right cursor key events (useful for video editing)
} mode = mouse;

void nextMode() {
	switch (mode) {
		case mouse:
			mode = arrows;
			#if defined(DEBUGGING) && defined(DEBUG_MODES)
			Serial.println("mode == ARROWS");
			#endif
			break;
		case arrows:
		default:		// something is broken, switch back to mouse
			mode = mouse;
			#if defined(DEBUGGING) && defined(DEBUG_MODES)
			Serial.println("mode == MOUSE");
			#endif
			break;
	}
}

void displayMode() {	// blink LED to display mode we're in
	switch (mode) {
		case mouse:
			blink(1, LED_PULSETIME);
			break;
		case arrows:
			blink(2, LED_PULSETIME);
			break;
		default:		// error!!!
			blink(10, LED_PULSETIME);
	}
}

class Button {
	private:
		int pin;
		String buttonName;
		int state;
		int clickVal;

	// constructor
	public:
		Button(int pinNum, String name, int click ) {
			pin = pinNum;
			state = HIGH;
			buttonName = name;
			clickVal = click;
			pinMode(pin, INPUT_PULLUP);
		}

		int State() {
			return state;
		}

		bool Update() {
			int buttonState = digitalRead(pin);
			if (buttonState != state) {			// state change?
				delay(DEBOUNCE_TIME);
				buttonState = digitalRead(pin);
				if (buttonState != state) {		// if really changed
					state = buttonState;
					#if defined(DEBUGGING) && defined(DEBUG_BUTTONS)
					Serial.print(buttonName);
					#endif
					if (!setupMode) {
						if (state == LOW) {
							#if defined(DEBUGGING) && defined(DEBUG_BUTTONS)
							Serial.println(" down");
							#endif
							Mouse.press(clickVal);
							// lowAction(clickVal);
						} else {
							#if defined(DEBUGGING) && defined(DEBUG_BUTTONS)
							Serial.println(" up");
							#endif
							Mouse.release(clickVal);
							// highAction(clickval);
						}
					}
				}
			}
		}
};

// define mouse buttons - this will need to be enhanced to support multiple modes
Button midButton(ENCODER_SWITCH, "middle button", MOUSE_MIDDLE);
Button leftButton(LEFT_SWITCH, "left button", MOUSE_LEFT);
Button rightButton(RIGHT_SWITCH, "right button", MOUSE_RIGHT);
Button sixButton(SIX_SWITCH, "six button", MOUSE_SIX);
Button sevenButton(SEVEN_SWITCH, "seven button", MOUSE_SEVEN);
Button hallEffectButton(HE_SWITCH, "hall effect", MOUSE_RIGHT);

//------------------------------------- encoder -------------------------------
int encoderCount = 0;
bool stateChanged = false;
bool clockWise = true;		// which way do we spin the mouse wheel

void encoderInterrupt() {
	stateChanged = true;
	if (digitalRead(ENCODER_DATA) == HIGH) {
		if (clockWise)
			encoderCount++;
		else
			encoderCount--;
	} else {
		if (clockWise)
			encoderCount--;
		else
			encoderCount++;
	}
}

void checkEncoder() {
	if (stateChanged) {
		#if defined(DEBUGGING) && defined(DEBUG_ENCODER)
		Serial.print("encoder count = "); Serial.println(encoderCount);
		#endif
		if (setupMode) {
			// switch to next mode
			nextMode();
		} else {
			switch (mode) {
				case mouse:
					Mouse.move(0, 0, encoderCount);
					break;
				case arrows:
					if (encoderCount > 0) {
						Keyboard.write(KEY_LEFT_ARROW);
						#if defined(DEBUGGING) && defined(DEBUG_ENCODER)
						Serial.println("LEFT ARROW");
						#endif
					} else {
						Keyboard.write(KEY_RIGHT_ARROW);
						#if defined(DEBUGGING) && defined(DEBUG_ENCODER)
						Serial.println("RIGHT ARROW");
						#endif
					}
					break;
			}
		}
		encoderCount = 0;
		stateChanged = false;
	}
}

//------------------------------------- support functions ---------------------
void updateButtons() {
	leftButton.Update();
	rightButton.Update();
	midButton.Update();
	sixButton.Update();
	sevenButton.Update();
	hallEffectButton.Update();
}

void toggleSetup() {
	if (!setupMode) {
		if (leftButton.State() == LOW && rightButton.State() == LOW && midButton.State() == LOW) {
			while (midButton.State() == LOW)	// wait for middle button to be released or we'll switch states again
				midButton.Update();
			setupMode = true;
			Mouse.release(MOUSE_LEFT);		// release the buttons so computer isn't locked up
			Mouse.release(MOUSE_RIGHT);
			Mouse.release(MOUSE_MIDDLE);
		}
	} else {
		if (midButton.State() == LOW) {
			setupMode = false;
		}
	}
}

//------------------------------------- start ---------------------------------
void setup() {
	// use falling edge because capacitor on input will cause a slow rising edge
	attachInterrupt(digitalPinToInterrupt(ENCODER_CLOCK), encoderInterrupt, FALLING);
	Mouse.begin();
	Keyboard.begin();
	Serial.begin(9600);

	// enable use of built-in LED
	pinMode(LED_BUILTIN, OUTPUT);

	// configure pin for external LED
	pinMode(EXTERN_LED, OUTPUT);
	// and turn it on (testing)
	externLed(LOW);
}

//-----------------------------------------------------------------------------
// blink LED without stopping processing
bool running = false;
unsigned long endTime;
int blinkCount = 0;
int blinkDuration = 0;
bool ledOn = false;

void blink(int count, int blinkTime) {
	// Serial.print("bink: count = "); Serial.print(count); Serial.print(" blinkTime = "); Serial.println(blinkTime);
	if (!running) {							// start new cycle, if already running, too bad, can't stack them up
		running = true;
		blinkCount = count + 1;					// remember pulses and their length
		blinkDuration = blinkTime;
		endTime = millis() + blinkDuration;	// set the timer
		// digitalWrite(LED_BUILTIN, HIGH);	// and turn on the LED
		externLed(HIGH);
		ledOn = true;
	}
}

void doBlink() {
	if (running) {
		if (endTime < millis()) {				// has delay elapsed?
			#if defined(DEBUGGING) && defined(DEBUG_BLINK)
			Serial.print("ledOn = "); Serial.println(ledOn);
			#endif
			if (ledOn) {						// toggle LED
				#if defined(DEBUGGING) && defined(DEBUG_BLINK)
				Serial.print("blinkCount = "); Serial.print(blinkCount); Serial.println(" turning off LED");
				#endif
				// digitalWrite(LED_BUILTIN, LOW);	// turn off
				externLed(HIGH);
				ledOn = false;
				blinkCount--;					// decrement count every time cycle completes
				if (blinkCount > 0) {			// if more to do
					endTime = millis() + blinkDuration;		// restart timer
				} else if (blinkCount == 0) {				// finished, delay a bit before allowing turn on again
					endTime = millis() + DEADTIME;	// long delay after last one cycle
					ledOn = true;				// say it's on so we hit the off code again
				} else {						// we're done
					running = false;
				}
			} else {
				#if defined(DEBUGGING) && defined(DEBUG_BLINK)
				Serial.print("blinkCount = "); Serial.print(blinkCount); Serial.println(" turning on LED");
				#endif
				// digitalWrite(LED_BUILTIN, HIGH);
				externLed(LOW);
				ledOn = true;
				endTime = millis() + blinkDuration;		// restart timer
			}
		}
	}
}

// external LED
void externLed(int state) {
	digitalWrite(EXTERN_LED, state);
}
//------------------------------------- main loop -----------------------------
void loop() {
	if (setupMode) {
		// display current mode (blink onboard LED)
		displayMode();
	}
	checkEncoder();
	updateButtons();
	toggleSetup();		// go to setup mode?
	doBlink();			// keep the LED running
}

Surprise, surprise, surprise…

All is not well with American Home Shield.

Huge surprise, right?

So, after the hour on the phone with customer support, where they claimed to have fixed the problem with their billing, weeks later I get another email telling me my account is past due for the bogus service fee. Later the same day, I get another invoice in the mail telling me my account is past due.

How many weeks does it take for them to correct their billing error? Will it ever happen? Not holding my breath.

Rather than waste hours on the phone with customer service again,, I’ve reached out to the one person that seemed to be able to do things correctly at AHS. I’m hoping this will work.

And all was good until…

The 4th of Dec. when I get the email telling me my account is about to be terminated for late payment on a service call. Hmm, only service call I had was when this AC nonsense started back in June.

Thirty five minutes listening to the same twenty second long clip of classical music, so horribly over modulated and distorted it was painful to hear (I may never listen to that particular piece for the rest of my life).

Finally get a human, back on hold while the “look up my account”. They must be using mid-eighties vintage IBM PC’s for it to take this long. Ok, ten or fifteen minutes later I’m told that when somebody reopened the incomplete service call they’d repeatedly closed, even though the work hadn’t been done, they put it in as a new service call, triggering the service charge.

About which I was never informed. So, they can add charges to your account, but the system never sends any notification to the customer so they can pay it? Never got a warning, no payment reminder. Not a word, until today when their system decided it was ready to terminate my account because I didn’t pay that service fee. Who wrote that system?

It only took another ten minutes listening to very annoying, but thankfully low volume, undistorted, generic, instrumental background music (thank you for not ruing any more classics for me) until the nice lady filled out the form telling requesting the charge be removed from my account.

I’m withholding final judgement until I see if it actually gets fixed.

How does one stay in business when providing such wonderful service?

Light At The End Of The Tunnel?

Hoping it’s not a train…

So, the nice person at American Home Shield called back today, as promised. Turns out the parts Total Air Care told me were on order twelve days ago, on 9/7, were not ordered until today. I didn’t ask if they were ordered before or after AHS spoke to Total Air Care (I’m betting on after). Supposedly, it’ll take two to three days for the parts to arrive and then Total Air Care will call to schedule installation.

The person helping me at AHS is going contact me again next week to verify Total Air Care has actually scheduled installation.

Then The Next Day, AHS Calls

They’re following up on my social media contact. They want to know if this has been resolved. LOL, right…

I explain that Total Air Care responded yesterday that parts were on order and they’d contact me as soon as they were in.

AHS says, we haven’t ordered any parts for you.

I say, Total Air Care told me they’re not allowed to order part and you’ve got to order them.

AHS says, we’ll call them and get back to you.

We’re hoping to get the AC working in time for the heating season.

We’ll Be Reaching Out To You Within 48 Hours

At least that’s what the email said.

Waited another ten days, then replied to the email claiming we had no coverage and got a prompt response:

YES SIR, THIS PART ORDERED, THOUGH, DUE TO THE WEATHER ,THE PART WILL MORE THEN LIKELY BE IN STOCK IN 2-3 DAYS IN WHICH TIME WE WILL CALL YOU AND GET YOU ON THE SCHEDULE AT OUR VERY EARLIEST AVAILABLE APPOINTMENT

Uh-huh…