package immobilizerditto import ( "bytes" "context" "encoding/json" "io" "net/http" "sync" "time" "github.com/fiskerinc/cloud-services/pkg/common" "github.com/fiskerinc/cloud-services/pkg/dbc/state" "github.com/fiskerinc/cloud-services/pkg/immobilizer/immobilizershared" "github.com/fiskerinc/cloud-services/pkg/logger" "github.com/fiskerinc/cloud-services/pkg/redisv2" ) // I should put these parked values as a constant somewhere TODO func (id *ImmobilizeDitto) VCUGearSig(vin string, newState int) { //0 "Undefined_initial_value" 1 "gear_P" 2 "gear_N" 3 "R_gear" 4 "D_gear" 5 "Reserved" 6 "gear_E" 7 "gear_S" 8 "Reserved" 9 "Reserved" 10 "Reserved" 11 "Reserved" 12 "Reserved" 13 "Reserved" 14 "Reserved" 15 "Reserved" ; id.WatchListSync.RLock() track, ok := id.WatchList[vin] id.WatchListSync.RUnlock() if !ok { return } switch track.State { case immobilizershared.STATE_IMMOBILIZED: // If we are immobilized, but we are out of park, thats an issue if newState == 1 || newState == 2 { // Car is parked, and possible already immobilized, but we are going to enforce in case we were wrong // id.sendLockCommand(vin) } else if newState > 2 { logger.Error().Str("VIN", vin).Int("New Gear", newState).Str("MSG", "Marked as Immobilized, but received driving gear").Msg("VCUGearSig") go alertAmericanLeaseMovingVehicle(vin) } case immobilizershared.STATE_IMMOBILIZING: if newState == 1 || newState == 2 { // Car is parked, send that lock command. We know the car is awake, so no need to send a wake up sms id.sendLockCommand(vin) } case immobilizershared.STATE_MOBILIZING: if newState > 2 { // unsure if we should accept this as done immobilizershared.UpdateCarMobilized(vin, id.RedisClient) } } } func (id *ImmobilizeDitto) sendLockCommand(vin string) { msg := common.RemoteCommandSource{} msg.Command = "doors_lock" // SafeQueueMessage auto deletes after one hour, which I do not want with this lock command, rather it sat around indefinitely data, _ := json.Marshal(common.Message{ Handler: "remote_command", Data: msg, }) res := id.RedisClient.RPush(context.Background(), redisv2.QueueKey(common.TRex.Key(vin)), data) err := res.Err() if err != nil { logger.Err(err).Msg("Failed to send lock command") } } func alertAmericanLeaseMovingVehicle(vin string) { url := "https://mobile.americanlease.net/api/v1/fisker/command/failure" token := "Bearer f3d795d3-5325-42d7-8dd8-ccb416c52ae2" type AmericanLeaseBody struct { VIN string `json:"vin"` UTCMobileAt time.Time `json:"utcMobileAt"` //"utcMobileAt":"2025-03-07T09:00:00", UUID string `json:"uuid"` //"uuid": "db6e407d-791b-4d00-a742-da856a42c4ba" } body := AmericanLeaseBody{ VIN: vin, UTCMobileAt: time.Now().UTC(), UUID: "00000000-0000-0000-0000-000000000000", } b, _ := json.Marshal(body) // Create the HTTP request req, err := http.NewRequest("POST", url, bytes.NewBuffer(b)) if err != nil { logger.Err(err).Msg("failed to make request to american lease") return } // Set headers req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", token) // Execute the request client := &http.Client{} resp, err := client.Do(req) if err != nil { logger.Err(err).Msg("failed to do request to american lease") return } defer resp.Body.Close() if resp.StatusCode >= 300 { respBody, _ := io.ReadAll(resp.Body) logger.Error().Str("response body", string(respBody)).Str("response header", resp.Status).Msg("failed to contact american lease about moving car") } } // So we got the VCU Immo sts, most likely a new value compared to what was had before func (id *ImmobilizeDitto) VCUIMMOSts(vin string, newState string) { // Possible values for newState // "Immo_Active", // "Immo_Inactive", // "Reserved", // "Invalid", // Check if we are tracking this vehicle id.WatchListSync.RLock() track, ok := id.WatchList[vin] id.WatchListSync.RUnlock() if !ok { return } logger.Debug().Str("New State", newState).Str("Tracked", track.State.String()).Str("VIN", vin).Msg("VCUIMMOSts signal") switch track.State { case immobilizershared.STATE_IMMOBILIZED: // Car is currently marked as immobilized if newState == "Immo_Inactive" { // This is an issue, we have the car as being immobilized, but it just got moving. Now this could possible be a timing issue between a different ditto // If this actually happens, we need to set the vehicle to Immobilizing logger.Error().Str("VIN", vin).Str("MSG", "Marked as Immobilized, but received Immo_Inactive").Msg("Ditto Immobilizer") } // if newState == "Immo_Active" { // // This is can be ignored // } case immobilizershared.STATE_IMMOBILIZING: // We are trying to immobilize the vehicle if newState == "Immo_Active" { immobilizershared.UpdateCarImmobilized(vin, id.RedisClient) // Send lock command some more id.sendLockCommand(vin) } case immobilizershared.STATE_MOBILIZING: // Hey great, we know that the car is now active if newState == "Immo_Inactive" { immobilizershared.UpdateCarMobilized(vin, id.RedisClient) } } } func (id *ImmobilizeDitto) PKC_KeyStsMod(vin string, newState string) { id.WatchListSync.RLock() track, ok := id.WatchList[vin] id.WatchListSync.RUnlock() if !ok { return } logger.Debug().Str("New State", newState).Str("Tracked", track.State.String()).Str("VIN", vin).Msg("PKC_KeyStsMode signal") switch track.State { case immobilizershared.STATE_IMMOBILIZED: // Car is currently marked as immobilized if newState == state.PKC_KeyStsMod_disabled { // we thought the car was immobilized, but we just got a signal saying its been mobilized logger.Error().Str("VIN", vin).Str("MSG", "Marked as Immobilized, but received PKC_KeyStsMod: disabled").Msg("Ditto Immobilizer") } case immobilizershared.STATE_IMMOBILIZING: // We were trying to immobilize, and we got confirmation that it is if newState == state.PKC_KeyStsMod_enabled { immobilizershared.UpdateCarImmobilized(vin, id.RedisClient) } case immobilizershared.STATE_MOBILIZING: // trying to get car moving, and got confirmation it is if newState == state.PKC_KeyStsMod_disabled { immobilizershared.UpdateCarMobilized(vin, id.RedisClient) } } } // Ongoing process, needs to be run in da background func (id *ImmobilizeDitto) ListenToRedisChanges() { subscription := id.RedisClient.Subscribe(context.Background(), immobilizershared.IMMOBILIZER_PUBSUB_CHANNEL) subChannel := subscription.Channel() for msg := range subChannel { rim := immobilizershared.RedisImmobilizerMsg{} err := json.Unmarshal([]byte(msg.Payload), &rim) if err != nil { logger.Err(err).Str("Payload", msg.Payload).Msg("ListenToRedisChanges: Failed to parse payload") continue } id.WatchListSync.Lock() if rim.State == immobilizershared.STATE_MOBILIZED { delete(id.WatchList, rim.VIN) } else { id.WatchList[rim.VIN] = immobilizershared.ImmobilizeTrack{State: rim.State} } id.WatchListSync.Unlock() } } // Read in redis to get out initial state func (id *ImmobilizeDitto) SeedLocalData() { res, err := immobilizershared.ReadVehicleStatuses(id.RedisClient) if err != nil { return } id.WatchListSync.Lock() id.WatchList = res id.WatchListSync.Unlock() } // Don't want to become de-synced, so will seed once an hour func (id *ImmobilizeDitto) SeedHourly() { id.SeedLocalData() time.AfterFunc(time.Hour, id.SeedHourly) } func InitImmobilizerDitto(connection *redisv2.Connection) (id *ImmobilizeDitto) { id = &ImmobilizeDitto{} id.RedisClient = connection go id.SeedHourly() go id.ListenToRedisChanges() return id } // Wether a car is to start mobilizing or immobilizing is a decision by the server, not from a cars messaging type ImmobilizeDitto struct { RedisClient *redisv2.Connection WatchList map[string]immobilizershared.ImmobilizeTrack WatchListSync sync.RWMutex }